Show More
The requested changes are too big and content was truncated. Show full diff
@@ -0,0 +1,97 b'' | |||||
|
1 | .. _integrations-ci: | |||
|
2 | ||||
|
3 | CI Server integration | |||
|
4 | ===================== | |||
|
5 | ||||
|
6 | ||||
|
7 | RhodeCode :ref:`integrations-webhook` integration is a powerfull tool to allow | |||
|
8 | interaction with systems like Jenkin, Bamboo, TeamCity, CircleCi or any other | |||
|
9 | CI server that allows triggering a build using HTTP call. | |||
|
10 | ||||
|
11 | Below are few examples on how to use :ref:`integrations-webhook` to trigger | |||
|
12 | a CI build. | |||
|
13 | ||||
|
14 | ||||
|
15 | General Webhook | |||
|
16 | +++++++++++++++ | |||
|
17 | ||||
|
18 | :ref:`integrations-webhook` allows sending a JSON payload information to specified | |||
|
19 | url with GET or POST methods. There are several variables that could be used | |||
|
20 | in the URL greatly extending the flexibility of this type of integration. | |||
|
21 | ||||
|
22 | Most of the modern CI systems such as Jenkins, TeamCity, Bamboo or CircleCi | |||
|
23 | allows triggering builds via GET or POST calls. | |||
|
24 | ||||
|
25 | :ref:`integrations-webhook` can be either specified per each repository or | |||
|
26 | globally, if your CI maps directly to all your projects a global | |||
|
27 | :ref:`integrations-webhook` integration can be created and will trigger builds | |||
|
28 | for each change in projects. If only some projects allow triggering builds a | |||
|
29 | global integration will also work because mostly a CI system will ignore a | |||
|
30 | call for unspecified builds. | |||
|
31 | ||||
|
32 | ||||
|
33 | .. note:: | |||
|
34 | ||||
|
35 | A quick note on security. It's recommended to allow IP restrictions | |||
|
36 | to only allow RhodeCode server to trigger builds. If you need to | |||
|
37 | specify username and password this could be done by embedding it into a | |||
|
38 | trigger URL, e.g. `http://user:password@server.com/job/${project_id} | |||
|
39 | ||||
|
40 | ||||
|
41 | If users require to provide any custom parameters, they can be stored for each | |||
|
42 | project inside the :ref:`repo-xtra`. For example to migrate a current job that | |||
|
43 | has a numeric build id, storing this as `jenkins_build_id` key extra field | |||
|
44 | the url would look like that:: | |||
|
45 | ||||
|
46 | http://server/job/${extra:jenkins_build_id}/ | |||
|
47 | ||||
|
48 | ||||
|
49 | .. note:: | |||
|
50 | ||||
|
51 | Please note that some variables will result in multiple calls. | |||
|
52 | e.g. for |HG| specifying `${branch}` will trigger as many builds as how | |||
|
53 | many branches the suer actually pushed. Same applies to `${commit_id}` | |||
|
54 | This will trigger many builds if many commits are pushed. This allows | |||
|
55 | triggering individual builds for each pushed commit. | |||
|
56 | ||||
|
57 | ||||
|
58 | Jenkins | |||
|
59 | +++++++ | |||
|
60 | ||||
|
61 | To use Jenkins CI with RhodeCode, a Jenkins Build with Parameters should be used. | |||
|
62 | Plugin details are available here: https://wiki.jenkins.io/display/JENKINS/Build+With+Parameters+Plugin | |||
|
63 | ||||
|
64 | If the plugin is configured, RhodeCode can trigger builds automatically by | |||
|
65 | calling such example url provided in :ref:`integrations-webhook` integration:: | |||
|
66 | ||||
|
67 | http://server/job/${project_id}/build-branch-${branch}/buildWithParameters?token=TOKEN&PARAMETER=value&PARAMETER2=value2 | |||
|
68 | ||||
|
69 | ||||
|
70 | Team City | |||
|
71 | +++++++++ | |||
|
72 | ||||
|
73 | To use TeamCity CI it's enough to call the API and provide a buildId. | |||
|
74 | Example url after configuring :ref:`repo-xtra` would look like that:: | |||
|
75 | ||||
|
76 | http://teacmtiyserver/viewType.html?buildTypeId=${extra:tc_build_id} | |||
|
77 | ||||
|
78 | ||||
|
79 | Each project can have many build configurations. | |||
|
80 | buildTypeId which is a unique ID for each build configuration (job). | |||
|
81 | ||||
|
82 | ||||
|
83 | CircleCi | |||
|
84 | ++++++++ | |||
|
85 | ||||
|
86 | To use CircleCi, a POST call needs to be triggered. Example build url would | |||
|
87 | look like this:: | |||
|
88 | ||||
|
89 | http://cicleCiServer/project/${repo_type}/${username}/${repo_id}/tree/${branch} | |||
|
90 | ||||
|
91 | ||||
|
92 | Circle Ci expects format of:: | |||
|
93 | ||||
|
94 | POST: /project/:vcs-type/:username/:project/tree/:branch | |||
|
95 | ||||
|
96 | ||||
|
97 | CircleCi API documentation can be found here: https://circleci.com/docs/api/v1-reference/ |
@@ -0,0 +1,103 b'' | |||||
|
1 | |RCE| 4.8.0 |RNS| | |||
|
2 | ----------------- | |||
|
3 | ||||
|
4 | Release Date | |||
|
5 | ^^^^^^^^^^^^ | |||
|
6 | ||||
|
7 | - 2017-06-30 | |||
|
8 | ||||
|
9 | ||||
|
10 | New Features | |||
|
11 | ^^^^^^^^^^^^ | |||
|
12 | ||||
|
13 | - Code Review: added new reviewers logic. This features now is Common Criteria | |||
|
14 | compatible and allows to define Mandatory (non-removable) reviewers. | |||
|
15 | In addition new options were added to forbid adding new reviewers or forbid | |||
|
16 | author of commits or the pull request itself to be a reviewer of the code. | |||
|
17 | - Audit logs: introducing new audit logs tracking most important actions in | |||
|
18 | the system. Admins can track important events such as deletion of resources, | |||
|
19 | permissions changes, user groups changes. Each event tracks users with his | |||
|
20 | IP and user agent. | |||
|
21 | - Mercurial: enabled evolve extensions. Each repository can be now configured | |||
|
22 | to support evolve, commit phases, and evolve state are also shown in | |||
|
23 | commit and changelog views. | |||
|
24 | - VCS: expose newly pushed bookmarks or branches as quick links to open a | |||
|
25 | pull request on client output. Allows easier pull request creation via CLI. | |||
|
26 | ||||
|
27 | ||||
|
28 | General | |||
|
29 | ^^^^^^^ | |||
|
30 | ||||
|
31 | - Core: ported many views into pure pyramid code with python3.6 compatibility. | |||
|
32 | Now almost 80% of the code is ported, and future ready. It's our ongoing | |||
|
33 | effort to allow support for modern python version. | |||
|
34 | - Comments: show author tag in pull request comments to easily | |||
|
35 | discover the author of changes in discussions. | |||
|
36 | - Files: allow specifying custom filename for uploaded files via web interface. | |||
|
37 | - Pull requests: changed who is allowed to close a pull request. Now it's only | |||
|
38 | super-admin, owner or person who can merge. | |||
|
39 | Before it was every reviewer can close. Which really doesn't make sense. | |||
|
40 | - Users: show that user is disabled when editing his properties. | |||
|
41 | - Integrations: expose user_id, and username in Webhook integration | |||
|
42 | templates arguments. | |||
|
43 | - Integrations: exposed extra repo variables in template arguments of | |||
|
44 | Webhook integration. | |||
|
45 | - Login: add link when using external auth to make it easier to login | |||
|
46 | using oauth providers, such as Google or Github. | |||
|
47 | - Maintenance: added svn verify command to tasks to be able to verify the | |||
|
48 | filesystem and repo formats from web interface. Allows much easier tracking | |||
|
49 | of incompatible filesystem storage of subversion repositories. | |||
|
50 | - Events: expose permalink urls for pull requests, and repositories. | |||
|
51 | Permalink url should provide a non-changeable url that can be used in | |||
|
52 | external system. | |||
|
53 | - Svn: increase possibility to specify compatibility to pre 1.9 version. | |||
|
54 | ||||
|
55 | ||||
|
56 | Security | |||
|
57 | ^^^^^^^^ | |||
|
58 | ||||
|
59 | - security(high): fixed possibility to delete other users inline comments | |||
|
60 | for users who were repository admins. | |||
|
61 | - security(med): fixed XSS inside the tooltip for author string. | |||
|
62 | - security(med): fixed stored XSS in notifications inbox. | |||
|
63 | - security(med): use custom writer for RST rendering to prevent injection of javascript: tags. | |||
|
64 | - security(med): escape flash messaged VCS errors to prevent reflected XSS attacks. | |||
|
65 | - security(low): use 404 instead of 403 code on permission decorator to | |||
|
66 | prevent brute force resource discovery attacks. | |||
|
67 | - security(low): fixed self XSS inside autocomplete files view. | |||
|
68 | - security(low): fixed self Xss inside repo strip view. | |||
|
69 | - security(low): fixed self Xss inside the email add functionality. | |||
|
70 | - security(none): use new safe escaped user attributes across the application. | |||
|
71 | Will prevent all possible XSS attack vectors from user stored attributes. | |||
|
72 | This specially can come from external authentication systems which doesn't | |||
|
73 | validate the data. | |||
|
74 | ||||
|
75 | ||||
|
76 | Performance | |||
|
77 | ^^^^^^^^^^^ | |||
|
78 | ||||
|
79 | ||||
|
80 | ||||
|
81 | ||||
|
82 | Fixes | |||
|
83 | ^^^^^ | |||
|
84 | ||||
|
85 | - Pull requests: make sure we process comments in the order of IDS when | |||
|
86 | linking them. In some edge cases it could lead to comments not displaying | |||
|
87 | correctly. | |||
|
88 | - Emails: fixed newlines in email templates that can break email sending code. | |||
|
89 | - Markdown: fixed hr and strong tags styling. | |||
|
90 | - Notifications: fixed problem with 500 errors on non-numeric entries in url. | |||
|
91 | - API: use simple schema validator to be consistent how we validate between | |||
|
92 | API and web views for create user and create user_group calls. | |||
|
93 | - Users: fixed problem with personal repo group wasn't shown for disabled users. | |||
|
94 | - Oauth: improve Google extraction of first/last name from returned data. | |||
|
95 | ||||
|
96 | ||||
|
97 | Upgrade notes | |||
|
98 | ^^^^^^^^^^^^^ | |||
|
99 | ||||
|
100 | ||||
|
101 | - API: the `update_pull_request` method will no longer support a close action. | |||
|
102 | Users should use the existing `close_pull_request` method which allows | |||
|
103 | specifying a message and status while closing a pull request. No newline at end of file |
@@ -0,0 +1,187 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 os | |||
|
22 | import csv | |||
|
23 | import datetime | |||
|
24 | ||||
|
25 | import pytest | |||
|
26 | ||||
|
27 | from rhodecode.tests import * | |||
|
28 | from rhodecode.tests.fixture import FIXTURES | |||
|
29 | from rhodecode.model.db import UserLog | |||
|
30 | from rhodecode.model.meta import Session | |||
|
31 | from rhodecode.lib.utils2 import safe_unicode | |||
|
32 | ||||
|
33 | ||||
|
34 | def route_path(name, params=None, **kwargs): | |||
|
35 | import urllib | |||
|
36 | from rhodecode.apps._base import ADMIN_PREFIX | |||
|
37 | ||||
|
38 | base_url = { | |||
|
39 | 'admin_home': ADMIN_PREFIX, | |||
|
40 | 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs', | |||
|
41 | ||||
|
42 | }[name].format(**kwargs) | |||
|
43 | ||||
|
44 | if params: | |||
|
45 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
46 | return base_url | |||
|
47 | ||||
|
48 | ||||
|
49 | class TestAdminController(TestController): | |||
|
50 | ||||
|
51 | @pytest.fixture(scope='class', autouse=True) | |||
|
52 | def prepare(self, request, pylonsapp): | |||
|
53 | UserLog.query().delete() | |||
|
54 | Session().commit() | |||
|
55 | ||||
|
56 | def strptime(val): | |||
|
57 | fmt = '%Y-%m-%d %H:%M:%S' | |||
|
58 | if '.' not in val: | |||
|
59 | return datetime.datetime.strptime(val, fmt) | |||
|
60 | ||||
|
61 | nofrag, frag = val.split(".") | |||
|
62 | date = datetime.datetime.strptime(nofrag, fmt) | |||
|
63 | ||||
|
64 | frag = frag[:6] # truncate to microseconds | |||
|
65 | frag += (6 - len(frag)) * '0' # add 0s | |||
|
66 | return date.replace(microsecond=int(frag)) | |||
|
67 | ||||
|
68 | with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f: | |||
|
69 | for row in csv.DictReader(f): | |||
|
70 | ul = UserLog() | |||
|
71 | for k, v in row.iteritems(): | |||
|
72 | v = safe_unicode(v) | |||
|
73 | if k == 'action_date': | |||
|
74 | v = strptime(v) | |||
|
75 | if k in ['user_id', 'repository_id']: | |||
|
76 | # nullable due to FK problems | |||
|
77 | v = None | |||
|
78 | setattr(ul, k, v) | |||
|
79 | Session().add(ul) | |||
|
80 | Session().commit() | |||
|
81 | ||||
|
82 | @request.addfinalizer | |||
|
83 | def cleanup(): | |||
|
84 | UserLog.query().delete() | |||
|
85 | Session().commit() | |||
|
86 | ||||
|
87 | def test_index(self): | |||
|
88 | self.log_user() | |||
|
89 | response = self.app.get(route_path('admin_audit_logs')) | |||
|
90 | response.mustcontain('Admin audit logs') | |||
|
91 | ||||
|
92 | def test_filter_all_entries(self): | |||
|
93 | self.log_user() | |||
|
94 | response = self.app.get(route_path('admin_audit_logs')) | |||
|
95 | all_count = UserLog.query().count() | |||
|
96 | response.mustcontain('%s entries' % all_count) | |||
|
97 | ||||
|
98 | def test_filter_journal_filter_exact_match_on_repository(self): | |||
|
99 | self.log_user() | |||
|
100 | response = self.app.get(route_path('admin_audit_logs', | |||
|
101 | params=dict(filter='repository:rhodecode'))) | |||
|
102 | response.mustcontain('3 entries') | |||
|
103 | ||||
|
104 | def test_filter_journal_filter_exact_match_on_repository_CamelCase(self): | |||
|
105 | self.log_user() | |||
|
106 | response = self.app.get(route_path('admin_audit_logs', | |||
|
107 | params=dict(filter='repository:RhodeCode'))) | |||
|
108 | response.mustcontain('3 entries') | |||
|
109 | ||||
|
110 | def test_filter_journal_filter_wildcard_on_repository(self): | |||
|
111 | self.log_user() | |||
|
112 | response = self.app.get(route_path('admin_audit_logs', | |||
|
113 | params=dict(filter='repository:*test*'))) | |||
|
114 | response.mustcontain('862 entries') | |||
|
115 | ||||
|
116 | def test_filter_journal_filter_prefix_on_repository(self): | |||
|
117 | self.log_user() | |||
|
118 | response = self.app.get(route_path('admin_audit_logs', | |||
|
119 | params=dict(filter='repository:test*'))) | |||
|
120 | response.mustcontain('257 entries') | |||
|
121 | ||||
|
122 | def test_filter_journal_filter_prefix_on_repository_CamelCase(self): | |||
|
123 | self.log_user() | |||
|
124 | response = self.app.get(route_path('admin_audit_logs', | |||
|
125 | params=dict(filter='repository:Test*'))) | |||
|
126 | response.mustcontain('257 entries') | |||
|
127 | ||||
|
128 | def test_filter_journal_filter_prefix_on_repository_and_user(self): | |||
|
129 | self.log_user() | |||
|
130 | response = self.app.get(route_path('admin_audit_logs', | |||
|
131 | params=dict(filter='repository:test* AND username:demo'))) | |||
|
132 | response.mustcontain('130 entries') | |||
|
133 | ||||
|
134 | def test_filter_journal_filter_prefix_on_repository_or_target_repo(self): | |||
|
135 | self.log_user() | |||
|
136 | response = self.app.get(route_path('admin_audit_logs', | |||
|
137 | params=dict(filter='repository:test* OR repository:rhodecode'))) | |||
|
138 | response.mustcontain('260 entries') # 257 + 3 | |||
|
139 | ||||
|
140 | def test_filter_journal_filter_exact_match_on_username(self): | |||
|
141 | self.log_user() | |||
|
142 | response = self.app.get(route_path('admin_audit_logs', | |||
|
143 | params=dict(filter='username:demo'))) | |||
|
144 | response.mustcontain('1087 entries') | |||
|
145 | ||||
|
146 | def test_filter_journal_filter_exact_match_on_username_camelCase(self): | |||
|
147 | self.log_user() | |||
|
148 | response = self.app.get(route_path('admin_audit_logs', | |||
|
149 | params=dict(filter='username:DemO'))) | |||
|
150 | response.mustcontain('1087 entries') | |||
|
151 | ||||
|
152 | def test_filter_journal_filter_wildcard_on_username(self): | |||
|
153 | self.log_user() | |||
|
154 | response = self.app.get(route_path('admin_audit_logs', | |||
|
155 | params=dict(filter='username:*test*'))) | |||
|
156 | entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count() | |||
|
157 | response.mustcontain('{} entries'.format(entries_count)) | |||
|
158 | ||||
|
159 | def test_filter_journal_filter_prefix_on_username(self): | |||
|
160 | self.log_user() | |||
|
161 | response = self.app.get(route_path('admin_audit_logs', | |||
|
162 | params=dict(filter='username:demo*'))) | |||
|
163 | response.mustcontain('1101 entries') | |||
|
164 | ||||
|
165 | def test_filter_journal_filter_prefix_on_user_or_other_user(self): | |||
|
166 | self.log_user() | |||
|
167 | response = self.app.get(route_path('admin_audit_logs', | |||
|
168 | params=dict(filter='username:demo OR username:volcan'))) | |||
|
169 | response.mustcontain('1095 entries') # 1087 + 8 | |||
|
170 | ||||
|
171 | def test_filter_journal_filter_wildcard_on_action(self): | |||
|
172 | self.log_user() | |||
|
173 | response = self.app.get(route_path('admin_audit_logs', | |||
|
174 | params=dict(filter='action:*pull_request*'))) | |||
|
175 | response.mustcontain('187 entries') | |||
|
176 | ||||
|
177 | def test_filter_journal_filter_on_date(self): | |||
|
178 | self.log_user() | |||
|
179 | response = self.app.get(route_path('admin_audit_logs', | |||
|
180 | params=dict(filter='date:20121010'))) | |||
|
181 | response.mustcontain('47 entries') | |||
|
182 | ||||
|
183 | def test_filter_journal_filter_on_date_2(self): | |||
|
184 | self.log_user() | |||
|
185 | response = self.app.get(route_path('admin_audit_logs', | |||
|
186 | params=dict(filter='date:20121020'))) | |||
|
187 | response.mustcontain('17 entries') |
@@ -0,0 +1,82 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 pytest | |||
|
22 | ||||
|
23 | from rhodecode.tests import TestController | |||
|
24 | from rhodecode.tests.fixture import Fixture | |||
|
25 | ||||
|
26 | fixture = Fixture() | |||
|
27 | ||||
|
28 | ||||
|
29 | def route_path(name, params=None, **kwargs): | |||
|
30 | import urllib | |||
|
31 | from rhodecode.apps._base import ADMIN_PREFIX | |||
|
32 | ||||
|
33 | base_url = { | |||
|
34 | 'admin_home': ADMIN_PREFIX, | |||
|
35 | 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}', | |||
|
36 | 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}', | |||
|
37 | 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}', | |||
|
38 | 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}', | |||
|
39 | ||||
|
40 | }[name].format(**kwargs) | |||
|
41 | ||||
|
42 | if params: | |||
|
43 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
44 | return base_url | |||
|
45 | ||||
|
46 | ||||
|
47 | class TestAdminMainView(TestController): | |||
|
48 | ||||
|
49 | def test_redirect_admin_home(self): | |||
|
50 | self.log_user() | |||
|
51 | response = self.app.get(route_path('admin_home'), status=302) | |||
|
52 | assert response.location.endswith('/audit_logs') | |||
|
53 | ||||
|
54 | def test_redirect_pull_request_view(self, view): | |||
|
55 | self.log_user() | |||
|
56 | self.app.get( | |||
|
57 | route_path(view, pull_request_id='xxxx'), | |||
|
58 | status=404) | |||
|
59 | ||||
|
60 | @pytest.mark.backends("git", "hg") | |||
|
61 | @pytest.mark.parametrize('view', [ | |||
|
62 | 'pull_requests_global', | |||
|
63 | 'pull_requests_global_0', | |||
|
64 | 'pull_requests_global_1', | |||
|
65 | ]) | |||
|
66 | def test_redirect_pull_request_view(self, view, pr_util): | |||
|
67 | self.log_user() | |||
|
68 | pull_request = pr_util.create_pull_request() | |||
|
69 | pull_request_id = pull_request.pull_request_id | |||
|
70 | ||||
|
71 | response = self.app.get( | |||
|
72 | route_path(view, pull_request_id=pull_request_id), | |||
|
73 | status=302) | |||
|
74 | assert response.location.endswith( | |||
|
75 | 'pull-request/{}'.format(pull_request_id)) | |||
|
76 | ||||
|
77 | repo_name = pull_request.target_repo.repo_name | |||
|
78 | redirect_url = route_path( | |||
|
79 | 'pullrequest_show', repo_name=repo_name, | |||
|
80 | pull_request_id=pull_request.pull_request_id) | |||
|
81 | ||||
|
82 | assert redirect_url in response.location |
@@ -0,0 +1,73 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 logging | |||
|
22 | ||||
|
23 | from pyramid.view import view_config | |||
|
24 | from sqlalchemy.orm import joinedload | |||
|
25 | ||||
|
26 | from rhodecode.apps._base import BaseAppView | |||
|
27 | from rhodecode.model.db import UserLog | |||
|
28 | from rhodecode.lib.user_log_filter import user_log_filter | |||
|
29 | from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator | |||
|
30 | from rhodecode.lib.utils2 import safe_int | |||
|
31 | from rhodecode.lib.helpers import Page | |||
|
32 | ||||
|
33 | log = logging.getLogger(__name__) | |||
|
34 | ||||
|
35 | ||||
|
36 | class AdminAuditLogsView(BaseAppView): | |||
|
37 | def load_default_context(self): | |||
|
38 | c = self._get_local_tmpl_context() | |||
|
39 | self._register_global_c(c) | |||
|
40 | return c | |||
|
41 | ||||
|
42 | @LoginRequired() | |||
|
43 | @HasPermissionAllDecorator('hg.admin') | |||
|
44 | @view_config( | |||
|
45 | route_name='admin_audit_logs', request_method='GET', | |||
|
46 | renderer='rhodecode:templates/admin/admin_audit_logs.mako') | |||
|
47 | def admin_audit_logs(self): | |||
|
48 | c = self.load_default_context() | |||
|
49 | ||||
|
50 | users_log = UserLog.query()\ | |||
|
51 | .options(joinedload(UserLog.user))\ | |||
|
52 | .options(joinedload(UserLog.repository)) | |||
|
53 | ||||
|
54 | # FILTERING | |||
|
55 | c.search_term = self.request.GET.get('filter') | |||
|
56 | try: | |||
|
57 | users_log = user_log_filter(users_log, c.search_term) | |||
|
58 | except Exception: | |||
|
59 | # we want this to crash for now | |||
|
60 | raise | |||
|
61 | ||||
|
62 | users_log = users_log.order_by(UserLog.action_date.desc()) | |||
|
63 | ||||
|
64 | p = safe_int(self.request.GET.get('page', 1), 1) | |||
|
65 | ||||
|
66 | def url_generator(**kw): | |||
|
67 | if c.search_term: | |||
|
68 | kw['filter'] = c.search_term | |||
|
69 | return self.request.current_route_path(_query=kw) | |||
|
70 | ||||
|
71 | c.audit_logs = Page(users_log, page=p, items_per_page=10, | |||
|
72 | url=url_generator) | |||
|
73 | return self._get_template_context(c) |
@@ -0,0 +1,63 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 logging | |||
|
22 | ||||
|
23 | ||||
|
24 | from pyramid.httpexceptions import HTTPFound | |||
|
25 | from pyramid.view import view_config | |||
|
26 | ||||
|
27 | from rhodecode.apps._base import BaseAppView | |||
|
28 | from rhodecode.lib import helpers as h | |||
|
29 | from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator) | |||
|
30 | from rhodecode.model.db import PullRequest | |||
|
31 | ||||
|
32 | ||||
|
33 | log = logging.getLogger(__name__) | |||
|
34 | ||||
|
35 | ||||
|
36 | class AdminMainView(BaseAppView): | |||
|
37 | ||||
|
38 | @LoginRequired() | |||
|
39 | @HasPermissionAllDecorator('hg.admin') | |||
|
40 | @view_config( | |||
|
41 | route_name='admin_home', request_method='GET') | |||
|
42 | def admin_main(self): | |||
|
43 | # redirect _admin to audit logs... | |||
|
44 | raise HTTPFound(h.route_path('admin_audit_logs')) | |||
|
45 | ||||
|
46 | @LoginRequired() | |||
|
47 | @view_config(route_name='pull_requests_global_0', request_method='GET') | |||
|
48 | @view_config(route_name='pull_requests_global_1', request_method='GET') | |||
|
49 | @view_config(route_name='pull_requests_global', request_method='GET') | |||
|
50 | def pull_requests(self): | |||
|
51 | """ | |||
|
52 | Global redirect for Pull Requests | |||
|
53 | ||||
|
54 | :param pull_request_id: id of pull requests in the system | |||
|
55 | """ | |||
|
56 | ||||
|
57 | pull_request_id = self.request.matchdict.get('pull_request_id') | |||
|
58 | pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True) | |||
|
59 | repo_name = pull_request.target_repo.repo_name | |||
|
60 | ||||
|
61 | raise HTTPFound( | |||
|
62 | h.route_path('pullrequest_show', repo_name=repo_name, | |||
|
63 | pull_request_id=pull_request_id)) |
@@ -0,0 +1,49 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | from rhodecode.config import routing_links | |||
|
21 | ||||
|
22 | ||||
|
23 | def includeme(config): | |||
|
24 | ||||
|
25 | config.add_route( | |||
|
26 | name='home', | |||
|
27 | pattern='/') | |||
|
28 | ||||
|
29 | config.add_route( | |||
|
30 | name='user_autocomplete_data', | |||
|
31 | pattern='/_users') | |||
|
32 | ||||
|
33 | config.add_route( | |||
|
34 | name='user_group_autocomplete_data', | |||
|
35 | pattern='/_user_groups') | |||
|
36 | ||||
|
37 | config.add_route( | |||
|
38 | name='repo_list_data', | |||
|
39 | pattern='/_repos') | |||
|
40 | ||||
|
41 | config.add_route( | |||
|
42 | name='goto_switcher_data', | |||
|
43 | pattern='/_goto_data') | |||
|
44 | ||||
|
45 | # register our static links via redirection mechanism | |||
|
46 | routing_links.connect_redirection_links(config) | |||
|
47 | ||||
|
48 | # Scan module for configuration decorators. | |||
|
49 | config.scan() |
@@ -0,0 +1,40 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | ||||
|
22 | def assert_and_get_content(result): | |||
|
23 | repos = [] | |||
|
24 | groups = [] | |||
|
25 | commits = [] | |||
|
26 | for data in result: | |||
|
27 | for data_item in data['children']: | |||
|
28 | assert data_item['id'] | |||
|
29 | assert data_item['text'] | |||
|
30 | assert data_item['url'] | |||
|
31 | if data_item['type'] == 'repo': | |||
|
32 | repos.append(data_item) | |||
|
33 | elif data_item['type'] == 'group': | |||
|
34 | groups.append(data_item) | |||
|
35 | elif data_item['type'] == 'commit': | |||
|
36 | commits.append(data_item) | |||
|
37 | else: | |||
|
38 | raise Exception('invalid type %s' % data_item['type']) | |||
|
39 | ||||
|
40 | return repos, groups, commits No newline at end of file |
@@ -0,0 +1,151 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 json | |||
|
22 | ||||
|
23 | import pytest | |||
|
24 | ||||
|
25 | from . import assert_and_get_content | |||
|
26 | from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN | |||
|
27 | from rhodecode.tests.fixture import Fixture | |||
|
28 | ||||
|
29 | from rhodecode.lib.utils import map_groups | |||
|
30 | from rhodecode.model.repo import RepoModel | |||
|
31 | from rhodecode.model.repo_group import RepoGroupModel | |||
|
32 | from rhodecode.model.db import Session, Repository, RepoGroup | |||
|
33 | ||||
|
34 | fixture = Fixture() | |||
|
35 | ||||
|
36 | ||||
|
37 | def route_path(name, params=None, **kwargs): | |||
|
38 | import urllib | |||
|
39 | ||||
|
40 | base_url = { | |||
|
41 | 'goto_switcher_data': '/_goto_data', | |||
|
42 | }[name].format(**kwargs) | |||
|
43 | ||||
|
44 | if params: | |||
|
45 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
46 | return base_url | |||
|
47 | ||||
|
48 | ||||
|
49 | class TestGotoSwitcherData(TestController): | |||
|
50 | ||||
|
51 | required_repos_with_groups = [ | |||
|
52 | 'abc', | |||
|
53 | 'abc-fork', | |||
|
54 | 'forks/abcd', | |||
|
55 | 'abcd', | |||
|
56 | 'abcde', | |||
|
57 | 'a/abc', | |||
|
58 | 'aa/abc', | |||
|
59 | 'aaa/abc', | |||
|
60 | 'aaaa/abc', | |||
|
61 | 'repos_abc/aaa/abc', | |||
|
62 | 'abc_repos/abc', | |||
|
63 | 'abc_repos/abcd', | |||
|
64 | 'xxx/xyz', | |||
|
65 | 'forked-abc/a/abc' | |||
|
66 | ] | |||
|
67 | ||||
|
68 | @pytest.fixture(autouse=True, scope='class') | |||
|
69 | def prepare(self, request, pylonsapp): | |||
|
70 | for repo_and_group in self.required_repos_with_groups: | |||
|
71 | # create structure of groups and return the last group | |||
|
72 | ||||
|
73 | repo_group = map_groups(repo_and_group) | |||
|
74 | ||||
|
75 | RepoModel()._create_repo( | |||
|
76 | repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN, | |||
|
77 | repo_group=getattr(repo_group, 'group_id', None)) | |||
|
78 | ||||
|
79 | Session().commit() | |||
|
80 | ||||
|
81 | request.addfinalizer(self.cleanup) | |||
|
82 | ||||
|
83 | def cleanup(self): | |||
|
84 | # first delete all repos | |||
|
85 | for repo_and_groups in self.required_repos_with_groups: | |||
|
86 | repo = Repository.get_by_repo_name(repo_and_groups) | |||
|
87 | if repo: | |||
|
88 | RepoModel().delete(repo) | |||
|
89 | Session().commit() | |||
|
90 | ||||
|
91 | # then delete all empty groups | |||
|
92 | for repo_and_groups in self.required_repos_with_groups: | |||
|
93 | if '/' in repo_and_groups: | |||
|
94 | r_group = repo_and_groups.rsplit('/', 1)[0] | |||
|
95 | repo_group = RepoGroup.get_by_group_name(r_group) | |||
|
96 | if not repo_group: | |||
|
97 | continue | |||
|
98 | parents = repo_group.parents | |||
|
99 | RepoGroupModel().delete(repo_group, force_delete=True) | |||
|
100 | Session().commit() | |||
|
101 | ||||
|
102 | for el in reversed(parents): | |||
|
103 | RepoGroupModel().delete(el, force_delete=True) | |||
|
104 | Session().commit() | |||
|
105 | ||||
|
106 | def test_returns_list_of_repos_and_groups(self, xhr_header): | |||
|
107 | self.log_user() | |||
|
108 | ||||
|
109 | response = self.app.get( | |||
|
110 | route_path('goto_switcher_data'), | |||
|
111 | extra_environ=xhr_header, status=200) | |||
|
112 | result = json.loads(response.body)['results'] | |||
|
113 | ||||
|
114 | repos, groups, commits = assert_and_get_content(result) | |||
|
115 | ||||
|
116 | assert len(repos) == len(Repository.get_all()) | |||
|
117 | assert len(groups) == len(RepoGroup.get_all()) | |||
|
118 | assert len(commits) == 0 | |||
|
119 | ||||
|
120 | def test_returns_list_of_repos_and_groups_filtered(self, xhr_header): | |||
|
121 | self.log_user() | |||
|
122 | ||||
|
123 | response = self.app.get( | |||
|
124 | route_path('goto_switcher_data'), | |||
|
125 | params={'query': 'abc'}, | |||
|
126 | extra_environ=xhr_header, status=200) | |||
|
127 | result = json.loads(response.body)['results'] | |||
|
128 | ||||
|
129 | repos, groups, commits = assert_and_get_content(result) | |||
|
130 | ||||
|
131 | assert len(repos) == 13 | |||
|
132 | assert len(groups) == 5 | |||
|
133 | assert len(commits) == 0 | |||
|
134 | ||||
|
135 | def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header): | |||
|
136 | self.log_user() | |||
|
137 | ||||
|
138 | response = self.app.get( | |||
|
139 | route_path('goto_switcher_data'), | |||
|
140 | params={'query': 'abc'}, | |||
|
141 | extra_environ=xhr_header, status=200) | |||
|
142 | result = json.loads(response.body)['results'] | |||
|
143 | ||||
|
144 | repos, groups, commits = assert_and_get_content(result) | |||
|
145 | ||||
|
146 | test_repos = [x['text'] for x in repos[:4]] | |||
|
147 | assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos | |||
|
148 | ||||
|
149 | test_groups = [x['text'] for x in groups[:4]] | |||
|
150 | assert ['abc_repos', 'repos_abc', | |||
|
151 | 'forked-abc', 'forked-abc/a'] == test_groups |
@@ -0,0 +1,103 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 json | |||
|
22 | ||||
|
23 | from . import assert_and_get_content | |||
|
24 | from rhodecode.tests import TestController | |||
|
25 | from rhodecode.tests.fixture import Fixture | |||
|
26 | from rhodecode.model.db import Repository | |||
|
27 | ||||
|
28 | fixture = Fixture() | |||
|
29 | ||||
|
30 | ||||
|
31 | def route_path(name, params=None, **kwargs): | |||
|
32 | import urllib | |||
|
33 | ||||
|
34 | base_url = { | |||
|
35 | 'repo_list_data': '/_repos', | |||
|
36 | }[name].format(**kwargs) | |||
|
37 | ||||
|
38 | if params: | |||
|
39 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
40 | return base_url | |||
|
41 | ||||
|
42 | ||||
|
43 | class TestRepoListData(TestController): | |||
|
44 | ||||
|
45 | def test_returns_list_of_repos_and_groups(self, xhr_header): | |||
|
46 | self.log_user() | |||
|
47 | ||||
|
48 | response = self.app.get( | |||
|
49 | route_path('repo_list_data'), | |||
|
50 | extra_environ=xhr_header, status=200) | |||
|
51 | result = json.loads(response.body)['results'] | |||
|
52 | ||||
|
53 | repos, groups, commits = assert_and_get_content(result) | |||
|
54 | ||||
|
55 | assert len(repos) == len(Repository.get_all()) | |||
|
56 | assert len(groups) == 0 | |||
|
57 | assert len(commits) == 0 | |||
|
58 | ||||
|
59 | def test_returns_list_of_repos_and_groups_filtered(self, xhr_header): | |||
|
60 | self.log_user() | |||
|
61 | ||||
|
62 | response = self.app.get( | |||
|
63 | route_path('repo_list_data'), | |||
|
64 | params={'query': 'vcs_test_git'}, | |||
|
65 | extra_environ=xhr_header, status=200) | |||
|
66 | result = json.loads(response.body)['results'] | |||
|
67 | ||||
|
68 | repos, groups, commits = assert_and_get_content(result) | |||
|
69 | ||||
|
70 | assert len(repos) == len(Repository.query().filter( | |||
|
71 | Repository.repo_name.ilike('%vcs_test_git%')).all()) | |||
|
72 | assert len(groups) == 0 | |||
|
73 | assert len(commits) == 0 | |||
|
74 | ||||
|
75 | def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header): | |||
|
76 | self.log_user() | |||
|
77 | ||||
|
78 | response = self.app.get( | |||
|
79 | route_path('repo_list_data'), | |||
|
80 | params={'query': 'vcs_test_git', 'repo_type': 'git'}, | |||
|
81 | extra_environ=xhr_header, status=200) | |||
|
82 | result = json.loads(response.body)['results'] | |||
|
83 | ||||
|
84 | repos, groups, commits = assert_and_get_content(result) | |||
|
85 | ||||
|
86 | assert len(repos) == len(Repository.query().filter( | |||
|
87 | Repository.repo_name.ilike('%vcs_test_git%')).all()) | |||
|
88 | assert len(groups) == 0 | |||
|
89 | assert len(commits) == 0 | |||
|
90 | ||||
|
91 | def test_returns_list_of_repos_non_ascii_query(self, xhr_header): | |||
|
92 | self.log_user() | |||
|
93 | response = self.app.get( | |||
|
94 | route_path('repo_list_data'), | |||
|
95 | params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, | |||
|
96 | extra_environ=xhr_header, status=200) | |||
|
97 | result = json.loads(response.body)['results'] | |||
|
98 | ||||
|
99 | repos, groups, commits = assert_and_get_content(result) | |||
|
100 | ||||
|
101 | assert len(repos) == 0 | |||
|
102 | assert len(groups) == 0 | |||
|
103 | assert len(commits) == 0 |
@@ -0,0 +1,112 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 json | |||
|
22 | import pytest | |||
|
23 | ||||
|
24 | from rhodecode.tests import TestController | |||
|
25 | from rhodecode.tests.fixture import Fixture | |||
|
26 | ||||
|
27 | ||||
|
28 | fixture = Fixture() | |||
|
29 | ||||
|
30 | ||||
|
31 | def route_path(name, params=None, **kwargs): | |||
|
32 | import urllib | |||
|
33 | ||||
|
34 | base_url = { | |||
|
35 | 'user_autocomplete_data': '/_users', | |||
|
36 | 'user_group_autocomplete_data': '/_user_groups' | |||
|
37 | }[name].format(**kwargs) | |||
|
38 | ||||
|
39 | if params: | |||
|
40 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
41 | return base_url | |||
|
42 | ||||
|
43 | ||||
|
44 | class TestUserAutocompleteData(TestController): | |||
|
45 | ||||
|
46 | def test_returns_list_of_users(self, user_util, xhr_header): | |||
|
47 | self.log_user() | |||
|
48 | user = user_util.create_user(active=True) | |||
|
49 | user_name = user.username | |||
|
50 | response = self.app.get( | |||
|
51 | route_path('user_autocomplete_data'), | |||
|
52 | extra_environ=xhr_header, status=200) | |||
|
53 | result = json.loads(response.body) | |||
|
54 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
55 | assert user_name in values | |||
|
56 | ||||
|
57 | def test_returns_inactive_users_when_active_flag_sent( | |||
|
58 | self, user_util, xhr_header): | |||
|
59 | self.log_user() | |||
|
60 | user = user_util.create_user(active=False) | |||
|
61 | user_name = user.username | |||
|
62 | ||||
|
63 | response = self.app.get( | |||
|
64 | route_path('user_autocomplete_data', | |||
|
65 | params=dict(user_groups='true', active='0')), | |||
|
66 | extra_environ=xhr_header, status=200) | |||
|
67 | result = json.loads(response.body) | |||
|
68 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
69 | assert user_name in values | |||
|
70 | ||||
|
71 | response = self.app.get( | |||
|
72 | route_path('user_autocomplete_data', | |||
|
73 | params=dict(user_groups='true', active='1')), | |||
|
74 | extra_environ=xhr_header, status=200) | |||
|
75 | result = json.loads(response.body) | |||
|
76 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
77 | assert user_name not in values | |||
|
78 | ||||
|
79 | def test_returns_groups_when_user_groups_flag_sent( | |||
|
80 | self, user_util, xhr_header): | |||
|
81 | self.log_user() | |||
|
82 | group = user_util.create_user_group(user_groups_active=True) | |||
|
83 | group_name = group.users_group_name | |||
|
84 | response = self.app.get( | |||
|
85 | route_path('user_autocomplete_data', | |||
|
86 | params=dict(user_groups='true')), | |||
|
87 | extra_environ=xhr_header, status=200) | |||
|
88 | result = json.loads(response.body) | |||
|
89 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
90 | assert group_name in values | |||
|
91 | ||||
|
92 | @pytest.mark.parametrize('query, count', [ | |||
|
93 | ('hello1', 0), | |||
|
94 | ('dev', 2), | |||
|
95 | ]) | |||
|
96 | def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, | |||
|
97 | query, count): | |||
|
98 | self.log_user() | |||
|
99 | ||||
|
100 | user_util._test_name = 'dev-test' | |||
|
101 | user_util.create_user() | |||
|
102 | ||||
|
103 | user_util._test_name = 'dev-group-test' | |||
|
104 | user_util.create_user_group() | |||
|
105 | ||||
|
106 | response = self.app.get( | |||
|
107 | route_path('user_autocomplete_data', | |||
|
108 | params=dict(user_groups='true', query=query)), | |||
|
109 | extra_environ=xhr_header, status=200) | |||
|
110 | ||||
|
111 | result = json.loads(response.body) | |||
|
112 | assert len(result['suggestions']) == count |
@@ -0,0 +1,117 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | # -*- coding: utf-8 -*- | |||
|
21 | ||||
|
22 | # Copyright (C) 2016-2017 RhodeCode GmbH | |||
|
23 | # | |||
|
24 | # This program is free software: you can redistribute it and/or modify | |||
|
25 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
26 | # (only), as published by the Free Software Foundation. | |||
|
27 | # | |||
|
28 | # This program is distributed in the hope that it will be useful, | |||
|
29 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
30 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
31 | # GNU General Public License for more details. | |||
|
32 | # | |||
|
33 | # You should have received a copy of the GNU Affero General Public License | |||
|
34 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
35 | # | |||
|
36 | # This program is dual-licensed. If you wish to learn more about the | |||
|
37 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
38 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
39 | ||||
|
40 | import json | |||
|
41 | ||||
|
42 | import pytest | |||
|
43 | ||||
|
44 | from rhodecode.tests import TestController | |||
|
45 | from rhodecode.tests.fixture import Fixture | |||
|
46 | ||||
|
47 | ||||
|
48 | fixture = Fixture() | |||
|
49 | ||||
|
50 | ||||
|
51 | def route_path(name, params=None, **kwargs): | |||
|
52 | import urllib | |||
|
53 | ||||
|
54 | base_url = { | |||
|
55 | 'user_autocomplete_data': '/_users', | |||
|
56 | 'user_group_autocomplete_data': '/_user_groups' | |||
|
57 | }[name].format(**kwargs) | |||
|
58 | ||||
|
59 | if params: | |||
|
60 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
61 | return base_url | |||
|
62 | ||||
|
63 | ||||
|
64 | class TestUserGroupAutocompleteData(TestController): | |||
|
65 | ||||
|
66 | def test_returns_list_of_user_groups(self, user_util, xhr_header): | |||
|
67 | self.log_user() | |||
|
68 | user_group = user_util.create_user_group(active=True) | |||
|
69 | user_group_name = user_group.users_group_name | |||
|
70 | response = self.app.get( | |||
|
71 | route_path('user_group_autocomplete_data'), | |||
|
72 | extra_environ=xhr_header, status=200) | |||
|
73 | result = json.loads(response.body) | |||
|
74 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
75 | assert user_group_name in values | |||
|
76 | ||||
|
77 | def test_returns_inactive_user_groups_when_active_flag_sent( | |||
|
78 | self, user_util, xhr_header): | |||
|
79 | self.log_user() | |||
|
80 | user_group = user_util.create_user_group(active=False) | |||
|
81 | user_group_name = user_group.users_group_name | |||
|
82 | ||||
|
83 | response = self.app.get( | |||
|
84 | route_path('user_group_autocomplete_data', | |||
|
85 | params=dict(active='0')), | |||
|
86 | extra_environ=xhr_header, status=200) | |||
|
87 | result = json.loads(response.body) | |||
|
88 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
89 | assert user_group_name in values | |||
|
90 | ||||
|
91 | response = self.app.get( | |||
|
92 | route_path('user_group_autocomplete_data', | |||
|
93 | params=dict(active='1')), | |||
|
94 | extra_environ=xhr_header, status=200) | |||
|
95 | result = json.loads(response.body) | |||
|
96 | values = [suggestion['value'] for suggestion in result['suggestions']] | |||
|
97 | assert user_group_name not in values | |||
|
98 | ||||
|
99 | @pytest.mark.parametrize('query, count', [ | |||
|
100 | ('hello1', 0), | |||
|
101 | ('dev', 1), | |||
|
102 | ]) | |||
|
103 | def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count): | |||
|
104 | self.log_user() | |||
|
105 | ||||
|
106 | user_util._test_name = 'dev-test' | |||
|
107 | user_util.create_user_group() | |||
|
108 | ||||
|
109 | response = self.app.get( | |||
|
110 | route_path('user_group_autocomplete_data', | |||
|
111 | params=dict(user_groups='true', | |||
|
112 | query=query)), | |||
|
113 | extra_environ=xhr_header, status=200) | |||
|
114 | ||||
|
115 | result = json.loads(response.body) | |||
|
116 | ||||
|
117 | assert len(result['suggestions']) == count |
@@ -0,0 +1,304 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 re | |||
|
22 | import logging | |||
|
23 | ||||
|
24 | from pyramid.view import view_config | |||
|
25 | ||||
|
26 | from rhodecode.apps._base import BaseAppView | |||
|
27 | from rhodecode.lib import helpers as h | |||
|
28 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, \ | |||
|
29 | HasRepoGroupPermissionAnyDecorator | |||
|
30 | from rhodecode.lib.index import searcher_from_config | |||
|
31 | from rhodecode.lib.utils2 import safe_unicode, str2bool | |||
|
32 | from rhodecode.lib.ext_json import json | |||
|
33 | from rhodecode.model.db import func, Repository, RepoGroup | |||
|
34 | from rhodecode.model.repo import RepoModel | |||
|
35 | from rhodecode.model.repo_group import RepoGroupModel | |||
|
36 | from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList | |||
|
37 | from rhodecode.model.user import UserModel | |||
|
38 | from rhodecode.model.user_group import UserGroupModel | |||
|
39 | ||||
|
40 | log = logging.getLogger(__name__) | |||
|
41 | ||||
|
42 | ||||
|
43 | class HomeView(BaseAppView): | |||
|
44 | ||||
|
45 | def load_default_context(self): | |||
|
46 | c = self._get_local_tmpl_context() | |||
|
47 | c.user = c.auth_user.get_instance() | |||
|
48 | self._register_global_c(c) | |||
|
49 | return c | |||
|
50 | ||||
|
51 | @LoginRequired() | |||
|
52 | @view_config( | |||
|
53 | route_name='user_autocomplete_data', request_method='GET', | |||
|
54 | renderer='json_ext', xhr=True) | |||
|
55 | def user_autocomplete_data(self): | |||
|
56 | query = self.request.GET.get('query') | |||
|
57 | active = str2bool(self.request.GET.get('active') or True) | |||
|
58 | include_groups = str2bool(self.request.GET.get('user_groups')) | |||
|
59 | expand_groups = str2bool(self.request.GET.get('user_groups_expand')) | |||
|
60 | skip_default_user = str2bool(self.request.GET.get('skip_default_user')) | |||
|
61 | ||||
|
62 | log.debug('generating user list, query:%s, active:%s, with_groups:%s', | |||
|
63 | query, active, include_groups) | |||
|
64 | ||||
|
65 | _users = UserModel().get_users( | |||
|
66 | name_contains=query, only_active=active) | |||
|
67 | ||||
|
68 | def maybe_skip_default_user(usr): | |||
|
69 | if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER: | |||
|
70 | return False | |||
|
71 | return True | |||
|
72 | _users = filter(maybe_skip_default_user, _users) | |||
|
73 | ||||
|
74 | if include_groups: | |||
|
75 | # extend with user groups | |||
|
76 | _user_groups = UserGroupModel().get_user_groups( | |||
|
77 | name_contains=query, only_active=active, | |||
|
78 | expand_groups=expand_groups) | |||
|
79 | _users = _users + _user_groups | |||
|
80 | ||||
|
81 | return {'suggestions': _users} | |||
|
82 | ||||
|
83 | @LoginRequired() | |||
|
84 | @NotAnonymous() | |||
|
85 | @view_config( | |||
|
86 | route_name='user_group_autocomplete_data', request_method='GET', | |||
|
87 | renderer='json_ext', xhr=True) | |||
|
88 | def user_group_autocomplete_data(self): | |||
|
89 | query = self.request.GET.get('query') | |||
|
90 | active = str2bool(self.request.GET.get('active') or True) | |||
|
91 | expand_groups = str2bool(self.request.GET.get('user_groups_expand')) | |||
|
92 | ||||
|
93 | log.debug('generating user group list, query:%s, active:%s', | |||
|
94 | query, active) | |||
|
95 | ||||
|
96 | _user_groups = UserGroupModel().get_user_groups( | |||
|
97 | name_contains=query, only_active=active, | |||
|
98 | expand_groups=expand_groups) | |||
|
99 | _user_groups = _user_groups | |||
|
100 | ||||
|
101 | return {'suggestions': _user_groups} | |||
|
102 | ||||
|
103 | def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): | |||
|
104 | query = Repository.query()\ | |||
|
105 | .order_by(func.length(Repository.repo_name))\ | |||
|
106 | .order_by(Repository.repo_name) | |||
|
107 | ||||
|
108 | if repo_type: | |||
|
109 | query = query.filter(Repository.repo_type == repo_type) | |||
|
110 | ||||
|
111 | if name_contains: | |||
|
112 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) | |||
|
113 | query = query.filter( | |||
|
114 | Repository.repo_name.ilike(ilike_expression)) | |||
|
115 | query = query.limit(limit) | |||
|
116 | ||||
|
117 | all_repos = query.all() | |||
|
118 | # permission checks are inside this function | |||
|
119 | repo_iter = ScmModel().get_repos(all_repos) | |||
|
120 | return [ | |||
|
121 | { | |||
|
122 | 'id': obj['name'], | |||
|
123 | 'text': obj['name'], | |||
|
124 | 'type': 'repo', | |||
|
125 | 'obj': obj['dbrepo'], | |||
|
126 | 'url': h.route_path('repo_summary', repo_name=obj['name']) | |||
|
127 | } | |||
|
128 | for obj in repo_iter] | |||
|
129 | ||||
|
130 | def _get_repo_group_list(self, name_contains=None, limit=20): | |||
|
131 | query = RepoGroup.query()\ | |||
|
132 | .order_by(func.length(RepoGroup.group_name))\ | |||
|
133 | .order_by(RepoGroup.group_name) | |||
|
134 | ||||
|
135 | if name_contains: | |||
|
136 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) | |||
|
137 | query = query.filter( | |||
|
138 | RepoGroup.group_name.ilike(ilike_expression)) | |||
|
139 | query = query.limit(limit) | |||
|
140 | ||||
|
141 | all_groups = query.all() | |||
|
142 | repo_groups_iter = ScmModel().get_repo_groups(all_groups) | |||
|
143 | return [ | |||
|
144 | { | |||
|
145 | 'id': obj.group_name, | |||
|
146 | 'text': obj.group_name, | |||
|
147 | 'type': 'group', | |||
|
148 | 'obj': {}, | |||
|
149 | 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name) | |||
|
150 | } | |||
|
151 | for obj in repo_groups_iter] | |||
|
152 | ||||
|
153 | def _get_hash_commit_list(self, auth_user, hash_starts_with=None): | |||
|
154 | if not hash_starts_with or len(hash_starts_with) < 3: | |||
|
155 | return [] | |||
|
156 | ||||
|
157 | commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) | |||
|
158 | ||||
|
159 | if len(commit_hashes) != 1: | |||
|
160 | return [] | |||
|
161 | ||||
|
162 | commit_hash_prefix = commit_hashes[0] | |||
|
163 | ||||
|
164 | searcher = searcher_from_config(self.request.registry.settings) | |||
|
165 | result = searcher.search( | |||
|
166 | 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user, | |||
|
167 | raise_on_exc=False) | |||
|
168 | ||||
|
169 | return [ | |||
|
170 | { | |||
|
171 | 'id': entry['commit_id'], | |||
|
172 | 'text': entry['commit_id'], | |||
|
173 | 'type': 'commit', | |||
|
174 | 'obj': {'repo': entry['repository']}, | |||
|
175 | 'url': h.url('changeset_home', | |||
|
176 | repo_name=entry['repository'], | |||
|
177 | revision=entry['commit_id']) | |||
|
178 | } | |||
|
179 | for entry in result['results']] | |||
|
180 | ||||
|
181 | @LoginRequired() | |||
|
182 | @view_config( | |||
|
183 | route_name='repo_list_data', request_method='GET', | |||
|
184 | renderer='json_ext', xhr=True) | |||
|
185 | def repo_list_data(self): | |||
|
186 | _ = self.request.translate | |||
|
187 | ||||
|
188 | query = self.request.GET.get('query') | |||
|
189 | repo_type = self.request.GET.get('repo_type') | |||
|
190 | log.debug('generating repo list, query:%s, repo_type:%s', | |||
|
191 | query, repo_type) | |||
|
192 | ||||
|
193 | res = [] | |||
|
194 | repos = self._get_repo_list(query, repo_type=repo_type) | |||
|
195 | if repos: | |||
|
196 | res.append({ | |||
|
197 | 'text': _('Repositories'), | |||
|
198 | 'children': repos | |||
|
199 | }) | |||
|
200 | ||||
|
201 | data = { | |||
|
202 | 'more': False, | |||
|
203 | 'results': res | |||
|
204 | } | |||
|
205 | return data | |||
|
206 | ||||
|
207 | @LoginRequired() | |||
|
208 | @view_config( | |||
|
209 | route_name='goto_switcher_data', request_method='GET', | |||
|
210 | renderer='json_ext', xhr=True) | |||
|
211 | def goto_switcher_data(self): | |||
|
212 | c = self.load_default_context() | |||
|
213 | ||||
|
214 | _ = self.request.translate | |||
|
215 | ||||
|
216 | query = self.request.GET.get('query') | |||
|
217 | log.debug('generating goto switcher list, query %s', query) | |||
|
218 | ||||
|
219 | res = [] | |||
|
220 | repo_groups = self._get_repo_group_list(query) | |||
|
221 | if repo_groups: | |||
|
222 | res.append({ | |||
|
223 | 'text': _('Groups'), | |||
|
224 | 'children': repo_groups | |||
|
225 | }) | |||
|
226 | ||||
|
227 | repos = self._get_repo_list(query) | |||
|
228 | if repos: | |||
|
229 | res.append({ | |||
|
230 | 'text': _('Repositories'), | |||
|
231 | 'children': repos | |||
|
232 | }) | |||
|
233 | ||||
|
234 | commits = self._get_hash_commit_list(c.auth_user, query) | |||
|
235 | if commits: | |||
|
236 | unique_repos = {} | |||
|
237 | for commit in commits: | |||
|
238 | unique_repos.setdefault(commit['obj']['repo'], [] | |||
|
239 | ).append(commit) | |||
|
240 | ||||
|
241 | for repo in unique_repos: | |||
|
242 | res.append({ | |||
|
243 | 'text': _('Commits in %(repo)s') % {'repo': repo}, | |||
|
244 | 'children': unique_repos[repo] | |||
|
245 | }) | |||
|
246 | ||||
|
247 | data = { | |||
|
248 | 'more': False, | |||
|
249 | 'results': res | |||
|
250 | } | |||
|
251 | return data | |||
|
252 | ||||
|
253 | def _get_groups_and_repos(self, repo_group_id=None): | |||
|
254 | # repo groups groups | |||
|
255 | repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) | |||
|
256 | _perms = ['group.read', 'group.write', 'group.admin'] | |||
|
257 | repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) | |||
|
258 | repo_group_data = RepoGroupModel().get_repo_groups_as_dict( | |||
|
259 | repo_group_list=repo_group_list_acl, admin=False) | |||
|
260 | ||||
|
261 | # repositories | |||
|
262 | repo_list = Repository.get_all_repos(group_id=repo_group_id) | |||
|
263 | _perms = ['repository.read', 'repository.write', 'repository.admin'] | |||
|
264 | repo_list_acl = RepoList(repo_list, perm_set=_perms) | |||
|
265 | repo_data = RepoModel().get_repos_as_dict( | |||
|
266 | repo_list=repo_list_acl, admin=False) | |||
|
267 | ||||
|
268 | return repo_data, repo_group_data | |||
|
269 | ||||
|
270 | @LoginRequired() | |||
|
271 | @view_config( | |||
|
272 | route_name='home', request_method='GET', | |||
|
273 | renderer='rhodecode:templates/index.mako') | |||
|
274 | def main_page(self): | |||
|
275 | c = self.load_default_context() | |||
|
276 | c.repo_group = None | |||
|
277 | ||||
|
278 | repo_data, repo_group_data = self._get_groups_and_repos() | |||
|
279 | # json used to render the grids | |||
|
280 | c.repos_data = json.dumps(repo_data) | |||
|
281 | c.repo_groups_data = json.dumps(repo_group_data) | |||
|
282 | ||||
|
283 | return self._get_template_context(c) | |||
|
284 | ||||
|
285 | @LoginRequired() | |||
|
286 | @HasRepoGroupPermissionAnyDecorator( | |||
|
287 | 'group.read', 'group.write', 'group.admin') | |||
|
288 | @view_config( | |||
|
289 | route_name='repo_group_home', request_method='GET', | |||
|
290 | renderer='rhodecode:templates/index_repo_group.mako') | |||
|
291 | @view_config( | |||
|
292 | route_name='repo_group_home_slash', request_method='GET', | |||
|
293 | renderer='rhodecode:templates/index_repo_group.mako') | |||
|
294 | def repo_group_main_page(self): | |||
|
295 | c = self.load_default_context() | |||
|
296 | c.repo_group = self.request.db_repo_group | |||
|
297 | repo_data, repo_group_data = self._get_groups_and_repos( | |||
|
298 | c.repo_group.group_id) | |||
|
299 | ||||
|
300 | # json used to render the grids | |||
|
301 | c.repos_data = json.dumps(repo_data) | |||
|
302 | c.repo_groups_data = json.dumps(repo_group_data) | |||
|
303 | ||||
|
304 | return self._get_template_context(c) |
@@ -0,0 +1,93 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 pytest | |||
|
22 | ||||
|
23 | from rhodecode.apps._base import ADMIN_PREFIX | |||
|
24 | from rhodecode.model.db import User, UserEmailMap | |||
|
25 | from rhodecode.tests import ( | |||
|
26 | TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, | |||
|
27 | assert_session_flash) | |||
|
28 | from rhodecode.tests.fixture import Fixture | |||
|
29 | ||||
|
30 | fixture = Fixture() | |||
|
31 | ||||
|
32 | ||||
|
33 | def route_path(name, **kwargs): | |||
|
34 | return { | |||
|
35 | 'my_account_emails': | |||
|
36 | ADMIN_PREFIX + '/my_account/emails', | |||
|
37 | 'my_account_emails_add': | |||
|
38 | ADMIN_PREFIX + '/my_account/emails/new', | |||
|
39 | 'my_account_emails_delete': | |||
|
40 | ADMIN_PREFIX + '/my_account/emails/delete', | |||
|
41 | }[name].format(**kwargs) | |||
|
42 | ||||
|
43 | ||||
|
44 | class TestMyAccountEmails(TestController): | |||
|
45 | def test_my_account_my_emails(self): | |||
|
46 | self.log_user() | |||
|
47 | response = self.app.get(route_path('my_account_emails')) | |||
|
48 | response.mustcontain('No additional emails specified') | |||
|
49 | ||||
|
50 | def test_my_account_my_emails_add_existing_email(self): | |||
|
51 | self.log_user() | |||
|
52 | response = self.app.get(route_path('my_account_emails')) | |||
|
53 | response.mustcontain('No additional emails specified') | |||
|
54 | response = self.app.post(route_path('my_account_emails_add'), | |||
|
55 | {'new_email': TEST_USER_REGULAR_EMAIL, | |||
|
56 | 'csrf_token': self.csrf_token}) | |||
|
57 | assert_session_flash(response, 'This e-mail address is already taken') | |||
|
58 | ||||
|
59 | def test_my_account_my_emails_add_mising_email_in_form(self): | |||
|
60 | self.log_user() | |||
|
61 | response = self.app.get(route_path('my_account_emails')) | |||
|
62 | response.mustcontain('No additional emails specified') | |||
|
63 | response = self.app.post(route_path('my_account_emails_add'), | |||
|
64 | {'csrf_token': self.csrf_token}) | |||
|
65 | assert_session_flash(response, 'Please enter an email address') | |||
|
66 | ||||
|
67 | def test_my_account_my_emails_add_remove(self): | |||
|
68 | self.log_user() | |||
|
69 | response = self.app.get(route_path('my_account_emails')) | |||
|
70 | response.mustcontain('No additional emails specified') | |||
|
71 | ||||
|
72 | response = self.app.post(route_path('my_account_emails_add'), | |||
|
73 | {'new_email': 'foo@barz.com', | |||
|
74 | 'csrf_token': self.csrf_token}) | |||
|
75 | ||||
|
76 | response = self.app.get(route_path('my_account_emails')) | |||
|
77 | ||||
|
78 | email_id = UserEmailMap.query().filter( | |||
|
79 | UserEmailMap.user == User.get_by_username( | |||
|
80 | TEST_USER_ADMIN_LOGIN)).filter( | |||
|
81 | UserEmailMap.email == 'foo@barz.com').one().email_id | |||
|
82 | ||||
|
83 | response.mustcontain('foo@barz.com') | |||
|
84 | response.mustcontain('<input id="del_email_id" name="del_email_id" ' | |||
|
85 | 'type="hidden" value="%s" />' % email_id) | |||
|
86 | ||||
|
87 | response = self.app.post( | |||
|
88 | route_path('my_account_emails_delete'), { | |||
|
89 | 'del_email_id': email_id, | |||
|
90 | 'csrf_token': self.csrf_token}) | |||
|
91 | assert_session_flash(response, 'Email successfully deleted') | |||
|
92 | response = self.app.get(route_path('my_account_emails')) | |||
|
93 | response.mustcontain('No additional emails specified') |
@@ -0,0 +1,76 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 pytest | |||
|
22 | ||||
|
23 | from rhodecode.apps._base import ADMIN_PREFIX | |||
|
24 | from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing | |||
|
25 | from rhodecode.tests import ( | |||
|
26 | TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, | |||
|
27 | assert_session_flash) | |||
|
28 | from rhodecode.tests.fixture import Fixture | |||
|
29 | ||||
|
30 | fixture = Fixture() | |||
|
31 | ||||
|
32 | ||||
|
33 | def route_path(name, **kwargs): | |||
|
34 | return { | |||
|
35 | 'my_account_repos': | |||
|
36 | ADMIN_PREFIX + '/my_account/repos', | |||
|
37 | 'my_account_watched': | |||
|
38 | ADMIN_PREFIX + '/my_account/watched', | |||
|
39 | 'my_account_perms': | |||
|
40 | ADMIN_PREFIX + '/my_account/perms', | |||
|
41 | 'my_account_notifications': | |||
|
42 | ADMIN_PREFIX + '/my_account/notifications', | |||
|
43 | }[name].format(**kwargs) | |||
|
44 | ||||
|
45 | ||||
|
46 | class TestMyAccountSimpleViews(TestController): | |||
|
47 | ||||
|
48 | def test_my_account_my_repos(self, autologin_user): | |||
|
49 | response = self.app.get(route_path('my_account_repos')) | |||
|
50 | repos = Repository.query().filter( | |||
|
51 | Repository.user == User.get_by_username( | |||
|
52 | TEST_USER_ADMIN_LOGIN)).all() | |||
|
53 | for repo in repos: | |||
|
54 | response.mustcontain('"name_raw": "%s"' % repo.repo_name) | |||
|
55 | ||||
|
56 | def test_my_account_my_watched(self, autologin_user): | |||
|
57 | response = self.app.get(route_path('my_account_watched')) | |||
|
58 | ||||
|
59 | repos = UserFollowing.query().filter( | |||
|
60 | UserFollowing.user == User.get_by_username( | |||
|
61 | TEST_USER_ADMIN_LOGIN)).all() | |||
|
62 | for repo in repos: | |||
|
63 | response.mustcontain( | |||
|
64 | '"name_raw": "%s"' % repo.follows_repository.repo_name) | |||
|
65 | ||||
|
66 | def test_my_account_perms(self, autologin_user): | |||
|
67 | response = self.app.get(route_path('my_account_perms')) | |||
|
68 | assert_response = response.assert_response() | |||
|
69 | assert assert_response.get_elements('.perm_tag.none') | |||
|
70 | assert assert_response.get_elements('.perm_tag.read') | |||
|
71 | assert assert_response.get_elements('.perm_tag.write') | |||
|
72 | assert assert_response.get_elements('.perm_tag.admin') | |||
|
73 | ||||
|
74 | def test_my_account_notifications(self, autologin_user): | |||
|
75 | response = self.app.get(route_path('my_account_notifications')) | |||
|
76 | response.mustcontain('Test flash message') |
@@ -0,0 +1,35 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | from rhodecode.config.routing import ADMIN_PREFIX | |||
|
22 | ||||
|
23 | ||||
|
24 | def admin_routes(config): | |||
|
25 | config.add_route( | |||
|
26 | name='ops_ping', | |||
|
27 | pattern='/ping') | |||
|
28 | ||||
|
29 | ||||
|
30 | def includeme(config): | |||
|
31 | ||||
|
32 | config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops') | |||
|
33 | ||||
|
34 | # Scan module for configuration decorators. | |||
|
35 | config.scan() |
@@ -0,0 +1,54 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 logging | |||
|
22 | ||||
|
23 | from pyramid.view import view_config | |||
|
24 | ||||
|
25 | from rhodecode.apps._base import BaseAppView | |||
|
26 | ||||
|
27 | ||||
|
28 | log = logging.getLogger(__name__) | |||
|
29 | ||||
|
30 | ||||
|
31 | class OpsView(BaseAppView): | |||
|
32 | ||||
|
33 | def load_default_context(self): | |||
|
34 | c = self._get_local_tmpl_context() | |||
|
35 | c.user = c.auth_user.get_instance() | |||
|
36 | self._register_global_c(c) | |||
|
37 | return c | |||
|
38 | ||||
|
39 | @view_config( | |||
|
40 | route_name='ops_ping', request_method='GET', | |||
|
41 | renderer='json_ext') | |||
|
42 | def ops_ping(self): | |||
|
43 | data = { | |||
|
44 | 'instance': self.request.registry.settings.get('instance_id'), | |||
|
45 | } | |||
|
46 | if getattr(self.request, 'user'): | |||
|
47 | data.update({ | |||
|
48 | 'caller_ip': self.request.user.ip_addr, | |||
|
49 | 'caller_name': self.request.user.username, | |||
|
50 | }) | |||
|
51 | return {'ok': data} | |||
|
52 | ||||
|
53 | ||||
|
54 |
@@ -0,0 +1,33 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | from rhodecode.apps._base import add_route_with_slash | |||
|
21 | ||||
|
22 | ||||
|
23 | def includeme(config): | |||
|
24 | ||||
|
25 | # Summary | |||
|
26 | add_route_with_slash( | |||
|
27 | config, | |||
|
28 | name='repo_group_home', | |||
|
29 | pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True) | |||
|
30 | ||||
|
31 | # Scan module for configuration decorators. | |||
|
32 | config.scan() | |||
|
33 |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 |
@@ -0,0 +1,83 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 pytest | |||
|
22 | from rhodecode.model.db import Repository | |||
|
23 | ||||
|
24 | ||||
|
25 | def route_path(name, params=None, **kwargs): | |||
|
26 | import urllib | |||
|
27 | ||||
|
28 | base_url = { | |||
|
29 | 'pullrequest_show_all': '/{repo_name}/pull-request', | |||
|
30 | 'pullrequest_show_all_data': '/{repo_name}/pull-request-data', | |||
|
31 | }[name].format(**kwargs) | |||
|
32 | ||||
|
33 | if params: | |||
|
34 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
35 | return base_url | |||
|
36 | ||||
|
37 | ||||
|
38 | @pytest.mark.backends("git", "hg") | |||
|
39 | @pytest.mark.usefixtures('autologin_user', 'app') | |||
|
40 | class TestPullRequestList(object): | |||
|
41 | ||||
|
42 | @pytest.mark.parametrize('params, expected_title', [ | |||
|
43 | ({'source': 0, 'closed': 1}, 'Closed Pull Requests'), | |||
|
44 | ({'source': 0, 'my': 1}, 'opened by me'), | |||
|
45 | ({'source': 0, 'awaiting_review': 1}, 'awaiting review'), | |||
|
46 | ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'), | |||
|
47 | ({'source': 1}, 'Pull Requests from'), | |||
|
48 | ]) | |||
|
49 | def test_showing_list_page(self, backend, pr_util, params, expected_title): | |||
|
50 | pull_request = pr_util.create_pull_request() | |||
|
51 | ||||
|
52 | response = self.app.get( | |||
|
53 | route_path('pullrequest_show_all', | |||
|
54 | repo_name=pull_request.target_repo.repo_name, | |||
|
55 | params=params)) | |||
|
56 | ||||
|
57 | assert_response = response.assert_response() | |||
|
58 | assert_response.element_equals_to('.panel-title', expected_title) | |||
|
59 | element = assert_response.get_element('.panel-title') | |||
|
60 | element_text = assert_response._element_to_string(element) | |||
|
61 | ||||
|
62 | def test_showing_list_page_data(self, backend, pr_util, xhr_header): | |||
|
63 | pull_request = pr_util.create_pull_request() | |||
|
64 | response = self.app.get( | |||
|
65 | route_path('pullrequest_show_all_data', | |||
|
66 | repo_name=pull_request.target_repo.repo_name), | |||
|
67 | extra_environ=xhr_header) | |||
|
68 | ||||
|
69 | assert response.json['recordsTotal'] == 1 | |||
|
70 | assert response.json['data'][0]['description'] == 'Description' | |||
|
71 | ||||
|
72 | def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header): | |||
|
73 | xss_description = "<script>alert('Hi!')</script>" | |||
|
74 | pull_request = pr_util.create_pull_request(description=xss_description) | |||
|
75 | ||||
|
76 | response = self.app.get( | |||
|
77 | route_path('pullrequest_show_all_data', | |||
|
78 | repo_name=pull_request.target_repo.repo_name), | |||
|
79 | extra_environ=xhr_header) | |||
|
80 | ||||
|
81 | assert response.json['recordsTotal'] == 1 | |||
|
82 | assert response.json['data'][0]['description'] == \ | |||
|
83 | "<script>alert('Hi!')</script>" |
@@ -0,0 +1,233 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 | 'edit_repo_advanced': '/{repo_name}/settings/advanced', | |||
|
42 | 'edit_repo_caches': '/{repo_name}/settings/caches', | |||
|
43 | 'edit_repo_perms': '/{repo_name}/settings/permissions', | |||
|
44 | }[name].format(**kwargs) | |||
|
45 | ||||
|
46 | if params: | |||
|
47 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
48 | return base_url | |||
|
49 | ||||
|
50 | ||||
|
51 | def _get_permission_for_user(user, repo): | |||
|
52 | perm = UserRepoToPerm.query()\ | |||
|
53 | .filter(UserRepoToPerm.repository == | |||
|
54 | Repository.get_by_repo_name(repo))\ | |||
|
55 | .filter(UserRepoToPerm.user == User.get_by_username(user))\ | |||
|
56 | .all() | |||
|
57 | return perm | |||
|
58 | ||||
|
59 | ||||
|
60 | @pytest.mark.usefixtures('autologin_user', 'app') | |||
|
61 | class TestAdminRepoSettings(object): | |||
|
62 | @pytest.mark.parametrize('urlname', [ | |||
|
63 | 'edit_repo', | |||
|
64 | 'edit_repo_caches', | |||
|
65 | 'edit_repo_perms', | |||
|
66 | 'edit_repo_advanced', | |||
|
67 | ]) | |||
|
68 | def test_show_page(self, urlname, app, backend): | |||
|
69 | app.get(route_path(urlname, repo_name=backend.repo_name), status=200) | |||
|
70 | ||||
|
71 | def test_edit_accessible_when_missing_requirements( | |||
|
72 | self, backend_hg, autologin_user): | |||
|
73 | scm_patcher = mock.patch.object( | |||
|
74 | Repository, 'scm_instance', side_effect=RepositoryRequirementError) | |||
|
75 | with scm_patcher: | |||
|
76 | self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name)) | |||
|
77 | ||||
|
78 | @pytest.mark.parametrize('urlname', [ | |||
|
79 | 'repo_vcs_settings', | |||
|
80 | 'repo_settings_issuetracker', | |||
|
81 | 'edit_repo_fields', | |||
|
82 | 'edit_repo_remote', | |||
|
83 | 'edit_repo_statistics', | |||
|
84 | ]) | |||
|
85 | def test_show_page_pylons(self, urlname, app): | |||
|
86 | app.get(url(urlname, repo_name=HG_REPO)) | |||
|
87 | ||||
|
88 | @pytest.mark.parametrize('update_settings', [ | |||
|
89 | {'repo_description': 'alter-desc'}, | |||
|
90 | {'repo_owner': TEST_USER_REGULAR_LOGIN}, | |||
|
91 | {'repo_private': 'true'}, | |||
|
92 | {'repo_enable_locking': 'true'}, | |||
|
93 | {'repo_enable_downloads': 'true'}, | |||
|
94 | ]) | |||
|
95 | def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util): | |||
|
96 | repo = user_util.create_repo(repo_type=backend.alias) | |||
|
97 | repo_name = repo.repo_name | |||
|
98 | ||||
|
99 | params = fixture._get_repo_create_params( | |||
|
100 | csrf_token=csrf_token, | |||
|
101 | repo_name=repo_name, | |||
|
102 | repo_type=backend.alias, | |||
|
103 | repo_owner=TEST_USER_ADMIN_LOGIN, | |||
|
104 | repo_description='DESC', | |||
|
105 | ||||
|
106 | repo_private='false', | |||
|
107 | repo_enable_locking='false', | |||
|
108 | repo_enable_downloads='false') | |||
|
109 | params.update(update_settings) | |||
|
110 | self.app.post( | |||
|
111 | route_path('edit_repo', repo_name=repo_name), | |||
|
112 | params=params, status=302) | |||
|
113 | ||||
|
114 | repo = Repository.get_by_repo_name(repo_name) | |||
|
115 | assert repo.user.username == \ | |||
|
116 | update_settings.get('repo_owner', repo.user.username) | |||
|
117 | ||||
|
118 | assert repo.description == \ | |||
|
119 | update_settings.get('repo_description', repo.description) | |||
|
120 | ||||
|
121 | assert repo.private == \ | |||
|
122 | str2bool(update_settings.get( | |||
|
123 | 'repo_private', repo.private)) | |||
|
124 | ||||
|
125 | assert repo.enable_locking == \ | |||
|
126 | str2bool(update_settings.get( | |||
|
127 | 'repo_enable_locking', repo.enable_locking)) | |||
|
128 | ||||
|
129 | assert repo.enable_downloads == \ | |||
|
130 | str2bool(update_settings.get( | |||
|
131 | 'repo_enable_downloads', repo.enable_downloads)) | |||
|
132 | ||||
|
133 | def test_update_repo_name_via_settings(self, csrf_token, user_util, backend): | |||
|
134 | repo = user_util.create_repo(repo_type=backend.alias) | |||
|
135 | repo_name = repo.repo_name | |||
|
136 | ||||
|
137 | repo_group = user_util.create_repo_group() | |||
|
138 | repo_group_name = repo_group.group_name | |||
|
139 | new_name = repo_group_name + '_' + repo_name | |||
|
140 | ||||
|
141 | params = fixture._get_repo_create_params( | |||
|
142 | csrf_token=csrf_token, | |||
|
143 | repo_name=new_name, | |||
|
144 | repo_type=backend.alias, | |||
|
145 | repo_owner=TEST_USER_ADMIN_LOGIN, | |||
|
146 | repo_description='DESC', | |||
|
147 | repo_private='false', | |||
|
148 | repo_enable_locking='false', | |||
|
149 | repo_enable_downloads='false') | |||
|
150 | self.app.post( | |||
|
151 | route_path('edit_repo', repo_name=repo_name), | |||
|
152 | params=params, status=302) | |||
|
153 | repo = Repository.get_by_repo_name(new_name) | |||
|
154 | assert repo.repo_name == new_name | |||
|
155 | ||||
|
156 | def test_update_repo_group_via_settings(self, csrf_token, user_util, backend): | |||
|
157 | repo = user_util.create_repo(repo_type=backend.alias) | |||
|
158 | repo_name = repo.repo_name | |||
|
159 | ||||
|
160 | repo_group = user_util.create_repo_group() | |||
|
161 | repo_group_name = repo_group.group_name | |||
|
162 | repo_group_id = repo_group.group_id | |||
|
163 | ||||
|
164 | new_name = repo_group_name + '/' + repo_name | |||
|
165 | params = fixture._get_repo_create_params( | |||
|
166 | csrf_token=csrf_token, | |||
|
167 | repo_name=repo_name, | |||
|
168 | repo_type=backend.alias, | |||
|
169 | repo_owner=TEST_USER_ADMIN_LOGIN, | |||
|
170 | repo_description='DESC', | |||
|
171 | repo_group=repo_group_id, | |||
|
172 | repo_private='false', | |||
|
173 | repo_enable_locking='false', | |||
|
174 | repo_enable_downloads='false') | |||
|
175 | self.app.post( | |||
|
176 | route_path('edit_repo', repo_name=repo_name), | |||
|
177 | params=params, status=302) | |||
|
178 | repo = Repository.get_by_repo_name(new_name) | |||
|
179 | assert repo.repo_name == new_name | |||
|
180 | ||||
|
181 | def test_set_private_flag_sets_default_user_permissions_to_none( | |||
|
182 | self, autologin_user, backend, csrf_token): | |||
|
183 | ||||
|
184 | # initially repository perm should be read | |||
|
185 | perm = _get_permission_for_user(user='default', repo=backend.repo_name) | |||
|
186 | assert len(perm) == 1 | |||
|
187 | assert perm[0].permission.permission_name == 'repository.read' | |||
|
188 | assert not backend.repo.private | |||
|
189 | ||||
|
190 | response = self.app.post( | |||
|
191 | route_path('edit_repo', repo_name=backend.repo_name), | |||
|
192 | params=fixture._get_repo_create_params( | |||
|
193 | repo_private='true', | |||
|
194 | repo_name=backend.repo_name, | |||
|
195 | repo_type=backend.alias, | |||
|
196 | repo_owner=TEST_USER_ADMIN_LOGIN, | |||
|
197 | csrf_token=csrf_token), status=302) | |||
|
198 | ||||
|
199 | assert_session_flash( | |||
|
200 | response, | |||
|
201 | msg='Repository %s updated successfully' % (backend.repo_name)) | |||
|
202 | ||||
|
203 | repo = Repository.get_by_repo_name(backend.repo_name) | |||
|
204 | assert repo.private is True | |||
|
205 | ||||
|
206 | # now the repo default permission should be None | |||
|
207 | perm = _get_permission_for_user(user='default', repo=backend.repo_name) | |||
|
208 | assert len(perm) == 1 | |||
|
209 | assert perm[0].permission.permission_name == 'repository.none' | |||
|
210 | ||||
|
211 | response = self.app.post( | |||
|
212 | route_path('edit_repo', repo_name=backend.repo_name), | |||
|
213 | params=fixture._get_repo_create_params( | |||
|
214 | repo_private='false', | |||
|
215 | repo_name=backend.repo_name, | |||
|
216 | repo_type=backend.alias, | |||
|
217 | repo_owner=TEST_USER_ADMIN_LOGIN, | |||
|
218 | csrf_token=csrf_token), status=302) | |||
|
219 | ||||
|
220 | assert_session_flash( | |||
|
221 | response, | |||
|
222 | msg='Repository %s updated successfully' % (backend.repo_name)) | |||
|
223 | assert backend.repo.private is False | |||
|
224 | ||||
|
225 | # we turn off private now the repo default permission should stay None | |||
|
226 | perm = _get_permission_for_user(user='default', repo=backend.repo_name) | |||
|
227 | assert len(perm) == 1 | |||
|
228 | assert perm[0].permission.permission_name == 'repository.none' | |||
|
229 | ||||
|
230 | # update this permission back | |||
|
231 | perm[0].permission = Permission.get_by_key('repository.read') | |||
|
232 | Session().add(perm[0]) | |||
|
233 | Session().commit() |
@@ -0,0 +1,150 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 pytest | |||
|
22 | ||||
|
23 | from rhodecode.lib.utils2 import safe_unicode, safe_str | |||
|
24 | from rhodecode.model.db import Repository | |||
|
25 | from rhodecode.model.repo import RepoModel | |||
|
26 | from rhodecode.tests import ( | |||
|
27 | HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator) | |||
|
28 | from rhodecode.tests.fixture import Fixture | |||
|
29 | from rhodecode.tests.utils import repo_on_filesystem | |||
|
30 | ||||
|
31 | fixture = Fixture() | |||
|
32 | ||||
|
33 | ||||
|
34 | def route_path(name, params=None, **kwargs): | |||
|
35 | import urllib | |||
|
36 | ||||
|
37 | base_url = { | |||
|
38 | 'repo_summary_explicit': '/{repo_name}/summary', | |||
|
39 | 'repo_summary': '/{repo_name}', | |||
|
40 | 'edit_repo_advanced': '/{repo_name}/settings/advanced', | |||
|
41 | 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete', | |||
|
42 | 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork', | |||
|
43 | 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking', | |||
|
44 | 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal', | |||
|
45 | ||||
|
46 | }[name].format(**kwargs) | |||
|
47 | ||||
|
48 | if params: | |||
|
49 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
50 | return base_url | |||
|
51 | ||||
|
52 | ||||
|
53 | @pytest.mark.usefixtures('autologin_user', 'app') | |||
|
54 | class TestAdminRepoSettingsAdvanced(object): | |||
|
55 | ||||
|
56 | def test_set_repo_fork_has_no_self_id(self, autologin_user, backend): | |||
|
57 | repo = backend.repo | |||
|
58 | response = self.app.get( | |||
|
59 | route_path('edit_repo_advanced', repo_name=backend.repo_name)) | |||
|
60 | opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id | |||
|
61 | response.mustcontain(no=[opt]) | |||
|
62 | ||||
|
63 | def test_set_fork_of_target_repo( | |||
|
64 | self, autologin_user, backend, csrf_token): | |||
|
65 | target_repo = 'target_%s' % backend.alias | |||
|
66 | fixture.create_repo(target_repo, repo_type=backend.alias) | |||
|
67 | repo2 = Repository.get_by_repo_name(target_repo) | |||
|
68 | response = self.app.post( | |||
|
69 | route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), | |||
|
70 | params={'id_fork_of': repo2.repo_id, | |||
|
71 | 'csrf_token': csrf_token}) | |||
|
72 | repo = Repository.get_by_repo_name(backend.repo_name) | |||
|
73 | repo2 = Repository.get_by_repo_name(target_repo) | |||
|
74 | assert_session_flash( | |||
|
75 | response, | |||
|
76 | 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name)) | |||
|
77 | ||||
|
78 | assert repo.fork == repo2 | |||
|
79 | response = response.follow() | |||
|
80 | # check if given repo is selected | |||
|
81 | ||||
|
82 | opt = 'This repository is a fork of <a href="%s">%s</a>' % ( | |||
|
83 | route_path('repo_summary', repo_name=repo2.repo_name), | |||
|
84 | repo2.repo_name) | |||
|
85 | ||||
|
86 | response.mustcontain(opt) | |||
|
87 | ||||
|
88 | fixture.destroy_repo(target_repo, forks='detach') | |||
|
89 | ||||
|
90 | @pytest.mark.backends("hg", "git") | |||
|
91 | def test_set_fork_of_other_type_repo( | |||
|
92 | self, autologin_user, backend, csrf_token): | |||
|
93 | TARGET_REPO_MAP = { | |||
|
94 | 'git': { | |||
|
95 | 'type': 'hg', | |||
|
96 | 'repo_name': HG_REPO}, | |||
|
97 | 'hg': { | |||
|
98 | 'type': 'git', | |||
|
99 | 'repo_name': GIT_REPO}, | |||
|
100 | } | |||
|
101 | target_repo = TARGET_REPO_MAP[backend.alias] | |||
|
102 | ||||
|
103 | repo2 = Repository.get_by_repo_name(target_repo['repo_name']) | |||
|
104 | response = self.app.post( | |||
|
105 | route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), | |||
|
106 | params={'id_fork_of': repo2.repo_id, | |||
|
107 | 'csrf_token': csrf_token}) | |||
|
108 | assert_session_flash( | |||
|
109 | response, | |||
|
110 | 'Cannot set repository as fork of repository with other type') | |||
|
111 | ||||
|
112 | def test_set_fork_of_none(self, autologin_user, backend, csrf_token): | |||
|
113 | # mark it as None | |||
|
114 | response = self.app.post( | |||
|
115 | route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), | |||
|
116 | params={'id_fork_of': None, '_method': 'put', | |||
|
117 | 'csrf_token': csrf_token}) | |||
|
118 | assert_session_flash( | |||
|
119 | response, | |||
|
120 | 'Marked repo %s as fork of %s' | |||
|
121 | % (backend.repo_name, "Nothing")) | |||
|
122 | assert backend.repo.fork is None | |||
|
123 | ||||
|
124 | def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token): | |||
|
125 | repo = Repository.get_by_repo_name(backend.repo_name) | |||
|
126 | response = self.app.post( | |||
|
127 | route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), | |||
|
128 | params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token}) | |||
|
129 | assert_session_flash( | |||
|
130 | response, 'An error occurred during this operation') | |||
|
131 | ||||
|
132 | @pytest.mark.parametrize( | |||
|
133 | "suffix", | |||
|
134 | ['', u'ąęł' , '123'], | |||
|
135 | ids=no_newline_id_generator) | |||
|
136 | def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token): | |||
|
137 | repo = backend.create_repo(name_suffix=suffix) | |||
|
138 | repo_name = repo.repo_name | |||
|
139 | repo_name_str = safe_str(repo.repo_name) | |||
|
140 | ||||
|
141 | response = self.app.post( | |||
|
142 | route_path('edit_repo_advanced_delete', repo_name=repo_name_str), | |||
|
143 | params={'csrf_token': csrf_token}) | |||
|
144 | assert_session_flash(response, | |||
|
145 | u'Deleted repository `{}`'.format(repo_name)) | |||
|
146 | response.follow() | |||
|
147 | ||||
|
148 | # check if repo was deleted from db | |||
|
149 | assert RepoModel().get_by_repo_name(repo_name) is None | |||
|
150 | assert not repo_on_filesystem(repo_name_str) |
@@ -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 |
@@ -0,0 +1,76 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | from rhodecode.lib import helpers as h | |||
|
22 | from rhodecode.lib.utils2 import safe_int | |||
|
23 | ||||
|
24 | ||||
|
25 | def reviewer_as_json(user, reasons=None, mandatory=False): | |||
|
26 | """ | |||
|
27 | Returns json struct of a reviewer for frontend | |||
|
28 | ||||
|
29 | :param user: the reviewer | |||
|
30 | :param reasons: list of strings of why they are reviewers | |||
|
31 | :param mandatory: bool, to set user as mandatory | |||
|
32 | """ | |||
|
33 | ||||
|
34 | return { | |||
|
35 | 'user_id': user.user_id, | |||
|
36 | 'reasons': reasons or [], | |||
|
37 | 'mandatory': mandatory, | |||
|
38 | 'username': user.username, | |||
|
39 | 'first_name': user.first_name, | |||
|
40 | 'last_name': user.last_name, | |||
|
41 | 'gravatar_link': h.gravatar_url(user.email, 14), | |||
|
42 | } | |||
|
43 | ||||
|
44 | ||||
|
45 | def get_default_reviewers_data( | |||
|
46 | current_user, source_repo, source_commit, target_repo, target_commit): | |||
|
47 | ||||
|
48 | """ Return json for default reviewers of a repository """ | |||
|
49 | ||||
|
50 | reasons = ['Default reviewer', 'Repository owner'] | |||
|
51 | default = reviewer_as_json( | |||
|
52 | user=current_user, reasons=reasons, mandatory=False) | |||
|
53 | ||||
|
54 | return { | |||
|
55 | 'api_ver': 'v1', # define version for later possible schema upgrade | |||
|
56 | 'reviewers': [default], | |||
|
57 | 'rules': {}, | |||
|
58 | 'rules_data': {}, | |||
|
59 | } | |||
|
60 | ||||
|
61 | ||||
|
62 | def validate_default_reviewers(review_members, reviewer_rules): | |||
|
63 | """ | |||
|
64 | Function to validate submitted reviewers against the saved rules | |||
|
65 | ||||
|
66 | """ | |||
|
67 | reviewers = [] | |||
|
68 | reviewer_by_id = {} | |||
|
69 | for r in review_members: | |||
|
70 | reviewer_user_id = safe_int(r['user_id']) | |||
|
71 | entry = (reviewer_user_id, r['reasons'], r['mandatory']) | |||
|
72 | ||||
|
73 | reviewer_by_id[reviewer_user_id] = entry | |||
|
74 | reviewers.append(entry) | |||
|
75 | ||||
|
76 | return reviewers |
@@ -0,0 +1,50 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 | import logging | |||
|
21 | ||||
|
22 | from pyramid.httpexceptions import HTTPNotFound | |||
|
23 | from pyramid.view import view_config | |||
|
24 | ||||
|
25 | from rhodecode.apps._base import BaseReferencesView | |||
|
26 | from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) | |||
|
27 | from rhodecode.lib import helpers as h | |||
|
28 | ||||
|
29 | log = logging.getLogger(__name__) | |||
|
30 | ||||
|
31 | ||||
|
32 | class RepoBookmarksView(BaseReferencesView): | |||
|
33 | ||||
|
34 | @LoginRequired() | |||
|
35 | @HasRepoPermissionAnyDecorator( | |||
|
36 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
37 | @view_config( | |||
|
38 | route_name='bookmarks_home', request_method='GET', | |||
|
39 | renderer='rhodecode:templates/bookmarks/bookmarks.mako') | |||
|
40 | def bookmarks(self): | |||
|
41 | c = self.load_default_context() | |||
|
42 | ||||
|
43 | if not h.is_hg(self.db_repo): | |||
|
44 | raise HTTPNotFound() | |||
|
45 | ||||
|
46 | ref_items = self.rhodecode_vcs_repo.bookmarks.items() | |||
|
47 | self.load_refs_context( | |||
|
48 | ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako') | |||
|
49 | ||||
|
50 | return self._get_template_context(c) |
@@ -0,0 +1,51 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | from pyramid.view import view_config | |||
|
23 | ||||
|
24 | from rhodecode.apps._base import BaseReferencesView | |||
|
25 | from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) | |||
|
26 | ||||
|
27 | ||||
|
28 | log = logging.getLogger(__name__) | |||
|
29 | ||||
|
30 | ||||
|
31 | class RepoBranchesView(BaseReferencesView): | |||
|
32 | ||||
|
33 | @LoginRequired() | |||
|
34 | @HasRepoPermissionAnyDecorator( | |||
|
35 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
36 | @view_config( | |||
|
37 | route_name='branches_home', request_method='GET', | |||
|
38 | renderer='rhodecode:templates/branches/branches.mako') | |||
|
39 | def branches(self): | |||
|
40 | c = self.load_default_context() | |||
|
41 | c.closed_branches = self.rhodecode_vcs_repo.branches_closed | |||
|
42 | # NOTE(marcink): | |||
|
43 | # we need this trick because of PartialRenderer still uses the | |||
|
44 | # global 'c', we might not need this after full pylons migration | |||
|
45 | self._register_global_c(c) | |||
|
46 | ||||
|
47 | ref_items = self.rhodecode_vcs_repo.branches_all.items() | |||
|
48 | self.load_refs_context( | |||
|
49 | ref_items=ref_items, partials_template='branches/branches_data.mako') | |||
|
50 | ||||
|
51 | return self._get_template_context(c) |
@@ -0,0 +1,78 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | ||||
|
23 | from pyramid.httpexceptions import HTTPFound | |||
|
24 | from pyramid.view import view_config | |||
|
25 | ||||
|
26 | from rhodecode.apps._base import RepoAppView | |||
|
27 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \ | |||
|
28 | CSRFRequired | |||
|
29 | from rhodecode.lib import helpers as h | |||
|
30 | from rhodecode.model.meta import Session | |||
|
31 | from rhodecode.model.scm import ScmModel | |||
|
32 | ||||
|
33 | log = logging.getLogger(__name__) | |||
|
34 | ||||
|
35 | ||||
|
36 | class RepoCachesView(RepoAppView): | |||
|
37 | def load_default_context(self): | |||
|
38 | c = self._get_local_tmpl_context() | |||
|
39 | ||||
|
40 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
41 | c.repo_info = self.db_repo | |||
|
42 | ||||
|
43 | self._register_global_c(c) | |||
|
44 | return c | |||
|
45 | ||||
|
46 | @LoginRequired() | |||
|
47 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
48 | @view_config( | |||
|
49 | route_name='edit_repo_caches', request_method='GET', | |||
|
50 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
51 | def repo_caches(self): | |||
|
52 | c = self.load_default_context() | |||
|
53 | c.active = 'caches' | |||
|
54 | ||||
|
55 | return self._get_template_context(c) | |||
|
56 | ||||
|
57 | @LoginRequired() | |||
|
58 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
59 | @CSRFRequired() | |||
|
60 | @view_config( | |||
|
61 | route_name='edit_repo_caches', request_method='POST') | |||
|
62 | def repo_caches_purge(self): | |||
|
63 | _ = self.request.translate | |||
|
64 | c = self.load_default_context() | |||
|
65 | c.active = 'caches' | |||
|
66 | ||||
|
67 | try: | |||
|
68 | ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) | |||
|
69 | Session().commit() | |||
|
70 | h.flash(_('Cache invalidation successful'), | |||
|
71 | category='success') | |||
|
72 | except Exception: | |||
|
73 | log.exception("Exception during cache invalidation") | |||
|
74 | h.flash(_('An error occurred during cache invalidation'), | |||
|
75 | category='error') | |||
|
76 | ||||
|
77 | raise HTTPFound(h.route_path( | |||
|
78 | 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file |
@@ -0,0 +1,98 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | ||||
|
23 | import deform | |||
|
24 | from pyramid.httpexceptions import HTTPFound | |||
|
25 | from pyramid.view import view_config | |||
|
26 | ||||
|
27 | from rhodecode.apps._base import RepoAppView | |||
|
28 | from rhodecode.forms import RcForm | |||
|
29 | from rhodecode.lib import helpers as h | |||
|
30 | from rhodecode.lib import audit_logger | |||
|
31 | from rhodecode.lib.auth import ( | |||
|
32 | LoginRequired, HasRepoPermissionAnyDecorator, | |||
|
33 | HasRepoPermissionAllDecorator, CSRFRequired) | |||
|
34 | from rhodecode.model.db import RepositoryField, RepoGroup | |||
|
35 | from rhodecode.model.forms import RepoPermsForm | |||
|
36 | from rhodecode.model.meta import Session | |||
|
37 | from rhodecode.model.repo import RepoModel | |||
|
38 | from rhodecode.model.scm import RepoGroupList, ScmModel | |||
|
39 | from rhodecode.model.validation_schema.schemas import repo_schema | |||
|
40 | ||||
|
41 | log = logging.getLogger(__name__) | |||
|
42 | ||||
|
43 | ||||
|
44 | class RepoSettingsPermissionsView(RepoAppView): | |||
|
45 | ||||
|
46 | def load_default_context(self): | |||
|
47 | c = self._get_local_tmpl_context() | |||
|
48 | ||||
|
49 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
50 | c.repo_info = self.db_repo | |||
|
51 | ||||
|
52 | self._register_global_c(c) | |||
|
53 | return c | |||
|
54 | ||||
|
55 | @LoginRequired() | |||
|
56 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
57 | @view_config( | |||
|
58 | route_name='edit_repo_perms', request_method='GET', | |||
|
59 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
60 | def edit_permissions(self): | |||
|
61 | c = self.load_default_context() | |||
|
62 | c.active = 'permissions' | |||
|
63 | return self._get_template_context(c) | |||
|
64 | ||||
|
65 | @LoginRequired() | |||
|
66 | @HasRepoPermissionAllDecorator('repository.admin') | |||
|
67 | @CSRFRequired() | |||
|
68 | @view_config( | |||
|
69 | route_name='edit_repo_perms', request_method='POST', | |||
|
70 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
71 | def edit_permissions_update(self): | |||
|
72 | _ = self.request.translate | |||
|
73 | c = self.load_default_context() | |||
|
74 | c.active = 'permissions' | |||
|
75 | data = self.request.POST | |||
|
76 | # store private flag outside of HTML to verify if we can modify | |||
|
77 | # default user permissions, prevents submition of FAKE post data | |||
|
78 | # into the form for private repos | |||
|
79 | data['repo_private'] = self.db_repo.private | |||
|
80 | form = RepoPermsForm()().to_python(data) | |||
|
81 | changes = RepoModel().update_permissions( | |||
|
82 | self.db_repo_name, form['perm_additions'], form['perm_updates'], | |||
|
83 | form['perm_deletions']) | |||
|
84 | ||||
|
85 | action_data = { | |||
|
86 | 'added': changes['added'], | |||
|
87 | 'updated': changes['updated'], | |||
|
88 | 'deleted': changes['deleted'], | |||
|
89 | } | |||
|
90 | audit_logger.store_web( | |||
|
91 | 'repo.edit.permissions', action_data=action_data, | |||
|
92 | user=self._rhodecode_user, repo=self.db_repo) | |||
|
93 | ||||
|
94 | Session().commit() | |||
|
95 | h.flash(_('Repository permissions updated'), category='success') | |||
|
96 | ||||
|
97 | raise HTTPFound( | |||
|
98 | self.request.route_path('edit_repo_perms', repo_name=self.db_repo_name)) |
This diff has been collapsed as it changes many lines, (584 lines changed) Show them Hide them | |||||
@@ -0,0 +1,584 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | ||||
|
23 | import collections | |||
|
24 | from pyramid.httpexceptions import HTTPFound, HTTPNotFound | |||
|
25 | from pyramid.view import view_config | |||
|
26 | ||||
|
27 | from rhodecode.apps._base import RepoAppView, DataGridAppView | |||
|
28 | from rhodecode.lib import helpers as h, diffs, codeblocks | |||
|
29 | from rhodecode.lib.auth import ( | |||
|
30 | LoginRequired, HasRepoPermissionAnyDecorator) | |||
|
31 | from rhodecode.lib.utils import PartialRenderer | |||
|
32 | from rhodecode.lib.utils2 import str2bool, safe_int, safe_str | |||
|
33 | from rhodecode.lib.vcs.backends.base import EmptyCommit | |||
|
34 | from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \ | |||
|
35 | RepositoryRequirementError, NodeDoesNotExistError | |||
|
36 | from rhodecode.model.comment import CommentsModel | |||
|
37 | from rhodecode.model.db import PullRequest, PullRequestVersion, \ | |||
|
38 | ChangesetComment, ChangesetStatus | |||
|
39 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck | |||
|
40 | ||||
|
41 | log = logging.getLogger(__name__) | |||
|
42 | ||||
|
43 | ||||
|
44 | class RepoPullRequestsView(RepoAppView, DataGridAppView): | |||
|
45 | ||||
|
46 | def load_default_context(self): | |||
|
47 | c = self._get_local_tmpl_context(include_app_defaults=True) | |||
|
48 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
49 | c.repo_info = self.db_repo | |||
|
50 | c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED | |||
|
51 | c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED | |||
|
52 | self._register_global_c(c) | |||
|
53 | return c | |||
|
54 | ||||
|
55 | def _get_pull_requests_list( | |||
|
56 | self, repo_name, source, filter_type, opened_by, statuses): | |||
|
57 | ||||
|
58 | draw, start, limit = self._extract_chunk(self.request) | |||
|
59 | search_q, order_by, order_dir = self._extract_ordering(self.request) | |||
|
60 | _render = PartialRenderer('data_table/_dt_elements.mako') | |||
|
61 | ||||
|
62 | # pagination | |||
|
63 | ||||
|
64 | if filter_type == 'awaiting_review': | |||
|
65 | pull_requests = PullRequestModel().get_awaiting_review( | |||
|
66 | repo_name, source=source, opened_by=opened_by, | |||
|
67 | statuses=statuses, offset=start, length=limit, | |||
|
68 | order_by=order_by, order_dir=order_dir) | |||
|
69 | pull_requests_total_count = PullRequestModel().count_awaiting_review( | |||
|
70 | repo_name, source=source, statuses=statuses, | |||
|
71 | opened_by=opened_by) | |||
|
72 | elif filter_type == 'awaiting_my_review': | |||
|
73 | pull_requests = PullRequestModel().get_awaiting_my_review( | |||
|
74 | repo_name, source=source, opened_by=opened_by, | |||
|
75 | user_id=self._rhodecode_user.user_id, statuses=statuses, | |||
|
76 | offset=start, length=limit, order_by=order_by, | |||
|
77 | order_dir=order_dir) | |||
|
78 | pull_requests_total_count = PullRequestModel().count_awaiting_my_review( | |||
|
79 | repo_name, source=source, user_id=self._rhodecode_user.user_id, | |||
|
80 | statuses=statuses, opened_by=opened_by) | |||
|
81 | else: | |||
|
82 | pull_requests = PullRequestModel().get_all( | |||
|
83 | repo_name, source=source, opened_by=opened_by, | |||
|
84 | statuses=statuses, offset=start, length=limit, | |||
|
85 | order_by=order_by, order_dir=order_dir) | |||
|
86 | pull_requests_total_count = PullRequestModel().count_all( | |||
|
87 | repo_name, source=source, statuses=statuses, | |||
|
88 | opened_by=opened_by) | |||
|
89 | ||||
|
90 | data = [] | |||
|
91 | comments_model = CommentsModel() | |||
|
92 | for pr in pull_requests: | |||
|
93 | comments = comments_model.get_all_comments( | |||
|
94 | self.db_repo.repo_id, pull_request=pr) | |||
|
95 | ||||
|
96 | data.append({ | |||
|
97 | 'name': _render('pullrequest_name', | |||
|
98 | pr.pull_request_id, pr.target_repo.repo_name), | |||
|
99 | 'name_raw': pr.pull_request_id, | |||
|
100 | 'status': _render('pullrequest_status', | |||
|
101 | pr.calculated_review_status()), | |||
|
102 | 'title': _render( | |||
|
103 | 'pullrequest_title', pr.title, pr.description), | |||
|
104 | 'description': h.escape(pr.description), | |||
|
105 | 'updated_on': _render('pullrequest_updated_on', | |||
|
106 | h.datetime_to_time(pr.updated_on)), | |||
|
107 | 'updated_on_raw': h.datetime_to_time(pr.updated_on), | |||
|
108 | 'created_on': _render('pullrequest_updated_on', | |||
|
109 | h.datetime_to_time(pr.created_on)), | |||
|
110 | 'created_on_raw': h.datetime_to_time(pr.created_on), | |||
|
111 | 'author': _render('pullrequest_author', | |||
|
112 | pr.author.full_contact, ), | |||
|
113 | 'author_raw': pr.author.full_name, | |||
|
114 | 'comments': _render('pullrequest_comments', len(comments)), | |||
|
115 | 'comments_raw': len(comments), | |||
|
116 | 'closed': pr.is_closed(), | |||
|
117 | }) | |||
|
118 | ||||
|
119 | data = ({ | |||
|
120 | 'draw': draw, | |||
|
121 | 'data': data, | |||
|
122 | 'recordsTotal': pull_requests_total_count, | |||
|
123 | 'recordsFiltered': pull_requests_total_count, | |||
|
124 | }) | |||
|
125 | return data | |||
|
126 | ||||
|
127 | @LoginRequired() | |||
|
128 | @HasRepoPermissionAnyDecorator( | |||
|
129 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
130 | @view_config( | |||
|
131 | route_name='pullrequest_show_all', request_method='GET', | |||
|
132 | renderer='rhodecode:templates/pullrequests/pullrequests.mako') | |||
|
133 | def pull_request_list(self): | |||
|
134 | c = self.load_default_context() | |||
|
135 | ||||
|
136 | req_get = self.request.GET | |||
|
137 | c.source = str2bool(req_get.get('source')) | |||
|
138 | c.closed = str2bool(req_get.get('closed')) | |||
|
139 | c.my = str2bool(req_get.get('my')) | |||
|
140 | c.awaiting_review = str2bool(req_get.get('awaiting_review')) | |||
|
141 | c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) | |||
|
142 | ||||
|
143 | c.active = 'open' | |||
|
144 | if c.my: | |||
|
145 | c.active = 'my' | |||
|
146 | if c.closed: | |||
|
147 | c.active = 'closed' | |||
|
148 | if c.awaiting_review and not c.source: | |||
|
149 | c.active = 'awaiting' | |||
|
150 | if c.source and not c.awaiting_review: | |||
|
151 | c.active = 'source' | |||
|
152 | if c.awaiting_my_review: | |||
|
153 | c.active = 'awaiting_my' | |||
|
154 | ||||
|
155 | return self._get_template_context(c) | |||
|
156 | ||||
|
157 | @LoginRequired() | |||
|
158 | @HasRepoPermissionAnyDecorator( | |||
|
159 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
160 | @view_config( | |||
|
161 | route_name='pullrequest_show_all_data', request_method='GET', | |||
|
162 | renderer='json_ext', xhr=True) | |||
|
163 | def pull_request_list_data(self): | |||
|
164 | ||||
|
165 | # additional filters | |||
|
166 | req_get = self.request.GET | |||
|
167 | source = str2bool(req_get.get('source')) | |||
|
168 | closed = str2bool(req_get.get('closed')) | |||
|
169 | my = str2bool(req_get.get('my')) | |||
|
170 | awaiting_review = str2bool(req_get.get('awaiting_review')) | |||
|
171 | awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) | |||
|
172 | ||||
|
173 | filter_type = 'awaiting_review' if awaiting_review \ | |||
|
174 | else 'awaiting_my_review' if awaiting_my_review \ | |||
|
175 | else None | |||
|
176 | ||||
|
177 | opened_by = None | |||
|
178 | if my: | |||
|
179 | opened_by = [self._rhodecode_user.user_id] | |||
|
180 | ||||
|
181 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] | |||
|
182 | if closed: | |||
|
183 | statuses = [PullRequest.STATUS_CLOSED] | |||
|
184 | ||||
|
185 | data = self._get_pull_requests_list( | |||
|
186 | repo_name=self.db_repo_name, source=source, | |||
|
187 | filter_type=filter_type, opened_by=opened_by, statuses=statuses) | |||
|
188 | ||||
|
189 | return data | |||
|
190 | ||||
|
191 | def _get_pr_version(self, pull_request_id, version=None): | |||
|
192 | pull_request_id = safe_int(pull_request_id) | |||
|
193 | at_version = None | |||
|
194 | ||||
|
195 | if version and version == 'latest': | |||
|
196 | pull_request_ver = PullRequest.get(pull_request_id) | |||
|
197 | pull_request_obj = pull_request_ver | |||
|
198 | _org_pull_request_obj = pull_request_obj | |||
|
199 | at_version = 'latest' | |||
|
200 | elif version: | |||
|
201 | pull_request_ver = PullRequestVersion.get_or_404(version) | |||
|
202 | pull_request_obj = pull_request_ver | |||
|
203 | _org_pull_request_obj = pull_request_ver.pull_request | |||
|
204 | at_version = pull_request_ver.pull_request_version_id | |||
|
205 | else: | |||
|
206 | _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404( | |||
|
207 | pull_request_id) | |||
|
208 | ||||
|
209 | pull_request_display_obj = PullRequest.get_pr_display_object( | |||
|
210 | pull_request_obj, _org_pull_request_obj) | |||
|
211 | ||||
|
212 | return _org_pull_request_obj, pull_request_obj, \ | |||
|
213 | pull_request_display_obj, at_version | |||
|
214 | ||||
|
215 | def _get_diffset(self, source_repo_name, source_repo, | |||
|
216 | source_ref_id, target_ref_id, | |||
|
217 | target_commit, source_commit, diff_limit, fulldiff, | |||
|
218 | file_limit, display_inline_comments): | |||
|
219 | ||||
|
220 | vcs_diff = PullRequestModel().get_diff( | |||
|
221 | source_repo, source_ref_id, target_ref_id) | |||
|
222 | ||||
|
223 | diff_processor = diffs.DiffProcessor( | |||
|
224 | vcs_diff, format='newdiff', diff_limit=diff_limit, | |||
|
225 | file_limit=file_limit, show_full_diff=fulldiff) | |||
|
226 | ||||
|
227 | _parsed = diff_processor.prepare() | |||
|
228 | ||||
|
229 | def _node_getter(commit): | |||
|
230 | def get_node(fname): | |||
|
231 | try: | |||
|
232 | return commit.get_node(fname) | |||
|
233 | except NodeDoesNotExistError: | |||
|
234 | return None | |||
|
235 | ||||
|
236 | return get_node | |||
|
237 | ||||
|
238 | diffset = codeblocks.DiffSet( | |||
|
239 | repo_name=self.db_repo_name, | |||
|
240 | source_repo_name=source_repo_name, | |||
|
241 | source_node_getter=_node_getter(target_commit), | |||
|
242 | target_node_getter=_node_getter(source_commit), | |||
|
243 | comments=display_inline_comments | |||
|
244 | ) | |||
|
245 | diffset = diffset.render_patchset( | |||
|
246 | _parsed, target_commit.raw_id, source_commit.raw_id) | |||
|
247 | ||||
|
248 | return diffset | |||
|
249 | ||||
|
250 | @LoginRequired() | |||
|
251 | @HasRepoPermissionAnyDecorator( | |||
|
252 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
253 | # @view_config( | |||
|
254 | # route_name='pullrequest_show', request_method='GET', | |||
|
255 | # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako') | |||
|
256 | def pull_request_show(self): | |||
|
257 | pull_request_id = safe_int( | |||
|
258 | self.request.matchdict.get('pull_request_id')) | |||
|
259 | c = self.load_default_context() | |||
|
260 | ||||
|
261 | version = self.request.GET.get('version') | |||
|
262 | from_version = self.request.GET.get('from_version') or version | |||
|
263 | merge_checks = self.request.GET.get('merge_checks') | |||
|
264 | c.fulldiff = str2bool(self.request.GET.get('fulldiff')) | |||
|
265 | ||||
|
266 | (pull_request_latest, | |||
|
267 | pull_request_at_ver, | |||
|
268 | pull_request_display_obj, | |||
|
269 | at_version) = self._get_pr_version( | |||
|
270 | pull_request_id, version=version) | |||
|
271 | pr_closed = pull_request_latest.is_closed() | |||
|
272 | ||||
|
273 | if pr_closed and (version or from_version): | |||
|
274 | # not allow to browse versions | |||
|
275 | raise HTTPFound(h.route_path( | |||
|
276 | 'pullrequest_show', repo_name=self.db_repo_name, | |||
|
277 | pull_request_id=pull_request_id)) | |||
|
278 | ||||
|
279 | versions = pull_request_display_obj.versions() | |||
|
280 | ||||
|
281 | c.at_version = at_version | |||
|
282 | c.at_version_num = (at_version | |||
|
283 | if at_version and at_version != 'latest' | |||
|
284 | else None) | |||
|
285 | c.at_version_pos = ChangesetComment.get_index_from_version( | |||
|
286 | c.at_version_num, versions) | |||
|
287 | ||||
|
288 | (prev_pull_request_latest, | |||
|
289 | prev_pull_request_at_ver, | |||
|
290 | prev_pull_request_display_obj, | |||
|
291 | prev_at_version) = self._get_pr_version( | |||
|
292 | pull_request_id, version=from_version) | |||
|
293 | ||||
|
294 | c.from_version = prev_at_version | |||
|
295 | c.from_version_num = (prev_at_version | |||
|
296 | if prev_at_version and prev_at_version != 'latest' | |||
|
297 | else None) | |||
|
298 | c.from_version_pos = ChangesetComment.get_index_from_version( | |||
|
299 | c.from_version_num, versions) | |||
|
300 | ||||
|
301 | # define if we're in COMPARE mode or VIEW at version mode | |||
|
302 | compare = at_version != prev_at_version | |||
|
303 | ||||
|
304 | # pull_requests repo_name we opened it against | |||
|
305 | # ie. target_repo must match | |||
|
306 | if self.db_repo_name != pull_request_at_ver.target_repo.repo_name: | |||
|
307 | raise HTTPNotFound() | |||
|
308 | ||||
|
309 | c.shadow_clone_url = PullRequestModel().get_shadow_clone_url( | |||
|
310 | pull_request_at_ver) | |||
|
311 | ||||
|
312 | c.pull_request = pull_request_display_obj | |||
|
313 | c.pull_request_latest = pull_request_latest | |||
|
314 | ||||
|
315 | if compare or (at_version and not at_version == 'latest'): | |||
|
316 | c.allowed_to_change_status = False | |||
|
317 | c.allowed_to_update = False | |||
|
318 | c.allowed_to_merge = False | |||
|
319 | c.allowed_to_delete = False | |||
|
320 | c.allowed_to_comment = False | |||
|
321 | c.allowed_to_close = False | |||
|
322 | else: | |||
|
323 | can_change_status = PullRequestModel().check_user_change_status( | |||
|
324 | pull_request_at_ver, self._rhodecode_user) | |||
|
325 | c.allowed_to_change_status = can_change_status and not pr_closed | |||
|
326 | ||||
|
327 | c.allowed_to_update = PullRequestModel().check_user_update( | |||
|
328 | pull_request_latest, self._rhodecode_user) and not pr_closed | |||
|
329 | c.allowed_to_merge = PullRequestModel().check_user_merge( | |||
|
330 | pull_request_latest, self._rhodecode_user) and not pr_closed | |||
|
331 | c.allowed_to_delete = PullRequestModel().check_user_delete( | |||
|
332 | pull_request_latest, self._rhodecode_user) and not pr_closed | |||
|
333 | c.allowed_to_comment = not pr_closed | |||
|
334 | c.allowed_to_close = c.allowed_to_merge and not pr_closed | |||
|
335 | ||||
|
336 | c.forbid_adding_reviewers = False | |||
|
337 | c.forbid_author_to_review = False | |||
|
338 | c.forbid_commit_author_to_review = False | |||
|
339 | ||||
|
340 | if pull_request_latest.reviewer_data and \ | |||
|
341 | 'rules' in pull_request_latest.reviewer_data: | |||
|
342 | rules = pull_request_latest.reviewer_data['rules'] or {} | |||
|
343 | try: | |||
|
344 | c.forbid_adding_reviewers = rules.get( | |||
|
345 | 'forbid_adding_reviewers') | |||
|
346 | c.forbid_author_to_review = rules.get( | |||
|
347 | 'forbid_author_to_review') | |||
|
348 | c.forbid_commit_author_to_review = rules.get( | |||
|
349 | 'forbid_commit_author_to_review') | |||
|
350 | except Exception: | |||
|
351 | pass | |||
|
352 | ||||
|
353 | # check merge capabilities | |||
|
354 | _merge_check = MergeCheck.validate( | |||
|
355 | pull_request_latest, user=self._rhodecode_user) | |||
|
356 | c.pr_merge_errors = _merge_check.error_details | |||
|
357 | c.pr_merge_possible = not _merge_check.failed | |||
|
358 | c.pr_merge_message = _merge_check.merge_msg | |||
|
359 | ||||
|
360 | c.pull_request_review_status = _merge_check.review_status | |||
|
361 | if merge_checks: | |||
|
362 | self.request.override_renderer = \ | |||
|
363 | 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako' | |||
|
364 | return self._get_template_context(c) | |||
|
365 | ||||
|
366 | comments_model = CommentsModel() | |||
|
367 | ||||
|
368 | # reviewers and statuses | |||
|
369 | c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses() | |||
|
370 | allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers] | |||
|
371 | ||||
|
372 | # GENERAL COMMENTS with versions # | |||
|
373 | q = comments_model._all_general_comments_of_pull_request(pull_request_latest) | |||
|
374 | q = q.order_by(ChangesetComment.comment_id.asc()) | |||
|
375 | general_comments = q | |||
|
376 | ||||
|
377 | # pick comments we want to render at current version | |||
|
378 | c.comment_versions = comments_model.aggregate_comments( | |||
|
379 | general_comments, versions, c.at_version_num) | |||
|
380 | c.comments = c.comment_versions[c.at_version_num]['until'] | |||
|
381 | ||||
|
382 | # INLINE COMMENTS with versions # | |||
|
383 | q = comments_model._all_inline_comments_of_pull_request(pull_request_latest) | |||
|
384 | q = q.order_by(ChangesetComment.comment_id.asc()) | |||
|
385 | inline_comments = q | |||
|
386 | ||||
|
387 | c.inline_versions = comments_model.aggregate_comments( | |||
|
388 | inline_comments, versions, c.at_version_num, inline=True) | |||
|
389 | ||||
|
390 | # inject latest version | |||
|
391 | latest_ver = PullRequest.get_pr_display_object( | |||
|
392 | pull_request_latest, pull_request_latest) | |||
|
393 | ||||
|
394 | c.versions = versions + [latest_ver] | |||
|
395 | ||||
|
396 | # if we use version, then do not show later comments | |||
|
397 | # than current version | |||
|
398 | display_inline_comments = collections.defaultdict( | |||
|
399 | lambda: collections.defaultdict(list)) | |||
|
400 | for co in inline_comments: | |||
|
401 | if c.at_version_num: | |||
|
402 | # pick comments that are at least UPTO given version, so we | |||
|
403 | # don't render comments for higher version | |||
|
404 | should_render = co.pull_request_version_id and \ | |||
|
405 | co.pull_request_version_id <= c.at_version_num | |||
|
406 | else: | |||
|
407 | # showing all, for 'latest' | |||
|
408 | should_render = True | |||
|
409 | ||||
|
410 | if should_render: | |||
|
411 | display_inline_comments[co.f_path][co.line_no].append(co) | |||
|
412 | ||||
|
413 | # load diff data into template context, if we use compare mode then | |||
|
414 | # diff is calculated based on changes between versions of PR | |||
|
415 | ||||
|
416 | source_repo = pull_request_at_ver.source_repo | |||
|
417 | source_ref_id = pull_request_at_ver.source_ref_parts.commit_id | |||
|
418 | ||||
|
419 | target_repo = pull_request_at_ver.target_repo | |||
|
420 | target_ref_id = pull_request_at_ver.target_ref_parts.commit_id | |||
|
421 | ||||
|
422 | if compare: | |||
|
423 | # in compare switch the diff base to latest commit from prev version | |||
|
424 | target_ref_id = prev_pull_request_display_obj.revisions[0] | |||
|
425 | ||||
|
426 | # despite opening commits for bookmarks/branches/tags, we always | |||
|
427 | # convert this to rev to prevent changes after bookmark or branch change | |||
|
428 | c.source_ref_type = 'rev' | |||
|
429 | c.source_ref = source_ref_id | |||
|
430 | ||||
|
431 | c.target_ref_type = 'rev' | |||
|
432 | c.target_ref = target_ref_id | |||
|
433 | ||||
|
434 | c.source_repo = source_repo | |||
|
435 | c.target_repo = target_repo | |||
|
436 | ||||
|
437 | c.commit_ranges = [] | |||
|
438 | source_commit = EmptyCommit() | |||
|
439 | target_commit = EmptyCommit() | |||
|
440 | c.missing_requirements = False | |||
|
441 | ||||
|
442 | source_scm = source_repo.scm_instance() | |||
|
443 | target_scm = target_repo.scm_instance() | |||
|
444 | ||||
|
445 | # try first shadow repo, fallback to regular repo | |||
|
446 | try: | |||
|
447 | commits_source_repo = pull_request_latest.get_shadow_repo() | |||
|
448 | except Exception: | |||
|
449 | log.debug('Failed to get shadow repo', exc_info=True) | |||
|
450 | commits_source_repo = source_scm | |||
|
451 | ||||
|
452 | c.commits_source_repo = commits_source_repo | |||
|
453 | commit_cache = {} | |||
|
454 | try: | |||
|
455 | pre_load = ["author", "branch", "date", "message"] | |||
|
456 | show_revs = pull_request_at_ver.revisions | |||
|
457 | for rev in show_revs: | |||
|
458 | comm = commits_source_repo.get_commit( | |||
|
459 | commit_id=rev, pre_load=pre_load) | |||
|
460 | c.commit_ranges.append(comm) | |||
|
461 | commit_cache[comm.raw_id] = comm | |||
|
462 | ||||
|
463 | # Order here matters, we first need to get target, and then | |||
|
464 | # the source | |||
|
465 | target_commit = commits_source_repo.get_commit( | |||
|
466 | commit_id=safe_str(target_ref_id)) | |||
|
467 | ||||
|
468 | source_commit = commits_source_repo.get_commit( | |||
|
469 | commit_id=safe_str(source_ref_id)) | |||
|
470 | ||||
|
471 | except CommitDoesNotExistError: | |||
|
472 | log.warning( | |||
|
473 | 'Failed to get commit from `{}` repo'.format( | |||
|
474 | commits_source_repo), exc_info=True) | |||
|
475 | except RepositoryRequirementError: | |||
|
476 | log.warning( | |||
|
477 | 'Failed to get all required data from repo', exc_info=True) | |||
|
478 | c.missing_requirements = True | |||
|
479 | ||||
|
480 | c.ancestor = None # set it to None, to hide it from PR view | |||
|
481 | ||||
|
482 | try: | |||
|
483 | ancestor_id = source_scm.get_common_ancestor( | |||
|
484 | source_commit.raw_id, target_commit.raw_id, target_scm) | |||
|
485 | c.ancestor_commit = source_scm.get_commit(ancestor_id) | |||
|
486 | except Exception: | |||
|
487 | c.ancestor_commit = None | |||
|
488 | ||||
|
489 | c.statuses = source_repo.statuses( | |||
|
490 | [x.raw_id for x in c.commit_ranges]) | |||
|
491 | ||||
|
492 | # auto collapse if we have more than limit | |||
|
493 | collapse_limit = diffs.DiffProcessor._collapse_commits_over | |||
|
494 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit | |||
|
495 | c.compare_mode = compare | |||
|
496 | ||||
|
497 | # diff_limit is the old behavior, will cut off the whole diff | |||
|
498 | # if the limit is applied otherwise will just hide the | |||
|
499 | # big files from the front-end | |||
|
500 | diff_limit = c.visual.cut_off_limit_diff | |||
|
501 | file_limit = c.visual.cut_off_limit_file | |||
|
502 | ||||
|
503 | c.missing_commits = False | |||
|
504 | if (c.missing_requirements | |||
|
505 | or isinstance(source_commit, EmptyCommit) | |||
|
506 | or source_commit == target_commit): | |||
|
507 | ||||
|
508 | c.missing_commits = True | |||
|
509 | else: | |||
|
510 | ||||
|
511 | c.diffset = self._get_diffset( | |||
|
512 | c.source_repo.repo_name, commits_source_repo, | |||
|
513 | source_ref_id, target_ref_id, | |||
|
514 | target_commit, source_commit, | |||
|
515 | diff_limit, c.fulldiff, file_limit, display_inline_comments) | |||
|
516 | ||||
|
517 | c.limited_diff = c.diffset.limited_diff | |||
|
518 | ||||
|
519 | # calculate removed files that are bound to comments | |||
|
520 | comment_deleted_files = [ | |||
|
521 | fname for fname in display_inline_comments | |||
|
522 | if fname not in c.diffset.file_stats] | |||
|
523 | ||||
|
524 | c.deleted_files_comments = collections.defaultdict(dict) | |||
|
525 | for fname, per_line_comments in display_inline_comments.items(): | |||
|
526 | if fname in comment_deleted_files: | |||
|
527 | c.deleted_files_comments[fname]['stats'] = 0 | |||
|
528 | c.deleted_files_comments[fname]['comments'] = list() | |||
|
529 | for lno, comments in per_line_comments.items(): | |||
|
530 | c.deleted_files_comments[fname]['comments'].extend( | |||
|
531 | comments) | |||
|
532 | ||||
|
533 | # this is a hack to properly display links, when creating PR, the | |||
|
534 | # compare view and others uses different notation, and | |||
|
535 | # compare_commits.mako renders links based on the target_repo. | |||
|
536 | # We need to swap that here to generate it properly on the html side | |||
|
537 | c.target_repo = c.source_repo | |||
|
538 | ||||
|
539 | c.commit_statuses = ChangesetStatus.STATUSES | |||
|
540 | ||||
|
541 | c.show_version_changes = not pr_closed | |||
|
542 | if c.show_version_changes: | |||
|
543 | cur_obj = pull_request_at_ver | |||
|
544 | prev_obj = prev_pull_request_at_ver | |||
|
545 | ||||
|
546 | old_commit_ids = prev_obj.revisions | |||
|
547 | new_commit_ids = cur_obj.revisions | |||
|
548 | commit_changes = PullRequestModel()._calculate_commit_id_changes( | |||
|
549 | old_commit_ids, new_commit_ids) | |||
|
550 | c.commit_changes_summary = commit_changes | |||
|
551 | ||||
|
552 | # calculate the diff for commits between versions | |||
|
553 | c.commit_changes = [] | |||
|
554 | mark = lambda cs, fw: list( | |||
|
555 | h.itertools.izip_longest([], cs, fillvalue=fw)) | |||
|
556 | for c_type, raw_id in mark(commit_changes.added, 'a') \ | |||
|
557 | + mark(commit_changes.removed, 'r') \ | |||
|
558 | + mark(commit_changes.common, 'c'): | |||
|
559 | ||||
|
560 | if raw_id in commit_cache: | |||
|
561 | commit = commit_cache[raw_id] | |||
|
562 | else: | |||
|
563 | try: | |||
|
564 | commit = commits_source_repo.get_commit(raw_id) | |||
|
565 | except CommitDoesNotExistError: | |||
|
566 | # in case we fail extracting still use "dummy" commit | |||
|
567 | # for display in commit diff | |||
|
568 | commit = h.AttributeDict( | |||
|
569 | {'raw_id': raw_id, | |||
|
570 | 'message': 'EMPTY or MISSING COMMIT'}) | |||
|
571 | c.commit_changes.append([c_type, commit]) | |||
|
572 | ||||
|
573 | # current user review statuses for each version | |||
|
574 | c.review_versions = {} | |||
|
575 | if self._rhodecode_user.user_id in allowed_reviewers: | |||
|
576 | for co in general_comments: | |||
|
577 | if co.author.user_id == self._rhodecode_user.user_id: | |||
|
578 | # each comment has a status change | |||
|
579 | status = co.status_change | |||
|
580 | if status: | |||
|
581 | _ver_pr = status[0].comment.pull_request_version_id | |||
|
582 | c.review_versions[_ver_pr] = status[0] | |||
|
583 | ||||
|
584 | return self._get_template_context(c) |
@@ -0,0 +1,64 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 logging | |||
|
22 | ||||
|
23 | from pyramid.view import view_config | |||
|
24 | ||||
|
25 | from rhodecode.apps._base import RepoAppView | |||
|
26 | from rhodecode.apps.repository.utils import get_default_reviewers_data | |||
|
27 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |||
|
28 | ||||
|
29 | log = logging.getLogger(__name__) | |||
|
30 | ||||
|
31 | ||||
|
32 | class RepoReviewRulesView(RepoAppView): | |||
|
33 | def load_default_context(self): | |||
|
34 | c = self._get_local_tmpl_context() | |||
|
35 | ||||
|
36 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
37 | c.repo_info = self.db_repo | |||
|
38 | ||||
|
39 | self._register_global_c(c) | |||
|
40 | return c | |||
|
41 | ||||
|
42 | @LoginRequired() | |||
|
43 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
44 | @view_config( | |||
|
45 | route_name='repo_reviewers', request_method='GET', | |||
|
46 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
47 | def repo_review_rules(self): | |||
|
48 | c = self.load_default_context() | |||
|
49 | c.active = 'reviewers' | |||
|
50 | ||||
|
51 | return self._get_template_context(c) | |||
|
52 | ||||
|
53 | @LoginRequired() | |||
|
54 | @HasRepoPermissionAnyDecorator( | |||
|
55 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
56 | @view_config( | |||
|
57 | route_name='repo_default_reviewers_data', request_method='GET', | |||
|
58 | renderer='json_ext') | |||
|
59 | def repo_default_reviewers_data(self): | |||
|
60 | review_data = get_default_reviewers_data( | |||
|
61 | self.db_repo.user, None, None, None, None) | |||
|
62 | return review_data | |||
|
63 | ||||
|
64 |
@@ -0,0 +1,179 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | ||||
|
23 | import deform | |||
|
24 | from pyramid.httpexceptions import HTTPFound | |||
|
25 | from pyramid.view import view_config | |||
|
26 | ||||
|
27 | from rhodecode.apps._base import RepoAppView | |||
|
28 | from rhodecode.forms import RcForm | |||
|
29 | from rhodecode.lib import helpers as h | |||
|
30 | from rhodecode.lib import audit_logger | |||
|
31 | from rhodecode.lib.auth import ( | |||
|
32 | LoginRequired, HasRepoPermissionAnyDecorator, | |||
|
33 | HasRepoPermissionAllDecorator, CSRFRequired) | |||
|
34 | from rhodecode.model.db import RepositoryField, RepoGroup | |||
|
35 | from rhodecode.model.meta import Session | |||
|
36 | from rhodecode.model.repo import RepoModel | |||
|
37 | from rhodecode.model.scm import RepoGroupList, ScmModel | |||
|
38 | from rhodecode.model.validation_schema.schemas import repo_schema | |||
|
39 | ||||
|
40 | log = logging.getLogger(__name__) | |||
|
41 | ||||
|
42 | ||||
|
43 | class RepoSettingsView(RepoAppView): | |||
|
44 | ||||
|
45 | def load_default_context(self): | |||
|
46 | c = self._get_local_tmpl_context() | |||
|
47 | ||||
|
48 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
49 | c.repo_info = self.db_repo | |||
|
50 | ||||
|
51 | acl_groups = RepoGroupList( | |||
|
52 | RepoGroup.query().all(), | |||
|
53 | perm_set=['group.write', 'group.admin']) | |||
|
54 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) | |||
|
55 | c.repo_groups_choices = map(lambda k: k[0], c.repo_groups) | |||
|
56 | ||||
|
57 | # in case someone no longer have a group.write access to a repository | |||
|
58 | # pre fill the list with this entry, we don't care if this is the same | |||
|
59 | # but it will allow saving repo data properly. | |||
|
60 | repo_group = self.db_repo.group | |||
|
61 | if repo_group and repo_group.group_id not in c.repo_groups_choices: | |||
|
62 | c.repo_groups_choices.append(repo_group.group_id) | |||
|
63 | c.repo_groups.append(RepoGroup._generate_choice(repo_group)) | |||
|
64 | ||||
|
65 | if c.repository_requirements_missing or self.rhodecode_vcs_repo is None: | |||
|
66 | # we might be in missing requirement state, so we load things | |||
|
67 | # without touching scm_instance() | |||
|
68 | c.landing_revs_choices, c.landing_revs = \ | |||
|
69 | ScmModel().get_repo_landing_revs() | |||
|
70 | else: | |||
|
71 | c.landing_revs_choices, c.landing_revs = \ | |||
|
72 | ScmModel().get_repo_landing_revs(self.db_repo) | |||
|
73 | ||||
|
74 | c.personal_repo_group = c.auth_user.personal_repo_group | |||
|
75 | c.repo_fields = RepositoryField.query()\ | |||
|
76 | .filter(RepositoryField.repository == self.db_repo).all() | |||
|
77 | ||||
|
78 | self._register_global_c(c) | |||
|
79 | return c | |||
|
80 | ||||
|
81 | def _get_schema(self, c, old_values=None): | |||
|
82 | return repo_schema.RepoSettingsSchema().bind( | |||
|
83 | repo_type=self.db_repo.repo_type, | |||
|
84 | repo_type_options=[self.db_repo.repo_type], | |||
|
85 | repo_ref_options=c.landing_revs_choices, | |||
|
86 | repo_ref_items=c.landing_revs, | |||
|
87 | repo_repo_group_options=c.repo_groups_choices, | |||
|
88 | repo_repo_group_items=c.repo_groups, | |||
|
89 | # user caller | |||
|
90 | user=self._rhodecode_user, | |||
|
91 | old_values=old_values | |||
|
92 | ) | |||
|
93 | ||||
|
94 | @LoginRequired() | |||
|
95 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
96 | @view_config( | |||
|
97 | route_name='edit_repo', request_method='GET', | |||
|
98 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
99 | def edit_settings(self): | |||
|
100 | c = self.load_default_context() | |||
|
101 | c.active = 'settings' | |||
|
102 | ||||
|
103 | defaults = RepoModel()._get_defaults(self.db_repo_name) | |||
|
104 | defaults['repo_owner'] = defaults['user'] | |||
|
105 | defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev'] | |||
|
106 | ||||
|
107 | schema = self._get_schema(c) | |||
|
108 | c.form = RcForm(schema, appstruct=defaults) | |||
|
109 | return self._get_template_context(c) | |||
|
110 | ||||
|
111 | @LoginRequired() | |||
|
112 | @HasRepoPermissionAllDecorator('repository.admin') | |||
|
113 | @CSRFRequired() | |||
|
114 | @view_config( | |||
|
115 | route_name='edit_repo', request_method='POST', | |||
|
116 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
117 | def edit_settings_update(self): | |||
|
118 | _ = self.request.translate | |||
|
119 | c = self.load_default_context() | |||
|
120 | c.active = 'settings' | |||
|
121 | old_repo_name = self.db_repo_name | |||
|
122 | ||||
|
123 | old_values = self.db_repo.get_api_data() | |||
|
124 | schema = self._get_schema(c, old_values=old_values) | |||
|
125 | ||||
|
126 | c.form = RcForm(schema) | |||
|
127 | pstruct = self.request.POST.items() | |||
|
128 | pstruct.append(('repo_type', self.db_repo.repo_type)) | |||
|
129 | try: | |||
|
130 | schema_data = c.form.validate(pstruct) | |||
|
131 | except deform.ValidationFailure as err_form: | |||
|
132 | return self._get_template_context(c) | |||
|
133 | ||||
|
134 | # data is now VALID, proceed with updates | |||
|
135 | # save validated data back into the updates dict | |||
|
136 | validated_updates = dict( | |||
|
137 | repo_name=schema_data['repo_group']['repo_name_without_group'], | |||
|
138 | repo_group=schema_data['repo_group']['repo_group_id'], | |||
|
139 | ||||
|
140 | user=schema_data['repo_owner'], | |||
|
141 | repo_description=schema_data['repo_description'], | |||
|
142 | repo_private=schema_data['repo_private'], | |||
|
143 | clone_uri=schema_data['repo_clone_uri'], | |||
|
144 | repo_landing_rev=schema_data['repo_landing_commit_ref'], | |||
|
145 | repo_enable_statistics=schema_data['repo_enable_statistics'], | |||
|
146 | repo_enable_locking=schema_data['repo_enable_locking'], | |||
|
147 | repo_enable_downloads=schema_data['repo_enable_downloads'], | |||
|
148 | ) | |||
|
149 | # detect if CLONE URI changed, if we get OLD means we keep old values | |||
|
150 | if schema_data['repo_clone_uri_change'] == 'OLD': | |||
|
151 | validated_updates['clone_uri'] = self.db_repo.clone_uri | |||
|
152 | ||||
|
153 | # use the new full name for redirect | |||
|
154 | new_repo_name = schema_data['repo_group']['repo_name_with_group'] | |||
|
155 | ||||
|
156 | # save extra fields into our validated data | |||
|
157 | for key, value in pstruct: | |||
|
158 | if key.startswith(RepositoryField.PREFIX): | |||
|
159 | validated_updates[key] = value | |||
|
160 | ||||
|
161 | try: | |||
|
162 | RepoModel().update(self.db_repo, **validated_updates) | |||
|
163 | ScmModel().mark_for_invalidation(new_repo_name) | |||
|
164 | ||||
|
165 | audit_logger.store_web( | |||
|
166 | 'repo.edit', action_data={'old_data': old_values}, | |||
|
167 | user=self._rhodecode_user, repo=self.db_repo) | |||
|
168 | ||||
|
169 | Session().commit() | |||
|
170 | ||||
|
171 | h.flash(_('Repository {} updated successfully').format( | |||
|
172 | old_repo_name), category='success') | |||
|
173 | except Exception: | |||
|
174 | log.exception("Exception during update of repository") | |||
|
175 | h.flash(_('Error occurred during update of repository {}').format( | |||
|
176 | old_repo_name), category='error') | |||
|
177 | ||||
|
178 | raise HTTPFound( | |||
|
179 | self.request.route_path('edit_repo', repo_name=new_repo_name)) |
@@ -0,0 +1,226 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | ||||
|
23 | from pyramid.view import view_config | |||
|
24 | from pyramid.httpexceptions import HTTPFound | |||
|
25 | ||||
|
26 | from rhodecode.apps._base import RepoAppView | |||
|
27 | from rhodecode.lib import helpers as h | |||
|
28 | from rhodecode.lib import audit_logger | |||
|
29 | from rhodecode.lib.auth import ( | |||
|
30 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) | |||
|
31 | from rhodecode.lib.exceptions import AttachedForksError | |||
|
32 | from rhodecode.lib.utils2 import safe_int | |||
|
33 | from rhodecode.lib.vcs import RepositoryError | |||
|
34 | from rhodecode.model.db import Session, UserFollowing, User, Repository | |||
|
35 | from rhodecode.model.repo import RepoModel | |||
|
36 | from rhodecode.model.scm import ScmModel | |||
|
37 | ||||
|
38 | log = logging.getLogger(__name__) | |||
|
39 | ||||
|
40 | ||||
|
41 | class RepoSettingsView(RepoAppView): | |||
|
42 | ||||
|
43 | def load_default_context(self): | |||
|
44 | c = self._get_local_tmpl_context() | |||
|
45 | ||||
|
46 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
47 | c.repo_info = self.db_repo | |||
|
48 | ||||
|
49 | self._register_global_c(c) | |||
|
50 | return c | |||
|
51 | ||||
|
52 | @LoginRequired() | |||
|
53 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
54 | @view_config( | |||
|
55 | route_name='edit_repo_advanced', request_method='GET', | |||
|
56 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
57 | def edit_advanced(self): | |||
|
58 | c = self.load_default_context() | |||
|
59 | c.active = 'advanced' | |||
|
60 | ||||
|
61 | c.default_user_id = User.get_default_user().user_id | |||
|
62 | c.in_public_journal = UserFollowing.query() \ | |||
|
63 | .filter(UserFollowing.user_id == c.default_user_id) \ | |||
|
64 | .filter(UserFollowing.follows_repository == c.repo_info).scalar() | |||
|
65 | ||||
|
66 | c.has_origin_repo_read_perm = False | |||
|
67 | if self.db_repo.fork: | |||
|
68 | c.has_origin_repo_read_perm = h.HasRepoPermissionAny( | |||
|
69 | 'repository.write', 'repository.read', 'repository.admin')( | |||
|
70 | self.db_repo.fork.repo_name, 'repo set as fork page') | |||
|
71 | ||||
|
72 | return self._get_template_context(c) | |||
|
73 | ||||
|
74 | @LoginRequired() | |||
|
75 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
76 | @CSRFRequired() | |||
|
77 | @view_config( | |||
|
78 | route_name='edit_repo_advanced_delete', request_method='POST', | |||
|
79 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
80 | def edit_advanced_delete(self): | |||
|
81 | """ | |||
|
82 | Deletes the repository, or shows warnings if deletion is not possible | |||
|
83 | because of attached forks or other errors. | |||
|
84 | """ | |||
|
85 | _ = self.request.translate | |||
|
86 | handle_forks = self.request.POST.get('forks', None) | |||
|
87 | ||||
|
88 | try: | |||
|
89 | _forks = self.db_repo.forks.count() | |||
|
90 | if _forks and handle_forks: | |||
|
91 | if handle_forks == 'detach_forks': | |||
|
92 | handle_forks = 'detach' | |||
|
93 | h.flash(_('Detached %s forks') % _forks, category='success') | |||
|
94 | elif handle_forks == 'delete_forks': | |||
|
95 | handle_forks = 'delete' | |||
|
96 | h.flash(_('Deleted %s forks') % _forks, category='success') | |||
|
97 | ||||
|
98 | old_data = self.db_repo.get_api_data() | |||
|
99 | RepoModel().delete(self.db_repo, forks=handle_forks) | |||
|
100 | ||||
|
101 | repo = audit_logger.RepoWrap(repo_id=None, | |||
|
102 | repo_name=self.db_repo.repo_name) | |||
|
103 | audit_logger.store_web( | |||
|
104 | 'repo.delete', action_data={'old_data': old_data}, | |||
|
105 | user=self._rhodecode_user, repo=repo) | |||
|
106 | ||||
|
107 | ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) | |||
|
108 | h.flash( | |||
|
109 | _('Deleted repository `%s`') % self.db_repo_name, | |||
|
110 | category='success') | |||
|
111 | Session().commit() | |||
|
112 | except AttachedForksError: | |||
|
113 | repo_advanced_url = h.route_path( | |||
|
114 | 'edit_repo_advanced', repo_name=self.db_repo_name, | |||
|
115 | _anchor='advanced-delete') | |||
|
116 | delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url) | |||
|
117 | h.flash(_('Cannot delete `{repo}` it still contains attached forks. ' | |||
|
118 | 'Try using {delete_or_detach} option.') | |||
|
119 | .format(repo=self.db_repo_name, delete_or_detach=delete_anchor), | |||
|
120 | category='warning') | |||
|
121 | ||||
|
122 | # redirect to advanced for forks handle action ? | |||
|
123 | raise HTTPFound(repo_advanced_url) | |||
|
124 | ||||
|
125 | except Exception: | |||
|
126 | log.exception("Exception during deletion of repository") | |||
|
127 | h.flash(_('An error occurred during deletion of `%s`') | |||
|
128 | % self.db_repo_name, category='error') | |||
|
129 | # redirect to advanced for more deletion options | |||
|
130 | raise HTTPFound( | |||
|
131 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name), | |||
|
132 | _anchor='advanced-delete') | |||
|
133 | ||||
|
134 | raise HTTPFound(h.route_path('home')) | |||
|
135 | ||||
|
136 | @LoginRequired() | |||
|
137 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
138 | @CSRFRequired() | |||
|
139 | @view_config( | |||
|
140 | route_name='edit_repo_advanced_journal', request_method='POST', | |||
|
141 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
142 | def edit_advanced_journal(self): | |||
|
143 | """ | |||
|
144 | Set's this repository to be visible in public journal, | |||
|
145 | in other words making default user to follow this repo | |||
|
146 | """ | |||
|
147 | _ = self.request.translate | |||
|
148 | ||||
|
149 | try: | |||
|
150 | user_id = User.get_default_user().user_id | |||
|
151 | ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id) | |||
|
152 | h.flash(_('Updated repository visibility in public journal'), | |||
|
153 | category='success') | |||
|
154 | Session().commit() | |||
|
155 | except Exception: | |||
|
156 | h.flash(_('An error occurred during setting this ' | |||
|
157 | 'repository in public journal'), | |||
|
158 | category='error') | |||
|
159 | ||||
|
160 | raise HTTPFound( | |||
|
161 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) | |||
|
162 | ||||
|
163 | @LoginRequired() | |||
|
164 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
165 | @CSRFRequired() | |||
|
166 | @view_config( | |||
|
167 | route_name='edit_repo_advanced_fork', request_method='POST', | |||
|
168 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
169 | def edit_advanced_fork(self): | |||
|
170 | """ | |||
|
171 | Mark given repository as a fork of another | |||
|
172 | """ | |||
|
173 | _ = self.request.translate | |||
|
174 | ||||
|
175 | new_fork_id = self.request.POST.get('id_fork_of') | |||
|
176 | try: | |||
|
177 | ||||
|
178 | if new_fork_id and not new_fork_id.isdigit(): | |||
|
179 | log.error('Given fork id %s is not an INT', new_fork_id) | |||
|
180 | ||||
|
181 | fork_id = safe_int(new_fork_id) | |||
|
182 | repo = ScmModel().mark_as_fork( | |||
|
183 | self.db_repo_name, fork_id, self._rhodecode_user.user_id) | |||
|
184 | fork = repo.fork.repo_name if repo.fork else _('Nothing') | |||
|
185 | Session().commit() | |||
|
186 | h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork), | |||
|
187 | category='success') | |||
|
188 | except RepositoryError as e: | |||
|
189 | log.exception("Repository Error occurred") | |||
|
190 | h.flash(str(e), category='error') | |||
|
191 | except Exception as e: | |||
|
192 | log.exception("Exception while editing fork") | |||
|
193 | h.flash(_('An error occurred during this operation'), | |||
|
194 | category='error') | |||
|
195 | ||||
|
196 | raise HTTPFound( | |||
|
197 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) | |||
|
198 | ||||
|
199 | @LoginRequired() | |||
|
200 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
201 | @CSRFRequired() | |||
|
202 | @view_config( | |||
|
203 | route_name='edit_repo_advanced_locking', request_method='POST', | |||
|
204 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') | |||
|
205 | def edit_advanced_locking(self): | |||
|
206 | """ | |||
|
207 | Toggle locking of repository | |||
|
208 | """ | |||
|
209 | _ = self.request.translate | |||
|
210 | set_lock = self.request.POST.get('set_lock') | |||
|
211 | set_unlock = self.request.POST.get('set_unlock') | |||
|
212 | ||||
|
213 | try: | |||
|
214 | if set_lock: | |||
|
215 | Repository.lock(self.db_repo, self._rhodecode_user.user_id, | |||
|
216 | lock_reason=Repository.LOCK_WEB) | |||
|
217 | h.flash(_('Locked repository'), category='success') | |||
|
218 | elif set_unlock: | |||
|
219 | Repository.unlock(self.db_repo) | |||
|
220 | h.flash(_('Unlocked repository'), category='success') | |||
|
221 | except Exception as e: | |||
|
222 | log.exception("Exception during unlocking") | |||
|
223 | h.flash(_('An error occurred during unlocking'), category='error') | |||
|
224 | ||||
|
225 | raise HTTPFound( | |||
|
226 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) |
@@ -0,0 +1,368 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | import string | |||
|
23 | ||||
|
24 | from pyramid.view import view_config | |||
|
25 | ||||
|
26 | from beaker.cache import cache_region | |||
|
27 | ||||
|
28 | ||||
|
29 | from rhodecode.controllers import utils | |||
|
30 | ||||
|
31 | from rhodecode.apps._base import RepoAppView | |||
|
32 | from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP) | |||
|
33 | from rhodecode.lib import caches, helpers as h | |||
|
34 | from rhodecode.lib.helpers import RepoPage | |||
|
35 | from rhodecode.lib.utils2 import safe_str, safe_int | |||
|
36 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |||
|
37 | from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links | |||
|
38 | from rhodecode.lib.ext_json import json | |||
|
39 | from rhodecode.lib.vcs.backends.base import EmptyCommit | |||
|
40 | from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError | |||
|
41 | from rhodecode.model.db import Statistics, CacheKey, User | |||
|
42 | from rhodecode.model.meta import Session | |||
|
43 | from rhodecode.model.repo import ReadmeFinder | |||
|
44 | from rhodecode.model.scm import ScmModel | |||
|
45 | ||||
|
46 | log = logging.getLogger(__name__) | |||
|
47 | ||||
|
48 | ||||
|
49 | class RepoSummaryView(RepoAppView): | |||
|
50 | ||||
|
51 | def load_default_context(self): | |||
|
52 | c = self._get_local_tmpl_context(include_app_defaults=True) | |||
|
53 | ||||
|
54 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
55 | c.repo_info = self.db_repo | |||
|
56 | c.rhodecode_repo = None | |||
|
57 | if not c.repository_requirements_missing: | |||
|
58 | c.rhodecode_repo = self.rhodecode_vcs_repo | |||
|
59 | ||||
|
60 | self._register_global_c(c) | |||
|
61 | return c | |||
|
62 | ||||
|
63 | def _get_readme_data(self, db_repo, default_renderer): | |||
|
64 | repo_name = db_repo.repo_name | |||
|
65 | log.debug('Looking for README file') | |||
|
66 | ||||
|
67 | @cache_region('long_term') | |||
|
68 | def _generate_readme(cache_key): | |||
|
69 | readme_data = None | |||
|
70 | readme_node = None | |||
|
71 | readme_filename = None | |||
|
72 | commit = self._get_landing_commit_or_none(db_repo) | |||
|
73 | if commit: | |||
|
74 | log.debug("Searching for a README file.") | |||
|
75 | readme_node = ReadmeFinder(default_renderer).search(commit) | |||
|
76 | if readme_node: | |||
|
77 | relative_url = h.url('files_raw_home', | |||
|
78 | repo_name=repo_name, | |||
|
79 | revision=commit.raw_id, | |||
|
80 | f_path=readme_node.path) | |||
|
81 | readme_data = self._render_readme_or_none( | |||
|
82 | commit, readme_node, relative_url) | |||
|
83 | readme_filename = readme_node.path | |||
|
84 | return readme_data, readme_filename | |||
|
85 | ||||
|
86 | invalidator_context = CacheKey.repo_context_cache( | |||
|
87 | _generate_readme, repo_name, CacheKey.CACHE_TYPE_README) | |||
|
88 | ||||
|
89 | with invalidator_context as context: | |||
|
90 | context.invalidate() | |||
|
91 | computed = context.compute() | |||
|
92 | ||||
|
93 | return computed | |||
|
94 | ||||
|
95 | def _get_landing_commit_or_none(self, db_repo): | |||
|
96 | log.debug("Getting the landing commit.") | |||
|
97 | try: | |||
|
98 | commit = db_repo.get_landing_commit() | |||
|
99 | if not isinstance(commit, EmptyCommit): | |||
|
100 | return commit | |||
|
101 | else: | |||
|
102 | log.debug("Repository is empty, no README to render.") | |||
|
103 | except CommitError: | |||
|
104 | log.exception( | |||
|
105 | "Problem getting commit when trying to render the README.") | |||
|
106 | ||||
|
107 | def _render_readme_or_none(self, commit, readme_node, relative_url): | |||
|
108 | log.debug( | |||
|
109 | 'Found README file `%s` rendering...', readme_node.path) | |||
|
110 | renderer = MarkupRenderer() | |||
|
111 | try: | |||
|
112 | html_source = renderer.render( | |||
|
113 | readme_node.content, filename=readme_node.path) | |||
|
114 | if relative_url: | |||
|
115 | return relative_links(html_source, relative_url) | |||
|
116 | return html_source | |||
|
117 | except Exception: | |||
|
118 | log.exception( | |||
|
119 | "Exception while trying to render the README") | |||
|
120 | ||||
|
121 | def _load_commits_context(self, c): | |||
|
122 | p = safe_int(self.request.GET.get('page'), 1) | |||
|
123 | size = safe_int(self.request.GET.get('size'), 10) | |||
|
124 | ||||
|
125 | def url_generator(**kw): | |||
|
126 | query_params = { | |||
|
127 | 'size': size | |||
|
128 | } | |||
|
129 | query_params.update(kw) | |||
|
130 | return h.route_path( | |||
|
131 | 'repo_summary_commits', | |||
|
132 | repo_name=c.rhodecode_db_repo.repo_name, _query=query_params) | |||
|
133 | ||||
|
134 | pre_load = ['author', 'branch', 'date', 'message'] | |||
|
135 | try: | |||
|
136 | collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load) | |||
|
137 | except EmptyRepositoryError: | |||
|
138 | collection = self.rhodecode_vcs_repo | |||
|
139 | ||||
|
140 | c.repo_commits = RepoPage( | |||
|
141 | collection, page=p, items_per_page=size, url=url_generator) | |||
|
142 | page_ids = [x.raw_id for x in c.repo_commits] | |||
|
143 | c.comments = self.db_repo.get_comments(page_ids) | |||
|
144 | c.statuses = self.db_repo.statuses(page_ids) | |||
|
145 | ||||
|
146 | @LoginRequired() | |||
|
147 | @HasRepoPermissionAnyDecorator( | |||
|
148 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
149 | @view_config( | |||
|
150 | route_name='repo_summary_commits', request_method='GET', | |||
|
151 | renderer='rhodecode:templates/summary/summary_commits.mako') | |||
|
152 | def summary_commits(self): | |||
|
153 | c = self.load_default_context() | |||
|
154 | self._load_commits_context(c) | |||
|
155 | return self._get_template_context(c) | |||
|
156 | ||||
|
157 | @LoginRequired() | |||
|
158 | @HasRepoPermissionAnyDecorator( | |||
|
159 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
160 | @view_config( | |||
|
161 | route_name='repo_summary', request_method='GET', | |||
|
162 | renderer='rhodecode:templates/summary/summary.mako') | |||
|
163 | @view_config( | |||
|
164 | route_name='repo_summary_slash', request_method='GET', | |||
|
165 | renderer='rhodecode:templates/summary/summary.mako') | |||
|
166 | def summary(self): | |||
|
167 | c = self.load_default_context() | |||
|
168 | ||||
|
169 | # Prepare the clone URL | |||
|
170 | username = '' | |||
|
171 | if self._rhodecode_user.username != User.DEFAULT_USER: | |||
|
172 | username = safe_str(self._rhodecode_user.username) | |||
|
173 | ||||
|
174 | _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl | |||
|
175 | if '{repo}' in _def_clone_uri: | |||
|
176 | _def_clone_uri_by_id = _def_clone_uri.replace( | |||
|
177 | '{repo}', '_{repoid}') | |||
|
178 | elif '{repoid}' in _def_clone_uri: | |||
|
179 | _def_clone_uri_by_id = _def_clone_uri.replace( | |||
|
180 | '_{repoid}', '{repo}') | |||
|
181 | ||||
|
182 | c.clone_repo_url = self.db_repo.clone_url( | |||
|
183 | user=username, uri_tmpl=_def_clone_uri) | |||
|
184 | c.clone_repo_url_id = self.db_repo.clone_url( | |||
|
185 | user=username, uri_tmpl=_def_clone_uri_by_id) | |||
|
186 | ||||
|
187 | # If enabled, get statistics data | |||
|
188 | ||||
|
189 | c.show_stats = bool(self.db_repo.enable_statistics) | |||
|
190 | ||||
|
191 | stats = Session().query(Statistics) \ | |||
|
192 | .filter(Statistics.repository == self.db_repo) \ | |||
|
193 | .scalar() | |||
|
194 | ||||
|
195 | c.stats_percentage = 0 | |||
|
196 | ||||
|
197 | if stats and stats.languages: | |||
|
198 | c.no_data = False is self.db_repo.enable_statistics | |||
|
199 | lang_stats_d = json.loads(stats.languages) | |||
|
200 | ||||
|
201 | # Sort first by decreasing count and second by the file extension, | |||
|
202 | # so we have a consistent output. | |||
|
203 | lang_stats_items = sorted(lang_stats_d.iteritems(), | |||
|
204 | key=lambda k: (-k[1], k[0]))[:10] | |||
|
205 | lang_stats = [(x, {"count": y, | |||
|
206 | "desc": LANGUAGES_EXTENSIONS_MAP.get(x)}) | |||
|
207 | for x, y in lang_stats_items] | |||
|
208 | ||||
|
209 | c.trending_languages = json.dumps(lang_stats) | |||
|
210 | else: | |||
|
211 | c.no_data = True | |||
|
212 | c.trending_languages = json.dumps({}) | |||
|
213 | ||||
|
214 | scm_model = ScmModel() | |||
|
215 | c.enable_downloads = self.db_repo.enable_downloads | |||
|
216 | c.repository_followers = scm_model.get_followers(self.db_repo) | |||
|
217 | c.repository_forks = scm_model.get_forks(self.db_repo) | |||
|
218 | c.repository_is_user_following = scm_model.is_following_repo( | |||
|
219 | self.db_repo_name, self._rhodecode_user.user_id) | |||
|
220 | ||||
|
221 | # first interaction with the VCS instance after here... | |||
|
222 | if c.repository_requirements_missing: | |||
|
223 | self.request.override_renderer = \ | |||
|
224 | 'rhodecode:templates/summary/missing_requirements.mako' | |||
|
225 | return self._get_template_context(c) | |||
|
226 | ||||
|
227 | c.readme_data, c.readme_file = \ | |||
|
228 | self._get_readme_data(self.db_repo, c.visual.default_renderer) | |||
|
229 | ||||
|
230 | # loads the summary commits template context | |||
|
231 | self._load_commits_context(c) | |||
|
232 | ||||
|
233 | return self._get_template_context(c) | |||
|
234 | ||||
|
235 | def get_request_commit_id(self): | |||
|
236 | return self.request.matchdict['commit_id'] | |||
|
237 | ||||
|
238 | @LoginRequired() | |||
|
239 | @HasRepoPermissionAnyDecorator( | |||
|
240 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
241 | @view_config( | |||
|
242 | route_name='repo_stats', request_method='GET', | |||
|
243 | renderer='json_ext') | |||
|
244 | def repo_stats(self): | |||
|
245 | commit_id = self.get_request_commit_id() | |||
|
246 | ||||
|
247 | _namespace = caches.get_repo_namespace_key( | |||
|
248 | caches.SUMMARY_STATS, self.db_repo_name) | |||
|
249 | show_stats = bool(self.db_repo.enable_statistics) | |||
|
250 | cache_manager = caches.get_cache_manager( | |||
|
251 | 'repo_cache_long', _namespace) | |||
|
252 | _cache_key = caches.compute_key_from_params( | |||
|
253 | self.db_repo_name, commit_id, show_stats) | |||
|
254 | ||||
|
255 | def compute_stats(): | |||
|
256 | code_stats = {} | |||
|
257 | size = 0 | |||
|
258 | try: | |||
|
259 | scm_instance = self.db_repo.scm_instance() | |||
|
260 | commit = scm_instance.get_commit(commit_id) | |||
|
261 | ||||
|
262 | for node in commit.get_filenodes_generator(): | |||
|
263 | size += node.size | |||
|
264 | if not show_stats: | |||
|
265 | continue | |||
|
266 | ext = string.lower(node.extension) | |||
|
267 | ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext) | |||
|
268 | if ext_info: | |||
|
269 | if ext in code_stats: | |||
|
270 | code_stats[ext]['count'] += 1 | |||
|
271 | else: | |||
|
272 | code_stats[ext] = {"count": 1, "desc": ext_info} | |||
|
273 | except EmptyRepositoryError: | |||
|
274 | pass | |||
|
275 | return {'size': h.format_byte_size_binary(size), | |||
|
276 | 'code_stats': code_stats} | |||
|
277 | ||||
|
278 | stats = cache_manager.get(_cache_key, createfunc=compute_stats) | |||
|
279 | return stats | |||
|
280 | ||||
|
281 | @LoginRequired() | |||
|
282 | @HasRepoPermissionAnyDecorator( | |||
|
283 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
284 | @view_config( | |||
|
285 | route_name='repo_refs_data', request_method='GET', | |||
|
286 | renderer='json_ext') | |||
|
287 | def repo_refs_data(self): | |||
|
288 | _ = self.request.translate | |||
|
289 | self.load_default_context() | |||
|
290 | ||||
|
291 | repo = self.rhodecode_vcs_repo | |||
|
292 | refs_to_create = [ | |||
|
293 | (_("Branch"), repo.branches, 'branch'), | |||
|
294 | (_("Tag"), repo.tags, 'tag'), | |||
|
295 | (_("Bookmark"), repo.bookmarks, 'book'), | |||
|
296 | ] | |||
|
297 | res = self._create_reference_data( | |||
|
298 | repo, self.db_repo_name, refs_to_create) | |||
|
299 | data = { | |||
|
300 | 'more': False, | |||
|
301 | 'results': res | |||
|
302 | } | |||
|
303 | return data | |||
|
304 | ||||
|
305 | @LoginRequired() | |||
|
306 | @HasRepoPermissionAnyDecorator( | |||
|
307 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
308 | @view_config( | |||
|
309 | route_name='repo_refs_changelog_data', request_method='GET', | |||
|
310 | renderer='json_ext') | |||
|
311 | def repo_refs_changelog_data(self): | |||
|
312 | _ = self.request.translate | |||
|
313 | self.load_default_context() | |||
|
314 | ||||
|
315 | repo = self.rhodecode_vcs_repo | |||
|
316 | ||||
|
317 | refs_to_create = [ | |||
|
318 | (_("Branches"), repo.branches, 'branch'), | |||
|
319 | (_("Closed branches"), repo.branches_closed, 'branch_closed'), | |||
|
320 | # TODO: enable when vcs can handle bookmarks filters | |||
|
321 | # (_("Bookmarks"), repo.bookmarks, "book"), | |||
|
322 | ] | |||
|
323 | res = self._create_reference_data( | |||
|
324 | repo, self.db_repo_name, refs_to_create) | |||
|
325 | data = { | |||
|
326 | 'more': False, | |||
|
327 | 'results': res | |||
|
328 | } | |||
|
329 | return data | |||
|
330 | ||||
|
331 | def _create_reference_data(self, repo, full_repo_name, refs_to_create): | |||
|
332 | format_ref_id = utils.get_format_ref_id(repo) | |||
|
333 | ||||
|
334 | result = [] | |||
|
335 | for title, refs, ref_type in refs_to_create: | |||
|
336 | if refs: | |||
|
337 | result.append({ | |||
|
338 | 'text': title, | |||
|
339 | 'children': self._create_reference_items( | |||
|
340 | repo, full_repo_name, refs, ref_type, | |||
|
341 | format_ref_id), | |||
|
342 | }) | |||
|
343 | return result | |||
|
344 | ||||
|
345 | def _create_reference_items(self, repo, full_repo_name, refs, ref_type, | |||
|
346 | format_ref_id): | |||
|
347 | result = [] | |||
|
348 | is_svn = h.is_svn(repo) | |||
|
349 | for ref_name, raw_id in refs.iteritems(): | |||
|
350 | files_url = self._create_files_url( | |||
|
351 | repo, full_repo_name, ref_name, raw_id, is_svn) | |||
|
352 | result.append({ | |||
|
353 | 'text': ref_name, | |||
|
354 | 'id': format_ref_id(ref_name, raw_id), | |||
|
355 | 'raw_id': raw_id, | |||
|
356 | 'type': ref_type, | |||
|
357 | 'files_url': files_url, | |||
|
358 | }) | |||
|
359 | return result | |||
|
360 | ||||
|
361 | def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn): | |||
|
362 | use_commit_id = '/' in ref_name or is_svn | |||
|
363 | return h.url( | |||
|
364 | 'files_home', | |||
|
365 | repo_name=full_repo_name, | |||
|
366 | f_path=ref_name if is_svn else '', | |||
|
367 | revision=raw_id if use_commit_id else ref_name, | |||
|
368 | at=ref_name) |
@@ -0,0 +1,45 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | from pyramid.view import view_config | |||
|
23 | ||||
|
24 | from rhodecode.apps._base import BaseReferencesView | |||
|
25 | from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) | |||
|
26 | ||||
|
27 | log = logging.getLogger(__name__) | |||
|
28 | ||||
|
29 | ||||
|
30 | class RepoTagsView(BaseReferencesView): | |||
|
31 | ||||
|
32 | @LoginRequired() | |||
|
33 | @HasRepoPermissionAnyDecorator( | |||
|
34 | 'repository.read', 'repository.write', 'repository.admin') | |||
|
35 | @view_config( | |||
|
36 | route_name='tags_home', request_method='GET', | |||
|
37 | renderer='rhodecode:templates/tags/tags.mako') | |||
|
38 | def tags(self): | |||
|
39 | c = self.load_default_context() | |||
|
40 | ||||
|
41 | ref_items = self.rhodecode_vcs_repo.tags.items() | |||
|
42 | self.load_refs_context( | |||
|
43 | ref_items=ref_items, partials_template='tags/tags_data.mako') | |||
|
44 | ||||
|
45 | return self._get_template_context(c) |
@@ -0,0 +1,44 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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 | from rhodecode.apps._base import ADMIN_PREFIX | |||
|
21 | ||||
|
22 | ||||
|
23 | def includeme(config): | |||
|
24 | ||||
|
25 | config.add_route( | |||
|
26 | name='search', | |||
|
27 | pattern=ADMIN_PREFIX + '/search') | |||
|
28 | ||||
|
29 | config.add_route( | |||
|
30 | name='search_repo', | |||
|
31 | pattern='/{repo_name:.*?[^/]}/search', repo_route=True) | |||
|
32 | ||||
|
33 | # Scan module for configuration decorators. | |||
|
34 | config.scan() | |||
|
35 | ||||
|
36 | ||||
|
37 | # # FULL TEXT SEARCH | |||
|
38 | # rmap.connect('search', '%s/search' % (ADMIN_PREFIX,), | |||
|
39 | # controller='search') | |||
|
40 | # rmap.connect('search_repo_home', '/{repo_name}/search', | |||
|
41 | # controller='search', | |||
|
42 | # action='index', | |||
|
43 | # conditions={'function': check_repo}, | |||
|
44 | # requirements=URL_NAME_REQUIREMENTS) No newline at end of file |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 |
@@ -0,0 +1,202 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 os | |||
|
22 | ||||
|
23 | import mock | |||
|
24 | import pytest | |||
|
25 | from whoosh import query | |||
|
26 | ||||
|
27 | from rhodecode.tests import ( | |||
|
28 | TestController, SkipTest, HG_REPO, | |||
|
29 | TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) | |||
|
30 | from rhodecode.tests.utils import AssertResponse | |||
|
31 | ||||
|
32 | ||||
|
33 | def route_path(name, **kwargs): | |||
|
34 | from rhodecode.apps._base import ADMIN_PREFIX | |||
|
35 | return { | |||
|
36 | 'search': | |||
|
37 | ADMIN_PREFIX + '/search', | |||
|
38 | 'search_repo': | |||
|
39 | '/{repo_name}/search', | |||
|
40 | ||||
|
41 | }[name].format(**kwargs) | |||
|
42 | ||||
|
43 | ||||
|
44 | class TestSearchController(TestController): | |||
|
45 | ||||
|
46 | def test_index(self): | |||
|
47 | self.log_user() | |||
|
48 | response = self.app.get(route_path('search')) | |||
|
49 | assert_response = AssertResponse(response) | |||
|
50 | assert_response.one_element_exists('input#q') | |||
|
51 | ||||
|
52 | def test_search_files_empty_search(self): | |||
|
53 | if os.path.isdir(self.index_location): | |||
|
54 | raise SkipTest('skipped due to existing index') | |||
|
55 | else: | |||
|
56 | self.log_user() | |||
|
57 | response = self.app.get(route_path('search'), | |||
|
58 | {'q': HG_REPO}) | |||
|
59 | response.mustcontain('There is no index to search in. ' | |||
|
60 | 'Please run whoosh indexer') | |||
|
61 | ||||
|
62 | def test_search_validation(self): | |||
|
63 | self.log_user() | |||
|
64 | response = self.app.get(route_path('search'), | |||
|
65 | {'q': query, 'type': 'content', 'page_limit': 1000}) | |||
|
66 | ||||
|
67 | response.mustcontain( | |||
|
68 | 'page_limit - 1000 is greater than maximum value 500') | |||
|
69 | ||||
|
70 | @pytest.mark.parametrize("query, expected_hits, expected_paths", [ | |||
|
71 | ('todo', 23, [ | |||
|
72 | 'vcs/backends/hg/inmemory.py', | |||
|
73 | 'vcs/tests/test_git.py']), | |||
|
74 | ('extension:rst installation', 6, [ | |||
|
75 | 'docs/index.rst', | |||
|
76 | 'docs/installation.rst']), | |||
|
77 | ('def repo', 87, [ | |||
|
78 | 'vcs/tests/test_git.py', | |||
|
79 | 'vcs/tests/test_changesets.py']), | |||
|
80 | ('repository:%s def test' % HG_REPO, 18, [ | |||
|
81 | 'vcs/tests/test_git.py', | |||
|
82 | 'vcs/tests/test_changesets.py']), | |||
|
83 | ('"def main"', 9, [ | |||
|
84 | 'vcs/__init__.py', | |||
|
85 | 'vcs/tests/__init__.py', | |||
|
86 | 'vcs/utils/progressbar.py']), | |||
|
87 | ('owner:test_admin', 358, [ | |||
|
88 | 'vcs/tests/base.py', | |||
|
89 | 'MANIFEST.in', | |||
|
90 | 'vcs/utils/termcolors.py', | |||
|
91 | 'docs/theme/ADC/static/documentation.png']), | |||
|
92 | ('owner:test_admin def main', 72, [ | |||
|
93 | 'vcs/__init__.py', | |||
|
94 | 'vcs/tests/test_utils_filesize.py', | |||
|
95 | 'vcs/tests/test_cli.py']), | |||
|
96 | ('owner:michał test', 0, []), | |||
|
97 | ]) | |||
|
98 | def test_search_files(self, query, expected_hits, expected_paths): | |||
|
99 | self.log_user() | |||
|
100 | response = self.app.get(route_path('search'), | |||
|
101 | {'q': query, 'type': 'content', 'page_limit': 500}) | |||
|
102 | ||||
|
103 | response.mustcontain('%s results' % expected_hits) | |||
|
104 | for path in expected_paths: | |||
|
105 | response.mustcontain(path) | |||
|
106 | ||||
|
107 | @pytest.mark.parametrize("query, expected_hits, expected_commits", [ | |||
|
108 | ('bother to ask where to fetch repo during tests', 3, [ | |||
|
109 | ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'), | |||
|
110 | ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'), | |||
|
111 | ('svn', '98')]), | |||
|
112 | ('michał', 0, []), | |||
|
113 | ('changed:tests/utils.py', 36, [ | |||
|
114 | ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]), | |||
|
115 | ('changed:vcs/utils/archivers.py', 11, [ | |||
|
116 | ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'), | |||
|
117 | ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'), | |||
|
118 | ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'), | |||
|
119 | ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'), | |||
|
120 | ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'), | |||
|
121 | ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'), | |||
|
122 | ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'), | |||
|
123 | ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]), | |||
|
124 | ('added:README.rst', 3, [ | |||
|
125 | ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'), | |||
|
126 | ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'), | |||
|
127 | ('svn', '8')]), | |||
|
128 | ('changed:lazy.py', 15, [ | |||
|
129 | ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'), | |||
|
130 | ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'), | |||
|
131 | ('svn', '82'), | |||
|
132 | ('svn', '262'), | |||
|
133 | ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'), | |||
|
134 | ('git', '33fa3223355104431402a888fa77a4e9956feb3e') | |||
|
135 | ]), | |||
|
136 | ('author:marcin@python-blog.com ' | |||
|
137 | 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ | |||
|
138 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |||
|
139 | ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ | |||
|
140 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |||
|
141 | ('b986218b', 1, [ | |||
|
142 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |||
|
143 | ]) | |||
|
144 | def test_search_commit_messages( | |||
|
145 | self, query, expected_hits, expected_commits, enabled_backends): | |||
|
146 | self.log_user() | |||
|
147 | response = self.app.get(route_path('search'), | |||
|
148 | {'q': query, 'type': 'commit', 'page_limit': 500}) | |||
|
149 | ||||
|
150 | response.mustcontain('%s results' % expected_hits) | |||
|
151 | for backend, commit_id in expected_commits: | |||
|
152 | if backend in enabled_backends: | |||
|
153 | response.mustcontain(commit_id) | |||
|
154 | ||||
|
155 | @pytest.mark.parametrize("query, expected_hits, expected_paths", [ | |||
|
156 | ('readme.rst', 3, []), | |||
|
157 | ('test*', 75, []), | |||
|
158 | ('*model*', 1, []), | |||
|
159 | ('extension:rst', 48, []), | |||
|
160 | ('extension:rst api', 24, []), | |||
|
161 | ]) | |||
|
162 | def test_search_file_paths(self, query, expected_hits, expected_paths): | |||
|
163 | self.log_user() | |||
|
164 | response = self.app.get(route_path('search'), | |||
|
165 | {'q': query, 'type': 'path', 'page_limit': 500}) | |||
|
166 | ||||
|
167 | response.mustcontain('%s results' % expected_hits) | |||
|
168 | for path in expected_paths: | |||
|
169 | response.mustcontain(path) | |||
|
170 | ||||
|
171 | def test_search_commit_message_specific_repo(self, backend): | |||
|
172 | self.log_user() | |||
|
173 | response = self.app.get( | |||
|
174 | route_path('search_repo',repo_name=backend.repo_name), | |||
|
175 | {'q': 'bother to ask where to fetch repo during tests', | |||
|
176 | 'type': 'commit'}) | |||
|
177 | ||||
|
178 | response.mustcontain('1 results') | |||
|
179 | ||||
|
180 | def test_filters_are_not_applied_for_admin_user(self): | |||
|
181 | self.log_user() | |||
|
182 | with mock.patch('whoosh.searching.Searcher.search') as search_mock: | |||
|
183 | self.app.get(route_path('search'), | |||
|
184 | {'q': 'test query', 'type': 'commit'}) | |||
|
185 | assert search_mock.call_count == 1 | |||
|
186 | _, kwargs = search_mock.call_args | |||
|
187 | assert kwargs['filter'] is None | |||
|
188 | ||||
|
189 | def test_filters_are_applied_for_normal_user(self, enabled_backends): | |||
|
190 | self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) | |||
|
191 | with mock.patch('whoosh.searching.Searcher.search') as search_mock: | |||
|
192 | self.app.get(route_path('search'), | |||
|
193 | {'q': 'test query', 'type': 'commit'}) | |||
|
194 | assert search_mock.call_count == 1 | |||
|
195 | _, kwargs = search_mock.call_args | |||
|
196 | assert isinstance(kwargs['filter'], query.Or) | |||
|
197 | expected_repositories = [ | |||
|
198 | 'vcs_test_{}'.format(b) for b in enabled_backends] | |||
|
199 | queried_repositories = [ | |||
|
200 | name for type_, name in kwargs['filter'].all_terms()] | |||
|
201 | for repository in expected_repositories: | |||
|
202 | assert repository in queried_repositories |
@@ -0,0 +1,133 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2011-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 logging | |||
|
22 | import urllib | |||
|
23 | from pyramid.view import view_config | |||
|
24 | from webhelpers.util import update_params | |||
|
25 | ||||
|
26 | from rhodecode.apps._base import BaseAppView, RepoAppView | |||
|
27 | from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) | |||
|
28 | from rhodecode.lib.helpers import Page | |||
|
29 | from rhodecode.lib.utils2 import safe_str, safe_int | |||
|
30 | from rhodecode.lib.index import searcher_from_config | |||
|
31 | from rhodecode.model import validation_schema | |||
|
32 | from rhodecode.model.validation_schema.schemas import search_schema | |||
|
33 | ||||
|
34 | log = logging.getLogger(__name__) | |||
|
35 | ||||
|
36 | ||||
|
37 | def search(request, tmpl_context, repo_name): | |||
|
38 | searcher = searcher_from_config(request.registry.settings) | |||
|
39 | formatted_results = [] | |||
|
40 | execution_time = '' | |||
|
41 | ||||
|
42 | schema = search_schema.SearchParamsSchema() | |||
|
43 | ||||
|
44 | search_params = {} | |||
|
45 | errors = [] | |||
|
46 | try: | |||
|
47 | search_params = schema.deserialize( | |||
|
48 | dict(search_query=request.GET.get('q'), | |||
|
49 | search_type=request.GET.get('type'), | |||
|
50 | search_sort=request.GET.get('sort'), | |||
|
51 | page_limit=request.GET.get('page_limit'), | |||
|
52 | requested_page=request.GET.get('page')) | |||
|
53 | ) | |||
|
54 | except validation_schema.Invalid as e: | |||
|
55 | errors = e.children | |||
|
56 | ||||
|
57 | def url_generator(**kw): | |||
|
58 | q = urllib.quote(safe_str(search_query)) | |||
|
59 | return update_params( | |||
|
60 | "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) | |||
|
61 | ||||
|
62 | c = tmpl_context | |||
|
63 | search_query = search_params.get('search_query') | |||
|
64 | search_type = search_params.get('search_type') | |||
|
65 | search_sort = search_params.get('search_sort') | |||
|
66 | if search_params.get('search_query'): | |||
|
67 | page_limit = search_params['page_limit'] | |||
|
68 | requested_page = search_params['requested_page'] | |||
|
69 | ||||
|
70 | try: | |||
|
71 | search_result = searcher.search( | |||
|
72 | search_query, search_type, c.auth_user, repo_name, | |||
|
73 | requested_page, page_limit, search_sort) | |||
|
74 | ||||
|
75 | formatted_results = Page( | |||
|
76 | search_result['results'], page=requested_page, | |||
|
77 | item_count=search_result['count'], | |||
|
78 | items_per_page=page_limit, url=url_generator) | |||
|
79 | finally: | |||
|
80 | searcher.cleanup() | |||
|
81 | ||||
|
82 | if not search_result['error']: | |||
|
83 | execution_time = '%s results (%.3f seconds)' % ( | |||
|
84 | search_result['count'], | |||
|
85 | search_result['runtime']) | |||
|
86 | elif not errors: | |||
|
87 | node = schema['search_query'] | |||
|
88 | errors = [ | |||
|
89 | validation_schema.Invalid(node, search_result['error'])] | |||
|
90 | ||||
|
91 | c.perm_user = c.auth_user | |||
|
92 | c.repo_name = repo_name | |||
|
93 | c.sort = search_sort | |||
|
94 | c.url_generator = url_generator | |||
|
95 | c.errors = errors | |||
|
96 | c.formatted_results = formatted_results | |||
|
97 | c.runtime = execution_time | |||
|
98 | c.cur_query = search_query | |||
|
99 | c.search_type = search_type | |||
|
100 | c.searcher = searcher | |||
|
101 | ||||
|
102 | ||||
|
103 | class SearchView(BaseAppView): | |||
|
104 | def load_default_context(self): | |||
|
105 | c = self._get_local_tmpl_context() | |||
|
106 | self._register_global_c(c) | |||
|
107 | return c | |||
|
108 | ||||
|
109 | @LoginRequired() | |||
|
110 | @view_config( | |||
|
111 | route_name='search', request_method='GET', | |||
|
112 | renderer='rhodecode:templates/search/search.mako') | |||
|
113 | def search(self): | |||
|
114 | c = self.load_default_context() | |||
|
115 | search(self.request, c, repo_name=None) | |||
|
116 | return self._get_template_context(c) | |||
|
117 | ||||
|
118 | ||||
|
119 | class SearchRepoView(RepoAppView): | |||
|
120 | def load_default_context(self): | |||
|
121 | c = self._get_local_tmpl_context() | |||
|
122 | self._register_global_c(c) | |||
|
123 | return c | |||
|
124 | ||||
|
125 | @LoginRequired() | |||
|
126 | @HasRepoPermissionAnyDecorator('repository.admin') | |||
|
127 | @view_config( | |||
|
128 | route_name='search_repo', request_method='GET', | |||
|
129 | renderer='rhodecode:templates/search/search.mako') | |||
|
130 | def search_repo(self): | |||
|
131 | c = self.load_default_context() | |||
|
132 | search(self.request, c, repo_name=self.db_repo_name) | |||
|
133 | return self._get_template_context(c) |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
@@ -1,5 +1,5 b'' | |||||
1 | [bumpversion] |
|
1 | [bumpversion] | |
2 |
current_version = 4. |
|
2 | current_version = 4.8.0 | |
3 | message = release: Bump version {current_version} to {new_version} |
|
3 | message = release: Bump version {current_version} to {new_version} | |
4 |
|
4 | |||
5 | [bumpversion:file:rhodecode/VERSION] |
|
5 | [bumpversion:file:rhodecode/VERSION] |
@@ -5,25 +5,20 b' done = false' | |||||
5 | done = true |
|
5 | done = true | |
6 |
|
6 | |||
7 | [task:rc_tools_pinned] |
|
7 | [task:rc_tools_pinned] | |
8 | done = true |
|
|||
9 |
|
8 | |||
10 | [task:fixes_on_stable] |
|
9 | [task:fixes_on_stable] | |
11 | done = true |
|
|||
12 |
|
10 | |||
13 | [task:pip2nix_generated] |
|
11 | [task:pip2nix_generated] | |
14 | done = true |
|
|||
15 |
|
12 | |||
16 | [task:changelog_updated] |
|
13 | [task:changelog_updated] | |
17 | done = true |
|
|||
18 |
|
14 | |||
19 | [task:generate_api_docs] |
|
15 | [task:generate_api_docs] | |
20 | done = true |
|
16 | ||
|
17 | [task:updated_translation] | |||
21 |
|
18 | |||
22 | [release] |
|
19 | [release] | |
23 |
state = |
|
20 | state = in_progress | |
24 |
version = 4. |
|
21 | version = 4.8.0 | |
25 |
|
||||
26 | [task:updated_translation] |
|
|||
27 |
|
22 | |||
28 | [task:generate_js_routes] |
|
23 | [task:generate_js_routes] | |
29 |
|
24 |
@@ -33,6 +33,7 b' include rhodecode/public/502.html' | |||||
33 | # images, css |
|
33 | # images, css | |
34 | include rhodecode/public/css/*.css |
|
34 | include rhodecode/public/css/*.css | |
35 | include rhodecode/public/images/*.* |
|
35 | include rhodecode/public/images/*.* | |
|
36 | include rhodecode/public/images/ee_features/*.* | |||
36 |
|
37 | |||
37 | # sound files |
|
38 | # sound files | |
38 | include rhodecode/public/sounds/*.mp3 |
|
39 | include rhodecode/public/sounds/*.mp3 |
@@ -174,32 +174,33 b' let' | |||||
174 | ''; |
|
174 | ''; | |
175 |
|
175 | |||
176 | postInstall = '' |
|
176 | postInstall = '' | |
|
177 | echo "Writing meta information for rccontrol to nix-support/rccontrol" | |||
|
178 | mkdir -p $out/nix-support/rccontrol | |||
|
179 | cp -v rhodecode/VERSION $out/nix-support/rccontrol/version | |||
|
180 | echo "DONE: Meta information for rccontrol written" | |||
|
181 | ||||
177 | # python based programs need to be wrapped |
|
182 | # python based programs need to be wrapped | |
|
183 | ln -s ${self.pyramid}/bin/* $out/bin/ | |||
|
184 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ | |||
178 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ |
|
185 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ | |
179 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ |
|
|||
180 | ln -s ${self.PasteScript}/bin/paster $out/bin/ |
|
186 | ln -s ${self.PasteScript}/bin/paster $out/bin/ | |
181 | ln -s ${self.channelstream}/bin/channelstream $out/bin/ |
|
187 | ln -s ${self.channelstream}/bin/channelstream $out/bin/ | |
182 | ln -s ${self.pyramid}/bin/* $out/bin/ #*/ |
|
|||
183 |
|
188 | |||
184 | # rhodecode-tools |
|
189 | # rhodecode-tools | |
185 | # TODO: johbo: re-think this. Do the tools import anything from enterprise? |
|
|||
186 | ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/ |
|
190 | ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/ | |
187 |
|
191 | |||
188 | # note that condition should be restricted when adding further tools |
|
192 | # note that condition should be restricted when adding further tools | |
189 |
for file in $out/bin/*; |
|
193 | for file in $out/bin/*; | |
|
194 | do | |||
190 | wrapProgram $file \ |
|
195 | wrapProgram $file \ | |
|
196 | --prefix PATH : $PATH \ | |||
191 | --prefix PYTHONPATH : $PYTHONPATH \ |
|
197 | --prefix PYTHONPATH : $PYTHONPATH \ | |
192 | --prefix PATH : $PATH \ |
|
|||
193 | --set PYTHONHASHSEED random |
|
198 | --set PYTHONHASHSEED random | |
194 | done |
|
199 | done | |
195 |
|
200 | |||
196 | mkdir $out/etc |
|
201 | mkdir $out/etc | |
197 | cp configs/production.ini $out/etc |
|
202 | cp configs/production.ini $out/etc | |
198 |
|
203 | |||
199 | echo "Writing meta information for rccontrol to nix-support/rccontrol" |
|
|||
200 | mkdir -p $out/nix-support/rccontrol |
|
|||
201 | cp -v rhodecode/VERSION $out/nix-support/rccontrol/version |
|
|||
202 | echo "DONE: Meta information for rccontrol written" |
|
|||
203 |
|
204 | |||
204 | # TODO: johbo: Make part of ac-tests |
|
205 | # TODO: johbo: Make part of ac-tests | |
205 | if [ ! -f rhodecode/public/js/scripts.js ]; then |
|
206 | if [ ! -f rhodecode/public/js/scripts.js ]; then |
@@ -62,6 +62,9 b' Below config if for an Apache Reverse Pr' | |||||
62 | ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On |
|
62 | ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On | |
63 | ProxyPassReverse / http://127.0.0.1:10002/ |
|
63 | ProxyPassReverse / http://127.0.0.1:10002/ | |
64 |
|
64 | |||
|
65 | # Increase headers for large Mercurial headers | |||
|
66 | LimitRequestLine 16380 | |||
|
67 | ||||
65 | # strict http prevents from https -> http downgrade |
|
68 | # strict http prevents from https -> http downgrade | |
66 | Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload" |
|
69 | Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload" | |
67 |
|
70 |
@@ -5,7 +5,10 b' Use the following example to configure N' | |||||
5 |
|
5 | |||
6 |
|
6 | |||
7 | .. code-block:: nginx |
|
7 | .. code-block:: nginx | |
|
8 | ## rate limiter for certain pages to prevent brute force attacks | |||
|
9 | limit_req_zone $binary_remote_addr zone=dl_limit:10m rate=1r/s; | |||
8 |
|
10 | |||
|
11 | ## custom log format | |||
9 | log_format log_custom '$remote_addr - $remote_user [$time_local] ' |
|
12 | log_format log_custom '$remote_addr - $remote_user [$time_local] ' | |
10 | '"$request" $status $body_bytes_sent ' |
|
13 | '"$request" $status $body_bytes_sent ' | |
11 | '"$http_referer" "$http_user_agent" ' |
|
14 | '"$http_referer" "$http_user_agent" ' | |
@@ -109,6 +112,12 b' Use the following example to configure N' | |||||
109 | proxy_set_header Connection "upgrade"; |
|
112 | proxy_set_header Connection "upgrade"; | |
110 | } |
|
113 | } | |
111 |
|
114 | |||
|
115 | location /_admin/login { | |||
|
116 | ## rate limit this endpoint | |||
|
117 | limit_req zone=dl_limit burst=10 nodelay; | |||
|
118 | try_files $uri @rhode; | |||
|
119 | } | |||
|
120 | ||||
112 | location / { |
|
121 | location / { | |
113 | try_files $uri @rhode; |
|
122 | try_files $uri @rhode; | |
114 | } |
|
123 | } |
@@ -3,8 +3,12 b'' | |||||
3 | Repository Extra Fields |
|
3 | Repository Extra Fields | |
4 | ======================= |
|
4 | ======================= | |
5 |
|
5 | |||
6 |
Extra fields attached to a |repo| allow you to configure additional |
|
6 | Extra fields attached to a |repo| allow you to configure additional fields for | |
7 | |RCX|. To install and read more about |RCX|, see the :ref:`install-rcx` section. |
|
7 | each repository. This allows storing custom data per-repository. | |
|
8 | ||||
|
9 | It can be used in :ref:`integrations-webhook` or in |RCX|. | |||
|
10 | To install and read more about |RCX|, see the :ref:`install-rcx` section. | |||
|
11 | ||||
8 |
|
12 | |||
9 | Enabling Extra Fields |
|
13 | Enabling Extra Fields | |
10 | --------------------- |
|
14 | --------------------- | |
@@ -27,9 +31,14 b' 2. On the |repo| settings page, select t' | |||||
27 |
|
31 | |||
28 | .. image:: ../images/extra-repo-fields.png |
|
32 | .. image:: ../images/extra-repo-fields.png | |
29 |
|
33 | |||
|
34 | The most important is the `New field key` variable which under the value will | |||
|
35 | be stored. It needs to be unique for each repository. The label and description | |||
|
36 | will be generated in repository settings where users can actually save some | |||
|
37 | values inside generated extra fields. | |||
30 |
|
38 | |||
31 | Example Usage |
|
39 | ||
32 | ------------- |
|
40 | Example Usage in extensions | |
|
41 | --------------------------- | |||
33 |
|
42 | |||
34 | To use the extra fields in an extension, see the example below. For more |
|
43 | To use the extra fields in an extension, see the example below. For more | |
35 | information and examples, see the :ref:`extensions-hooks-ref` section. |
|
44 | information and examples, see the :ref:`extensions-hooks-ref` section. |
@@ -7,7 +7,7 b' The VCS Server handles |RCM| backend fun' | |||||
7 | a VCS Server to run with a |RCM| instance. If you do not, you will be missing |
|
7 | a VCS Server to run with a |RCM| instance. If you do not, you will be missing | |
8 | the connection between |RCM| and its |repos|. This will cause error messages |
|
8 | the connection between |RCM| and its |repos|. This will cause error messages | |
9 | on the web interface. You can run your setup in the following configurations, |
|
9 | on the web interface. You can run your setup in the following configurations, | |
10 |
currently the best performance is one |
|
10 | currently the best performance is one of following: | |
11 |
|
11 | |||
12 | * One VCS Server per |RCM| instance. |
|
12 | * One VCS Server per |RCM| instance. | |
13 | * One VCS Server handling multiple instances. |
|
13 | * One VCS Server handling multiple instances. | |
@@ -59,7 +59,8 b' instance in the' | |||||
59 | \vcs.backends <available-vcs-systems> |
|
59 | \vcs.backends <available-vcs-systems> | |
60 | Set a comma-separated list of the |repo| options available from the |
|
60 | Set a comma-separated list of the |repo| options available from the | |
61 | web interface. The default is ``hg, git, svn``, |
|
61 | web interface. The default is ``hg, git, svn``, | |
62 | which is all |repo| types available. |
|
62 | which is all |repo| types available. The order of backends is also the | |
|
63 | order backend will try to detect requests type. | |||
63 |
|
64 | |||
64 | \vcs.connection_timeout <seconds> |
|
65 | \vcs.connection_timeout <seconds> | |
65 | Set the length of time in seconds that the VCS Server waits for |
|
66 | Set the length of time in seconds that the VCS Server waits for | |
@@ -159,9 +160,10 b' for full details see the :ref:`RhodeCode' | |||||
159 |
|
160 | |||
160 | - NAME: vcsserver-1 |
|
161 | - NAME: vcsserver-1 | |
161 | - STATUS: RUNNING |
|
162 | - STATUS: RUNNING | |
162 | - TYPE: VCSServer |
|
163 | logs:/home/ubuntu/.rccontrol/vcsserver-1/vcsserver.log | |
163 |
- VERSION: |
|
164 | - VERSION: 4.7.2 VCSServer | |
164 |
- URL: http://127.0.0.1:1000 |
|
165 | - URL: http://127.0.0.1:10008 | |
|
166 | - CONFIG: /home/ubuntu/.rccontrol/vcsserver-1/vcsserver.ini | |||
165 |
|
167 | |||
166 | $ rccontrol restart vcsserver-1 |
|
168 | $ rccontrol restart vcsserver-1 | |
167 | Instance "vcsserver-1" successfully stopped. |
|
169 | Instance "vcsserver-1" successfully stopped. | |
@@ -181,7 +183,9 b' For a more detailed explanation of the l' | |||||
181 | .. rst-class:: dl-horizontal |
|
183 | .. rst-class:: dl-horizontal | |
182 |
|
184 | |||
183 | \host <ip-address> |
|
185 | \host <ip-address> | |
184 | Set the host on which the VCS Server will run. |
|
186 | Set the host on which the VCS Server will run. VCSServer is not | |
|
187 | protected by any authentication, so we *highly* recommend running it | |||
|
188 | under localhost ip that is `127.0.0.1` | |||
185 |
|
189 | |||
186 | \port <int> |
|
190 | \port <int> | |
187 | Set the port number on which the VCS Server will be available. |
|
191 | Set the port number on which the VCS Server will be available. | |
@@ -189,13 +193,22 b' For a more detailed explanation of the l' | |||||
189 | \locale <locale_utf> |
|
193 | \locale <locale_utf> | |
190 | Set the locale the VCS Server expects. |
|
194 | Set the locale the VCS Server expects. | |
191 |
|
195 | |||
192 |
\ |
|
196 | \workers <int> | |
193 | Set the size of the threadpool used to communicate |
|
197 | Set the number of process workers.Recommended | |
194 | with the WSGI workers. This should be at least 6 times the number of |
|
198 | value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers | |
195 | WSGI worker processes. |
|
|||
196 |
|
199 | |||
197 | \timeout <seconds> |
|
200 | \max_requests <int> | |
198 | Set the timeout for RPC communication in seconds. |
|
201 | The maximum number of requests a worker will process before restarting. | |
|
202 | Any value greater than zero will limit the number of requests a work | |||
|
203 | will process before automatically restarting. This is a simple method | |||
|
204 | to help limit the damage of memory leaks. | |||
|
205 | ||||
|
206 | \max_requests_jitter <int> | |||
|
207 | The maximum jitter to add to the max_requests setting. | |||
|
208 | The jitter causes the restart per worker to be randomized by | |||
|
209 | randint(0, max_requests_jitter). This is intended to stagger worker | |||
|
210 | restarts to avoid all workers restarting at the same time. | |||
|
211 | ||||
199 |
|
212 | |||
200 | .. note:: |
|
213 | .. note:: | |
201 |
|
214 | |||
@@ -204,27 +217,54 b' For a more detailed explanation of the l' | |||||
204 | .. code-block:: ini |
|
217 | .. code-block:: ini | |
205 |
|
218 | |||
206 | ################################################################################ |
|
219 | ################################################################################ | |
207 |
# RhodeCode VCSServer - configuration |
|
220 | # RhodeCode VCSServer with HTTP Backend - configuration # | |
208 | # # |
|
221 | # # | |
209 | ################################################################################ |
|
222 | ################################################################################ | |
210 |
|
223 | |||
211 | [DEFAULT] |
|
224 | ||
|
225 | [server:main] | |||
|
226 | ## COMMON ## | |||
212 | host = 127.0.0.1 |
|
227 | host = 127.0.0.1 | |
213 |
port = |
|
228 | port = 10002 | |
|
229 | ||||
|
230 | ########################## | |||
|
231 | ## GUNICORN WSGI SERVER ## | |||
|
232 | ########################## | |||
|
233 | ## run with gunicorn --log-config vcsserver.ini --paste vcsserver.ini | |||
|
234 | use = egg:gunicorn#main | |||
|
235 | ## Sets the number of process workers. Recommended | |||
|
236 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers | |||
|
237 | workers = 3 | |||
|
238 | ## process name | |||
|
239 | proc_name = rhodecode_vcsserver | |||
|
240 | ## type of worker class, one of sync, gevent | |||
|
241 | ## recommended for bigger setup is using of of other than sync one | |||
|
242 | worker_class = sync | |||
|
243 | ## The maximum number of simultaneous clients. Valid only for Gevent | |||
|
244 | #worker_connections = 10 | |||
|
245 | ## max number of requests that worker will handle before being gracefully | |||
|
246 | ## restarted, could prevent memory leaks | |||
|
247 | max_requests = 1000 | |||
|
248 | max_requests_jitter = 30 | |||
|
249 | ## amount of time a worker can spend with handling a request before it | |||
|
250 | ## gets killed and restarted. Set to 6hrs | |||
|
251 | timeout = 21600 | |||
|
252 | ||||
|
253 | [app:main] | |||
|
254 | use = egg:rhodecode-vcsserver | |||
|
255 | ||||
|
256 | pyramid.default_locale_name = en | |||
|
257 | pyramid.includes = | |||
|
258 | ||||
|
259 | ## default locale used by VCS systems | |||
214 | locale = en_US.UTF-8 |
|
260 | locale = en_US.UTF-8 | |
215 | # number of worker threads, this should be set based on a formula threadpool=N*6 |
|
|||
216 | # where N is number of RhodeCode Enterprise workers, eg. running 2 instances |
|
|||
217 | # 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96 |
|
|||
218 | threadpool_size = 16 |
|
|||
219 | timeout = 0 |
|
|||
220 |
|
261 | |||
221 | # cache regions, please don't change |
|
262 | # cache regions, please don't change | |
222 | beaker.cache.regions = repo_object |
|
263 | beaker.cache.regions = repo_object | |
223 | beaker.cache.repo_object.type = memorylru |
|
264 | beaker.cache.repo_object.type = memorylru | |
224 |
beaker.cache.repo_object.max_items = 100 |
|
265 | beaker.cache.repo_object.max_items = 100 | |
225 |
|
||||
226 | # cache auto-expires after N seconds |
|
266 | # cache auto-expires after N seconds | |
227 |
beaker.cache.repo_object.expire = |
|
267 | beaker.cache.repo_object.expire = 300 | |
228 | beaker.cache.repo_object.enabled = true |
|
268 | beaker.cache.repo_object.enabled = true | |
229 |
|
269 | |||
230 |
|
270 | |||
@@ -270,20 +310,6 b' For a more detailed explanation of the l' | |||||
270 | level = DEBUG |
|
310 | level = DEBUG | |
271 | formatter = generic |
|
311 | formatter = generic | |
272 |
|
312 | |||
273 | [handler_file] |
|
|||
274 | class = FileHandler |
|
|||
275 | args = ('vcsserver.log', 'a',) |
|
|||
276 | level = DEBUG |
|
|||
277 | formatter = generic |
|
|||
278 |
|
||||
279 | [handler_file_rotating] |
|
|||
280 | class = logging.handlers.TimedRotatingFileHandler |
|
|||
281 | # 'D', 5 - rotate every 5days |
|
|||
282 | # you can set 'h', 'midnight' |
|
|||
283 | args = ('vcsserver.log', 'D', 5, 10,) |
|
|||
284 | level = DEBUG |
|
|||
285 | formatter = generic |
|
|||
286 |
|
||||
287 | ################ |
|
313 | ################ | |
288 | ## FORMATTERS ## |
|
314 | ## FORMATTERS ## | |
289 | ################ |
|
315 | ################ |
@@ -6,7 +6,7 b' pull_request methods' | |||||
6 | close_pull_request |
|
6 | close_pull_request | |
7 | ------------------ |
|
7 | ------------------ | |
8 |
|
8 | |||
9 | .. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>) |
|
9 | .. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>, message=<Optional:''>) | |
10 |
|
10 | |||
11 | Close the pull request specified by `pullrequestid`. |
|
11 | Close the pull request specified by `pullrequestid`. | |
12 |
|
12 | |||
@@ -19,6 +19,9 b' close_pull_request' | |||||
19 | :type pullrequestid: int |
|
19 | :type pullrequestid: int | |
20 | :param userid: Close the pull request as this user. |
|
20 | :param userid: Close the pull request as this user. | |
21 | :type userid: Optional(str or int) |
|
21 | :type userid: Optional(str or int) | |
|
22 | :param message: Optional message to close the Pull Request with. If not | |||
|
23 | specified it will be generated automatically. | |||
|
24 | :type message: Optional(str) | |||
22 |
|
25 | |||
23 | Example output: |
|
26 | Example output: | |
24 |
|
27 | |||
@@ -27,6 +30,7 b' close_pull_request' | |||||
27 | "id": <id_given_in_input>, |
|
30 | "id": <id_given_in_input>, | |
28 | "result": { |
|
31 | "result": { | |
29 | "pull_request_id": "<int>", |
|
32 | "pull_request_id": "<int>", | |
|
33 | "close_status": "<str:status_lbl>, | |||
30 | "closed": "<bool>" |
|
34 | "closed": "<bool>" | |
31 | }, |
|
35 | }, | |
32 | "error": null |
|
36 | "error": null | |
@@ -105,10 +109,12 b' create_pull_request' | |||||
105 | :param description: Set the pull request description. |
|
109 | :param description: Set the pull request description. | |
106 | :type description: Optional(str) |
|
110 | :type description: Optional(str) | |
107 | :param reviewers: Set the new pull request reviewers list. |
|
111 | :param reviewers: Set the new pull request reviewers list. | |
|
112 | Reviewer defined by review rules will be added automatically to the | |||
|
113 | defined list. | |||
108 | :type reviewers: Optional(list) |
|
114 | :type reviewers: Optional(list) | |
109 | Accepts username strings or objects of the format: |
|
115 | Accepts username strings or objects of the format: | |
110 |
|
116 | |||
111 | {'username': 'nick', 'reasons': ['original author']} |
|
117 | [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}] | |
112 |
|
118 | |||
113 |
|
119 | |||
114 | get_pull_request |
|
120 | get_pull_request | |
@@ -320,7 +326,7 b' merge_pull_request' | |||||
320 | update_pull_request |
|
326 | update_pull_request | |
321 | ------------------- |
|
327 | ------------------- | |
322 |
|
328 | |||
323 |
.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional: |
|
329 | .. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>) | |
324 |
|
330 | |||
325 | Updates a pull request. |
|
331 | Updates a pull request. | |
326 |
|
332 | |||
@@ -336,10 +342,12 b' update_pull_request' | |||||
336 | :type description: Optional(str) |
|
342 | :type description: Optional(str) | |
337 | :param reviewers: Update pull request reviewers list with new value. |
|
343 | :param reviewers: Update pull request reviewers list with new value. | |
338 | :type reviewers: Optional(list) |
|
344 | :type reviewers: Optional(list) | |
|
345 | Accepts username strings or objects of the format: | |||
|
346 | ||||
|
347 | [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}] | |||
|
348 | ||||
339 | :param update_commits: Trigger update of commits for this pull request |
|
349 | :param update_commits: Trigger update of commits for this pull request | |
340 | :type: update_commits: Optional(bool) |
|
350 | :type: update_commits: Optional(bool) | |
341 | :param close_pull_request: Close this pull request with rejected state |
|
|||
342 | :type: close_pull_request: Optional(bool) |
|
|||
343 |
|
351 | |||
344 | Example output: |
|
352 | Example output: | |
345 |
|
353 |
@@ -527,6 +527,7 b' get_repo_settings' | |||||
527 | "id": 237, |
|
527 | "id": 237, | |
528 | "result": { |
|
528 | "result": { | |
529 | "extensions_largefiles": true, |
|
529 | "extensions_largefiles": true, | |
|
530 | "extensions_evolve": true, | |||
530 | "hooks_changegroup_push_logger": true, |
|
531 | "hooks_changegroup_push_logger": true, | |
531 | "hooks_changegroup_repo_size": false, |
|
532 | "hooks_changegroup_repo_size": false, | |
532 | "hooks_outgoing_pull_logger": true, |
|
533 | "hooks_outgoing_pull_logger": true, | |
@@ -762,6 +763,49 b' lock' | |||||
762 | } |
|
763 | } | |
763 |
|
764 | |||
764 |
|
765 | |||
|
766 | maintenance | |||
|
767 | ----------- | |||
|
768 | ||||
|
769 | .. py:function:: maintenance(apiuser, repoid) | |||
|
770 | ||||
|
771 | Triggers a maintenance on the given repository. | |||
|
772 | ||||
|
773 | This command can only be run using an |authtoken| with admin | |||
|
774 | rights to the specified repository. For more information, | |||
|
775 | see :ref:`config-token-ref`. | |||
|
776 | ||||
|
777 | This command takes the following options: | |||
|
778 | ||||
|
779 | :param apiuser: This is filled automatically from the |authtoken|. | |||
|
780 | :type apiuser: AuthUser | |||
|
781 | :param repoid: The repository name or repository ID. | |||
|
782 | :type repoid: str or int | |||
|
783 | ||||
|
784 | Example output: | |||
|
785 | ||||
|
786 | .. code-block:: bash | |||
|
787 | ||||
|
788 | id : <id_given_in_input> | |||
|
789 | result : { | |||
|
790 | "msg": "executed maintenance command", | |||
|
791 | "executed_actions": [ | |||
|
792 | <action_message>, <action_message2>... | |||
|
793 | ], | |||
|
794 | "repository": "<repository name>" | |||
|
795 | } | |||
|
796 | error : null | |||
|
797 | ||||
|
798 | Example error output: | |||
|
799 | ||||
|
800 | .. code-block:: bash | |||
|
801 | ||||
|
802 | id : <id_given_in_input> | |||
|
803 | result : null | |||
|
804 | error : { | |||
|
805 | "Unable to execute maintenance on `<reponame>`" | |||
|
806 | } | |||
|
807 | ||||
|
808 | ||||
765 | pull |
|
809 | pull | |
766 | ---- |
|
810 | ---- | |
767 |
|
811 |
@@ -66,7 +66,7 b' RhodeCode VCSServer repositories into th' | |||||
66 | RhodeCode currently is using Mercurial Version Control System, please make sure |
|
66 | RhodeCode currently is using Mercurial Version Control System, please make sure | |
67 | you have it installed before continuing. |
|
67 | you have it installed before continuing. | |
68 |
|
68 | |||
69 | To obtain the required sources, use the following commands: |
|
69 | To obtain the required sources, use the following commands:: | |
70 |
|
|
70 | ||
71 | mkdir rhodecode-develop && cd rhodecode-develop |
|
71 | mkdir rhodecode-develop && cd rhodecode-develop | |
72 | hg clone https://code.rhodecode.com/rhodecode-enterprise-ce |
|
72 | hg clone https://code.rhodecode.com/rhodecode-enterprise-ce | |
@@ -80,9 +80,9 b' To obtain the required sources, use the ' | |||||
80 | Install some required libraries |
|
80 | Install some required libraries | |
81 | ------------------------------- |
|
81 | ------------------------------- | |
82 |
|
82 | |||
83 |
There are some required drivers that we need to install to |
|
83 | There are some required drivers and dev libraries that we need to install to | |
84 |
under different types of databases. For example in Ubuntu we |
|
84 | test RhodeCode under different types of databases. For example in Ubuntu we | |
85 | the following. |
|
85 | need to install the following. | |
86 |
|
86 | |||
87 | required libraries:: |
|
87 | required libraries:: | |
88 |
|
88 |
@@ -20,7 +20,7 b' and commit files and |repos| while manag' | |||||
20 | * Migration from existing databases. |
|
20 | * Migration from existing databases. | |
21 | * |RCM| SDK. |
|
21 | * |RCM| SDK. | |
22 | * Built-in analytics |
|
22 | * Built-in analytics | |
23 |
* Built in integrations including: Slack, Jenkins |
|
23 | * Built in integrations including: Slack, Webhooks (used for Jenkins/TeamCity and other CIs), Jira, Redmine, Hipchat | |
24 | * Pluggable authentication system. |
|
24 | * Pluggable authentication system. | |
25 | * Support for AD, |LDAP|, Crowd, CAS, PAM. |
|
25 | * Support for AD, |LDAP|, Crowd, CAS, PAM. | |
26 | * Support for external authentication via Oauth Google, Github, Bitbucket, Twitter. |
|
26 | * Support for external authentication via Oauth Google, Github, Bitbucket, Twitter. |
@@ -17,7 +17,8 b' Type/Name |RC| Edi' | |||||
17 | :ref:`integrations-slack` |RCCEshort| https://slack.com/ |
|
17 | :ref:`integrations-slack` |RCCEshort| https://slack.com/ | |
18 | :ref:`integrations-hipchat` |RCCEshort| https://www.hipchat.com/ |
|
18 | :ref:`integrations-hipchat` |RCCEshort| https://www.hipchat.com/ | |
19 | :ref:`integrations-webhook` |RCCEshort| POST events as `json` to a custom url |
|
19 | :ref:`integrations-webhook` |RCCEshort| POST events as `json` to a custom url | |
20 |
:ref:`integrations- |
|
20 | :ref:`integrations-ci` |RCCEshort| Trigger Builds for Common CI Systems | |
|
21 | :ref:`integrations-email` |RCCEshort| Send repo push commits by email | |||
21 | :ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues |
|
22 | :ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues | |
22 | :ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues |
|
23 | :ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues | |
23 | ============================ ============ ===================================== |
|
24 | ============================ ============ ===================================== |
@@ -3,9 +3,9 b'' | |||||
3 | Webhook integration |
|
3 | Webhook integration | |
4 | =================== |
|
4 | =================== | |
5 |
|
5 | |||
6 |
The |
|
6 | The :ref:`creating-integrations` integration allows you to POST events such as | |
7 |
or pull requests to a custom http endpoint as a |
|
7 | repository pushes or pull requests to a custom http endpoint as a JSON dict | |
8 | event. |
|
8 | with details of the event. | |
9 |
|
9 | |||
10 | Starting from 4.5.0 release, webhook integration allows to use variables |
|
10 | Starting from 4.5.0 release, webhook integration allows to use variables | |
11 | inside the URL. For example in URL `https://server-example.com/${repo_name}` |
|
11 | inside the URL. For example in URL `https://server-example.com/${repo_name}` | |
@@ -14,8 +14,10 b' triggered from. Some of the variables li' | |||||
14 | `${branch}` will result in webhook be called multiple times when multiple |
|
14 | `${branch}` will result in webhook be called multiple times when multiple | |
15 | branches are pushed. |
|
15 | branches are pushed. | |
16 |
|
16 | |||
17 | Some of the variables like `${pull_request_id}` will be replaced only in |
|
17 | Starting from 4.8.0 also repository extra fields can be used. A format to use | |
18 | the pull request related events. |
|
18 | them is `${extra:field_key}`. It's usefull to use them to specify custom | |
|
19 | repo only parameters. Some of the variables like `${pull_request_id}` | |||
|
20 | will be replaced only in the pull request related events. | |||
19 |
|
21 | |||
20 | To create a webhook integration, select "webhook" in the integration settings |
|
22 | To create a webhook integration, select "webhook" in the integration settings | |
21 | and use the URL and key from your any previous custom webhook created. See |
|
23 | and use the URL and key from your any previous custom webhook created. See |
@@ -9,6 +9,7 b' Release Notes' | |||||
9 | .. toctree:: |
|
9 | .. toctree:: | |
10 | :maxdepth: 1 |
|
10 | :maxdepth: 1 | |
11 |
|
11 | |||
|
12 | release-notes-4.8.0.rst | |||
12 | release-notes-4.7.2.rst |
|
13 | release-notes-4.7.2.rst | |
13 | release-notes-4.7.1.rst |
|
14 | release-notes-4.7.1.rst | |
14 | release-notes-4.7.0.rst |
|
15 | release-notes-4.7.0.rst |
@@ -3,8 +3,13 b'' | |||||
3 | Getting Started with VCS |
|
3 | Getting Started with VCS | |
4 | ------------------------ |
|
4 | ------------------------ | |
5 |
|
5 | |||
6 | When using |RCM|, you will be working with |git| or |hg| |repos| from the |
|
6 | When using |RCM|, you will be working with |git|, |svn| or |hg| |repos| from the | |
7 | command line. |
|
7 | command line or using a GUI client such as Tortoise, Tower or SourceTree. | |
|
8 | ||||
|
9 | |RCM| uses a standard |git|, |svn| and |hg| protocols. So all tools that | |||
|
10 | can interact with there protocols are supported, including Eclipse or PyCharm | |||
|
11 | plugins. | |||
|
12 | ||||
8 |
|
13 | |||
9 | If you have never used either before, the following information should |
|
14 | If you have never used either before, the following information should | |
10 | help you set up your local machine so that you can sync changes with the |
|
15 | help you set up your local machine so that you can sync changes with the |
@@ -7,7 +7,7 b' buildEnv { name = "bower-env"; ignoreCol' | |||||
7 | (fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k") |
|
7 | (fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k") | |
8 | (fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m") |
|
8 | (fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m") | |
9 | (fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w") |
|
9 | (fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w") | |
10 |
(fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" " |
|
10 | (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "1b1z3112ggjdflgrwbpmnbsh3kgcm4hn255wshvrlzds4w069gja") | |
11 | (fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq") |
|
11 | (fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq") | |
12 | (fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv") |
|
12 | (fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv") | |
13 | (fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl") |
|
13 | (fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl") |
@@ -601,13 +601,13 b'' | |||||
601 | }; |
|
601 | }; | |
602 | }; |
|
602 | }; | |
603 | deform = super.buildPythonPackage { |
|
603 | deform = super.buildPythonPackage { | |
604 |
name = "deform-2.0 |
|
604 | name = "deform-2.0.4"; | |
605 | buildInputs = with self; []; |
|
605 | buildInputs = with self; []; | |
606 | doCheck = false; |
|
606 | doCheck = false; | |
607 | propagatedBuildInputs = with self; [Chameleon colander peppercorn translationstring zope.deprecation]; |
|
607 | propagatedBuildInputs = with self; [Chameleon colander iso8601 peppercorn translationstring zope.deprecation]; | |
608 | src = fetchurl { |
|
608 | src = fetchurl { | |
609 |
url = "https://pypi.python.org/packages/ |
|
609 | url = "https://pypi.python.org/packages/66/3b/eefcb07abcab7a97f6665aa2d0cf1af741d9d6e78a2e4657fd2b89f89880/deform-2.0.4.tar.gz"; | |
610 | md5 = "7a90d41f7fbc18002ce74f39bd90a5e4"; |
|
610 | md5 = "34756e42cf50dd4b4430809116c4ec0a"; | |
611 | }; |
|
611 | }; | |
612 | meta = { |
|
612 | meta = { | |
613 | license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; |
|
613 | license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; | |
@@ -887,13 +887,13 b'' | |||||
887 | }; |
|
887 | }; | |
888 | }; |
|
888 | }; | |
889 | ipython-genutils = super.buildPythonPackage { |
|
889 | ipython-genutils = super.buildPythonPackage { | |
890 |
name = "ipython-genutils-0. |
|
890 | name = "ipython-genutils-0.2.0"; | |
891 | buildInputs = with self; []; |
|
891 | buildInputs = with self; []; | |
892 | doCheck = false; |
|
892 | doCheck = false; | |
893 | propagatedBuildInputs = with self; []; |
|
893 | propagatedBuildInputs = with self; []; | |
894 | src = fetchurl { |
|
894 | src = fetchurl { | |
895 |
url = "https://pypi.python.org/packages/71 |
|
895 | url = "https://pypi.python.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz"; | |
896 | md5 = "9a8afbe0978adbcbfcb3b35b2d015a56"; |
|
896 | md5 = "5a4f9781f78466da0ea1a648f3e1f79f"; | |
897 | }; |
|
897 | }; | |
898 | meta = { |
|
898 | meta = { | |
899 | license = [ pkgs.lib.licenses.bsdOriginal ]; |
|
899 | license = [ pkgs.lib.licenses.bsdOriginal ]; | |
@@ -1004,13 +1004,13 b'' | |||||
1004 | }; |
|
1004 | }; | |
1005 | }; |
|
1005 | }; | |
1006 | mistune = super.buildPythonPackage { |
|
1006 | mistune = super.buildPythonPackage { | |
1007 |
name = "mistune-0.7. |
|
1007 | name = "mistune-0.7.4"; | |
1008 | buildInputs = with self; []; |
|
1008 | buildInputs = with self; []; | |
1009 | doCheck = false; |
|
1009 | doCheck = false; | |
1010 | propagatedBuildInputs = with self; []; |
|
1010 | propagatedBuildInputs = with self; []; | |
1011 | src = fetchurl { |
|
1011 | src = fetchurl { | |
1012 |
url = "https://pypi.python.org/packages/ |
|
1012 | url = "https://pypi.python.org/packages/25/a4/12a584c0c59c9fed529f8b3c47ca8217c0cf8bcc5e1089d3256410cfbdbc/mistune-0.7.4.tar.gz"; | |
1013 | md5 = "4eba50bd121b83716fa4be6a4049004b"; |
|
1013 | md5 = "92d01cb717e9e74429e9bde9d29ac43b"; | |
1014 | }; |
|
1014 | }; | |
1015 | meta = { |
|
1015 | meta = { | |
1016 | license = [ pkgs.lib.licenses.bsdOriginal ]; |
|
1016 | license = [ pkgs.lib.licenses.bsdOriginal ]; | |
@@ -1082,13 +1082,13 b'' | |||||
1082 | }; |
|
1082 | }; | |
1083 | }; |
|
1083 | }; | |
1084 | objgraph = super.buildPythonPackage { |
|
1084 | objgraph = super.buildPythonPackage { | |
1085 |
name = "objgraph- |
|
1085 | name = "objgraph-3.1.0"; | |
1086 | buildInputs = with self; []; |
|
1086 | buildInputs = with self; []; | |
1087 | doCheck = false; |
|
1087 | doCheck = false; | |
1088 | propagatedBuildInputs = with self; []; |
|
1088 | propagatedBuildInputs = with self; []; | |
1089 | src = fetchurl { |
|
1089 | src = fetchurl { | |
1090 | url = "https://pypi.python.org/packages/d7/33/ace750b59247496ed769b170586c5def7202683f3d98e737b75b767ff29e/objgraph-2.0.0.tar.gz"; |
|
1090 | url = "https://pypi.python.org/packages/f4/b3/082e54e62094cb2ec84f8d5a49e0142cef99016491cecba83309cff920ae/objgraph-3.1.0.tar.gz"; | |
1091 | md5 = "25b0d5e5adc74aa63ead15699614159c"; |
|
1091 | md5 = "eddbd96039796bfbd13eee403701e64a"; | |
1092 | }; |
|
1092 | }; | |
1093 | meta = { |
|
1093 | meta = { | |
1094 | license = [ pkgs.lib.licenses.mit ]; |
|
1094 | license = [ pkgs.lib.licenses.mit ]; | |
@@ -1186,13 +1186,13 b'' | |||||
1186 | }; |
|
1186 | }; | |
1187 | }; |
|
1187 | }; | |
1188 | prompt-toolkit = super.buildPythonPackage { |
|
1188 | prompt-toolkit = super.buildPythonPackage { | |
1189 |
name = "prompt-toolkit-1.0.1 |
|
1189 | name = "prompt-toolkit-1.0.14"; | |
1190 | buildInputs = with self; []; |
|
1190 | buildInputs = with self; []; | |
1191 | doCheck = false; |
|
1191 | doCheck = false; | |
1192 | propagatedBuildInputs = with self; [six wcwidth]; |
|
1192 | propagatedBuildInputs = with self; [six wcwidth]; | |
1193 | src = fetchurl { |
|
1193 | src = fetchurl { | |
1194 |
url = "https://pypi.python.org/packages/23 |
|
1194 | url = "https://pypi.python.org/packages/55/56/8c39509b614bda53e638b7500f12577d663ac1b868aef53426fc6a26c3f5/prompt_toolkit-1.0.14.tar.gz"; | |
1195 | md5 = "427b496d2c147bd3819bc3a7f6e0d493"; |
|
1195 | md5 = "f24061ae133ed32c6b764e92bd48c496"; | |
1196 | }; |
|
1196 | }; | |
1197 | meta = { |
|
1197 | meta = { | |
1198 | license = [ pkgs.lib.licenses.bsdOriginal ]; |
|
1198 | license = [ pkgs.lib.licenses.bsdOriginal ]; | |
@@ -1641,7 +1641,7 b'' | |||||
1641 | }; |
|
1641 | }; | |
1642 | }; |
|
1642 | }; | |
1643 | rhodecode-enterprise-ce = super.buildPythonPackage { |
|
1643 | rhodecode-enterprise-ce = super.buildPythonPackage { | |
1644 |
name = "rhodecode-enterprise-ce-4. |
|
1644 | name = "rhodecode-enterprise-ce-4.8.0"; | |
1645 | buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj]; |
|
1645 | buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj]; | |
1646 | doCheck = true; |
|
1646 | doCheck = true; | |
1647 | propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; |
|
1647 | propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; |
@@ -16,7 +16,7 b' colander==1.2' | |||||
16 | configobj==5.0.6 |
|
16 | configobj==5.0.6 | |
17 | cssselect==1.0.1 |
|
17 | cssselect==1.0.1 | |
18 | decorator==4.0.11 |
|
18 | decorator==4.0.11 | |
19 |
deform==2.0 |
|
19 | deform==2.0.4 | |
20 | docutils==0.12 |
|
20 | docutils==0.12 | |
21 | dogpile.cache==0.6.1 |
|
21 | dogpile.cache==0.6.1 | |
22 | dogpile.core==0.4.1 |
|
22 | dogpile.core==0.4.1 | |
@@ -38,7 +38,7 b' meld3==1.0.2' | |||||
38 | msgpack-python==0.4.8 |
|
38 | msgpack-python==0.4.8 | |
39 | MySQL-python==1.2.5 |
|
39 | MySQL-python==1.2.5 | |
40 | nose==1.3.6 |
|
40 | nose==1.3.6 | |
41 |
objgraph== |
|
41 | objgraph==3.1.0 | |
42 | packaging==15.2 |
|
42 | packaging==15.2 | |
43 | paramiko==1.15.1 |
|
43 | paramiko==1.15.1 | |
44 | Paste==2.0.3 |
|
44 | Paste==2.0.3 |
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}' | |||||
51 | EXTENSIONS = {} |
|
51 | EXTENSIONS = {} | |
52 |
|
52 | |||
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
54 |
__dbversion__ = 7 |
|
54 | __dbversion__ = 78 # defines current db version for migrations | |
55 | __platform__ = platform.system() |
|
55 | __platform__ = platform.system() | |
56 | __license__ = 'AGPLv3, and Commercial License' |
|
56 | __license__ = 'AGPLv3, and Commercial License' | |
57 | __author__ = 'RhodeCode GmbH' |
|
57 | __author__ = 'RhodeCode GmbH' |
@@ -35,8 +35,9 b' from pyramid.httpexceptions import HTTPN' | |||||
35 |
|
35 | |||
36 | from rhodecode.api.exc import ( |
|
36 | from rhodecode.api.exc import ( | |
37 | JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) |
|
37 | JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) | |
|
38 | from rhodecode.apps._base import TemplateArgs | |||
38 | from rhodecode.lib.auth import AuthUser |
|
39 | from rhodecode.lib.auth import AuthUser | |
39 | from rhodecode.lib.base import get_ip_addr |
|
40 | from rhodecode.lib.base import get_ip_addr, attach_context_attributes | |
40 | from rhodecode.lib.ext_json import json |
|
41 | from rhodecode.lib.ext_json import json | |
41 | from rhodecode.lib.utils2 import safe_str |
|
42 | from rhodecode.lib.utils2 import safe_str | |
42 | from rhodecode.lib.plugins.utils import get_plugin_settings |
|
43 | from rhodecode.lib.plugins.utils import get_plugin_settings | |
@@ -278,6 +279,11 b' def request_view(request):' | |||||
278 | 'request': request, |
|
279 | 'request': request, | |
279 | 'apiuser': auth_u |
|
280 | 'apiuser': auth_u | |
280 | }) |
|
281 | }) | |
|
282 | ||||
|
283 | # register some common functions for usage | |||
|
284 | attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id, | |||
|
285 | attach_to_request=True) | |||
|
286 | ||||
281 | try: |
|
287 | try: | |
282 | ret_value = func(**call_params) |
|
288 | ret_value = func(**call_params) | |
283 | return jsonrpc_response(request, ret_value) |
|
289 | return jsonrpc_response(request, ret_value) |
@@ -43,16 +43,16 b' class TestClosePullRequest(object):' | |||||
43 | response = api_call(self.app, params) |
|
43 | response = api_call(self.app, params) | |
44 | expected = { |
|
44 | expected = { | |
45 | 'pull_request_id': pull_request_id, |
|
45 | 'pull_request_id': pull_request_id, | |
|
46 | 'close_status': 'Rejected', | |||
46 | 'closed': True, |
|
47 | 'closed': True, | |
47 | } |
|
48 | } | |
48 | assert_ok(id_, expected, response.body) |
|
49 | assert_ok(id_, expected, response.body) | |
49 | action = 'user_closed_pull_request:%d' % pull_request_id |
|
|||
50 | journal = UserLog.query()\ |
|
50 | journal = UserLog.query()\ | |
51 | .filter(UserLog.user_id == author)\ |
|
51 | .filter(UserLog.user_id == author) \ | |
|
52 | .order_by('user_log_id') \ | |||
52 | .filter(UserLog.repository_id == repo)\ |
|
53 | .filter(UserLog.repository_id == repo)\ | |
53 | .filter(UserLog.action == action)\ |
|
|||
54 | .all() |
|
54 | .all() | |
55 | assert len(journal) == 1 |
|
55 | assert journal[-1].action == 'repo.pull_request.close' | |
56 |
|
56 | |||
57 | @pytest.mark.backends("git", "hg") |
|
57 | @pytest.mark.backends("git", "hg") | |
58 | def test_api_close_pull_request_already_closed_error(self, pr_util): |
|
58 | def test_api_close_pull_request_already_closed_error(self, pr_util): |
@@ -62,13 +62,12 b' class TestCommentPullRequest(object):' | |||||
62 | } |
|
62 | } | |
63 | assert_ok(id_, expected, response.body) |
|
63 | assert_ok(id_, expected, response.body) | |
64 |
|
64 | |||
65 | action = 'user_commented_pull_request:%d' % pull_request_id |
|
|||
66 | journal = UserLog.query()\ |
|
65 | journal = UserLog.query()\ | |
67 | .filter(UserLog.user_id == author)\ |
|
66 | .filter(UserLog.user_id == author)\ | |
68 | .filter(UserLog.repository_id == repo)\ |
|
67 | .filter(UserLog.repository_id == repo) \ | |
69 | .filter(UserLog.action == action)\ |
|
68 | .order_by('user_log_id') \ | |
70 | .all() |
|
69 | .all() | |
71 | assert len(journal) == 2 |
|
70 | assert journal[-1].action == 'repo.pull_request.comment.create' | |
72 |
|
71 | |||
73 | @pytest.mark.backends("git", "hg") |
|
72 | @pytest.mark.backends("git", "hg") | |
74 | def test_api_comment_pull_request_change_status( |
|
73 | def test_api_comment_pull_request_change_status( |
@@ -77,7 +77,7 b' class TestCreatePullRequestApi(object):' | |||||
77 | assert pull_request.source_repo.repo_name == data['source_repo'] |
|
77 | assert pull_request.source_repo.repo_name == data['source_repo'] | |
78 | assert pull_request.target_repo.repo_name == data['target_repo'] |
|
78 | assert pull_request.target_repo.repo_name == data['target_repo'] | |
79 | assert pull_request.revisions == [self.commit_ids['change']] |
|
79 | assert pull_request.revisions == [self.commit_ids['change']] | |
80 |
assert pull_request.reviewers == |
|
80 | assert len(pull_request.reviewers) == 1 | |
81 |
|
81 | |||
82 | @pytest.mark.backends("git", "hg") |
|
82 | @pytest.mark.backends("git", "hg") | |
83 | def test_create_with_empty_description(self, backend): |
|
83 | def test_create_with_empty_description(self, backend): | |
@@ -98,7 +98,12 b' class TestCreatePullRequestApi(object):' | |||||
98 | def test_create_with_reviewers_specified_by_names( |
|
98 | def test_create_with_reviewers_specified_by_names( | |
99 | self, backend, no_notifications): |
|
99 | self, backend, no_notifications): | |
100 | data = self._prepare_data(backend) |
|
100 | data = self._prepare_data(backend) | |
101 | reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN] |
|
101 | reviewers = [ | |
|
102 | {'username': TEST_USER_REGULAR_LOGIN, | |||
|
103 | 'reasons': ['added manually']}, | |||
|
104 | {'username': TEST_USER_ADMIN_LOGIN, | |||
|
105 | 'reasons': ['added manually']}, | |||
|
106 | ] | |||
102 | data['reviewers'] = reviewers |
|
107 | data['reviewers'] = reviewers | |
103 | id_, params = build_data( |
|
108 | id_, params = build_data( | |
104 | self.apikey_regular, 'create_pull_request', **data) |
|
109 | self.apikey_regular, 'create_pull_request', **data) | |
@@ -110,16 +115,26 b' class TestCreatePullRequestApi(object):' | |||||
110 | assert result['result']['msg'] == expected_message |
|
115 | assert result['result']['msg'] == expected_message | |
111 | pull_request_id = result['result']['pull_request_id'] |
|
116 | pull_request_id = result['result']['pull_request_id'] | |
112 | pull_request = PullRequestModel().get(pull_request_id) |
|
117 | pull_request = PullRequestModel().get(pull_request_id) | |
113 | actual_reviewers = [r.user.username for r in pull_request.reviewers] |
|
118 | actual_reviewers = [ | |
|
119 | {'username': r.user.username, | |||
|
120 | 'reasons': ['added manually'], | |||
|
121 | } for r in pull_request.reviewers | |||
|
122 | ] | |||
114 | assert sorted(actual_reviewers) == sorted(reviewers) |
|
123 | assert sorted(actual_reviewers) == sorted(reviewers) | |
115 |
|
124 | |||
116 | @pytest.mark.backends("git", "hg") |
|
125 | @pytest.mark.backends("git", "hg") | |
117 | def test_create_with_reviewers_specified_by_ids( |
|
126 | def test_create_with_reviewers_specified_by_ids( | |
118 | self, backend, no_notifications): |
|
127 | self, backend, no_notifications): | |
119 | data = self._prepare_data(backend) |
|
128 | data = self._prepare_data(backend) | |
120 | reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN] |
|
|||
121 | reviewers = [ |
|
129 | reviewers = [ | |
122 |
UserModel().get_by_username( |
|
130 | {'username': UserModel().get_by_username( | |
|
131 | TEST_USER_REGULAR_LOGIN).user_id, | |||
|
132 | 'reasons': ['added manually']}, | |||
|
133 | {'username': UserModel().get_by_username( | |||
|
134 | TEST_USER_ADMIN_LOGIN).user_id, | |||
|
135 | 'reasons': ['added manually']}, | |||
|
136 | ] | |||
|
137 | ||||
123 | data['reviewers'] = reviewers |
|
138 | data['reviewers'] = reviewers | |
124 | id_, params = build_data( |
|
139 | id_, params = build_data( | |
125 | self.apikey_regular, 'create_pull_request', **data) |
|
140 | self.apikey_regular, 'create_pull_request', **data) | |
@@ -131,14 +146,17 b' class TestCreatePullRequestApi(object):' | |||||
131 | assert result['result']['msg'] == expected_message |
|
146 | assert result['result']['msg'] == expected_message | |
132 | pull_request_id = result['result']['pull_request_id'] |
|
147 | pull_request_id = result['result']['pull_request_id'] | |
133 | pull_request = PullRequestModel().get(pull_request_id) |
|
148 | pull_request = PullRequestModel().get(pull_request_id) | |
134 | actual_reviewers = [r.user.username for r in pull_request.reviewers] |
|
149 | actual_reviewers = [ | |
135 | assert sorted(actual_reviewers) == sorted(reviewer_names) |
|
150 | {'username': r.user.user_id, | |
|
151 | 'reasons': ['added manually'], | |||
|
152 | } for r in pull_request.reviewers | |||
|
153 | ] | |||
|
154 | assert sorted(actual_reviewers) == sorted(reviewers) | |||
136 |
|
155 | |||
137 | @pytest.mark.backends("git", "hg") |
|
156 | @pytest.mark.backends("git", "hg") | |
138 | def test_create_fails_when_the_reviewer_is_not_found(self, backend): |
|
157 | def test_create_fails_when_the_reviewer_is_not_found(self, backend): | |
139 | data = self._prepare_data(backend) |
|
158 | data = self._prepare_data(backend) | |
140 | reviewers = ['somebody'] |
|
159 | data['reviewers'] = [{'username': 'somebody'}] | |
141 | data['reviewers'] = reviewers |
|
|||
142 | id_, params = build_data( |
|
160 | id_, params = build_data( | |
143 | self.apikey_regular, 'create_pull_request', **data) |
|
161 | self.apikey_regular, 'create_pull_request', **data) | |
144 | response = api_call(self.app, params) |
|
162 | response = api_call(self.app, params) | |
@@ -153,7 +171,7 b' class TestCreatePullRequestApi(object):' | |||||
153 | id_, params = build_data( |
|
171 | id_, params = build_data( | |
154 | self.apikey_regular, 'create_pull_request', **data) |
|
172 | self.apikey_regular, 'create_pull_request', **data) | |
155 | response = api_call(self.app, params) |
|
173 | response = api_call(self.app, params) | |
156 | expected_message = 'reviewers should be specified as a list' |
|
174 | expected_message = {u'': '"test_regular,test_admin" is not iterable'} | |
157 | assert_error(id_, expected_message, given=response.body) |
|
175 | assert_error(id_, expected_message, given=response.body) | |
158 |
|
176 | |||
159 | @pytest.mark.backends("git", "hg") |
|
177 | @pytest.mark.backends("git", "hg") |
@@ -59,6 +59,21 b' class TestCreateUser(object):' | |||||
59 | expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,) |
|
59 | expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,) | |
60 | assert_error(id_, expected, given=response.body) |
|
60 | assert_error(id_, expected, given=response.body) | |
61 |
|
61 | |||
|
62 | def test_api_create_user_with_wrong_username(self): | |||
|
63 | bad_username = '<> HELLO WORLD <>' | |||
|
64 | id_, params = build_data( | |||
|
65 | self.apikey, 'create_user', | |||
|
66 | username=bad_username, | |||
|
67 | email='new@email.com', | |||
|
68 | password='trololo') | |||
|
69 | response = api_call(self.app, params) | |||
|
70 | ||||
|
71 | expected = {'username': | |||
|
72 | "Username may only contain alphanumeric characters " | |||
|
73 | "underscores, periods or dashes and must begin with " | |||
|
74 | "alphanumeric character or underscore"} | |||
|
75 | assert_error(id_, expected, given=response.body) | |||
|
76 | ||||
62 | def test_api_create_user(self): |
|
77 | def test_api_create_user(self): | |
63 | username = 'test_new_api_user' |
|
78 | username = 'test_new_api_user' | |
64 | email = username + "@foo.com" |
|
79 | email = username + "@foo.com" | |
@@ -175,7 +190,6 b' class TestCreateUser(object):' | |||||
175 | fixture.destroy_repo_group(username) |
|
190 | fixture.destroy_repo_group(username) | |
176 | fixture.destroy_user(usr.user_id) |
|
191 | fixture.destroy_user(usr.user_id) | |
177 |
|
192 | |||
178 |
|
||||
179 | @mock.patch.object(UserModel, 'create_or_update', crash) |
|
193 | @mock.patch.object(UserModel, 'create_or_update', crash) | |
180 | def test_api_create_user_when_exception_happened(self): |
|
194 | def test_api_create_user_when_exception_happened(self): | |
181 |
|
195 |
@@ -112,3 +112,16 b' class TestCreateUserGroup(object):' | |||||
112 |
|
112 | |||
113 | expected = 'failed to create group `%s`' % (group_name,) |
|
113 | expected = 'failed to create group `%s`' % (group_name,) | |
114 | assert_error(id_, expected, given=response.body) |
|
114 | assert_error(id_, expected, given=response.body) | |
|
115 | ||||
|
116 | def test_api_create_user_group_with_wrong_name(self, user_util): | |||
|
117 | ||||
|
118 | group_name = 'wrong NAME <>' | |||
|
119 | id_, params = build_data( | |||
|
120 | self.apikey, 'create_user_group', group_name=group_name) | |||
|
121 | response = api_call(self.app, params) | |||
|
122 | ||||
|
123 | expected = {"user_group_name": | |||
|
124 | "Allowed in name are letters, numbers, and `-`, `_`, " | |||
|
125 | "`.` Name must start with a letter or number. " | |||
|
126 | "Got `{}`".format(group_name)} | |||
|
127 | assert_error(id_, expected, given=response.body) |
@@ -30,43 +30,45 b' from rhodecode.api.tests.utils import (' | |||||
30 | class TestApiDeleteRepo(object): |
|
30 | class TestApiDeleteRepo(object): | |
31 | def test_api_delete_repo(self, backend): |
|
31 | def test_api_delete_repo(self, backend): | |
32 | repo = backend.create_repo() |
|
32 | repo = backend.create_repo() | |
33 |
|
33 | repo_name = repo.repo_name | ||
34 | id_, params = build_data( |
|
34 | id_, params = build_data( | |
35 | self.apikey, 'delete_repo', repoid=repo.repo_name, ) |
|
35 | self.apikey, 'delete_repo', repoid=repo.repo_name, ) | |
36 | response = api_call(self.app, params) |
|
36 | response = api_call(self.app, params) | |
37 |
|
37 | |||
38 | expected = { |
|
38 | expected = { | |
39 |
'msg': 'Deleted repository `%s`' % ( |
|
39 | 'msg': 'Deleted repository `%s`' % (repo_name,), | |
40 | 'success': True |
|
40 | 'success': True | |
41 | } |
|
41 | } | |
42 | assert_ok(id_, expected, given=response.body) |
|
42 | assert_ok(id_, expected, given=response.body) | |
43 |
|
43 | |||
44 | def test_api_delete_repo_by_non_admin(self, backend, user_regular): |
|
44 | def test_api_delete_repo_by_non_admin(self, backend, user_regular): | |
45 | repo = backend.create_repo(cur_user=user_regular.username) |
|
45 | repo = backend.create_repo(cur_user=user_regular.username) | |
|
46 | repo_name = repo.repo_name | |||
46 | id_, params = build_data( |
|
47 | id_, params = build_data( | |
47 | user_regular.api_key, 'delete_repo', repoid=repo.repo_name, ) |
|
48 | user_regular.api_key, 'delete_repo', repoid=repo.repo_name, ) | |
48 | response = api_call(self.app, params) |
|
49 | response = api_call(self.app, params) | |
49 |
|
50 | |||
50 | expected = { |
|
51 | expected = { | |
51 |
'msg': 'Deleted repository `%s`' % ( |
|
52 | 'msg': 'Deleted repository `%s`' % (repo_name,), | |
52 | 'success': True |
|
53 | 'success': True | |
53 | } |
|
54 | } | |
54 | assert_ok(id_, expected, given=response.body) |
|
55 | assert_ok(id_, expected, given=response.body) | |
55 |
|
56 | |||
56 | def test_api_delete_repo_by_non_admin_no_permission(self, backend): |
|
57 | def test_api_delete_repo_by_non_admin_no_permission(self, backend): | |
57 | repo = backend.create_repo() |
|
58 | repo = backend.create_repo() | |
|
59 | repo_name = repo.repo_name | |||
58 | id_, params = build_data( |
|
60 | id_, params = build_data( | |
59 | self.apikey_regular, 'delete_repo', repoid=repo.repo_name, ) |
|
61 | self.apikey_regular, 'delete_repo', repoid=repo.repo_name, ) | |
60 | response = api_call(self.app, params) |
|
62 | response = api_call(self.app, params) | |
61 |
expected = 'repository `%s` does not exist' % ( |
|
63 | expected = 'repository `%s` does not exist' % (repo_name) | |
62 | assert_error(id_, expected, given=response.body) |
|
64 | assert_error(id_, expected, given=response.body) | |
63 |
|
65 | |||
64 | def test_api_delete_repo_exception_occurred(self, backend): |
|
66 | def test_api_delete_repo_exception_occurred(self, backend): | |
65 | repo = backend.create_repo() |
|
67 | repo = backend.create_repo() | |
|
68 | repo_name = repo.repo_name | |||
66 | id_, params = build_data( |
|
69 | id_, params = build_data( | |
67 | self.apikey, 'delete_repo', repoid=repo.repo_name, ) |
|
70 | self.apikey, 'delete_repo', repoid=repo.repo_name, ) | |
68 | with mock.patch.object(RepoModel, 'delete', crash): |
|
71 | with mock.patch.object(RepoModel, 'delete', crash): | |
69 | response = api_call(self.app, params) |
|
72 | response = api_call(self.app, params) | |
70 | expected = 'failed to delete repository `%s`' % ( |
|
73 | expected = 'failed to delete repository `%s`' % (repo_name,) | |
71 | repo.repo_name,) |
|
|||
72 | assert_error(id_, expected, given=response.body) |
|
74 | assert_error(id_, expected, given=response.body) |
@@ -28,7 +28,7 b' from rhodecode.api.tests.utils import (' | |||||
28 |
|
28 | |||
29 | @pytest.mark.usefixtures("testuser_api", "app") |
|
29 | @pytest.mark.usefixtures("testuser_api", "app") | |
30 | class TestApiGetGist(object): |
|
30 | class TestApiGetGist(object): | |
31 | def test_api_get_gist(self, gist_util): |
|
31 | def test_api_get_gist(self, gist_util, http_host_stub): | |
32 | gist = gist_util.create_gist() |
|
32 | gist = gist_util.create_gist() | |
33 | gist_id = gist.gist_access_id |
|
33 | gist_id = gist.gist_access_id | |
34 | gist_created_on = gist.created_on |
|
34 | gist_created_on = gist.created_on | |
@@ -45,14 +45,14 b' class TestApiGetGist(object):' | |||||
45 | 'expires': -1.0, |
|
45 | 'expires': -1.0, | |
46 | 'gist_id': int(gist_id), |
|
46 | 'gist_id': int(gist_id), | |
47 | 'type': 'public', |
|
47 | 'type': 'public', | |
48 |
'url': 'http:// |
|
48 | 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), | |
49 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, |
|
49 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, | |
50 | 'content': None, |
|
50 | 'content': None, | |
51 | } |
|
51 | } | |
52 |
|
52 | |||
53 | assert_ok(id_, expected, given=response.body) |
|
53 | assert_ok(id_, expected, given=response.body) | |
54 |
|
54 | |||
55 | def test_api_get_gist_with_content(self, gist_util): |
|
55 | def test_api_get_gist_with_content(self, gist_util, http_host_stub): | |
56 | mapping = { |
|
56 | mapping = { | |
57 | u'filename1.txt': {'content': u'hello world'}, |
|
57 | u'filename1.txt': {'content': u'hello world'}, | |
58 | u'filename1ą.txt': {'content': u'hello worldę'} |
|
58 | u'filename1ą.txt': {'content': u'hello worldę'} | |
@@ -73,7 +73,7 b' class TestApiGetGist(object):' | |||||
73 | 'expires': -1.0, |
|
73 | 'expires': -1.0, | |
74 | 'gist_id': int(gist_id), |
|
74 | 'gist_id': int(gist_id), | |
75 | 'type': 'public', |
|
75 | 'type': 'public', | |
76 |
'url': 'http:// |
|
76 | 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), | |
77 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, |
|
77 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, | |
78 | 'content': { |
|
78 | 'content': { | |
79 | u'filename1.txt': u'hello world', |
|
79 | u'filename1.txt': u'hello world', |
@@ -19,13 +19,13 b'' | |||||
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
21 |
|
21 | |||
22 | import mock |
|
|||
23 | import pytest |
|
22 | import pytest | |
24 | import urlobject |
|
23 | import urlobject | |
25 | from pylons import url |
|
24 | from pylons import url | |
26 |
|
25 | |||
27 | from rhodecode.api.tests.utils import ( |
|
26 | from rhodecode.api.tests.utils import ( | |
28 | build_data, api_call, assert_error, assert_ok) |
|
27 | build_data, api_call, assert_error, assert_ok) | |
|
28 | from rhodecode.lib.utils2 import safe_unicode | |||
29 |
|
29 | |||
30 | pytestmark = pytest.mark.backends("git", "hg") |
|
30 | pytestmark = pytest.mark.backends("git", "hg") | |
31 |
|
31 | |||
@@ -33,7 +33,7 b' pytestmark = pytest.mark.backends("git",' | |||||
33 | @pytest.mark.usefixtures("testuser_api", "app") |
|
33 | @pytest.mark.usefixtures("testuser_api", "app") | |
34 | class TestGetPullRequest(object): |
|
34 | class TestGetPullRequest(object): | |
35 |
|
35 | |||
36 | def test_api_get_pull_request(self, pr_util): |
|
36 | def test_api_get_pull_request(self, pr_util, http_host_only_stub): | |
37 | from rhodecode.model.pull_request import PullRequestModel |
|
37 | from rhodecode.model.pull_request import PullRequestModel | |
38 | pull_request = pr_util.create_pull_request(mergeable=True) |
|
38 | pull_request = pr_util.create_pull_request(mergeable=True) | |
39 | id_, params = build_data( |
|
39 | id_, params = build_data( | |
@@ -50,16 +50,16 b' class TestGetPullRequest(object):' | |||||
50 | 'pullrequest_show', |
|
50 | 'pullrequest_show', | |
51 | repo_name=pull_request.target_repo.repo_name, |
|
51 | repo_name=pull_request.target_repo.repo_name, | |
52 | pull_request_id=pull_request.pull_request_id, qualified=True)) |
|
52 | pull_request_id=pull_request.pull_request_id, qualified=True)) | |
53 | pr_url = unicode( |
|
53 | ||
54 | url_obj.with_netloc('test.example.com:80')) |
|
54 | pr_url = safe_unicode( | |
55 | source_url = unicode( |
|
55 | url_obj.with_netloc(http_host_only_stub)) | |
56 | pull_request.source_repo.clone_url() |
|
56 | source_url = safe_unicode( | |
57 | .with_netloc('test.example.com:80')) |
|
57 | pull_request.source_repo.clone_url().with_netloc(http_host_only_stub)) | |
58 | target_url = unicode( |
|
58 | target_url = safe_unicode( | |
59 | pull_request.target_repo.clone_url() |
|
59 | pull_request.target_repo.clone_url().with_netloc(http_host_only_stub)) | |
60 | .with_netloc('test.example.com:80')) |
|
60 | shadow_url = safe_unicode( | |
61 | shadow_url = unicode( |
|
|||
62 | PullRequestModel().get_shadow_clone_url(pull_request)) |
|
61 | PullRequestModel().get_shadow_clone_url(pull_request)) | |
|
62 | ||||
63 | expected = { |
|
63 | expected = { | |
64 | 'pull_request_id': pull_request.pull_request_id, |
|
64 | 'pull_request_id': pull_request.pull_request_id, | |
65 | 'url': pr_url, |
|
65 | 'url': pr_url, | |
@@ -109,7 +109,8 b' class TestGetPullRequest(object):' | |||||
109 | 'reasons': reasons, |
|
109 | 'reasons': reasons, | |
110 | 'review_status': st[0][1].status if st else 'not_reviewed', |
|
110 | 'review_status': st[0][1].status if st else 'not_reviewed', | |
111 | } |
|
111 | } | |
112 |
for reviewer, reasons, st in |
|
112 | for reviewer, reasons, mandatory, st in | |
|
113 | pull_request.reviewers_statuses() | |||
113 | ] |
|
114 | ] | |
114 | } |
|
115 | } | |
115 | assert_ok(id_, expected, response.body) |
|
116 | assert_ok(id_, expected, response.body) |
@@ -95,13 +95,13 b' class TestMergePullRequest(object):' | |||||
95 |
|
95 | |||
96 | assert_ok(id_, expected, response.body) |
|
96 | assert_ok(id_, expected, response.body) | |
97 |
|
97 | |||
98 | action = 'user_merged_pull_request:%d' % (pull_request_id, ) |
|
|||
99 | journal = UserLog.query()\ |
|
98 | journal = UserLog.query()\ | |
100 | .filter(UserLog.user_id == author)\ |
|
99 | .filter(UserLog.user_id == author)\ | |
101 | .filter(UserLog.repository_id == repo)\ |
|
100 | .filter(UserLog.repository_id == repo) \ | |
102 | .filter(UserLog.action == action)\ |
|
101 | .order_by('user_log_id') \ | |
103 | .all() |
|
102 | .all() | |
104 | assert len(journal) == 1 |
|
103 | assert journal[-2].action == 'repo.pull_request.merge' | |
|
104 | assert journal[-1].action == 'repo.pull_request.close' | |||
105 |
|
105 | |||
106 | id_, params = build_data( |
|
106 | id_, params = build_data( | |
107 | self.apikey, 'merge_pull_request', |
|
107 | self.apikey, 'merge_pull_request', |
@@ -33,7 +33,7 b' class TestUpdatePullRequest(object):' | |||||
33 |
|
33 | |||
34 | @pytest.mark.backends("git", "hg") |
|
34 | @pytest.mark.backends("git", "hg") | |
35 | def test_api_update_pull_request_title_or_description( |
|
35 | def test_api_update_pull_request_title_or_description( | |
36 |
self, pr_util, |
|
36 | self, pr_util, no_notifications): | |
37 | pull_request = pr_util.create_pull_request() |
|
37 | pull_request = pr_util.create_pull_request() | |
38 |
|
38 | |||
39 | id_, params = build_data( |
|
39 | id_, params = build_data( | |
@@ -61,7 +61,7 b' class TestUpdatePullRequest(object):' | |||||
61 |
|
61 | |||
62 | @pytest.mark.backends("git", "hg") |
|
62 | @pytest.mark.backends("git", "hg") | |
63 | def test_api_try_update_closed_pull_request( |
|
63 | def test_api_try_update_closed_pull_request( | |
64 |
self, pr_util, |
|
64 | self, pr_util, no_notifications): | |
65 | pull_request = pr_util.create_pull_request() |
|
65 | pull_request = pr_util.create_pull_request() | |
66 | PullRequestModel().close_pull_request( |
|
66 | PullRequestModel().close_pull_request( | |
67 | pull_request, TEST_USER_ADMIN_LOGIN) |
|
67 | pull_request, TEST_USER_ADMIN_LOGIN) | |
@@ -78,8 +78,7 b' class TestUpdatePullRequest(object):' | |||||
78 | assert_error(id_, expected, response.body) |
|
78 | assert_error(id_, expected, response.body) | |
79 |
|
79 | |||
80 | @pytest.mark.backends("git", "hg") |
|
80 | @pytest.mark.backends("git", "hg") | |
81 | def test_api_update_update_commits( |
|
81 | def test_api_update_update_commits(self, pr_util, no_notifications): | |
82 | self, pr_util, silence_action_logger, no_notifications): |
|
|||
83 | commits = [ |
|
82 | commits = [ | |
84 | {'message': 'a'}, |
|
83 | {'message': 'a'}, | |
85 | {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]}, |
|
84 | {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]}, | |
@@ -119,20 +118,28 b' class TestUpdatePullRequest(object):' | |||||
119 |
|
118 | |||
120 | @pytest.mark.backends("git", "hg") |
|
119 | @pytest.mark.backends("git", "hg") | |
121 | def test_api_update_change_reviewers( |
|
120 | def test_api_update_change_reviewers( | |
122 |
self, |
|
121 | self, user_util, pr_util, no_notifications): | |
|
122 | a = user_util.create_user() | |||
|
123 | b = user_util.create_user() | |||
|
124 | c = user_util.create_user() | |||
|
125 | new_reviewers = [ | |||
|
126 | {'username': b.username,'reasons': ['updated via API'], | |||
|
127 | 'mandatory':False}, | |||
|
128 | {'username': c.username, 'reasons': ['updated via API'], | |||
|
129 | 'mandatory':False}, | |||
|
130 | ] | |||
123 |
|
131 | |||
124 |
|
|
132 | added = [b.username, c.username] | |
125 |
|
|
133 | removed = [a.username] | |
126 | removed = sorted(new) |
|
|||
127 | added = sorted(users) |
|
|||
128 |
|
134 | |||
129 |
pull_request = pr_util.create_pull_request( |
|
135 | pull_request = pr_util.create_pull_request( | |
|
136 | reviewers=[(a.username, ['added via API'], False)]) | |||
130 |
|
137 | |||
131 | id_, params = build_data( |
|
138 | id_, params = build_data( | |
132 | self.apikey, 'update_pull_request', |
|
139 | self.apikey, 'update_pull_request', | |
133 | repoid=pull_request.target_repo.repo_name, |
|
140 | repoid=pull_request.target_repo.repo_name, | |
134 | pullrequestid=pull_request.pull_request_id, |
|
141 | pullrequestid=pull_request.pull_request_id, | |
135 |
reviewers= |
|
142 | reviewers=new_reviewers) | |
136 | response = api_call(self.app, params) |
|
143 | response = api_call(self.app, params) | |
137 | expected = { |
|
144 | expected = { | |
138 | "msg": "Updated pull request `{}`".format( |
|
145 | "msg": "Updated pull request `{}`".format( | |
@@ -152,7 +159,7 b' class TestUpdatePullRequest(object):' | |||||
152 | self.apikey, 'update_pull_request', |
|
159 | self.apikey, 'update_pull_request', | |
153 | repoid=pull_request.target_repo.repo_name, |
|
160 | repoid=pull_request.target_repo.repo_name, | |
154 | pullrequestid=pull_request.pull_request_id, |
|
161 | pullrequestid=pull_request.pull_request_id, | |
155 | reviewers=['bad_name']) |
|
162 | reviewers=[{'username': 'bad_name'}]) | |
156 | response = api_call(self.app, params) |
|
163 | response = api_call(self.app, params) | |
157 |
|
164 | |||
158 | expected = 'user `bad_name` does not exist' |
|
165 | expected = 'user `bad_name` does not exist' | |
@@ -165,7 +172,7 b' class TestUpdatePullRequest(object):' | |||||
165 | self.apikey, 'update_pull_request', |
|
172 | self.apikey, 'update_pull_request', | |
166 | repoid='fake', |
|
173 | repoid='fake', | |
167 | pullrequestid='fake', |
|
174 | pullrequestid='fake', | |
168 | reviewers=['bad_name']) |
|
175 | reviewers=[{'username': 'bad_name'}]) | |
169 | response = api_call(self.app, params) |
|
176 | response = api_call(self.app, params) | |
170 |
|
177 | |||
171 | expected = 'repository `fake` does not exist' |
|
178 | expected = 'repository `fake` does not exist' | |
@@ -181,7 +188,7 b' class TestUpdatePullRequest(object):' | |||||
181 | self.apikey, 'update_pull_request', |
|
188 | self.apikey, 'update_pull_request', | |
182 | repoid=pull_request.target_repo.repo_name, |
|
189 | repoid=pull_request.target_repo.repo_name, | |
183 | pullrequestid=999999, |
|
190 | pullrequestid=999999, | |
184 | reviewers=['bad_name']) |
|
191 | reviewers=[{'username': 'bad_name'}]) | |
185 | response = api_call(self.app, params) |
|
192 | response = api_call(self.app, params) | |
186 |
|
193 | |||
187 | expected = 'pull request `999999` does not exist' |
|
194 | expected = 'pull request `999999` does not exist' |
@@ -26,7 +26,7 b' from rhodecode.tests import TEST_USER_AD' | |||||
26 | from rhodecode.api.tests.utils import ( |
|
26 | from rhodecode.api.tests.utils import ( | |
27 | build_data, api_call, assert_error, assert_ok, crash, jsonify) |
|
27 | build_data, api_call, assert_error, assert_ok, crash, jsonify) | |
28 | from rhodecode.tests.fixture import Fixture |
|
28 | from rhodecode.tests.fixture import Fixture | |
29 |
|
29 | from rhodecode.tests.plugin import http_host_stub, http_host_only_stub | ||
30 |
|
30 | |||
31 | fixture = Fixture() |
|
31 | fixture = Fixture() | |
32 |
|
32 | |||
@@ -71,14 +71,15 b' class TestApiUpdateRepo(object):' | |||||
71 | ({'repo_name': 'new_repo_name'}, |
|
71 | ({'repo_name': 'new_repo_name'}, | |
72 | { |
|
72 | { | |
73 | 'repo_name': 'new_repo_name', |
|
73 | 'repo_name': 'new_repo_name', | |
74 |
'url': 'http:// |
|
74 | 'url': 'http://{}/new_repo_name'.format(http_host_only_stub()) | |
75 | }), |
|
75 | }), | |
76 |
|
76 | |||
77 | ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), |
|
77 | ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), | |
78 | '_group': 'test_group_for_update'}, |
|
78 | '_group': 'test_group_for_update'}, | |
79 | { |
|
79 | { | |
80 | 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), |
|
80 | 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), | |
81 |
'url': 'http:// |
|
81 | 'url': 'http://{}/test_group_for_update/{}'.format( | |
|
82 | http_host_only_stub(), UPDATE_REPO_NAME) | |||
82 | }), |
|
83 | }), | |
83 | ]) |
|
84 | ]) | |
84 | def test_api_update_repo(self, updates, expected, backend): |
|
85 | def test_api_update_repo(self, updates, expected, backend): | |
@@ -115,7 +116,8 b' class TestApiUpdateRepo(object):' | |||||
115 | master_repo = backend.create_repo() |
|
116 | master_repo = backend.create_repo() | |
116 | repo = backend.create_repo() |
|
117 | repo = backend.create_repo() | |
117 | updates = { |
|
118 | updates = { | |
118 | 'fork_of': master_repo.repo_name |
|
119 | 'fork_of': master_repo.repo_name, | |
|
120 | 'fork_of_id': master_repo.repo_id | |||
119 | } |
|
121 | } | |
120 | expected_api_data = repo.get_api_data(include_secrets=True) |
|
122 | expected_api_data = repo.get_api_data(include_secrets=True) | |
121 | expected_api_data.update(updates) |
|
123 | expected_api_data.update(updates) | |
@@ -130,6 +132,7 b' class TestApiUpdateRepo(object):' | |||||
130 | assert_ok(id_, expected, given=response.body) |
|
132 | assert_ok(id_, expected, given=response.body) | |
131 | result = response.json['result']['repository'] |
|
133 | result = response.json['result']['repository'] | |
132 | assert result['fork_of'] == master_repo.repo_name |
|
134 | assert result['fork_of'] == master_repo.repo_name | |
|
135 | assert result['fork_of_id'] == master_repo.repo_id | |||
133 |
|
136 | |||
134 | def test_api_update_repo_fork_of_not_found(self, backend): |
|
137 | def test_api_update_repo_fork_of_not_found(self, backend): | |
135 | master_repo_name = 'fake-parent-repo' |
|
138 | master_repo_name = 'fake-parent-repo' |
@@ -21,7 +21,8 b'' | |||||
21 |
|
21 | |||
22 | import logging |
|
22 | import logging | |
23 |
|
23 | |||
24 | from rhodecode.api import jsonrpc_method, JSONRPCError |
|
24 | from rhodecode import events | |
|
25 | from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError | |||
25 | from rhodecode.api.utils import ( |
|
26 | from rhodecode.api.utils import ( | |
26 | has_superadmin_permission, Optional, OAttr, get_repo_or_error, |
|
27 | has_superadmin_permission, Optional, OAttr, get_repo_or_error, | |
27 | get_pull_request_or_error, get_commit_or_error, get_user_or_error, |
|
28 | get_pull_request_or_error, get_commit_or_error, get_user_or_error, | |
@@ -34,6 +35,9 b' from rhodecode.model.comment import Comm' | |||||
34 | from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment |
|
35 | from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment | |
35 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck |
|
36 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck | |
36 | from rhodecode.model.settings import SettingsModel |
|
37 | from rhodecode.model.settings import SettingsModel | |
|
38 | from rhodecode.model.validation_schema import Invalid | |||
|
39 | from rhodecode.model.validation_schema.schemas.reviewer_schema import( | |||
|
40 | ReviewerListSchema) | |||
37 |
|
41 | |||
38 | log = logging.getLogger(__name__) |
|
42 | log = logging.getLogger(__name__) | |
39 |
|
43 | |||
@@ -224,8 +228,9 b' def get_pull_requests(request, apiuser, ' | |||||
224 |
|
228 | |||
225 |
|
229 | |||
226 | @jsonrpc_method() |
|
230 | @jsonrpc_method() | |
227 | def merge_pull_request(request, apiuser, repoid, pullrequestid, |
|
231 | def merge_pull_request( | |
228 | userid=Optional(OAttr('apiuser'))): |
|
232 | request, apiuser, repoid, pullrequestid, | |
|
233 | userid=Optional(OAttr('apiuser'))): | |||
229 | """ |
|
234 | """ | |
230 | Merge the pull request specified by `pullrequestid` into its target |
|
235 | Merge the pull request specified by `pullrequestid` into its target | |
231 | repository. |
|
236 | repository. | |
@@ -273,7 +278,12 b' def merge_pull_request(request, apiuser,' | |||||
273 | merge_possible = not check.failed |
|
278 | merge_possible = not check.failed | |
274 |
|
279 | |||
275 | if not merge_possible: |
|
280 | if not merge_possible: | |
276 | reasons = ','.join([msg for _e, msg in check.errors]) |
|
281 | error_messages = [] | |
|
282 | for err_type, error_msg in check.errors: | |||
|
283 | error_msg = request.translate(error_msg) | |||
|
284 | error_messages.append(error_msg) | |||
|
285 | ||||
|
286 | reasons = ','.join(error_messages) | |||
277 | raise JSONRPCError( |
|
287 | raise JSONRPCError( | |
278 | 'merge not possible for following reasons: {}'.format(reasons)) |
|
288 | 'merge not possible for following reasons: {}'.format(reasons)) | |
279 |
|
289 | |||
@@ -300,63 +310,6 b' def merge_pull_request(request, apiuser,' | |||||
300 |
|
310 | |||
301 |
|
311 | |||
302 | @jsonrpc_method() |
|
312 | @jsonrpc_method() | |
303 | def close_pull_request(request, apiuser, repoid, pullrequestid, |
|
|||
304 | userid=Optional(OAttr('apiuser'))): |
|
|||
305 | """ |
|
|||
306 | Close the pull request specified by `pullrequestid`. |
|
|||
307 |
|
||||
308 | :param apiuser: This is filled automatically from the |authtoken|. |
|
|||
309 | :type apiuser: AuthUser |
|
|||
310 | :param repoid: Repository name or repository ID to which the pull |
|
|||
311 | request belongs. |
|
|||
312 | :type repoid: str or int |
|
|||
313 | :param pullrequestid: ID of the pull request to be closed. |
|
|||
314 | :type pullrequestid: int |
|
|||
315 | :param userid: Close the pull request as this user. |
|
|||
316 | :type userid: Optional(str or int) |
|
|||
317 |
|
||||
318 | Example output: |
|
|||
319 |
|
||||
320 | .. code-block:: bash |
|
|||
321 |
|
||||
322 | "id": <id_given_in_input>, |
|
|||
323 | "result": { |
|
|||
324 | "pull_request_id": "<int>", |
|
|||
325 | "closed": "<bool>" |
|
|||
326 | }, |
|
|||
327 | "error": null |
|
|||
328 |
|
||||
329 | """ |
|
|||
330 | repo = get_repo_or_error(repoid) |
|
|||
331 | if not isinstance(userid, Optional): |
|
|||
332 | if (has_superadmin_permission(apiuser) or |
|
|||
333 | HasRepoPermissionAnyApi('repository.admin')( |
|
|||
334 | user=apiuser, repo_name=repo.repo_name)): |
|
|||
335 | apiuser = get_user_or_error(userid) |
|
|||
336 | else: |
|
|||
337 | raise JSONRPCError('userid is not the same as your user') |
|
|||
338 |
|
||||
339 | pull_request = get_pull_request_or_error(pullrequestid) |
|
|||
340 | if not PullRequestModel().check_user_update( |
|
|||
341 | pull_request, apiuser, api=True): |
|
|||
342 | raise JSONRPCError( |
|
|||
343 | 'pull request `%s` close failed, no permission to close.' % ( |
|
|||
344 | pullrequestid,)) |
|
|||
345 | if pull_request.is_closed(): |
|
|||
346 | raise JSONRPCError( |
|
|||
347 | 'pull request `%s` is already closed' % (pullrequestid,)) |
|
|||
348 |
|
||||
349 | PullRequestModel().close_pull_request( |
|
|||
350 | pull_request.pull_request_id, apiuser) |
|
|||
351 | Session().commit() |
|
|||
352 | data = { |
|
|||
353 | 'pull_request_id': pull_request.pull_request_id, |
|
|||
354 | 'closed': True, |
|
|||
355 | } |
|
|||
356 | return data |
|
|||
357 |
|
||||
358 |
|
||||
359 | @jsonrpc_method() |
|
|||
360 | def comment_pull_request( |
|
313 | def comment_pull_request( | |
361 | request, apiuser, repoid, pullrequestid, message=Optional(None), |
|
314 | request, apiuser, repoid, pullrequestid, message=Optional(None), | |
362 | commit_id=Optional(None), status=Optional(None), |
|
315 | commit_id=Optional(None), status=Optional(None), | |
@@ -529,24 +482,26 b' def create_pull_request(' | |||||
529 | :param description: Set the pull request description. |
|
482 | :param description: Set the pull request description. | |
530 | :type description: Optional(str) |
|
483 | :type description: Optional(str) | |
531 | :param reviewers: Set the new pull request reviewers list. |
|
484 | :param reviewers: Set the new pull request reviewers list. | |
|
485 | Reviewer defined by review rules will be added automatically to the | |||
|
486 | defined list. | |||
532 | :type reviewers: Optional(list) |
|
487 | :type reviewers: Optional(list) | |
533 | Accepts username strings or objects of the format: |
|
488 | Accepts username strings or objects of the format: | |
534 |
|
489 | |||
535 | {'username': 'nick', 'reasons': ['original author']} |
|
490 | [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}] | |
536 | """ |
|
491 | """ | |
537 |
|
492 | |||
538 | source = get_repo_or_error(source_repo) |
|
493 | source_db_repo = get_repo_or_error(source_repo) | |
539 | target = get_repo_or_error(target_repo) |
|
494 | target_db_repo = get_repo_or_error(target_repo) | |
540 | if not has_superadmin_permission(apiuser): |
|
495 | if not has_superadmin_permission(apiuser): | |
541 | _perms = ('repository.admin', 'repository.write', 'repository.read',) |
|
496 | _perms = ('repository.admin', 'repository.write', 'repository.read',) | |
542 | validate_repo_permissions(apiuser, source_repo, source, _perms) |
|
497 | validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms) | |
543 |
|
498 | |||
544 | full_source_ref = resolve_ref_or_error(source_ref, source) |
|
499 | full_source_ref = resolve_ref_or_error(source_ref, source_db_repo) | |
545 | full_target_ref = resolve_ref_or_error(target_ref, target) |
|
500 | full_target_ref = resolve_ref_or_error(target_ref, target_db_repo) | |
546 | source_commit = get_commit_or_error(full_source_ref, source) |
|
501 | source_commit = get_commit_or_error(full_source_ref, source_db_repo) | |
547 | target_commit = get_commit_or_error(full_target_ref, target) |
|
502 | target_commit = get_commit_or_error(full_target_ref, target_db_repo) | |
548 | source_scm = source.scm_instance() |
|
503 | source_scm = source_db_repo.scm_instance() | |
549 | target_scm = target.scm_instance() |
|
504 | target_scm = target_db_repo.scm_instance() | |
550 |
|
505 | |||
551 | commit_ranges = target_scm.compare( |
|
506 | commit_ranges = target_scm.compare( | |
552 | target_commit.raw_id, source_commit.raw_id, source_scm, |
|
507 | target_commit.raw_id, source_commit.raw_id, source_scm, | |
@@ -562,20 +517,36 b' def create_pull_request(' | |||||
562 | raise JSONRPCError('no common ancestor found') |
|
517 | raise JSONRPCError('no common ancestor found') | |
563 |
|
518 | |||
564 | reviewer_objects = Optional.extract(reviewers) or [] |
|
519 | reviewer_objects = Optional.extract(reviewers) or [] | |
565 | if not isinstance(reviewer_objects, list): |
|
520 | ||
566 | raise JSONRPCError('reviewers should be specified as a list') |
|
521 | if reviewer_objects: | |
|
522 | schema = ReviewerListSchema() | |||
|
523 | try: | |||
|
524 | reviewer_objects = schema.deserialize(reviewer_objects) | |||
|
525 | except Invalid as err: | |||
|
526 | raise JSONRPCValidationError(colander_exc=err) | |||
|
527 | ||||
|
528 | # validate users | |||
|
529 | for reviewer_object in reviewer_objects: | |||
|
530 | user = get_user_or_error(reviewer_object['username']) | |||
|
531 | reviewer_object['user_id'] = user.user_id | |||
567 |
|
532 | |||
568 | reviewers_reasons = [] |
|
533 | get_default_reviewers_data, get_validated_reviewers = \ | |
569 | for reviewer_object in reviewer_objects: |
|
534 | PullRequestModel().get_reviewer_functions() | |
570 | reviewer_reasons = [] |
|
535 | ||
571 | if isinstance(reviewer_object, (basestring, int)): |
|
536 | reviewer_rules = get_default_reviewers_data( | |
572 | reviewer_username = reviewer_object |
|
537 | apiuser.get_instance(), source_db_repo, | |
573 | else: |
|
538 | source_commit, target_db_repo, target_commit) | |
574 | reviewer_username = reviewer_object['username'] |
|
|||
575 | reviewer_reasons = reviewer_object.get('reasons', []) |
|
|||
576 |
|
539 | |||
577 | user = get_user_or_error(reviewer_username) |
|
540 | # specified rules are later re-validated, thus we can assume users will | |
578 | reviewers_reasons.append((user.user_id, reviewer_reasons)) |
|
541 | # eventually provide those that meet the reviewer criteria. | |
|
542 | if not reviewer_objects: | |||
|
543 | reviewer_objects = reviewer_rules['reviewers'] | |||
|
544 | ||||
|
545 | try: | |||
|
546 | reviewers = get_validated_reviewers( | |||
|
547 | reviewer_objects, reviewer_rules) | |||
|
548 | except ValueError as e: | |||
|
549 | raise JSONRPCError('Reviewers Validation: {}'.format(e)) | |||
579 |
|
550 | |||
580 | pull_request_model = PullRequestModel() |
|
551 | pull_request_model = PullRequestModel() | |
581 | pull_request = pull_request_model.create( |
|
552 | pull_request = pull_request_model.create( | |
@@ -586,7 +557,7 b' def create_pull_request(' | |||||
586 | target_ref=full_target_ref, |
|
557 | target_ref=full_target_ref, | |
587 | revisions=reversed( |
|
558 | revisions=reversed( | |
588 | [commit.raw_id for commit in reversed(commit_ranges)]), |
|
559 | [commit.raw_id for commit in reversed(commit_ranges)]), | |
589 |
reviewers=reviewers |
|
560 | reviewers=reviewers, | |
590 | title=title, |
|
561 | title=title, | |
591 | description=Optional.extract(description) |
|
562 | description=Optional.extract(description) | |
592 | ) |
|
563 | ) | |
@@ -603,7 +574,7 b' def create_pull_request(' | |||||
603 | def update_pull_request( |
|
574 | def update_pull_request( | |
604 | request, apiuser, repoid, pullrequestid, title=Optional(''), |
|
575 | request, apiuser, repoid, pullrequestid, title=Optional(''), | |
605 | description=Optional(''), reviewers=Optional(None), |
|
576 | description=Optional(''), reviewers=Optional(None), | |
606 |
update_commits=Optional(None) |
|
577 | update_commits=Optional(None)): | |
607 | """ |
|
578 | """ | |
608 | Updates a pull request. |
|
579 | Updates a pull request. | |
609 |
|
580 | |||
@@ -619,10 +590,12 b' def update_pull_request(' | |||||
619 | :type description: Optional(str) |
|
590 | :type description: Optional(str) | |
620 | :param reviewers: Update pull request reviewers list with new value. |
|
591 | :param reviewers: Update pull request reviewers list with new value. | |
621 | :type reviewers: Optional(list) |
|
592 | :type reviewers: Optional(list) | |
|
593 | Accepts username strings or objects of the format: | |||
|
594 | ||||
|
595 | [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}] | |||
|
596 | ||||
622 | :param update_commits: Trigger update of commits for this pull request |
|
597 | :param update_commits: Trigger update of commits for this pull request | |
623 | :type: update_commits: Optional(bool) |
|
598 | :type: update_commits: Optional(bool) | |
624 | :param close_pull_request: Close this pull request with rejected state |
|
|||
625 | :type: close_pull_request: Optional(bool) |
|
|||
626 |
|
599 | |||
627 | Example output: |
|
600 | Example output: | |
628 |
|
601 | |||
@@ -665,29 +638,38 b' def update_pull_request(' | |||||
665 | pullrequestid,)) |
|
638 | pullrequestid,)) | |
666 |
|
639 | |||
667 | reviewer_objects = Optional.extract(reviewers) or [] |
|
640 | reviewer_objects = Optional.extract(reviewers) or [] | |
668 | if not isinstance(reviewer_objects, list): |
|
641 | ||
669 | raise JSONRPCError('reviewers should be specified as a list') |
|
642 | if reviewer_objects: | |
|
643 | schema = ReviewerListSchema() | |||
|
644 | try: | |||
|
645 | reviewer_objects = schema.deserialize(reviewer_objects) | |||
|
646 | except Invalid as err: | |||
|
647 | raise JSONRPCValidationError(colander_exc=err) | |||
|
648 | ||||
|
649 | # validate users | |||
|
650 | for reviewer_object in reviewer_objects: | |||
|
651 | user = get_user_or_error(reviewer_object['username']) | |||
|
652 | reviewer_object['user_id'] = user.user_id | |||
670 |
|
653 | |||
671 | reviewers_reasons = [] |
|
654 | get_default_reviewers_data, get_validated_reviewers = \ | |
672 | reviewer_ids = set() |
|
655 | PullRequestModel().get_reviewer_functions() | |
673 | for reviewer_object in reviewer_objects: |
|
|||
674 | reviewer_reasons = [] |
|
|||
675 | if isinstance(reviewer_object, (int, basestring)): |
|
|||
676 | reviewer_username = reviewer_object |
|
|||
677 | else: |
|
|||
678 | reviewer_username = reviewer_object['username'] |
|
|||
679 | reviewer_reasons = reviewer_object.get('reasons', []) |
|
|||
680 |
|
656 | |||
681 | user = get_user_or_error(reviewer_username) |
|
657 | # re-use stored rules | |
682 | reviewer_ids.add(user.user_id) |
|
658 | reviewer_rules = pull_request.reviewer_data | |
683 | reviewers_reasons.append((user.user_id, reviewer_reasons)) |
|
659 | try: | |
|
660 | reviewers = get_validated_reviewers( | |||
|
661 | reviewer_objects, reviewer_rules) | |||
|
662 | except ValueError as e: | |||
|
663 | raise JSONRPCError('Reviewers Validation: {}'.format(e)) | |||
|
664 | else: | |||
|
665 | reviewers = [] | |||
684 |
|
666 | |||
685 | title = Optional.extract(title) |
|
667 | title = Optional.extract(title) | |
686 | description = Optional.extract(description) |
|
668 | description = Optional.extract(description) | |
687 | if title or description: |
|
669 | if title or description: | |
688 | PullRequestModel().edit( |
|
670 | PullRequestModel().edit( | |
689 | pull_request, title or pull_request.title, |
|
671 | pull_request, title or pull_request.title, | |
690 | description or pull_request.description) |
|
672 | description or pull_request.description, apiuser) | |
691 | Session().commit() |
|
673 | Session().commit() | |
692 |
|
674 | |||
693 | commit_changes = {"added": [], "common": [], "removed": []} |
|
675 | commit_changes = {"added": [], "common": [], "removed": []} | |
@@ -699,9 +681,9 b' def update_pull_request(' | |||||
699 | Session().commit() |
|
681 | Session().commit() | |
700 |
|
682 | |||
701 | reviewers_changes = {"added": [], "removed": []} |
|
683 | reviewers_changes = {"added": [], "removed": []} | |
702 |
if reviewer |
|
684 | if reviewers: | |
703 | added_reviewers, removed_reviewers = \ |
|
685 | added_reviewers, removed_reviewers = \ | |
704 |
PullRequestModel().update_reviewers(pull_request, reviewers |
|
686 | PullRequestModel().update_reviewers(pull_request, reviewers, apiuser) | |
705 |
|
687 | |||
706 | reviewers_changes['added'] = sorted( |
|
688 | reviewers_changes['added'] = sorted( | |
707 | [get_user_or_error(n).username for n in added_reviewers]) |
|
689 | [get_user_or_error(n).username for n in added_reviewers]) | |
@@ -709,11 +691,6 b' def update_pull_request(' | |||||
709 | [get_user_or_error(n).username for n in removed_reviewers]) |
|
691 | [get_user_or_error(n).username for n in removed_reviewers]) | |
710 | Session().commit() |
|
692 | Session().commit() | |
711 |
|
693 | |||
712 | if str2bool(Optional.extract(close_pull_request)): |
|
|||
713 | PullRequestModel().close_pull_request_with_comment( |
|
|||
714 | pull_request, apiuser, repo) |
|
|||
715 | Session().commit() |
|
|||
716 |
|
||||
717 | data = { |
|
694 | data = { | |
718 | 'msg': 'Updated pull request `{}`'.format( |
|
695 | 'msg': 'Updated pull request `{}`'.format( | |
719 | pull_request.pull_request_id), |
|
696 | pull_request.pull_request_id), | |
@@ -723,3 +700,80 b' def update_pull_request(' | |||||
723 | } |
|
700 | } | |
724 |
|
701 | |||
725 | return data |
|
702 | return data | |
|
703 | ||||
|
704 | ||||
|
705 | @jsonrpc_method() | |||
|
706 | def close_pull_request( | |||
|
707 | request, apiuser, repoid, pullrequestid, | |||
|
708 | userid=Optional(OAttr('apiuser')), message=Optional('')): | |||
|
709 | """ | |||
|
710 | Close the pull request specified by `pullrequestid`. | |||
|
711 | ||||
|
712 | :param apiuser: This is filled automatically from the |authtoken|. | |||
|
713 | :type apiuser: AuthUser | |||
|
714 | :param repoid: Repository name or repository ID to which the pull | |||
|
715 | request belongs. | |||
|
716 | :type repoid: str or int | |||
|
717 | :param pullrequestid: ID of the pull request to be closed. | |||
|
718 | :type pullrequestid: int | |||
|
719 | :param userid: Close the pull request as this user. | |||
|
720 | :type userid: Optional(str or int) | |||
|
721 | :param message: Optional message to close the Pull Request with. If not | |||
|
722 | specified it will be generated automatically. | |||
|
723 | :type message: Optional(str) | |||
|
724 | ||||
|
725 | Example output: | |||
|
726 | ||||
|
727 | .. code-block:: bash | |||
|
728 | ||||
|
729 | "id": <id_given_in_input>, | |||
|
730 | "result": { | |||
|
731 | "pull_request_id": "<int>", | |||
|
732 | "close_status": "<str:status_lbl>, | |||
|
733 | "closed": "<bool>" | |||
|
734 | }, | |||
|
735 | "error": null | |||
|
736 | ||||
|
737 | """ | |||
|
738 | _ = request.translate | |||
|
739 | ||||
|
740 | repo = get_repo_or_error(repoid) | |||
|
741 | if not isinstance(userid, Optional): | |||
|
742 | if (has_superadmin_permission(apiuser) or | |||
|
743 | HasRepoPermissionAnyApi('repository.admin')( | |||
|
744 | user=apiuser, repo_name=repo.repo_name)): | |||
|
745 | apiuser = get_user_or_error(userid) | |||
|
746 | else: | |||
|
747 | raise JSONRPCError('userid is not the same as your user') | |||
|
748 | ||||
|
749 | pull_request = get_pull_request_or_error(pullrequestid) | |||
|
750 | ||||
|
751 | if pull_request.is_closed(): | |||
|
752 | raise JSONRPCError( | |||
|
753 | 'pull request `%s` is already closed' % (pullrequestid,)) | |||
|
754 | ||||
|
755 | # only owner or admin or person with write permissions | |||
|
756 | allowed_to_close = PullRequestModel().check_user_update( | |||
|
757 | pull_request, apiuser, api=True) | |||
|
758 | ||||
|
759 | if not allowed_to_close: | |||
|
760 | raise JSONRPCError( | |||
|
761 | 'pull request `%s` close failed, no permission to close.' % ( | |||
|
762 | pullrequestid,)) | |||
|
763 | ||||
|
764 | # message we're using to close the PR, else it's automatically generated | |||
|
765 | message = Optional.extract(message) | |||
|
766 | ||||
|
767 | # finally close the PR, with proper message comment | |||
|
768 | comment, status = PullRequestModel().close_pull_request_with_comment( | |||
|
769 | pull_request, apiuser, repo, message=message) | |||
|
770 | status_lbl = ChangesetStatus.get_status_lbl(status) | |||
|
771 | ||||
|
772 | Session().commit() | |||
|
773 | ||||
|
774 | data = { | |||
|
775 | 'pull_request_id': pull_request.pull_request_id, | |||
|
776 | 'close_status': status_lbl, | |||
|
777 | 'closed': True, | |||
|
778 | } | |||
|
779 | return data |
@@ -29,6 +29,8 b' from rhodecode.api.utils import (' | |||||
29 | get_user_group_or_error, get_user_or_error, validate_repo_permissions, |
|
29 | get_user_group_or_error, get_user_or_error, validate_repo_permissions, | |
30 | get_perm_or_error, parse_args, get_origin, build_commit_data, |
|
30 | get_perm_or_error, parse_args, get_origin, build_commit_data, | |
31 | validate_set_owner_permissions) |
|
31 | validate_set_owner_permissions) | |
|
32 | from rhodecode.lib import audit_logger | |||
|
33 | from rhodecode.lib import repo_maintenance | |||
32 | from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi |
|
34 | from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi | |
33 | from rhodecode.lib.utils2 import str2bool, time_to_datetime |
|
35 | from rhodecode.lib.utils2 import str2bool, time_to_datetime | |
34 | from rhodecode.lib.ext_json import json |
|
36 | from rhodecode.lib.ext_json import json | |
@@ -915,12 +917,13 b' def update_repo(' | |||||
915 |
|
917 | |||
916 | ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo) |
|
918 | ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo) | |
917 |
|
919 | |||
|
920 | old_values = repo.get_api_data() | |||
918 | schema = repo_schema.RepoSchema().bind( |
|
921 | schema = repo_schema.RepoSchema().bind( | |
919 | repo_type_options=rhodecode.BACKENDS.keys(), |
|
922 | repo_type_options=rhodecode.BACKENDS.keys(), | |
920 | repo_ref_options=ref_choices, |
|
923 | repo_ref_options=ref_choices, | |
921 | # user caller |
|
924 | # user caller | |
922 | user=apiuser, |
|
925 | user=apiuser, | |
923 |
old_values= |
|
926 | old_values=old_values) | |
924 | try: |
|
927 | try: | |
925 | schema_data = schema.deserialize(dict( |
|
928 | schema_data = schema.deserialize(dict( | |
926 | # we save old value, users cannot change type |
|
929 | # we save old value, users cannot change type | |
@@ -965,6 +968,9 b' def update_repo(' | |||||
965 |
|
968 | |||
966 | try: |
|
969 | try: | |
967 | RepoModel().update(repo, **validated_updates) |
|
970 | RepoModel().update(repo, **validated_updates) | |
|
971 | audit_logger.store_api( | |||
|
972 | 'repo.edit', action_data={'old_data': old_values}, | |||
|
973 | user=apiuser, repo=repo) | |||
968 | Session().commit() |
|
974 | Session().commit() | |
969 | return { |
|
975 | return { | |
970 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), |
|
976 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), | |
@@ -1153,6 +1159,7 b' def delete_repo(request, apiuser, repoid' | |||||
1153 | """ |
|
1159 | """ | |
1154 |
|
1160 | |||
1155 | repo = get_repo_or_error(repoid) |
|
1161 | repo = get_repo_or_error(repoid) | |
|
1162 | repo_name = repo.repo_name | |||
1156 | if not has_superadmin_permission(apiuser): |
|
1163 | if not has_superadmin_permission(apiuser): | |
1157 | _perms = ('repository.admin',) |
|
1164 | _perms = ('repository.admin',) | |
1158 | validate_repo_permissions(apiuser, repoid, repo, _perms) |
|
1165 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |
@@ -1170,18 +1177,26 b' def delete_repo(request, apiuser, repoid' | |||||
1170 | 'Cannot delete `%s` it still contains attached forks' % |
|
1177 | 'Cannot delete `%s` it still contains attached forks' % | |
1171 | (repo.repo_name,) |
|
1178 | (repo.repo_name,) | |
1172 | ) |
|
1179 | ) | |
|
1180 | old_data = repo.get_api_data() | |||
|
1181 | RepoModel().delete(repo, forks=forks) | |||
1173 |
|
1182 | |||
1174 | RepoModel().delete(repo, forks=forks) |
|
1183 | repo = audit_logger.RepoWrap(repo_id=None, | |
|
1184 | repo_name=repo.repo_name) | |||
|
1185 | ||||
|
1186 | audit_logger.store_api( | |||
|
1187 | 'repo.delete', action_data={'old_data': old_data}, | |||
|
1188 | user=apiuser, repo=repo) | |||
|
1189 | ||||
|
1190 | ScmModel().mark_for_invalidation(repo_name, delete=True) | |||
1175 | Session().commit() |
|
1191 | Session().commit() | |
1176 | return { |
|
1192 | return { | |
1177 | 'msg': 'Deleted repository `%s`%s' % ( |
|
1193 | 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg), | |
1178 | repo.repo_name, _forks_msg), |
|
|||
1179 | 'success': True |
|
1194 | 'success': True | |
1180 | } |
|
1195 | } | |
1181 | except Exception: |
|
1196 | except Exception: | |
1182 | log.exception("Exception occurred while trying to delete repo") |
|
1197 | log.exception("Exception occurred while trying to delete repo") | |
1183 | raise JSONRPCError( |
|
1198 | raise JSONRPCError( | |
1184 |
'failed to delete repository `%s`' % ( |
|
1199 | 'failed to delete repository `%s`' % (repo_name,) | |
1185 | ) |
|
1200 | ) | |
1186 |
|
1201 | |||
1187 |
|
1202 | |||
@@ -1460,7 +1475,7 b' def comment_commit(' | |||||
1460 | rc_config = SettingsModel().get_all_settings() |
|
1475 | rc_config = SettingsModel().get_all_settings() | |
1461 | renderer = rc_config.get('rhodecode_markup_renderer', 'rst') |
|
1476 | renderer = rc_config.get('rhodecode_markup_renderer', 'rst') | |
1462 | status_change_label = ChangesetStatus.get_status_lbl(status) |
|
1477 | status_change_label = ChangesetStatus.get_status_lbl(status) | |
1463 | comm = CommentsModel().create( |
|
1478 | comment = CommentsModel().create( | |
1464 | message, repo, user, commit_id=commit_id, |
|
1479 | message, repo, user, commit_id=commit_id, | |
1465 | status_change=status_change_label, |
|
1480 | status_change=status_change_label, | |
1466 | status_change_type=status, |
|
1481 | status_change_type=status, | |
@@ -1472,7 +1487,7 b' def comment_commit(' | |||||
1472 | # also do a status change |
|
1487 | # also do a status change | |
1473 | try: |
|
1488 | try: | |
1474 | ChangesetStatusModel().set_status( |
|
1489 | ChangesetStatusModel().set_status( | |
1475 | repo, status, user, comm, revision=commit_id, |
|
1490 | repo, status, user, comment, revision=commit_id, | |
1476 | dont_allow_on_closed_pull_request=True |
|
1491 | dont_allow_on_closed_pull_request=True | |
1477 | ) |
|
1492 | ) | |
1478 | except StatusChangeOnClosedPullRequestError: |
|
1493 | except StatusChangeOnClosedPullRequestError: | |
@@ -1486,7 +1501,7 b' def comment_commit(' | |||||
1486 | return { |
|
1501 | return { | |
1487 | 'msg': ( |
|
1502 | 'msg': ( | |
1488 | 'Commented on commit `%s` for repository `%s`' % ( |
|
1503 | 'Commented on commit `%s` for repository `%s`' % ( | |
1489 | comm.revision, repo.repo_name)), |
|
1504 | comment.revision, repo.repo_name)), | |
1490 | 'status_change': status, |
|
1505 | 'status_change': status, | |
1491 | 'success': True, |
|
1506 | 'success': True, | |
1492 | } |
|
1507 | } | |
@@ -1867,6 +1882,11 b' def strip(request, apiuser, repoid, revi' | |||||
1867 |
|
1882 | |||
1868 | try: |
|
1883 | try: | |
1869 | ScmModel().strip(repo, revision, branch) |
|
1884 | ScmModel().strip(repo, revision, branch) | |
|
1885 | audit_logger.store_api( | |||
|
1886 | 'repo.commit.strip', action_data={'commit_id': revision}, | |||
|
1887 | repo=repo, | |||
|
1888 | user=apiuser, commit=True) | |||
|
1889 | ||||
1870 | return { |
|
1890 | return { | |
1871 | 'msg': 'Stripped commit %s from repo `%s`' % ( |
|
1891 | 'msg': 'Stripped commit %s from repo `%s`' % ( | |
1872 | revision, repo.repo_name), |
|
1892 | revision, repo.repo_name), | |
@@ -1902,6 +1922,7 b' def get_repo_settings(request, apiuser, ' | |||||
1902 | "id": 237, |
|
1922 | "id": 237, | |
1903 | "result": { |
|
1923 | "result": { | |
1904 | "extensions_largefiles": true, |
|
1924 | "extensions_largefiles": true, | |
|
1925 | "extensions_evolve": true, | |||
1905 | "hooks_changegroup_push_logger": true, |
|
1926 | "hooks_changegroup_push_logger": true, | |
1906 | "hooks_changegroup_repo_size": false, |
|
1927 | "hooks_changegroup_repo_size": false, | |
1907 | "hooks_outgoing_pull_logger": true, |
|
1928 | "hooks_outgoing_pull_logger": true, | |
@@ -1985,3 +2006,65 b' def set_repo_settings(request, apiuser, ' | |||||
1985 |
|
2006 | |||
1986 | # Indicate success. |
|
2007 | # Indicate success. | |
1987 | return True |
|
2008 | return True | |
|
2009 | ||||
|
2010 | ||||
|
2011 | @jsonrpc_method() | |||
|
2012 | def maintenance(request, apiuser, repoid): | |||
|
2013 | """ | |||
|
2014 | Triggers a maintenance on the given repository. | |||
|
2015 | ||||
|
2016 | This command can only be run using an |authtoken| with admin | |||
|
2017 | rights to the specified repository. For more information, | |||
|
2018 | see :ref:`config-token-ref`. | |||
|
2019 | ||||
|
2020 | This command takes the following options: | |||
|
2021 | ||||
|
2022 | :param apiuser: This is filled automatically from the |authtoken|. | |||
|
2023 | :type apiuser: AuthUser | |||
|
2024 | :param repoid: The repository name or repository ID. | |||
|
2025 | :type repoid: str or int | |||
|
2026 | ||||
|
2027 | Example output: | |||
|
2028 | ||||
|
2029 | .. code-block:: bash | |||
|
2030 | ||||
|
2031 | id : <id_given_in_input> | |||
|
2032 | result : { | |||
|
2033 | "msg": "executed maintenance command", | |||
|
2034 | "executed_actions": [ | |||
|
2035 | <action_message>, <action_message2>... | |||
|
2036 | ], | |||
|
2037 | "repository": "<repository name>" | |||
|
2038 | } | |||
|
2039 | error : null | |||
|
2040 | ||||
|
2041 | Example error output: | |||
|
2042 | ||||
|
2043 | .. code-block:: bash | |||
|
2044 | ||||
|
2045 | id : <id_given_in_input> | |||
|
2046 | result : null | |||
|
2047 | error : { | |||
|
2048 | "Unable to execute maintenance on `<reponame>`" | |||
|
2049 | } | |||
|
2050 | ||||
|
2051 | """ | |||
|
2052 | ||||
|
2053 | repo = get_repo_or_error(repoid) | |||
|
2054 | if not has_superadmin_permission(apiuser): | |||
|
2055 | _perms = ('repository.admin',) | |||
|
2056 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
|
2057 | ||||
|
2058 | try: | |||
|
2059 | maintenance = repo_maintenance.RepoMaintenance() | |||
|
2060 | executed_actions = maintenance.execute(repo) | |||
|
2061 | ||||
|
2062 | return { | |||
|
2063 | 'msg': 'executed maintenance command', | |||
|
2064 | 'executed_actions': executed_actions, | |||
|
2065 | 'repository': repo.repo_name | |||
|
2066 | } | |||
|
2067 | except Exception: | |||
|
2068 | log.exception("Exception occurred while trying to run maintenance") | |||
|
2069 | raise JSONRPCError( | |||
|
2070 | 'Unable to execute maintenance on `%s`' % repo.repo_name) |
@@ -27,6 +27,7 b' from rhodecode.api.utils import (' | |||||
27 | has_superadmin_permission, Optional, OAttr, get_user_or_error, |
|
27 | has_superadmin_permission, Optional, OAttr, get_user_or_error, | |
28 | get_repo_group_or_error, get_perm_or_error, get_user_group_or_error, |
|
28 | get_repo_group_or_error, get_perm_or_error, get_user_group_or_error, | |
29 | get_origin, validate_repo_group_permissions, validate_set_owner_permissions) |
|
29 | get_origin, validate_repo_group_permissions, validate_set_owner_permissions) | |
|
30 | from rhodecode.lib import audit_logger | |||
30 | from rhodecode.lib.auth import ( |
|
31 | from rhodecode.lib.auth import ( | |
31 | HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi) |
|
32 | HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi) | |
32 | from rhodecode.model.db import Session |
|
33 | from rhodecode.model.db import Session | |
@@ -222,6 +223,13 b' def create_repo_group(' | |||||
222 | group_name=validated_group_name, |
|
223 | group_name=validated_group_name, | |
223 | group_description=schema_data['repo_group_name'], |
|
224 | group_description=schema_data['repo_group_name'], | |
224 | copy_permissions=schema_data['repo_group_copy_permissions']) |
|
225 | copy_permissions=schema_data['repo_group_copy_permissions']) | |
|
226 | Session().flush() | |||
|
227 | ||||
|
228 | repo_group_data = repo_group.get_api_data() | |||
|
229 | audit_logger.store_api( | |||
|
230 | 'repo_group.create', action_data={'data': repo_group_data}, | |||
|
231 | user=apiuser) | |||
|
232 | ||||
225 | Session().commit() |
|
233 | Session().commit() | |
226 | return { |
|
234 | return { | |
227 | 'msg': 'Created new repo group `%s`' % validated_group_name, |
|
235 | 'msg': 'Created new repo group `%s`' % validated_group_name, | |
@@ -310,8 +318,13 b' def update_repo_group(' | |||||
310 | enable_locking=schema_data['repo_group_enable_locking'], |
|
318 | enable_locking=schema_data['repo_group_enable_locking'], | |
311 | ) |
|
319 | ) | |
312 |
|
320 | |||
|
321 | old_data = repo_group.get_api_data() | |||
313 | try: |
|
322 | try: | |
314 | RepoGroupModel().update(repo_group, validated_updates) |
|
323 | RepoGroupModel().update(repo_group, validated_updates) | |
|
324 | audit_logger.store_api( | |||
|
325 | 'repo_group.edit', action_data={'old_data': old_data}, | |||
|
326 | user=apiuser) | |||
|
327 | ||||
315 | Session().commit() |
|
328 | Session().commit() | |
316 | return { |
|
329 | return { | |
317 | 'msg': 'updated repository group ID:%s %s' % ( |
|
330 | 'msg': 'updated repository group ID:%s %s' % ( | |
@@ -365,8 +378,12 b' def delete_repo_group(request, apiuser, ' | |||||
365 | validate_repo_group_permissions( |
|
378 | validate_repo_group_permissions( | |
366 | apiuser, repogroupid, repo_group, ('group.admin',)) |
|
379 | apiuser, repogroupid, repo_group, ('group.admin',)) | |
367 |
|
380 | |||
|
381 | old_data = repo_group.get_api_data() | |||
368 | try: |
|
382 | try: | |
369 | RepoGroupModel().delete(repo_group) |
|
383 | RepoGroupModel().delete(repo_group) | |
|
384 | audit_logger.store_api( | |||
|
385 | 'repo_group.delete', action_data={'old_data': old_data}, | |||
|
386 | user=apiuser) | |||
370 | Session().commit() |
|
387 | Session().commit() | |
371 | return { |
|
388 | return { | |
372 | 'msg': 'deleted repo group ID:%s %s' % |
|
389 | 'msg': 'deleted repo group ID:%s %s' % |
@@ -20,14 +20,18 b'' | |||||
20 |
|
20 | |||
21 | import logging |
|
21 | import logging | |
22 |
|
22 | |||
23 | from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden |
|
23 | from rhodecode.api import ( | |
|
24 | jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) | |||
24 | from rhodecode.api.utils import ( |
|
25 | from rhodecode.api.utils import ( | |
25 | Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) |
|
26 | Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) | |
|
27 | from rhodecode.lib import audit_logger | |||
26 | from rhodecode.lib.auth import AuthUser, PasswordGenerator |
|
28 | from rhodecode.lib.auth import AuthUser, PasswordGenerator | |
27 | from rhodecode.lib.exceptions import DefaultUserException |
|
29 | from rhodecode.lib.exceptions import DefaultUserException | |
28 | from rhodecode.lib.utils2 import safe_int, str2bool |
|
30 | from rhodecode.lib.utils2 import safe_int, str2bool | |
29 | from rhodecode.model.db import Session, User, Repository |
|
31 | from rhodecode.model.db import Session, User, Repository | |
30 | from rhodecode.model.user import UserModel |
|
32 | from rhodecode.model.user import UserModel | |
|
33 | from rhodecode.model import validation_schema | |||
|
34 | from rhodecode.model.validation_schema.schemas import user_schema | |||
31 |
|
35 | |||
32 | log = logging.getLogger(__name__) |
|
36 | log = logging.getLogger(__name__) | |
33 |
|
37 | |||
@@ -237,20 +241,54 b' def create_user(request, apiuser, userna' | |||||
237 | if isinstance(create_repo_group, basestring): |
|
241 | if isinstance(create_repo_group, basestring): | |
238 | create_repo_group = str2bool(create_repo_group) |
|
242 | create_repo_group = str2bool(create_repo_group) | |
239 |
|
243 | |||
|
244 | username = Optional.extract(username) | |||
|
245 | password = Optional.extract(password) | |||
|
246 | email = Optional.extract(email) | |||
|
247 | first_name = Optional.extract(firstname) | |||
|
248 | last_name = Optional.extract(lastname) | |||
|
249 | active = Optional.extract(active) | |||
|
250 | admin = Optional.extract(admin) | |||
|
251 | extern_type = Optional.extract(extern_type) | |||
|
252 | extern_name = Optional.extract(extern_name) | |||
|
253 | ||||
|
254 | schema = user_schema.UserSchema().bind( | |||
|
255 | # user caller | |||
|
256 | user=apiuser) | |||
|
257 | try: | |||
|
258 | schema_data = schema.deserialize(dict( | |||
|
259 | username=username, | |||
|
260 | email=email, | |||
|
261 | password=password, | |||
|
262 | first_name=first_name, | |||
|
263 | last_name=last_name, | |||
|
264 | active=active, | |||
|
265 | admin=admin, | |||
|
266 | extern_type=extern_type, | |||
|
267 | extern_name=extern_name, | |||
|
268 | )) | |||
|
269 | except validation_schema.Invalid as err: | |||
|
270 | raise JSONRPCValidationError(colander_exc=err) | |||
|
271 | ||||
240 | try: |
|
272 | try: | |
241 | user = UserModel().create_or_update( |
|
273 | user = UserModel().create_or_update( | |
242 |
username= |
|
274 | username=schema_data['username'], | |
243 |
password= |
|
275 | password=schema_data['password'], | |
244 |
email= |
|
276 | email=schema_data['email'], | |
245 |
firstname= |
|
277 | firstname=schema_data['first_name'], | |
246 |
lastname= |
|
278 | lastname=schema_data['last_name'], | |
247 |
active= |
|
279 | active=schema_data['active'], | |
248 |
admin= |
|
280 | admin=schema_data['admin'], | |
249 |
extern_type= |
|
281 | extern_type=schema_data['extern_type'], | |
250 |
extern_name= |
|
282 | extern_name=schema_data['extern_name'], | |
251 | force_password_change=Optional.extract(force_password_change), |
|
283 | force_password_change=Optional.extract(force_password_change), | |
252 | create_repo_group=create_repo_group |
|
284 | create_repo_group=create_repo_group | |
253 | ) |
|
285 | ) | |
|
286 | Session().flush() | |||
|
287 | creation_data = user.get_api_data() | |||
|
288 | audit_logger.store_api( | |||
|
289 | 'user.create', action_data={'data': creation_data}, | |||
|
290 | user=apiuser) | |||
|
291 | ||||
254 | Session().commit() |
|
292 | Session().commit() | |
255 | return { |
|
293 | return { | |
256 | 'msg': 'created new user `%s`' % username, |
|
294 | 'msg': 'created new user `%s`' % username, | |
@@ -326,7 +364,7 b' def update_user(request, apiuser, userid' | |||||
326 | raise JSONRPCForbidden() |
|
364 | raise JSONRPCForbidden() | |
327 |
|
365 | |||
328 | user = get_user_or_error(userid) |
|
366 | user = get_user_or_error(userid) | |
329 |
|
367 | old_data = user.get_api_data() | ||
330 | # only non optional arguments will be stored in updates |
|
368 | # only non optional arguments will be stored in updates | |
331 | updates = {} |
|
369 | updates = {} | |
332 |
|
370 | |||
@@ -343,6 +381,9 b' def update_user(request, apiuser, userid' | |||||
343 | store_update(updates, extern_type, 'extern_type') |
|
381 | store_update(updates, extern_type, 'extern_type') | |
344 |
|
382 | |||
345 | user = UserModel().update_user(user, **updates) |
|
383 | user = UserModel().update_user(user, **updates) | |
|
384 | audit_logger.store_api( | |||
|
385 | 'user.edit', action_data={'old_data': old_data}, | |||
|
386 | user=apiuser) | |||
346 | Session().commit() |
|
387 | Session().commit() | |
347 | return { |
|
388 | return { | |
348 | 'msg': 'updated user ID:%s %s' % (user.user_id, user.username), |
|
389 | 'msg': 'updated user ID:%s %s' % (user.user_id, user.username), | |
@@ -405,9 +446,13 b' def delete_user(request, apiuser, userid' | |||||
405 | raise JSONRPCForbidden() |
|
446 | raise JSONRPCForbidden() | |
406 |
|
447 | |||
407 | user = get_user_or_error(userid) |
|
448 | user = get_user_or_error(userid) | |
408 |
|
449 | old_data = user.get_api_data() | ||
409 | try: |
|
450 | try: | |
410 | UserModel().delete(userid) |
|
451 | UserModel().delete(userid) | |
|
452 | audit_logger.store_api( | |||
|
453 | 'user.delete', action_data={'old_data': old_data}, | |||
|
454 | user=apiuser) | |||
|
455 | ||||
411 | Session().commit() |
|
456 | Session().commit() | |
412 | return { |
|
457 | return { | |
413 | 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username), |
|
458 | 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username), |
@@ -20,15 +20,19 b'' | |||||
20 |
|
20 | |||
21 | import logging |
|
21 | import logging | |
22 |
|
22 | |||
23 | from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden |
|
23 | from rhodecode.api import ( | |
|
24 | jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) | |||
24 | from rhodecode.api.utils import ( |
|
25 | from rhodecode.api.utils import ( | |
25 | Optional, OAttr, store_update, has_superadmin_permission, get_origin, |
|
26 | Optional, OAttr, store_update, has_superadmin_permission, get_origin, | |
26 | get_user_or_error, get_user_group_or_error, get_perm_or_error) |
|
27 | get_user_or_error, get_user_group_or_error, get_perm_or_error) | |
|
28 | from rhodecode.lib import audit_logger | |||
27 | from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi |
|
29 | from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi | |
28 | from rhodecode.lib.exceptions import UserGroupAssignedException |
|
30 | from rhodecode.lib.exceptions import UserGroupAssignedException | |
29 | from rhodecode.model.db import Session |
|
31 | from rhodecode.model.db import Session | |
30 | from rhodecode.model.scm import UserGroupList |
|
32 | from rhodecode.model.scm import UserGroupList | |
31 | from rhodecode.model.user_group import UserGroupModel |
|
33 | from rhodecode.model.user_group import UserGroupModel | |
|
34 | from rhodecode.model import validation_schema | |||
|
35 | from rhodecode.model.validation_schema.schemas import user_group_schema | |||
32 |
|
36 | |||
33 | log = logging.getLogger(__name__) |
|
37 | log = logging.getLogger(__name__) | |
34 |
|
38 | |||
@@ -210,20 +214,41 b' def create_user_group(' | |||||
210 | if UserGroupModel().get_by_name(group_name): |
|
214 | if UserGroupModel().get_by_name(group_name): | |
211 | raise JSONRPCError("user group `%s` already exist" % (group_name,)) |
|
215 | raise JSONRPCError("user group `%s` already exist" % (group_name,)) | |
212 |
|
216 | |||
213 | try: |
|
217 | if isinstance(owner, Optional): | |
214 | if isinstance(owner, Optional): |
|
218 | owner = apiuser.user_id | |
215 | owner = apiuser.user_id |
|
219 | ||
|
220 | owner = get_user_or_error(owner) | |||
|
221 | active = Optional.extract(active) | |||
|
222 | description = Optional.extract(description) | |||
216 |
|
223 | |||
217 | owner = get_user_or_error(owner) |
|
224 | schema = user_group_schema.UserGroupSchema().bind( | |
218 | active = Optional.extract(active) |
|
225 | # user caller | |
219 | description = Optional.extract(description) |
|
226 | user=apiuser) | |
220 | ug = UserGroupModel().create( |
|
227 | try: | |
221 | name=group_name, description=description, owner=owner, |
|
228 | schema_data = schema.deserialize(dict( | |
222 | active=active) |
|
229 | user_group_name=group_name, | |
|
230 | user_group_description=description, | |||
|
231 | user_group_owner=owner.username, | |||
|
232 | user_group_active=active, | |||
|
233 | )) | |||
|
234 | except validation_schema.Invalid as err: | |||
|
235 | raise JSONRPCValidationError(colander_exc=err) | |||
|
236 | ||||
|
237 | try: | |||
|
238 | user_group = UserGroupModel().create( | |||
|
239 | name=schema_data['user_group_name'], | |||
|
240 | description=schema_data['user_group_description'], | |||
|
241 | owner=owner, | |||
|
242 | active=schema_data['user_group_active']) | |||
|
243 | Session().flush() | |||
|
244 | creation_data = user_group.get_api_data() | |||
|
245 | audit_logger.store_api( | |||
|
246 | 'user_group.create', action_data={'data': creation_data}, | |||
|
247 | user=apiuser) | |||
223 | Session().commit() |
|
248 | Session().commit() | |
224 | return { |
|
249 | return { | |
225 | 'msg': 'created new user group `%s`' % group_name, |
|
250 | 'msg': 'created new user group `%s`' % group_name, | |
226 |
'user_group': |
|
251 | 'user_group': creation_data | |
227 | } |
|
252 | } | |
228 | except Exception: |
|
253 | except Exception: | |
229 | log.exception("Error occurred during creation of user group") |
|
254 | log.exception("Error occurred during creation of user group") | |
@@ -291,6 +316,7 b' def update_user_group(request, apiuser, ' | |||||
291 | if not isinstance(owner, Optional): |
|
316 | if not isinstance(owner, Optional): | |
292 | owner = get_user_or_error(owner) |
|
317 | owner = get_user_or_error(owner) | |
293 |
|
318 | |||
|
319 | old_data = user_group.get_api_data() | |||
294 | updates = {} |
|
320 | updates = {} | |
295 | store_update(updates, group_name, 'users_group_name') |
|
321 | store_update(updates, group_name, 'users_group_name') | |
296 | store_update(updates, description, 'user_group_description') |
|
322 | store_update(updates, description, 'user_group_description') | |
@@ -298,6 +324,9 b' def update_user_group(request, apiuser, ' | |||||
298 | store_update(updates, active, 'users_group_active') |
|
324 | store_update(updates, active, 'users_group_active') | |
299 | try: |
|
325 | try: | |
300 | UserGroupModel().update(user_group, updates) |
|
326 | UserGroupModel().update(user_group, updates) | |
|
327 | audit_logger.store_api( | |||
|
328 | 'user_group.edit', action_data={'old_data': old_data}, | |||
|
329 | user=apiuser) | |||
301 | Session().commit() |
|
330 | Session().commit() | |
302 | return { |
|
331 | return { | |
303 | 'msg': 'updated user group ID:%s %s' % ( |
|
332 | 'msg': 'updated user group ID:%s %s' % ( | |
@@ -359,8 +388,12 b' def delete_user_group(request, apiuser, ' | |||||
359 | raise JSONRPCError( |
|
388 | raise JSONRPCError( | |
360 | 'user group `%s` does not exist' % (usergroupid,)) |
|
389 | 'user group `%s` does not exist' % (usergroupid,)) | |
361 |
|
390 | |||
|
391 | old_data = user_group.get_api_data() | |||
362 | try: |
|
392 | try: | |
363 | UserGroupModel().delete(user_group) |
|
393 | UserGroupModel().delete(user_group) | |
|
394 | audit_logger.store_api( | |||
|
395 | 'user_group.delete', action_data={'old_data': old_data}, | |||
|
396 | user=apiuser) | |||
364 | Session().commit() |
|
397 | Session().commit() | |
365 | return { |
|
398 | return { | |
366 | 'msg': 'deleted user group ID:%s %s' % ( |
|
399 | 'msg': 'deleted user group ID:%s %s' % ( | |
@@ -438,6 +471,12 b' def add_user_to_user_group(request, apiu' | |||||
438 | user.username, user_group.users_group_name |
|
471 | user.username, user_group.users_group_name | |
439 | ) |
|
472 | ) | |
440 | msg = msg if success else 'User is already in that group' |
|
473 | msg = msg if success else 'User is already in that group' | |
|
474 | if success: | |||
|
475 | user_data = user.get_api_data() | |||
|
476 | audit_logger.store_api( | |||
|
477 | 'user_group.edit.member.add', action_data={'user': user_data}, | |||
|
478 | user=apiuser) | |||
|
479 | ||||
441 | Session().commit() |
|
480 | Session().commit() | |
442 |
|
481 | |||
443 | return { |
|
482 | return { | |
@@ -501,6 +540,12 b' def remove_user_from_user_group(request,' | |||||
501 | user.username, user_group.users_group_name |
|
540 | user.username, user_group.users_group_name | |
502 | ) |
|
541 | ) | |
503 | msg = msg if success else "User wasn't in group" |
|
542 | msg = msg if success else "User wasn't in group" | |
|
543 | if success: | |||
|
544 | user_data = user.get_api_data() | |||
|
545 | audit_logger.store_api( | |||
|
546 | 'user_group.edit.member.delete', action_data={'user': user_data}, | |||
|
547 | user=apiuser) | |||
|
548 | ||||
504 | Session().commit() |
|
549 | Session().commit() | |
505 | return {'success': success, 'msg': msg} |
|
550 | return {'success': success, 'msg': msg} | |
506 | except Exception: |
|
551 | except Exception: |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-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/ |
@@ -24,8 +24,12 b' from pylons import tmpl_context as c' | |||||
24 | from pyramid.httpexceptions import HTTPFound |
|
24 | from pyramid.httpexceptions import HTTPFound | |
25 |
|
25 | |||
26 | from rhodecode.lib import helpers as h |
|
26 | from rhodecode.lib import helpers as h | |
27 |
from rhodecode.lib.utils |
|
27 | from rhodecode.lib.utils import PartialRenderer | |
|
28 | from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time | |||
|
29 | from rhodecode.lib.vcs.exceptions import RepositoryRequirementError | |||
|
30 | from rhodecode.lib.ext_json import json | |||
28 | from rhodecode.model import repo |
|
31 | from rhodecode.model import repo | |
|
32 | from rhodecode.model import repo_group | |||
29 | from rhodecode.model.db import User |
|
33 | from rhodecode.model.db import User | |
30 | from rhodecode.model.scm import ScmModel |
|
34 | from rhodecode.model.scm import ScmModel | |
31 |
|
35 | |||
@@ -36,6 +40,30 b" ADMIN_PREFIX = '/_admin'" | |||||
36 | STATIC_FILE_PREFIX = '/_static' |
|
40 | STATIC_FILE_PREFIX = '/_static' | |
37 |
|
41 | |||
38 |
|
42 | |||
|
43 | def add_route_with_slash(config,name, pattern, **kw): | |||
|
44 | config.add_route(name, pattern, **kw) | |||
|
45 | if not pattern.endswith('/'): | |||
|
46 | config.add_route(name + '_slash', pattern + '/', **kw) | |||
|
47 | ||||
|
48 | ||||
|
49 | def get_format_ref_id(repo): | |||
|
50 | """Returns a `repo` specific reference formatter function""" | |||
|
51 | if h.is_svn(repo): | |||
|
52 | return _format_ref_id_svn | |||
|
53 | else: | |||
|
54 | return _format_ref_id | |||
|
55 | ||||
|
56 | ||||
|
57 | def _format_ref_id(name, raw_id): | |||
|
58 | """Default formatting of a given reference `name`""" | |||
|
59 | return name | |||
|
60 | ||||
|
61 | ||||
|
62 | def _format_ref_id_svn(name, raw_id): | |||
|
63 | """Special way of formatting a reference for Subversion including path""" | |||
|
64 | return '%s@%s' % (name, raw_id) | |||
|
65 | ||||
|
66 | ||||
39 | class TemplateArgs(StrictAttributeDict): |
|
67 | class TemplateArgs(StrictAttributeDict): | |
40 | pass |
|
68 | pass | |
41 |
|
69 | |||
@@ -77,9 +105,14 b' class BaseAppView(object):' | |||||
77 | raise HTTPFound( |
|
105 | raise HTTPFound( | |
78 | self.request.route_path('my_account_password')) |
|
106 | self.request.route_path('my_account_password')) | |
79 |
|
107 | |||
80 | def _get_local_tmpl_context(self): |
|
108 | def _get_local_tmpl_context(self, include_app_defaults=False): | |
81 | c = TemplateArgs() |
|
109 | c = TemplateArgs() | |
82 | c.auth_user = self.request.user |
|
110 | c.auth_user = self.request.user | |
|
111 | if include_app_defaults: | |||
|
112 | # NOTE(marcink): after full pyramid migration include_app_defaults | |||
|
113 | # should be turned on by default | |||
|
114 | from rhodecode.lib.base import attach_context_attributes | |||
|
115 | attach_context_attributes(c, self.request, self.request.user.user_id) | |||
83 | return c |
|
116 | return c | |
84 |
|
117 | |||
85 | def _register_global_c(self, tmpl_args): |
|
118 | def _register_global_c(self, tmpl_args): | |
@@ -121,15 +154,106 b' class RepoAppView(BaseAppView):' | |||||
121 | self.db_repo_name = self.db_repo.repo_name |
|
154 | self.db_repo_name = self.db_repo.repo_name | |
122 | self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo) |
|
155 | self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo) | |
123 |
|
156 | |||
124 | def _get_local_tmpl_context(self): |
|
157 | def _handle_missing_requirements(self, error): | |
125 | c = super(RepoAppView, self)._get_local_tmpl_context() |
|
158 | log.error( | |
|
159 | 'Requirements are missing for repository %s: %s', | |||
|
160 | self.db_repo_name, error.message) | |||
|
161 | ||||
|
162 | def _get_local_tmpl_context(self, include_app_defaults=False): | |||
|
163 | c = super(RepoAppView, self)._get_local_tmpl_context( | |||
|
164 | include_app_defaults=include_app_defaults) | |||
|
165 | ||||
126 | # register common vars for this type of view |
|
166 | # register common vars for this type of view | |
127 | c.rhodecode_db_repo = self.db_repo |
|
167 | c.rhodecode_db_repo = self.db_repo | |
128 | c.repo_name = self.db_repo_name |
|
168 | c.repo_name = self.db_repo_name | |
129 | c.repository_pull_requests = self.db_repo_pull_requests |
|
169 | c.repository_pull_requests = self.db_repo_pull_requests | |
|
170 | ||||
|
171 | c.repository_requirements_missing = False | |||
|
172 | try: | |||
|
173 | self.rhodecode_vcs_repo = self.db_repo.scm_instance() | |||
|
174 | except RepositoryRequirementError as e: | |||
|
175 | c.repository_requirements_missing = True | |||
|
176 | self._handle_missing_requirements(e) | |||
|
177 | ||||
130 | return c |
|
178 | return c | |
131 |
|
179 | |||
132 |
|
180 | |||
|
181 | class DataGridAppView(object): | |||
|
182 | """ | |||
|
183 | Common class to have re-usable grid rendering components | |||
|
184 | """ | |||
|
185 | ||||
|
186 | def _extract_ordering(self, request, column_map=None): | |||
|
187 | column_map = column_map or {} | |||
|
188 | column_index = safe_int(request.GET.get('order[0][column]')) | |||
|
189 | order_dir = request.GET.get( | |||
|
190 | 'order[0][dir]', 'desc') | |||
|
191 | order_by = request.GET.get( | |||
|
192 | 'columns[%s][data][sort]' % column_index, 'name_raw') | |||
|
193 | ||||
|
194 | # translate datatable to DB columns | |||
|
195 | order_by = column_map.get(order_by) or order_by | |||
|
196 | ||||
|
197 | search_q = request.GET.get('search[value]') | |||
|
198 | return search_q, order_by, order_dir | |||
|
199 | ||||
|
200 | def _extract_chunk(self, request): | |||
|
201 | start = safe_int(request.GET.get('start'), 0) | |||
|
202 | length = safe_int(request.GET.get('length'), 25) | |||
|
203 | draw = safe_int(request.GET.get('draw')) | |||
|
204 | return draw, start, length | |||
|
205 | ||||
|
206 | ||||
|
207 | class BaseReferencesView(RepoAppView): | |||
|
208 | """ | |||
|
209 | Base for reference view for branches, tags and bookmarks. | |||
|
210 | """ | |||
|
211 | def load_default_context(self): | |||
|
212 | c = self._get_local_tmpl_context() | |||
|
213 | ||||
|
214 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |||
|
215 | c.repo_info = self.db_repo | |||
|
216 | ||||
|
217 | self._register_global_c(c) | |||
|
218 | return c | |||
|
219 | ||||
|
220 | def load_refs_context(self, ref_items, partials_template): | |||
|
221 | _render = PartialRenderer(partials_template) | |||
|
222 | _data = [] | |||
|
223 | pre_load = ["author", "date", "message"] | |||
|
224 | ||||
|
225 | is_svn = h.is_svn(self.rhodecode_vcs_repo) | |||
|
226 | format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo) | |||
|
227 | ||||
|
228 | for ref_name, commit_id in ref_items: | |||
|
229 | commit = self.rhodecode_vcs_repo.get_commit( | |||
|
230 | commit_id=commit_id, pre_load=pre_load) | |||
|
231 | ||||
|
232 | # TODO: johbo: Unify generation of reference links | |||
|
233 | use_commit_id = '/' in ref_name or is_svn | |||
|
234 | files_url = h.url( | |||
|
235 | 'files_home', | |||
|
236 | repo_name=c.repo_name, | |||
|
237 | f_path=ref_name if is_svn else '', | |||
|
238 | revision=commit_id if use_commit_id else ref_name, | |||
|
239 | at=ref_name) | |||
|
240 | ||||
|
241 | _data.append({ | |||
|
242 | "name": _render('name', ref_name, files_url), | |||
|
243 | "name_raw": ref_name, | |||
|
244 | "date": _render('date', commit.date), | |||
|
245 | "date_raw": datetime_to_time(commit.date), | |||
|
246 | "author": _render('author', commit.author), | |||
|
247 | "commit": _render( | |||
|
248 | 'commit', commit.message, commit.raw_id, commit.idx), | |||
|
249 | "commit_raw": commit.idx, | |||
|
250 | "compare": _render( | |||
|
251 | 'compare', format_ref_id(ref_name, commit.raw_id)), | |||
|
252 | }) | |||
|
253 | c.has_references = bool(_data) | |||
|
254 | c.data = json.dumps(_data) | |||
|
255 | ||||
|
256 | ||||
133 | class RepoRoutePredicate(object): |
|
257 | class RepoRoutePredicate(object): | |
134 | def __init__(self, val, config): |
|
258 | def __init__(self, val, config): | |
135 | self.val = val |
|
259 | self.val = val | |
@@ -140,11 +264,15 b' class RepoRoutePredicate(object):' | |||||
140 | phash = text |
|
264 | phash = text | |
141 |
|
265 | |||
142 | def __call__(self, info, request): |
|
266 | def __call__(self, info, request): | |
|
267 | ||||
|
268 | if hasattr(request, 'vcs_call'): | |||
|
269 | # skip vcs calls | |||
|
270 | return | |||
|
271 | ||||
143 | repo_name = info['match']['repo_name'] |
|
272 | repo_name = info['match']['repo_name'] | |
144 | repo_model = repo.RepoModel() |
|
273 | repo_model = repo.RepoModel() | |
145 | by_name_match = repo_model.get_by_repo_name(repo_name, cache=True) |
|
274 | by_name_match = repo_model.get_by_repo_name(repo_name, cache=True) | |
146 | # if we match quickly from database, short circuit the operation, |
|
275 | ||
147 | # and validate repo based on the type. |
|
|||
148 | if by_name_match: |
|
276 | if by_name_match: | |
149 | # register this as request object we can re-use later |
|
277 | # register this as request object we can re-use later | |
150 | request.db_repo = by_name_match |
|
278 | request.db_repo = by_name_match | |
@@ -158,6 +286,72 b' class RepoRoutePredicate(object):' | |||||
158 | return False |
|
286 | return False | |
159 |
|
287 | |||
160 |
|
288 | |||
|
289 | class RepoTypeRoutePredicate(object): | |||
|
290 | def __init__(self, val, config): | |||
|
291 | self.val = val or ['hg', 'git', 'svn'] | |||
|
292 | ||||
|
293 | def text(self): | |||
|
294 | return 'repo_accepted_type = %s' % self.val | |||
|
295 | ||||
|
296 | phash = text | |||
|
297 | ||||
|
298 | def __call__(self, info, request): | |||
|
299 | if hasattr(request, 'vcs_call'): | |||
|
300 | # skip vcs calls | |||
|
301 | return | |||
|
302 | ||||
|
303 | rhodecode_db_repo = request.db_repo | |||
|
304 | ||||
|
305 | log.debug( | |||
|
306 | '%s checking repo type for %s in %s', | |||
|
307 | self.__class__.__name__, rhodecode_db_repo.repo_type, self.val) | |||
|
308 | ||||
|
309 | if rhodecode_db_repo.repo_type in self.val: | |||
|
310 | return True | |||
|
311 | else: | |||
|
312 | log.warning('Current view is not supported for repo type:%s', | |||
|
313 | rhodecode_db_repo.repo_type) | |||
|
314 | # | |||
|
315 | # h.flash(h.literal( | |||
|
316 | # _('Action not supported for %s.' % rhodecode_repo.alias)), | |||
|
317 | # category='warning') | |||
|
318 | # return redirect( | |||
|
319 | # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name)) | |||
|
320 | ||||
|
321 | return False | |||
|
322 | ||||
|
323 | ||||
|
324 | class RepoGroupRoutePredicate(object): | |||
|
325 | def __init__(self, val, config): | |||
|
326 | self.val = val | |||
|
327 | ||||
|
328 | def text(self): | |||
|
329 | return 'repo_group_route = %s' % self.val | |||
|
330 | ||||
|
331 | phash = text | |||
|
332 | ||||
|
333 | def __call__(self, info, request): | |||
|
334 | if hasattr(request, 'vcs_call'): | |||
|
335 | # skip vcs calls | |||
|
336 | return | |||
|
337 | ||||
|
338 | repo_group_name = info['match']['repo_group_name'] | |||
|
339 | repo_group_model = repo_group.RepoGroupModel() | |||
|
340 | by_name_match = repo_group_model.get_by_group_name( | |||
|
341 | repo_group_name, cache=True) | |||
|
342 | ||||
|
343 | if by_name_match: | |||
|
344 | # register this as request object we can re-use later | |||
|
345 | request.db_repo_group = by_name_match | |||
|
346 | return True | |||
|
347 | ||||
|
348 | return False | |||
|
349 | ||||
|
350 | ||||
161 | def includeme(config): |
|
351 | def includeme(config): | |
162 | config.add_route_predicate( |
|
352 | config.add_route_predicate( | |
163 | 'repo_route', RepoRoutePredicate) |
|
353 | 'repo_route', RepoRoutePredicate) | |
|
354 | config.add_route_predicate( | |||
|
355 | 'repo_accepted_types', RepoTypeRoutePredicate) | |||
|
356 | config.add_route_predicate( | |||
|
357 | 'repo_group_route', RepoGroupRoutePredicate) |
@@ -30,6 +30,20 b' def admin_routes(config):' | |||||
30 | """ |
|
30 | """ | |
31 |
|
31 | |||
32 | config.add_route( |
|
32 | config.add_route( | |
|
33 | name='admin_audit_logs', | |||
|
34 | pattern='/audit_logs') | |||
|
35 | ||||
|
36 | config.add_route( | |||
|
37 | name='pull_requests_global_0', # backward compat | |||
|
38 | pattern='/pull_requests/{pull_request_id:[0-9]+}') | |||
|
39 | config.add_route( | |||
|
40 | name='pull_requests_global_1', # backward compat | |||
|
41 | pattern='/pull-requests/{pull_request_id:[0-9]+}') | |||
|
42 | config.add_route( | |||
|
43 | name='pull_requests_global', | |||
|
44 | pattern='/pull-request/{pull_request_id:[0-9]+}') | |||
|
45 | ||||
|
46 | config.add_route( | |||
33 | name='admin_settings_open_source', |
|
47 | name='admin_settings_open_source', | |
34 | pattern='/settings/open_source') |
|
48 | pattern='/settings/open_source') | |
35 | config.add_route( |
|
49 | config.add_route( | |
@@ -50,6 +64,11 b' def admin_routes(config):' | |||||
50 | name='admin_settings_sessions_cleanup', |
|
64 | name='admin_settings_sessions_cleanup', | |
51 | pattern='/settings/sessions/cleanup') |
|
65 | pattern='/settings/sessions/cleanup') | |
52 |
|
66 | |||
|
67 | # global permissions | |||
|
68 | config.add_route( | |||
|
69 | name='admin_permissions_ips', | |||
|
70 | pattern='/permissions/ips') | |||
|
71 | ||||
53 | # users admin |
|
72 | # users admin | |
54 | config.add_route( |
|
73 | config.add_route( | |
55 | name='users', |
|
74 | name='users', | |
@@ -70,6 +89,28 b' def admin_routes(config):' | |||||
70 | name='edit_user_auth_tokens_delete', |
|
89 | name='edit_user_auth_tokens_delete', | |
71 | pattern='/users/{user_id:\d+}/edit/auth_tokens/delete') |
|
90 | pattern='/users/{user_id:\d+}/edit/auth_tokens/delete') | |
72 |
|
91 | |||
|
92 | # user emails | |||
|
93 | config.add_route( | |||
|
94 | name='edit_user_emails', | |||
|
95 | pattern='/users/{user_id:\d+}/edit/emails') | |||
|
96 | config.add_route( | |||
|
97 | name='edit_user_emails_add', | |||
|
98 | pattern='/users/{user_id:\d+}/edit/emails/new') | |||
|
99 | config.add_route( | |||
|
100 | name='edit_user_emails_delete', | |||
|
101 | pattern='/users/{user_id:\d+}/edit/emails/delete') | |||
|
102 | ||||
|
103 | # user IPs | |||
|
104 | config.add_route( | |||
|
105 | name='edit_user_ips', | |||
|
106 | pattern='/users/{user_id:\d+}/edit/ips') | |||
|
107 | config.add_route( | |||
|
108 | name='edit_user_ips_add', | |||
|
109 | pattern='/users/{user_id:\d+}/edit/ips/new') | |||
|
110 | config.add_route( | |||
|
111 | name='edit_user_ips_delete', | |||
|
112 | pattern='/users/{user_id:\d+}/edit/ips/delete') | |||
|
113 | ||||
73 | # user groups management |
|
114 | # user groups management | |
74 | config.add_route( |
|
115 | config.add_route( | |
75 | name='edit_user_groups_management', |
|
116 | name='edit_user_groups_management', | |
@@ -93,6 +134,8 b' def includeme(config):' | |||||
93 | navigation_registry = NavigationRegistry(labs_active=labs_active) |
|
134 | navigation_registry = NavigationRegistry(labs_active=labs_active) | |
94 | config.registry.registerUtility(navigation_registry) |
|
135 | config.registry.registerUtility(navigation_registry) | |
95 |
|
136 | |||
|
137 | # main admin routes | |||
|
138 | config.add_route(name='admin_home', pattern=ADMIN_PREFIX) | |||
96 | config.include(admin_routes, route_prefix=ADMIN_PREFIX) |
|
139 | config.include(admin_routes, route_prefix=ADMIN_PREFIX) | |
97 |
|
140 | |||
98 | # Scan module for configuration decorators. |
|
141 | # Scan module for configuration decorators. |
@@ -20,7 +20,9 b'' | |||||
20 |
|
20 | |||
21 | import pytest |
|
21 | import pytest | |
22 |
|
22 | |||
23 | from rhodecode.model.db import User, UserApiKeys |
|
23 | from rhodecode.model.db import User, UserApiKeys, UserEmailMap | |
|
24 | from rhodecode.model.meta import Session | |||
|
25 | from rhodecode.model.user import UserModel | |||
24 |
|
26 | |||
25 | from rhodecode.tests import ( |
|
27 | from rhodecode.tests import ( | |
26 | TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) |
|
28 | TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) | |
@@ -44,6 +46,20 b' def route_path(name, params=None, **kwar' | |||||
44 | ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new', |
|
46 | ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new', | |
45 | 'edit_user_auth_tokens_delete': |
|
47 | 'edit_user_auth_tokens_delete': | |
46 | ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete', |
|
48 | ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete', | |
|
49 | ||||
|
50 | 'edit_user_emails': | |||
|
51 | ADMIN_PREFIX + '/users/{user_id}/edit/emails', | |||
|
52 | 'edit_user_emails_add': | |||
|
53 | ADMIN_PREFIX + '/users/{user_id}/edit/emails/new', | |||
|
54 | 'edit_user_emails_delete': | |||
|
55 | ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete', | |||
|
56 | ||||
|
57 | 'edit_user_ips': | |||
|
58 | ADMIN_PREFIX + '/users/{user_id}/edit/ips', | |||
|
59 | 'edit_user_ips_add': | |||
|
60 | ADMIN_PREFIX + '/users/{user_id}/edit/ips/new', | |||
|
61 | 'edit_user_ips_delete': | |||
|
62 | ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete', | |||
47 | }[name].format(**kwargs) |
|
63 | }[name].format(**kwargs) | |
48 |
|
64 | |||
49 | if params: |
|
65 | if params: | |
@@ -135,8 +151,131 b' class TestAdminUsersView(TestController)' | |||||
135 |
|
151 | |||
136 | response = self.app.post( |
|
152 | response = self.app.post( | |
137 | route_path('edit_user_auth_tokens_delete', user_id=user_id), |
|
153 | route_path('edit_user_auth_tokens_delete', user_id=user_id), | |
138 |
{'del_auth_token': keys[0].api_key, |
|
154 | {'del_auth_token': keys[0].user_api_key_id, | |
|
155 | 'csrf_token': self.csrf_token}) | |||
139 |
|
156 | |||
140 | assert_session_flash(response, 'Auth token successfully deleted') |
|
157 | assert_session_flash(response, 'Auth token successfully deleted') | |
141 | keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all() |
|
158 | keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all() | |
142 | assert 2 == len(keys) |
|
159 | assert 2 == len(keys) | |
|
160 | ||||
|
161 | def test_ips(self): | |||
|
162 | self.log_user() | |||
|
163 | user = User.get_by_username(TEST_USER_REGULAR_LOGIN) | |||
|
164 | response = self.app.get(route_path('edit_user_ips', user_id=user.user_id)) | |||
|
165 | response.mustcontain('All IP addresses are allowed') | |||
|
166 | ||||
|
167 | @pytest.mark.parametrize("test_name, ip, ip_range, failure", [ | |||
|
168 | ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False), | |||
|
169 | ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False), | |||
|
170 | ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False), | |||
|
171 | ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False), | |||
|
172 | ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True), | |||
|
173 | ('127_bad_ip', 'foobar', 'foobar', True), | |||
|
174 | ]) | |||
|
175 | def test_ips_add(self, user_util, test_name, ip, ip_range, failure): | |||
|
176 | self.log_user() | |||
|
177 | user = user_util.create_user(username=test_name) | |||
|
178 | user_id = user.user_id | |||
|
179 | ||||
|
180 | response = self.app.post( | |||
|
181 | route_path('edit_user_ips_add', user_id=user_id), | |||
|
182 | params={'new_ip': ip, 'csrf_token': self.csrf_token}) | |||
|
183 | ||||
|
184 | if failure: | |||
|
185 | assert_session_flash( | |||
|
186 | response, 'Please enter a valid IPv4 or IpV6 address') | |||
|
187 | response = self.app.get(route_path('edit_user_ips', user_id=user_id)) | |||
|
188 | ||||
|
189 | response.mustcontain(no=[ip]) | |||
|
190 | response.mustcontain(no=[ip_range]) | |||
|
191 | ||||
|
192 | else: | |||
|
193 | response = self.app.get(route_path('edit_user_ips', user_id=user_id)) | |||
|
194 | response.mustcontain(ip) | |||
|
195 | response.mustcontain(ip_range) | |||
|
196 | ||||
|
197 | def test_ips_delete(self, user_util): | |||
|
198 | self.log_user() | |||
|
199 | user = user_util.create_user() | |||
|
200 | user_id = user.user_id | |||
|
201 | ip = '127.0.0.1/32' | |||
|
202 | ip_range = '127.0.0.1 - 127.0.0.1' | |||
|
203 | new_ip = UserModel().add_extra_ip(user_id, ip) | |||
|
204 | Session().commit() | |||
|
205 | new_ip_id = new_ip.ip_id | |||
|
206 | ||||
|
207 | response = self.app.get(route_path('edit_user_ips', user_id=user_id)) | |||
|
208 | response.mustcontain(ip) | |||
|
209 | response.mustcontain(ip_range) | |||
|
210 | ||||
|
211 | self.app.post( | |||
|
212 | route_path('edit_user_ips_delete', user_id=user_id), | |||
|
213 | params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token}) | |||
|
214 | ||||
|
215 | response = self.app.get(route_path('edit_user_ips', user_id=user_id)) | |||
|
216 | response.mustcontain('All IP addresses are allowed') | |||
|
217 | response.mustcontain(no=[ip]) | |||
|
218 | response.mustcontain(no=[ip_range]) | |||
|
219 | ||||
|
220 | def test_emails(self): | |||
|
221 | self.log_user() | |||
|
222 | user = User.get_by_username(TEST_USER_REGULAR_LOGIN) | |||
|
223 | response = self.app.get(route_path('edit_user_emails', user_id=user.user_id)) | |||
|
224 | response.mustcontain('No additional emails specified') | |||
|
225 | ||||
|
226 | def test_emails_add(self, user_util): | |||
|
227 | self.log_user() | |||
|
228 | user = user_util.create_user() | |||
|
229 | user_id = user.user_id | |||
|
230 | ||||
|
231 | self.app.post( | |||
|
232 | route_path('edit_user_emails_add', user_id=user_id), | |||
|
233 | params={'new_email': 'example@rhodecode.com', | |||
|
234 | 'csrf_token': self.csrf_token}) | |||
|
235 | ||||
|
236 | response = self.app.get(route_path('edit_user_emails', user_id=user_id)) | |||
|
237 | response.mustcontain('example@rhodecode.com') | |||
|
238 | ||||
|
239 | def test_emails_add_existing_email(self, user_util, user_regular): | |||
|
240 | existing_email = user_regular.email | |||
|
241 | ||||
|
242 | self.log_user() | |||
|
243 | user = user_util.create_user() | |||
|
244 | user_id = user.user_id | |||
|
245 | ||||
|
246 | response = self.app.post( | |||
|
247 | route_path('edit_user_emails_add', user_id=user_id), | |||
|
248 | params={'new_email': existing_email, | |||
|
249 | 'csrf_token': self.csrf_token}) | |||
|
250 | assert_session_flash( | |||
|
251 | response, 'This e-mail address is already taken') | |||
|
252 | ||||
|
253 | response = self.app.get(route_path('edit_user_emails', user_id=user_id)) | |||
|
254 | response.mustcontain(no=[existing_email]) | |||
|
255 | ||||
|
256 | def test_emails_delete(self, user_util): | |||
|
257 | self.log_user() | |||
|
258 | user = user_util.create_user() | |||
|
259 | user_id = user.user_id | |||
|
260 | ||||
|
261 | self.app.post( | |||
|
262 | route_path('edit_user_emails_add', user_id=user_id), | |||
|
263 | params={'new_email': 'example@rhodecode.com', | |||
|
264 | 'csrf_token': self.csrf_token}) | |||
|
265 | ||||
|
266 | response = self.app.get(route_path('edit_user_emails', user_id=user_id)) | |||
|
267 | response.mustcontain('example@rhodecode.com') | |||
|
268 | ||||
|
269 | user_email = UserEmailMap.query()\ | |||
|
270 | .filter(UserEmailMap.email == 'example@rhodecode.com') \ | |||
|
271 | .filter(UserEmailMap.user_id == user_id)\ | |||
|
272 | .one() | |||
|
273 | ||||
|
274 | del_email_id = user_email.email_id | |||
|
275 | self.app.post( | |||
|
276 | route_path('edit_user_emails_delete', user_id=user_id), | |||
|
277 | params={'del_email_id': del_email_id, | |||
|
278 | 'csrf_token': self.csrf_token}) | |||
|
279 | ||||
|
280 | response = self.app.get(route_path('edit_user_emails', user_id=user_id)) | |||
|
281 | response.mustcontain(no=['example@rhodecode.com']) No newline at end of file |
@@ -21,7 +21,7 b'' | |||||
21 | import collections |
|
21 | import collections | |
22 | import logging |
|
22 | import logging | |
23 |
|
23 | |||
24 | from pylons import tmpl_context as c |
|
24 | ||
25 | from pyramid.view import view_config |
|
25 | from pyramid.view import view_config | |
26 |
|
26 | |||
27 | from rhodecode.apps._base import BaseAppView |
|
27 | from rhodecode.apps._base import BaseAppView | |
@@ -34,15 +34,21 b' log = logging.getLogger(__name__)' | |||||
34 |
|
34 | |||
35 | class OpenSourceLicensesAdminSettingsView(BaseAppView): |
|
35 | class OpenSourceLicensesAdminSettingsView(BaseAppView): | |
36 |
|
36 | |||
|
37 | def load_default_context(self): | |||
|
38 | c = self._get_local_tmpl_context() | |||
|
39 | self._register_global_c(c) | |||
|
40 | return c | |||
|
41 | ||||
37 | @LoginRequired() |
|
42 | @LoginRequired() | |
38 | @HasPermissionAllDecorator('hg.admin') |
|
43 | @HasPermissionAllDecorator('hg.admin') | |
39 | @view_config( |
|
44 | @view_config( | |
40 | route_name='admin_settings_open_source', request_method='GET', |
|
45 | route_name='admin_settings_open_source', request_method='GET', | |
41 | renderer='rhodecode:templates/admin/settings/settings.mako') |
|
46 | renderer='rhodecode:templates/admin/settings/settings.mako') | |
42 | def open_source_licenses(self): |
|
47 | def open_source_licenses(self): | |
|
48 | c = self.load_default_context() | |||
43 | c.active = 'open_source' |
|
49 | c.active = 'open_source' | |
44 | c.navlist = navigation_list(self.request) |
|
50 | c.navlist = navigation_list(self.request) | |
45 | c.opensource_licenses = collections.OrderedDict( |
|
51 | c.opensource_licenses = collections.OrderedDict( | |
46 | sorted(read_opensource_licenses().items(), key=lambda t: t[0])) |
|
52 | sorted(read_opensource_licenses().items(), key=lambda t: t[0])) | |
47 |
|
53 | |||
48 | return {} |
|
54 | return self._get_template_context(c) |
@@ -20,7 +20,6 b'' | |||||
20 |
|
20 | |||
21 | import logging |
|
21 | import logging | |
22 |
|
22 | |||
23 | from pylons import tmpl_context as c |
|
|||
24 | from pyramid.view import view_config |
|
23 | from pyramid.view import view_config | |
25 | from pyramid.httpexceptions import HTTPFound |
|
24 | from pyramid.httpexceptions import HTTPFound | |
26 |
|
25 | |||
@@ -37,6 +36,11 b' log = logging.getLogger(__name__)' | |||||
37 |
|
36 | |||
38 |
|
37 | |||
39 | class AdminSessionSettingsView(BaseAppView): |
|
38 | class AdminSessionSettingsView(BaseAppView): | |
|
39 | def load_default_context(self): | |||
|
40 | c = self._get_local_tmpl_context() | |||
|
41 | ||||
|
42 | self._register_global_c(c) | |||
|
43 | return c | |||
40 |
|
44 | |||
41 | @LoginRequired() |
|
45 | @LoginRequired() | |
42 | @HasPermissionAllDecorator('hg.admin') |
|
46 | @HasPermissionAllDecorator('hg.admin') | |
@@ -44,6 +48,8 b' class AdminSessionSettingsView(BaseAppVi' | |||||
44 | route_name='admin_settings_sessions', request_method='GET', |
|
48 | route_name='admin_settings_sessions', request_method='GET', | |
45 | renderer='rhodecode:templates/admin/settings/settings.mako') |
|
49 | renderer='rhodecode:templates/admin/settings/settings.mako') | |
46 | def settings_sessions(self): |
|
50 | def settings_sessions(self): | |
|
51 | c = self.load_default_context() | |||
|
52 | ||||
47 | c.active = 'sessions' |
|
53 | c.active = 'sessions' | |
48 | c.navlist = navigation_list(self.request) |
|
54 | c.navlist = navigation_list(self.request) | |
49 |
|
55 | |||
@@ -59,11 +65,11 b' class AdminSessionSettingsView(BaseAppVi' | |||||
59 | c.session_expired_count = c.session_model.get_expired_count( |
|
65 | c.session_expired_count = c.session_model.get_expired_count( | |
60 | older_than_seconds) |
|
66 | older_than_seconds) | |
61 |
|
67 | |||
62 | return {} |
|
68 | return self._get_template_context(c) | |
63 |
|
69 | |||
64 | @LoginRequired() |
|
70 | @LoginRequired() | |
|
71 | @HasPermissionAllDecorator('hg.admin') | |||
65 | @CSRFRequired() |
|
72 | @CSRFRequired() | |
66 | @HasPermissionAllDecorator('hg.admin') |
|
|||
67 | @view_config( |
|
73 | @view_config( | |
68 | route_name='admin_settings_sessions_cleanup', request_method='POST') |
|
74 | route_name='admin_settings_sessions_cleanup', request_method='POST') | |
69 | def settings_sessions_cleanup(self): |
|
75 | def settings_sessions_cleanup(self): |
@@ -33,8 +33,8 b' log = logging.getLogger(__name__)' | |||||
33 | class SvnConfigAdminSettingsView(BaseAppView): |
|
33 | class SvnConfigAdminSettingsView(BaseAppView): | |
34 |
|
34 | |||
35 | @LoginRequired() |
|
35 | @LoginRequired() | |
|
36 | @HasPermissionAllDecorator('hg.admin') | |||
36 | @CSRFRequired() |
|
37 | @CSRFRequired() | |
37 | @HasPermissionAllDecorator('hg.admin') |
|
|||
38 | @view_config( |
|
38 | @view_config( | |
39 | route_name='admin_settings_vcs_svn_generate_cfg', |
|
39 | route_name='admin_settings_vcs_svn_generate_cfg', | |
40 | request_method='POST', renderer='json') |
|
40 | request_method='POST', renderer='json') |
@@ -22,7 +22,6 b' import logging' | |||||
22 | import urllib2 |
|
22 | import urllib2 | |
23 | import packaging.version |
|
23 | import packaging.version | |
24 |
|
24 | |||
25 | from pylons import tmpl_context as c |
|
|||
26 | from pyramid.view import view_config |
|
25 | from pyramid.view import view_config | |
27 |
|
26 | |||
28 | import rhodecode |
|
27 | import rhodecode | |
@@ -39,6 +38,10 b' log = logging.getLogger(__name__)' | |||||
39 |
|
38 | |||
40 |
|
39 | |||
41 | class AdminSystemInfoSettingsView(BaseAppView): |
|
40 | class AdminSystemInfoSettingsView(BaseAppView): | |
|
41 | def load_default_context(self): | |||
|
42 | c = self._get_local_tmpl_context() | |||
|
43 | self._register_global_c(c) | |||
|
44 | return c | |||
42 |
|
45 | |||
43 | @staticmethod |
|
46 | @staticmethod | |
44 | def get_update_data(update_url): |
|
47 | def get_update_data(update_url): | |
@@ -64,6 +67,7 b' class AdminSystemInfoSettingsView(BaseAp' | |||||
64 | renderer='rhodecode:templates/admin/settings/settings.mako') |
|
67 | renderer='rhodecode:templates/admin/settings/settings.mako') | |
65 | def settings_system_info(self): |
|
68 | def settings_system_info(self): | |
66 | _ = self.request.translate |
|
69 | _ = self.request.translate | |
|
70 | c = self.load_default_context() | |||
67 |
|
71 | |||
68 | c.active = 'system' |
|
72 | c.active = 'system' | |
69 | c.navlist = navigation_list(self.request) |
|
73 | c.navlist = navigation_list(self.request) | |
@@ -106,6 +110,7 b' class AdminSystemInfoSettingsView(BaseAp' | |||||
106 | (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')), |
|
110 | (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')), | |
107 | (_('RhodeCode Server ID'), val('server')['server_id'], state('server')), |
|
111 | (_('RhodeCode Server ID'), val('server')['server_id'], state('server')), | |
108 | (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')), |
|
112 | (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')), | |
|
113 | (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')), | |||
109 | (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')), |
|
114 | (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')), | |
110 | (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')), |
|
115 | (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')), | |
111 | ('', '', ''), # spacer |
|
116 | ('', '', ''), # spacer | |
@@ -163,7 +168,7 b' class AdminSystemInfoSettingsView(BaseAp' | |||||
163 | else: |
|
168 | else: | |
164 | self.request.session.flash( |
|
169 | self.request.session.flash( | |
165 | 'You are not allowed to do this', queue='warning') |
|
170 | 'You are not allowed to do this', queue='warning') | |
166 | return {} |
|
171 | return self._get_template_context(c) | |
167 |
|
172 | |||
168 | @LoginRequired() |
|
173 | @LoginRequired() | |
169 | @HasPermissionAllDecorator('hg.admin') |
|
174 | @HasPermissionAllDecorator('hg.admin') | |
@@ -172,6 +177,7 b' class AdminSystemInfoSettingsView(BaseAp' | |||||
172 | renderer='rhodecode:templates/admin/settings/settings_system_update.mako') |
|
177 | renderer='rhodecode:templates/admin/settings/settings_system_update.mako') | |
173 | def settings_system_info_check_update(self): |
|
178 | def settings_system_info_check_update(self): | |
174 | _ = self.request.translate |
|
179 | _ = self.request.translate | |
|
180 | c = self.load_default_context() | |||
175 |
|
181 | |||
176 | update_url = self.get_update_url() |
|
182 | update_url = self.get_update_url() | |
177 |
|
183 | |||
@@ -200,4 +206,4 b' class AdminSystemInfoSettingsView(BaseAp' | |||||
200 | c.should_upgrade = True |
|
206 | c.should_upgrade = True | |
201 | c.important_notices = latest['general'] |
|
207 | c.important_notices = latest['general'] | |
202 |
|
208 | |||
203 | return {} |
|
209 | return self._get_template_context(c) |
@@ -20,15 +20,16 b'' | |||||
20 |
|
20 | |||
21 | import logging |
|
21 | import logging | |
22 | import datetime |
|
22 | import datetime | |
|
23 | import formencode | |||
23 |
|
24 | |||
24 | from pyramid.httpexceptions import HTTPFound |
|
25 | from pyramid.httpexceptions import HTTPFound | |
25 | from pyramid.view import view_config |
|
26 | from pyramid.view import view_config | |
26 | from sqlalchemy.sql.functions import coalesce |
|
27 | from sqlalchemy.sql.functions import coalesce | |
27 |
|
28 | |||
28 | from rhodecode.lib.helpers import Page |
|
29 | from rhodecode.apps._base import BaseAppView, DataGridAppView | |
29 | from rhodecode_tools.lib.ext_json import json |
|
|||
30 |
|
30 | |||
31 |
from rhodecode. |
|
31 | from rhodecode.lib import audit_logger | |
|
32 | from rhodecode.lib.ext_json import json | |||
32 | from rhodecode.lib.auth import ( |
|
33 | from rhodecode.lib.auth import ( | |
33 | LoginRequired, HasPermissionAllDecorator, CSRFRequired) |
|
34 | LoginRequired, HasPermissionAllDecorator, CSRFRequired) | |
34 | from rhodecode.lib import helpers as h |
|
35 | from rhodecode.lib import helpers as h | |
@@ -37,13 +38,13 b' from rhodecode.lib.utils2 import safe_in' | |||||
37 | from rhodecode.model.auth_token import AuthTokenModel |
|
38 | from rhodecode.model.auth_token import AuthTokenModel | |
38 | from rhodecode.model.user import UserModel |
|
39 | from rhodecode.model.user import UserModel | |
39 | from rhodecode.model.user_group import UserGroupModel |
|
40 | from rhodecode.model.user_group import UserGroupModel | |
40 | from rhodecode.model.db import User, or_ |
|
41 | from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys | |
41 | from rhodecode.model.meta import Session |
|
42 | from rhodecode.model.meta import Session | |
42 |
|
43 | |||
43 | log = logging.getLogger(__name__) |
|
44 | log = logging.getLogger(__name__) | |
44 |
|
45 | |||
45 |
|
46 | |||
46 | class AdminUsersView(BaseAppView): |
|
47 | class AdminUsersView(BaseAppView, DataGridAppView): | |
47 | ALLOW_SCOPED_TOKENS = False |
|
48 | ALLOW_SCOPED_TOKENS = False | |
48 | """ |
|
49 | """ | |
49 | This view has alternative version inside EE, if modified please take a look |
|
50 | This view has alternative version inside EE, if modified please take a look | |
@@ -64,28 +65,6 b' class AdminUsersView(BaseAppView):' | |||||
64 | # is a pyramid view |
|
65 | # is a pyramid view | |
65 | raise HTTPFound('/') |
|
66 | raise HTTPFound('/') | |
66 |
|
67 | |||
67 | def _extract_ordering(self, request): |
|
|||
68 | column_index = safe_int(request.GET.get('order[0][column]')) |
|
|||
69 | order_dir = request.GET.get( |
|
|||
70 | 'order[0][dir]', 'desc') |
|
|||
71 | order_by = request.GET.get( |
|
|||
72 | 'columns[%s][data][sort]' % column_index, 'name_raw') |
|
|||
73 |
|
||||
74 | # translate datatable to DB columns |
|
|||
75 | order_by = { |
|
|||
76 | 'first_name': 'name', |
|
|||
77 | 'last_name': 'lastname', |
|
|||
78 | }.get(order_by) or order_by |
|
|||
79 |
|
||||
80 | search_q = request.GET.get('search[value]') |
|
|||
81 | return search_q, order_by, order_dir |
|
|||
82 |
|
||||
83 | def _extract_chunk(self, request): |
|
|||
84 | start = safe_int(request.GET.get('start'), 0) |
|
|||
85 | length = safe_int(request.GET.get('length'), 25) |
|
|||
86 | draw = safe_int(request.GET.get('draw')) |
|
|||
87 | return draw, start, length |
|
|||
88 |
|
||||
89 | @HasPermissionAllDecorator('hg.admin') |
|
68 | @HasPermissionAllDecorator('hg.admin') | |
90 | @view_config( |
|
69 | @view_config( | |
91 | route_name='users', request_method='GET', |
|
70 | route_name='users', request_method='GET', | |
@@ -97,8 +76,8 b' class AdminUsersView(BaseAppView):' | |||||
97 | @HasPermissionAllDecorator('hg.admin') |
|
76 | @HasPermissionAllDecorator('hg.admin') | |
98 | @view_config( |
|
77 | @view_config( | |
99 | # renderer defined below |
|
78 | # renderer defined below | |
100 |
route_name='users_data', request_method='GET', |
|
79 | route_name='users_data', request_method='GET', | |
101 | xhr=True) |
|
80 | renderer='json_ext', xhr=True) | |
102 | def users_list_data(self): |
|
81 | def users_list_data(self): | |
103 | draw, start, limit = self._extract_chunk(self.request) |
|
82 | draw, start, limit = self._extract_chunk(self.request) | |
104 | search_q, order_by, order_dir = self._extract_ordering(self.request) |
|
83 | search_q, order_by, order_dir = self._extract_ordering(self.request) | |
@@ -149,8 +128,8 b' class AdminUsersView(BaseAppView):' | |||||
149 | users_data.append({ |
|
128 | users_data.append({ | |
150 | "username": h.gravatar_with_user(user.username), |
|
129 | "username": h.gravatar_with_user(user.username), | |
151 | "email": user.email, |
|
130 | "email": user.email, | |
152 |
"first_name": |
|
131 | "first_name": user.first_name, | |
153 |
"last_name": |
|
132 | "last_name": user.last_name, | |
154 | "last_login": h.format_date(user.last_login), |
|
133 | "last_login": h.format_date(user.last_login), | |
155 | "last_activity": h.format_date(user.last_activity), |
|
134 | "last_activity": h.format_date(user.last_activity), | |
156 | "active": h.bool2icon(user.active), |
|
135 | "active": h.bool2icon(user.active), | |
@@ -216,15 +195,23 b' class AdminUsersView(BaseAppView):' | |||||
216 |
|
195 | |||
217 | user_id = self.request.matchdict.get('user_id') |
|
196 | user_id = self.request.matchdict.get('user_id') | |
218 | c.user = User.get_or_404(user_id, pyramid_exc=True) |
|
197 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |
|
198 | ||||
219 | self._redirect_for_default_user(c.user.username) |
|
199 | self._redirect_for_default_user(c.user.username) | |
220 |
|
200 | |||
|
201 | user_data = c.user.get_api_data() | |||
221 | lifetime = safe_int(self.request.POST.get('lifetime'), -1) |
|
202 | lifetime = safe_int(self.request.POST.get('lifetime'), -1) | |
222 | description = self.request.POST.get('description') |
|
203 | description = self.request.POST.get('description') | |
223 | role = self.request.POST.get('role') |
|
204 | role = self.request.POST.get('role') | |
224 |
|
205 | |||
225 | token = AuthTokenModel().create( |
|
206 | token = AuthTokenModel().create( | |
226 | c.user.user_id, description, lifetime, role) |
|
207 | c.user.user_id, description, lifetime, role) | |
|
208 | token_data = token.get_api_data() | |||
|
209 | ||||
227 | self.maybe_attach_token_scope(token) |
|
210 | self.maybe_attach_token_scope(token) | |
|
211 | audit_logger.store_web( | |||
|
212 | 'user.edit.token.add', action_data={ | |||
|
213 | 'data': {'token': token_data, 'user': user_data}}, | |||
|
214 | user=self._rhodecode_user, ) | |||
228 | Session().commit() |
|
215 | Session().commit() | |
229 |
|
216 | |||
230 | h.flash(_("Auth token successfully created"), category='success') |
|
217 | h.flash(_("Auth token successfully created"), category='success') | |
@@ -242,11 +229,19 b' class AdminUsersView(BaseAppView):' | |||||
242 | user_id = self.request.matchdict.get('user_id') |
|
229 | user_id = self.request.matchdict.get('user_id') | |
243 | c.user = User.get_or_404(user_id, pyramid_exc=True) |
|
230 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |
244 | self._redirect_for_default_user(c.user.username) |
|
231 | self._redirect_for_default_user(c.user.username) | |
|
232 | user_data = c.user.get_api_data() | |||
245 |
|
233 | |||
246 | del_auth_token = self.request.POST.get('del_auth_token') |
|
234 | del_auth_token = self.request.POST.get('del_auth_token') | |
247 |
|
235 | |||
248 | if del_auth_token: |
|
236 | if del_auth_token: | |
|
237 | token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True) | |||
|
238 | token_data = token.get_api_data() | |||
|
239 | ||||
249 | AuthTokenModel().delete(del_auth_token, c.user.user_id) |
|
240 | AuthTokenModel().delete(del_auth_token, c.user.user_id) | |
|
241 | audit_logger.store_web( | |||
|
242 | 'user.edit.token.delete', action_data={ | |||
|
243 | 'data': {'token': token_data, 'user': user_data}}, | |||
|
244 | user=self._rhodecode_user,) | |||
250 | Session().commit() |
|
245 | Session().commit() | |
251 | h.flash(_("Auth token successfully deleted"), category='success') |
|
246 | h.flash(_("Auth token successfully deleted"), category='success') | |
252 |
|
247 | |||
@@ -255,6 +250,186 b' class AdminUsersView(BaseAppView):' | |||||
255 | @LoginRequired() |
|
250 | @LoginRequired() | |
256 | @HasPermissionAllDecorator('hg.admin') |
|
251 | @HasPermissionAllDecorator('hg.admin') | |
257 | @view_config( |
|
252 | @view_config( | |
|
253 | route_name='edit_user_emails', request_method='GET', | |||
|
254 | renderer='rhodecode:templates/admin/users/user_edit.mako') | |||
|
255 | def emails(self): | |||
|
256 | _ = self.request.translate | |||
|
257 | c = self.load_default_context() | |||
|
258 | ||||
|
259 | user_id = self.request.matchdict.get('user_id') | |||
|
260 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |||
|
261 | self._redirect_for_default_user(c.user.username) | |||
|
262 | ||||
|
263 | c.active = 'emails' | |||
|
264 | c.user_email_map = UserEmailMap.query() \ | |||
|
265 | .filter(UserEmailMap.user == c.user).all() | |||
|
266 | ||||
|
267 | return self._get_template_context(c) | |||
|
268 | ||||
|
269 | @LoginRequired() | |||
|
270 | @HasPermissionAllDecorator('hg.admin') | |||
|
271 | @CSRFRequired() | |||
|
272 | @view_config( | |||
|
273 | route_name='edit_user_emails_add', request_method='POST') | |||
|
274 | def emails_add(self): | |||
|
275 | _ = self.request.translate | |||
|
276 | c = self.load_default_context() | |||
|
277 | ||||
|
278 | user_id = self.request.matchdict.get('user_id') | |||
|
279 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |||
|
280 | self._redirect_for_default_user(c.user.username) | |||
|
281 | ||||
|
282 | email = self.request.POST.get('new_email') | |||
|
283 | user_data = c.user.get_api_data() | |||
|
284 | try: | |||
|
285 | UserModel().add_extra_email(c.user.user_id, email) | |||
|
286 | audit_logger.store_web( | |||
|
287 | 'user.edit.email.add', action_data={'email': email, 'user': user_data}, | |||
|
288 | user=self._rhodecode_user) | |||
|
289 | Session().commit() | |||
|
290 | h.flash(_("Added new email address `%s` for user account") % email, | |||
|
291 | category='success') | |||
|
292 | except formencode.Invalid as error: | |||
|
293 | h.flash(h.escape(error.error_dict['email']), category='error') | |||
|
294 | except Exception: | |||
|
295 | log.exception("Exception during email saving") | |||
|
296 | h.flash(_('An error occurred during email saving'), | |||
|
297 | category='error') | |||
|
298 | raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id)) | |||
|
299 | ||||
|
300 | @LoginRequired() | |||
|
301 | @HasPermissionAllDecorator('hg.admin') | |||
|
302 | @CSRFRequired() | |||
|
303 | @view_config( | |||
|
304 | route_name='edit_user_emails_delete', request_method='POST') | |||
|
305 | def emails_delete(self): | |||
|
306 | _ = self.request.translate | |||
|
307 | c = self.load_default_context() | |||
|
308 | ||||
|
309 | user_id = self.request.matchdict.get('user_id') | |||
|
310 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |||
|
311 | self._redirect_for_default_user(c.user.username) | |||
|
312 | ||||
|
313 | email_id = self.request.POST.get('del_email_id') | |||
|
314 | user_model = UserModel() | |||
|
315 | ||||
|
316 | email = UserEmailMap.query().get(email_id).email | |||
|
317 | user_data = c.user.get_api_data() | |||
|
318 | user_model.delete_extra_email(c.user.user_id, email_id) | |||
|
319 | audit_logger.store_web( | |||
|
320 | 'user.edit.email.delete', action_data={'email': email, 'user': user_data}, | |||
|
321 | user=self._rhodecode_user) | |||
|
322 | Session().commit() | |||
|
323 | h.flash(_("Removed email address from user account"), | |||
|
324 | category='success') | |||
|
325 | raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id)) | |||
|
326 | ||||
|
327 | @LoginRequired() | |||
|
328 | @HasPermissionAllDecorator('hg.admin') | |||
|
329 | @view_config( | |||
|
330 | route_name='edit_user_ips', request_method='GET', | |||
|
331 | renderer='rhodecode:templates/admin/users/user_edit.mako') | |||
|
332 | def ips(self): | |||
|
333 | _ = self.request.translate | |||
|
334 | c = self.load_default_context() | |||
|
335 | ||||
|
336 | user_id = self.request.matchdict.get('user_id') | |||
|
337 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |||
|
338 | self._redirect_for_default_user(c.user.username) | |||
|
339 | ||||
|
340 | c.active = 'ips' | |||
|
341 | c.user_ip_map = UserIpMap.query() \ | |||
|
342 | .filter(UserIpMap.user == c.user).all() | |||
|
343 | ||||
|
344 | c.inherit_default_ips = c.user.inherit_default_permissions | |||
|
345 | c.default_user_ip_map = UserIpMap.query() \ | |||
|
346 | .filter(UserIpMap.user == User.get_default_user()).all() | |||
|
347 | ||||
|
348 | return self._get_template_context(c) | |||
|
349 | ||||
|
350 | @LoginRequired() | |||
|
351 | @HasPermissionAllDecorator('hg.admin') | |||
|
352 | @CSRFRequired() | |||
|
353 | @view_config( | |||
|
354 | route_name='edit_user_ips_add', request_method='POST') | |||
|
355 | def ips_add(self): | |||
|
356 | _ = self.request.translate | |||
|
357 | c = self.load_default_context() | |||
|
358 | ||||
|
359 | user_id = self.request.matchdict.get('user_id') | |||
|
360 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |||
|
361 | # NOTE(marcink): this view is allowed for default users, as we can | |||
|
362 | # edit their IP white list | |||
|
363 | ||||
|
364 | user_model = UserModel() | |||
|
365 | desc = self.request.POST.get('description') | |||
|
366 | try: | |||
|
367 | ip_list = user_model.parse_ip_range( | |||
|
368 | self.request.POST.get('new_ip')) | |||
|
369 | except Exception as e: | |||
|
370 | ip_list = [] | |||
|
371 | log.exception("Exception during ip saving") | |||
|
372 | h.flash(_('An error occurred during ip saving:%s' % (e,)), | |||
|
373 | category='error') | |||
|
374 | added = [] | |||
|
375 | user_data = c.user.get_api_data() | |||
|
376 | for ip in ip_list: | |||
|
377 | try: | |||
|
378 | user_model.add_extra_ip(c.user.user_id, ip, desc) | |||
|
379 | audit_logger.store_web( | |||
|
380 | 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data}, | |||
|
381 | user=self._rhodecode_user) | |||
|
382 | Session().commit() | |||
|
383 | added.append(ip) | |||
|
384 | except formencode.Invalid as error: | |||
|
385 | msg = error.error_dict['ip'] | |||
|
386 | h.flash(msg, category='error') | |||
|
387 | except Exception: | |||
|
388 | log.exception("Exception during ip saving") | |||
|
389 | h.flash(_('An error occurred during ip saving'), | |||
|
390 | category='error') | |||
|
391 | if added: | |||
|
392 | h.flash( | |||
|
393 | _("Added ips %s to user whitelist") % (', '.join(ip_list), ), | |||
|
394 | category='success') | |||
|
395 | if 'default_user' in self.request.POST: | |||
|
396 | # case for editing global IP list we do it for 'DEFAULT' user | |||
|
397 | raise HTTPFound(h.route_path('admin_permissions_ips')) | |||
|
398 | raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id)) | |||
|
399 | ||||
|
400 | @LoginRequired() | |||
|
401 | @HasPermissionAllDecorator('hg.admin') | |||
|
402 | @CSRFRequired() | |||
|
403 | @view_config( | |||
|
404 | route_name='edit_user_ips_delete', request_method='POST') | |||
|
405 | def ips_delete(self): | |||
|
406 | _ = self.request.translate | |||
|
407 | c = self.load_default_context() | |||
|
408 | ||||
|
409 | user_id = self.request.matchdict.get('user_id') | |||
|
410 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |||
|
411 | # NOTE(marcink): this view is allowed for default users, as we can | |||
|
412 | # edit their IP white list | |||
|
413 | ||||
|
414 | ip_id = self.request.POST.get('del_ip_id') | |||
|
415 | user_model = UserModel() | |||
|
416 | user_data = c.user.get_api_data() | |||
|
417 | ip = UserIpMap.query().get(ip_id).ip_addr | |||
|
418 | user_model.delete_extra_ip(c.user.user_id, ip_id) | |||
|
419 | audit_logger.store_web( | |||
|
420 | 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data}, | |||
|
421 | user=self._rhodecode_user) | |||
|
422 | Session().commit() | |||
|
423 | h.flash(_("Removed ip address from user whitelist"), category='success') | |||
|
424 | ||||
|
425 | if 'default_user' in self.request.POST: | |||
|
426 | # case for editing global IP list we do it for 'DEFAULT' user | |||
|
427 | raise HTTPFound(h.route_path('admin_permissions_ips')) | |||
|
428 | raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id)) | |||
|
429 | ||||
|
430 | @LoginRequired() | |||
|
431 | @HasPermissionAllDecorator('hg.admin') | |||
|
432 | @view_config( | |||
258 | route_name='edit_user_groups_management', request_method='GET', |
|
433 | route_name='edit_user_groups_management', request_method='GET', | |
259 | renderer='rhodecode:templates/admin/users/user_edit.mako') |
|
434 | renderer='rhodecode:templates/admin/users/user_edit.mako') | |
260 | def groups_management(self): |
|
435 | def groups_management(self): | |
@@ -264,7 +439,8 b' class AdminUsersView(BaseAppView):' | |||||
264 | c.user = User.get_or_404(user_id, pyramid_exc=True) |
|
439 | c.user = User.get_or_404(user_id, pyramid_exc=True) | |
265 | c.data = c.user.group_member |
|
440 | c.data = c.user.group_member | |
266 | self._redirect_for_default_user(c.user.username) |
|
441 | self._redirect_for_default_user(c.user.username) | |
267 |
groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) |
|
442 | groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) | |
|
443 | for group in c.user.group_member] | |||
268 | c.groups = json.dumps(groups) |
|
444 | c.groups = json.dumps(groups) | |
269 | c.active = 'groups' |
|
445 | c.active = 'groups' | |
270 |
|
446 | |||
@@ -272,6 +448,7 b' class AdminUsersView(BaseAppView):' | |||||
272 |
|
448 | |||
273 | @LoginRequired() |
|
449 | @LoginRequired() | |
274 | @HasPermissionAllDecorator('hg.admin') |
|
450 | @HasPermissionAllDecorator('hg.admin') | |
|
451 | @CSRFRequired() | |||
275 | @view_config( |
|
452 | @view_config( | |
276 | route_name='edit_user_groups_management_updates', request_method='POST') |
|
453 | route_name='edit_user_groups_management_updates', request_method='POST') | |
277 | def groups_management_updates(self): |
|
454 | def groups_management_updates(self): | |
@@ -314,15 +491,15 b' class AdminUsersView(BaseAppView):' | |||||
314 | p = safe_int(self.request.GET.get('page', 1), 1) |
|
491 | p = safe_int(self.request.GET.get('page', 1), 1) | |
315 |
|
492 | |||
316 | filter_term = self.request.GET.get('filter') |
|
493 | filter_term = self.request.GET.get('filter') | |
317 |
|
|
494 | user_log = UserModel().get_user_log(c.user, filter_term) | |
318 |
|
495 | |||
319 | def url_generator(**kw): |
|
496 | def url_generator(**kw): | |
320 | if filter_term: |
|
497 | if filter_term: | |
321 | kw['filter'] = filter_term |
|
498 | kw['filter'] = filter_term | |
322 | return self.request.current_route_path(_query=kw) |
|
499 | return self.request.current_route_path(_query=kw) | |
323 |
|
500 | |||
324 | c.user_log = Page(c.user_log, page=p, items_per_page=10, |
|
501 | c.audit_logs = h.Page( | |
325 | url=url_generator) |
|
502 | user_log, page=p, items_per_page=10, url=url_generator) | |
326 | c.filter_term = filter_term |
|
503 | c.filter_term = filter_term | |
327 | return self._get_template_context(c) |
|
504 | return self._get_template_context(c) | |
328 |
|
505 |
@@ -30,8 +30,6 b' Channel Stream controller for rhodecode' | |||||
30 | import logging |
|
30 | import logging | |
31 | import uuid |
|
31 | import uuid | |
32 |
|
32 | |||
33 | from pylons import tmpl_context as c |
|
|||
34 | from pyramid.settings import asbool |
|
|||
35 | from pyramid.view import view_config |
|
33 | from pyramid.view import view_config | |
36 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway |
|
34 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway | |
37 |
|
35 | |||
@@ -46,7 +44,6 b' from rhodecode.lib.channelstream import ' | |||||
46 | update_history_from_logs, |
|
44 | update_history_from_logs, | |
47 | STATE_PUBLIC_KEYS) |
|
45 | STATE_PUBLIC_KEYS) | |
48 | from rhodecode.lib.auth import NotAnonymous |
|
46 | from rhodecode.lib.auth import NotAnonymous | |
49 | from rhodecode.lib.utils2 import str2bool |
|
|||
50 |
|
47 | |||
51 | log = logging.getLogger(__name__) |
|
48 | log = logging.getLogger(__name__) | |
52 |
|
49 | |||
@@ -82,7 +79,7 b' class ChannelstreamView(object):' | |||||
82 | log.error('Incorrect permissions for requested channels') |
|
79 | log.error('Incorrect permissions for requested channels') | |
83 | raise HTTPForbidden() |
|
80 | raise HTTPForbidden() | |
84 |
|
81 | |||
85 |
user = |
|
82 | user = self._rhodecode_user | |
86 | if user.user_id: |
|
83 | if user.user_id: | |
87 | user_data = get_user_data(user.user_id) |
|
84 | user_data = get_user_data(user.user_id) | |
88 | else: |
|
85 | else: | |
@@ -95,7 +92,7 b' class ChannelstreamView(object):' | |||||
95 | 'display_name': None, |
|
92 | 'display_name': None, | |
96 | 'display_link': None, |
|
93 | 'display_link': None, | |
97 | } |
|
94 | } | |
98 |
user_data['permissions'] = |
|
95 | user_data['permissions'] = self._rhodecode_user.permissions | |
99 | payload = { |
|
96 | payload = { | |
100 | 'username': user.username, |
|
97 | 'username': user.username, | |
101 | 'user_state': user_data, |
|
98 | 'user_state': user_data, |
@@ -18,31 +18,34 b'' | |||||
18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
21 | import json |
|
|||
22 |
|
21 | |||
23 | from mock import patch |
|
|||
24 | import pytest |
|
22 | import pytest | |
25 | from pylons import tmpl_context as c |
|
|||
26 |
|
23 | |||
27 | import rhodecode |
|
24 | import rhodecode | |
28 |
from rhodecode.l |
|
25 | from rhodecode.model.db import Repository | |
29 | from rhodecode.model.db import Repository, User, RepoGroup |
|
|||
30 | from rhodecode.model.meta import Session |
|
26 | from rhodecode.model.meta import Session | |
31 | from rhodecode.model.repo import RepoModel |
|
27 | from rhodecode.model.repo import RepoModel | |
32 | from rhodecode.model.repo_group import RepoGroupModel |
|
28 | from rhodecode.model.repo_group import RepoGroupModel | |
33 | from rhodecode.model.settings import SettingsModel |
|
29 | from rhodecode.model.settings import SettingsModel | |
34 |
from rhodecode.tests import TestController |
|
30 | from rhodecode.tests import TestController | |
35 | from rhodecode.tests.fixture import Fixture |
|
31 | from rhodecode.tests.fixture import Fixture | |
|
32 | from rhodecode.lib import helpers as h | |||
|
33 | ||||
|
34 | fixture = Fixture() | |||
36 |
|
35 | |||
37 |
|
36 | |||
38 | fixture = Fixture() |
|
37 | def route_path(name, **kwargs): | |
|
38 | return { | |||
|
39 | 'home': '/', | |||
|
40 | 'repo_group_home': '/{repo_group_name}' | |||
|
41 | }[name].format(**kwargs) | |||
39 |
|
42 | |||
40 |
|
43 | |||
41 | class TestHomeController(TestController): |
|
44 | class TestHomeController(TestController): | |
42 |
|
45 | |||
43 | def test_index(self): |
|
46 | def test_index(self): | |
44 | self.log_user() |
|
47 | self.log_user() | |
45 |
response = self.app.get( |
|
48 | response = self.app.get(route_path('home')) | |
46 | # if global permission is set |
|
49 | # if global permission is set | |
47 | response.mustcontain('Add Repository') |
|
50 | response.mustcontain('Add Repository') | |
48 |
|
51 | |||
@@ -51,8 +54,10 b' class TestHomeController(TestController)' | |||||
51 | response.mustcontain('"name_raw": "%s"' % repo.repo_name) |
|
54 | response.mustcontain('"name_raw": "%s"' % repo.repo_name) | |
52 |
|
55 | |||
53 | def test_index_contains_statics_with_ver(self): |
|
56 | def test_index_contains_statics_with_ver(self): | |
|
57 | from pylons import tmpl_context as c | |||
|
58 | ||||
54 | self.log_user() |
|
59 | self.log_user() | |
55 |
response = self.app.get( |
|
60 | response = self.app.get(route_path('home')) | |
56 |
|
61 | |||
57 | rhodecode_version_hash = c.rhodecode_version_hash |
|
62 | rhodecode_version_hash = c.rhodecode_version_hash | |
58 | response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash)) |
|
63 | response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash)) | |
@@ -60,7 +65,7 b' class TestHomeController(TestController)' | |||||
60 |
|
65 | |||
61 | def test_index_contains_backend_specific_details(self, backend): |
|
66 | def test_index_contains_backend_specific_details(self, backend): | |
62 | self.log_user() |
|
67 | self.log_user() | |
63 |
response = self.app.get( |
|
68 | response = self.app.get(route_path('home')) | |
64 | tip = backend.repo.get_commit().raw_id |
|
69 | tip = backend.repo.get_commit().raw_id | |
65 |
|
70 | |||
66 | # html in javascript variable: |
|
71 | # html in javascript variable: | |
@@ -72,17 +77,16 b' class TestHomeController(TestController)' | |||||
72 |
|
77 | |||
73 | def test_index_with_anonymous_access_disabled(self): |
|
78 | def test_index_with_anonymous_access_disabled(self): | |
74 | with fixture.anon_access(False): |
|
79 | with fixture.anon_access(False): | |
75 |
response = self.app.get( |
|
80 | response = self.app.get(route_path('home'), status=302) | |
76 | status=302) |
|
|||
77 | assert 'login' in response.location |
|
81 | assert 'login' in response.location | |
78 |
|
82 | |||
79 | def test_index_page_on_groups(self, autologin_user, repo_group): |
|
83 | def test_index_page_on_groups(self, autologin_user, repo_group): | |
80 |
response = self.app.get( |
|
84 | response = self.app.get(route_path('repo_group_home', repo_group_name='gr1')) | |
81 | response.mustcontain("gr1/repo_in_group") |
|
85 | response.mustcontain("gr1/repo_in_group") | |
82 |
|
86 | |||
83 | def test_index_page_on_group_with_trailing_slash( |
|
87 | def test_index_page_on_group_with_trailing_slash( | |
84 | self, autologin_user, repo_group): |
|
88 | self, autologin_user, repo_group): | |
85 |
response = self.app.get( |
|
89 | response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/') | |
86 | response.mustcontain("gr1/repo_in_group") |
|
90 | response.mustcontain("gr1/repo_in_group") | |
87 |
|
91 | |||
88 | @pytest.fixture(scope='class') |
|
92 | @pytest.fixture(scope='class') | |
@@ -96,21 +100,19 b' class TestHomeController(TestController)' | |||||
96 | RepoGroupModel().delete(repo_group='gr1', force_delete=True) |
|
100 | RepoGroupModel().delete(repo_group='gr1', force_delete=True) | |
97 | Session().commit() |
|
101 | Session().commit() | |
98 |
|
102 | |||
99 | def test_index_with_name_with_tags(self, autologin_user): |
|
103 | def test_index_with_name_with_tags(self, user_util, autologin_user): | |
100 |
user = |
|
104 | user = user_util.create_user() | |
|
105 | username = user.username | |||
101 | user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">' |
|
106 | user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">' | |
102 | user.lastname = ( |
|
107 | user.lastname = '#"><img src=x onerror=prompt(document.cookie);>' | |
103 | '<img src="/image2" onload="alert(\'Hello, World!\');">') |
|
108 | ||
104 | Session().add(user) |
|
109 | Session().add(user) | |
105 | Session().commit() |
|
110 | Session().commit() | |
|
111 | user_util.create_repo(owner=username) | |||
106 |
|
112 | |||
107 |
response = self.app.get( |
|
113 | response = self.app.get(route_path('home')) | |
108 | response.mustcontain( |
|
114 | response.mustcontain(h.html_escape(user.first_name)) | |
109 | '<img src="/image1" onload="' |
|
115 | response.mustcontain(h.html_escape(user.last_name)) | |
110 | 'alert('Hello, World!');">') |
|
|||
111 | response.mustcontain( |
|
|||
112 | '<img src="/image2" onload="' |
|
|||
113 | 'alert('Hello, World!');">') |
|
|||
114 |
|
116 | |||
115 | @pytest.mark.parametrize("name, state", [ |
|
117 | @pytest.mark.parametrize("name, state", [ | |
116 | ('Disabled', False), |
|
118 | ('Disabled', False), | |
@@ -125,266 +127,8 b' class TestHomeController(TestController)' | |||||
125 | Session().commit() |
|
127 | Session().commit() | |
126 | SettingsModel().invalidate_settings_cache() |
|
128 | SettingsModel().invalidate_settings_cache() | |
127 |
|
129 | |||
128 |
response = self.app.get( |
|
130 | response = self.app.get(route_path('home')) | |
129 | if state is True: |
|
131 | if state is True: | |
130 | response.mustcontain(version_string) |
|
132 | response.mustcontain(version_string) | |
131 | if state is False: |
|
133 | if state is False: | |
132 | response.mustcontain(no=[version_string]) |
|
134 | response.mustcontain(no=[version_string]) | |
133 |
|
||||
134 |
|
||||
135 | class TestUserAutocompleteData(TestController): |
|
|||
136 | def test_returns_list_of_users(self, user_util): |
|
|||
137 | self.log_user() |
|
|||
138 | user = user_util.create_user(is_active=True) |
|
|||
139 | user_name = user.username |
|
|||
140 | response = self.app.get( |
|
|||
141 | url(controller='home', action='user_autocomplete_data'), |
|
|||
142 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
|||
143 | result = json.loads(response.body) |
|
|||
144 | values = [suggestion['value'] for suggestion in result['suggestions']] |
|
|||
145 | assert user_name in values |
|
|||
146 |
|
||||
147 | def test_returns_inactive_users_when_active_flag_sent(self, user_util): |
|
|||
148 | self.log_user() |
|
|||
149 | user = user_util.create_user(is_active=False) |
|
|||
150 | user_name = user.username |
|
|||
151 | response = self.app.get( |
|
|||
152 | url(controller='home', action='user_autocomplete_data', |
|
|||
153 | user_groups='true', active='0'), |
|
|||
154 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
|||
155 | result = json.loads(response.body) |
|
|||
156 | values = [suggestion['value'] for suggestion in result['suggestions']] |
|
|||
157 | assert user_name in values |
|
|||
158 |
|
||||
159 | def test_returns_groups_when_user_groups_sent(self, user_util): |
|
|||
160 | self.log_user() |
|
|||
161 | group = user_util.create_user_group(user_groups_active=True) |
|
|||
162 | group_name = group.users_group_name |
|
|||
163 | response = self.app.get( |
|
|||
164 | url(controller='home', action='user_autocomplete_data', |
|
|||
165 | user_groups='true'), |
|
|||
166 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
|||
167 | result = json.loads(response.body) |
|
|||
168 | values = [suggestion['value'] for suggestion in result['suggestions']] |
|
|||
169 | assert group_name in values |
|
|||
170 |
|
||||
171 | def test_result_is_limited_when_query_is_sent(self): |
|
|||
172 | self.log_user() |
|
|||
173 | fake_result = [ |
|
|||
174 | { |
|
|||
175 | 'first_name': 'John', |
|
|||
176 | 'value_display': 'hello{} (John Smith)'.format(i), |
|
|||
177 | 'icon_link': '/images/user14.png', |
|
|||
178 | 'value': 'hello{}'.format(i), |
|
|||
179 | 'last_name': 'Smith', |
|
|||
180 | 'username': 'hello{}'.format(i), |
|
|||
181 | 'id': i, |
|
|||
182 | 'value_type': u'user' |
|
|||
183 | } |
|
|||
184 | for i in range(10) |
|
|||
185 | ] |
|
|||
186 | users_patcher = patch.object( |
|
|||
187 | RepoModel, 'get_users', return_value=fake_result) |
|
|||
188 | groups_patcher = patch.object( |
|
|||
189 | RepoModel, 'get_user_groups', return_value=fake_result) |
|
|||
190 |
|
||||
191 | query = 'hello' |
|
|||
192 | with users_patcher as users_mock, groups_patcher as groups_mock: |
|
|||
193 | response = self.app.get( |
|
|||
194 | url(controller='home', action='user_autocomplete_data', |
|
|||
195 | user_groups='true', query=query), |
|
|||
196 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
|||
197 |
|
||||
198 | result = json.loads(response.body) |
|
|||
199 | users_mock.assert_called_once_with( |
|
|||
200 | name_contains=query, only_active=True) |
|
|||
201 | groups_mock.assert_called_once_with( |
|
|||
202 | name_contains=query, only_active=True) |
|
|||
203 | assert len(result['suggestions']) == 20 |
|
|||
204 |
|
||||
205 |
|
||||
206 | def assert_and_get_content(result): |
|
|||
207 | repos = [] |
|
|||
208 | groups = [] |
|
|||
209 | commits = [] |
|
|||
210 | for data in result: |
|
|||
211 | for data_item in data['children']: |
|
|||
212 | assert data_item['id'] |
|
|||
213 | assert data_item['text'] |
|
|||
214 | assert data_item['url'] |
|
|||
215 | if data_item['type'] == 'repo': |
|
|||
216 | repos.append(data_item) |
|
|||
217 | elif data_item['type'] == 'group': |
|
|||
218 | groups.append(data_item) |
|
|||
219 | elif data_item['type'] == 'commit': |
|
|||
220 | commits.append(data_item) |
|
|||
221 | else: |
|
|||
222 | raise Exception('invalid type %s' % data_item['type']) |
|
|||
223 |
|
||||
224 | return repos, groups, commits |
|
|||
225 |
|
||||
226 |
|
||||
227 | class TestGotoSwitcherData(TestController): |
|
|||
228 | required_repos_with_groups = [ |
|
|||
229 | 'abc', |
|
|||
230 | 'abc-fork', |
|
|||
231 | 'forks/abcd', |
|
|||
232 | 'abcd', |
|
|||
233 | 'abcde', |
|
|||
234 | 'a/abc', |
|
|||
235 | 'aa/abc', |
|
|||
236 | 'aaa/abc', |
|
|||
237 | 'aaaa/abc', |
|
|||
238 | 'repos_abc/aaa/abc', |
|
|||
239 | 'abc_repos/abc', |
|
|||
240 | 'abc_repos/abcd', |
|
|||
241 | 'xxx/xyz', |
|
|||
242 | 'forked-abc/a/abc' |
|
|||
243 | ] |
|
|||
244 |
|
||||
245 | @pytest.fixture(autouse=True, scope='class') |
|
|||
246 | def prepare(self, request, pylonsapp): |
|
|||
247 | for repo_and_group in self.required_repos_with_groups: |
|
|||
248 | # create structure of groups and return the last group |
|
|||
249 |
|
||||
250 | repo_group = map_groups(repo_and_group) |
|
|||
251 |
|
||||
252 | RepoModel()._create_repo( |
|
|||
253 | repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN, |
|
|||
254 | repo_group=getattr(repo_group, 'group_id', None)) |
|
|||
255 |
|
||||
256 | Session().commit() |
|
|||
257 |
|
||||
258 | request.addfinalizer(self.cleanup) |
|
|||
259 |
|
||||
260 | def cleanup(self): |
|
|||
261 | # first delete all repos |
|
|||
262 | for repo_and_groups in self.required_repos_with_groups: |
|
|||
263 | repo = Repository.get_by_repo_name(repo_and_groups) |
|
|||
264 | if repo: |
|
|||
265 | RepoModel().delete(repo) |
|
|||
266 | Session().commit() |
|
|||
267 |
|
||||
268 | # then delete all empty groups |
|
|||
269 | for repo_and_groups in self.required_repos_with_groups: |
|
|||
270 | if '/' in repo_and_groups: |
|
|||
271 | r_group = repo_and_groups.rsplit('/', 1)[0] |
|
|||
272 | repo_group = RepoGroup.get_by_group_name(r_group) |
|
|||
273 | if not repo_group: |
|
|||
274 | continue |
|
|||
275 | parents = repo_group.parents |
|
|||
276 | RepoGroupModel().delete(repo_group, force_delete=True) |
|
|||
277 | Session().commit() |
|
|||
278 |
|
||||
279 | for el in reversed(parents): |
|
|||
280 | RepoGroupModel().delete(el, force_delete=True) |
|
|||
281 | Session().commit() |
|
|||
282 |
|
||||
283 | def test_returns_list_of_repos_and_groups(self): |
|
|||
284 | self.log_user() |
|
|||
285 |
|
||||
286 | response = self.app.get( |
|
|||
287 | url(controller='home', action='goto_switcher_data'), |
|
|||
288 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
|||
289 | result = json.loads(response.body)['results'] |
|
|||
290 |
|
||||
291 | repos, groups, commits = assert_and_get_content(result) |
|
|||
292 |
|
||||
293 | assert len(repos) == len(Repository.get_all()) |
|
|||
294 | assert len(groups) == len(RepoGroup.get_all()) |
|
|||
295 | assert len(commits) == 0 |
|
|||
296 |
|
||||
297 | def test_returns_list_of_repos_and_groups_filtered(self): |
|
|||
298 | self.log_user() |
|
|||
299 |
|
||||
300 | response = self.app.get( |
|
|||
301 | url(controller='home', action='goto_switcher_data'), |
|
|||
302 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
|||
303 | params={'query': 'abc'}, status=200) |
|
|||
304 | result = json.loads(response.body)['results'] |
|
|||
305 |
|
||||
306 | repos, groups, commits = assert_and_get_content(result) |
|
|||
307 |
|
||||
308 | assert len(repos) == 13 |
|
|||
309 | assert len(groups) == 5 |
|
|||
310 | assert len(commits) == 0 |
|
|||
311 |
|
||||
312 | def test_returns_list_of_properly_sorted_and_filtered(self): |
|
|||
313 | self.log_user() |
|
|||
314 |
|
||||
315 | response = self.app.get( |
|
|||
316 | url(controller='home', action='goto_switcher_data'), |
|
|||
317 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
|||
318 | params={'query': 'abc'}, status=200) |
|
|||
319 | result = json.loads(response.body)['results'] |
|
|||
320 |
|
||||
321 | repos, groups, commits = assert_and_get_content(result) |
|
|||
322 |
|
||||
323 | test_repos = [x['text'] for x in repos[:4]] |
|
|||
324 | assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos |
|
|||
325 |
|
||||
326 | test_groups = [x['text'] for x in groups[:4]] |
|
|||
327 | assert ['abc_repos', 'repos_abc', |
|
|||
328 | 'forked-abc', 'forked-abc/a'] == test_groups |
|
|||
329 |
|
||||
330 |
|
||||
331 | class TestRepoListData(TestController): |
|
|||
332 | def test_returns_list_of_repos_and_groups(self, user_util): |
|
|||
333 | self.log_user() |
|
|||
334 |
|
||||
335 | response = self.app.get( |
|
|||
336 | url(controller='home', action='repo_list_data'), |
|
|||
337 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
|||
338 | result = json.loads(response.body)['results'] |
|
|||
339 |
|
||||
340 | repos, groups, commits = assert_and_get_content(result) |
|
|||
341 |
|
||||
342 | assert len(repos) == len(Repository.get_all()) |
|
|||
343 | assert len(groups) == 0 |
|
|||
344 | assert len(commits) == 0 |
|
|||
345 |
|
||||
346 | def test_returns_list_of_repos_and_groups_filtered(self): |
|
|||
347 | self.log_user() |
|
|||
348 |
|
||||
349 | response = self.app.get( |
|
|||
350 | url(controller='home', action='repo_list_data'), |
|
|||
351 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
|||
352 | params={'query': 'vcs_test_git'}, status=200) |
|
|||
353 | result = json.loads(response.body)['results'] |
|
|||
354 |
|
||||
355 | repos, groups, commits = assert_and_get_content(result) |
|
|||
356 |
|
||||
357 | assert len(repos) == len(Repository.query().filter( |
|
|||
358 | Repository.repo_name.ilike('%vcs_test_git%')).all()) |
|
|||
359 | assert len(groups) == 0 |
|
|||
360 | assert len(commits) == 0 |
|
|||
361 |
|
||||
362 | def test_returns_list_of_repos_and_groups_filtered_with_type(self): |
|
|||
363 | self.log_user() |
|
|||
364 |
|
||||
365 | response = self.app.get( |
|
|||
366 | url(controller='home', action='repo_list_data'), |
|
|||
367 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
|||
368 | params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200) |
|
|||
369 | result = json.loads(response.body)['results'] |
|
|||
370 |
|
||||
371 | repos, groups, commits = assert_and_get_content(result) |
|
|||
372 |
|
||||
373 | assert len(repos) == len(Repository.query().filter( |
|
|||
374 | Repository.repo_name.ilike('%vcs_test_git%')).all()) |
|
|||
375 | assert len(groups) == 0 |
|
|||
376 | assert len(commits) == 0 |
|
|||
377 |
|
||||
378 | def test_returns_list_of_repos_non_ascii_query(self): |
|
|||
379 | self.log_user() |
|
|||
380 | response = self.app.get( |
|
|||
381 | url(controller='home', action='repo_list_data'), |
|
|||
382 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
|||
383 | params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200) |
|
|||
384 | result = json.loads(response.body)['results'] |
|
|||
385 |
|
||||
386 | repos, groups, commits = assert_and_get_content(result) |
|
|||
387 |
|
||||
388 | assert len(repos) == 0 |
|
|||
389 | assert len(groups) == 0 |
|
|||
390 | assert len(commits) == 0 |
|
@@ -25,7 +25,6 b' import formencode' | |||||
25 | import logging |
|
25 | import logging | |
26 | import urlparse |
|
26 | import urlparse | |
27 |
|
27 | |||
28 | from pylons import url |
|
|||
29 | from pyramid.httpexceptions import HTTPFound |
|
28 | from pyramid.httpexceptions import HTTPFound | |
30 | from pyramid.view import view_config |
|
29 | from pyramid.view import view_config | |
31 | from recaptcha.client.captcha import submit |
|
30 | from recaptcha.client.captcha import submit | |
@@ -34,6 +33,7 b' from rhodecode.apps._base import BaseApp' | |||||
34 | from rhodecode.authentication.base import authenticate, HTTP_TYPE |
|
33 | from rhodecode.authentication.base import authenticate, HTTP_TYPE | |
35 | from rhodecode.events import UserRegistered |
|
34 | from rhodecode.events import UserRegistered | |
36 | from rhodecode.lib import helpers as h |
|
35 | from rhodecode.lib import helpers as h | |
|
36 | from rhodecode.lib import audit_logger | |||
37 | from rhodecode.lib.auth import ( |
|
37 | from rhodecode.lib.auth import ( | |
38 | AuthUser, HasPermissionAnyDecorator, CSRFRequired) |
|
38 | AuthUser, HasPermissionAnyDecorator, CSRFRequired) | |
39 | from rhodecode.lib.base import get_ip_addr |
|
39 | from rhodecode.lib.base import get_ip_addr | |
@@ -90,20 +90,21 b' def get_came_from(request):' | |||||
90 | came_from = safe_str(request.GET.get('came_from', '')) |
|
90 | came_from = safe_str(request.GET.get('came_from', '')) | |
91 | parsed = urlparse.urlparse(came_from) |
|
91 | parsed = urlparse.urlparse(came_from) | |
92 | allowed_schemes = ['http', 'https'] |
|
92 | allowed_schemes = ['http', 'https'] | |
|
93 | default_came_from = h.route_path('home') | |||
93 | if parsed.scheme and parsed.scheme not in allowed_schemes: |
|
94 | if parsed.scheme and parsed.scheme not in allowed_schemes: | |
94 | log.error('Suspicious URL scheme detected %s for url %s' % |
|
95 | log.error('Suspicious URL scheme detected %s for url %s' % | |
95 | (parsed.scheme, parsed)) |
|
96 | (parsed.scheme, parsed)) | |
96 |
came_from = |
|
97 | came_from = default_came_from | |
97 | elif parsed.netloc and request.host != parsed.netloc: |
|
98 | elif parsed.netloc and request.host != parsed.netloc: | |
98 | log.error('Suspicious NETLOC detected %s for url %s server url ' |
|
99 | log.error('Suspicious NETLOC detected %s for url %s server url ' | |
99 | 'is: %s' % (parsed.netloc, parsed, request.host)) |
|
100 | 'is: %s' % (parsed.netloc, parsed, request.host)) | |
100 |
came_from = |
|
101 | came_from = default_came_from | |
101 | elif any(bad_str in parsed.path for bad_str in ('\r', '\n')): |
|
102 | elif any(bad_str in parsed.path for bad_str in ('\r', '\n')): | |
102 | log.error('Header injection detected `%s` for url %s server url ' % |
|
103 | log.error('Header injection detected `%s` for url %s server url ' % | |
103 | (parsed.path, parsed)) |
|
104 | (parsed.path, parsed)) | |
104 |
came_from = |
|
105 | came_from = default_came_from | |
105 |
|
106 | |||
106 |
return came_from or |
|
107 | return came_from or default_came_from | |
107 |
|
108 | |||
108 |
|
109 | |||
109 | class LoginView(BaseAppView): |
|
110 | class LoginView(BaseAppView): | |
@@ -166,6 +167,15 b' class LoginView(BaseAppView):' | |||||
166 | username=form_result['username'], |
|
167 | username=form_result['username'], | |
167 | remember=form_result['remember']) |
|
168 | remember=form_result['remember']) | |
168 | log.debug('Redirecting to "%s" after login.', c.came_from) |
|
169 | log.debug('Redirecting to "%s" after login.', c.came_from) | |
|
170 | ||||
|
171 | audit_user = audit_logger.UserWrap( | |||
|
172 | username=self.request.params.get('username'), | |||
|
173 | ip_addr=self.request.remote_addr) | |||
|
174 | action_data = {'user_agent': self.request.user_agent} | |||
|
175 | audit_logger.store_web( | |||
|
176 | 'user.login.success', action_data=action_data, | |||
|
177 | user=audit_user, commit=True) | |||
|
178 | ||||
169 | raise HTTPFound(c.came_from, headers=headers) |
|
179 | raise HTTPFound(c.came_from, headers=headers) | |
170 | except formencode.Invalid as errors: |
|
180 | except formencode.Invalid as errors: | |
171 | defaults = errors.value |
|
181 | defaults = errors.value | |
@@ -176,6 +186,14 b' class LoginView(BaseAppView):' | |||||
176 | 'errors': errors.error_dict, |
|
186 | 'errors': errors.error_dict, | |
177 | 'defaults': defaults, |
|
187 | 'defaults': defaults, | |
178 | }) |
|
188 | }) | |
|
189 | ||||
|
190 | audit_user = audit_logger.UserWrap( | |||
|
191 | username=self.request.params.get('username'), | |||
|
192 | ip_addr=self.request.remote_addr) | |||
|
193 | action_data = {'user_agent': self.request.user_agent} | |||
|
194 | audit_logger.store_web( | |||
|
195 | 'user.login.failure', action_data=action_data, | |||
|
196 | user=audit_user, commit=True) | |||
179 | return render_ctx |
|
197 | return render_ctx | |
180 |
|
198 | |||
181 | except UserCreationError as e: |
|
199 | except UserCreationError as e: | |
@@ -191,8 +209,13 b' class LoginView(BaseAppView):' | |||||
191 | def logout(self): |
|
209 | def logout(self): | |
192 | auth_user = self._rhodecode_user |
|
210 | auth_user = self._rhodecode_user | |
193 | log.info('Deleting session for user: `%s`', auth_user) |
|
211 | log.info('Deleting session for user: `%s`', auth_user) | |
|
212 | ||||
|
213 | action_data = {'user_agent': self.request.user_agent} | |||
|
214 | audit_logger.store_web( | |||
|
215 | 'user.logout', action_data=action_data, | |||
|
216 | user=auth_user, commit=True) | |||
194 | self.session.delete() |
|
217 | self.session.delete() | |
195 |
return HTTPFound( |
|
218 | return HTTPFound(h.route_path('home')) | |
196 |
|
219 | |||
197 | @HasPermissionAnyDecorator( |
|
220 | @HasPermissionAnyDecorator( | |
198 | 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') |
|
221 | 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') | |
@@ -338,6 +361,12 b' class LoginView(BaseAppView):' | |||||
338 | form_result, password_reset_url) |
|
361 | form_result, password_reset_url) | |
339 | # Display success message and redirect. |
|
362 | # Display success message and redirect. | |
340 | self.session.flash(msg, queue='success') |
|
363 | self.session.flash(msg, queue='success') | |
|
364 | ||||
|
365 | action_data = {'email': user_email, | |||
|
366 | 'user_agent': self.request.user_agent} | |||
|
367 | audit_logger.store_web( | |||
|
368 | 'user.password.reset_request', action_data=action_data, | |||
|
369 | user=self._rhodecode_user, commit=True) | |||
341 | return HTTPFound(self.request.route_path('reset_password')) |
|
370 | return HTTPFound(self.request.route_path('reset_password')) | |
342 |
|
371 | |||
343 | except formencode.Invalid as errors: |
|
372 | except formencode.Invalid as errors: |
@@ -41,12 +41,45 b' def includeme(config):' | |||||
41 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens') |
|
41 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens') | |
42 | config.add_route( |
|
42 | config.add_route( | |
43 | name='my_account_auth_tokens_add', |
|
43 | name='my_account_auth_tokens_add', | |
44 |
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new' |
|
44 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new') | |
45 | ) |
|
|||
46 | config.add_route( |
|
45 | config.add_route( | |
47 | name='my_account_auth_tokens_delete', |
|
46 | name='my_account_auth_tokens_delete', | |
48 |
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete' |
|
47 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete') | |
49 | ) |
|
48 | ||
|
49 | config.add_route( | |||
|
50 | name='my_account_emails', | |||
|
51 | pattern=ADMIN_PREFIX + '/my_account/emails') | |||
|
52 | config.add_route( | |||
|
53 | name='my_account_emails_add', | |||
|
54 | pattern=ADMIN_PREFIX + '/my_account/emails/new') | |||
|
55 | config.add_route( | |||
|
56 | name='my_account_emails_delete', | |||
|
57 | pattern=ADMIN_PREFIX + '/my_account/emails/delete') | |||
|
58 | ||||
|
59 | config.add_route( | |||
|
60 | name='my_account_repos', | |||
|
61 | pattern=ADMIN_PREFIX + '/my_account/repos') | |||
|
62 | ||||
|
63 | config.add_route( | |||
|
64 | name='my_account_watched', | |||
|
65 | pattern=ADMIN_PREFIX + '/my_account/watched') | |||
|
66 | ||||
|
67 | config.add_route( | |||
|
68 | name='my_account_perms', | |||
|
69 | pattern=ADMIN_PREFIX + '/my_account/perms') | |||
|
70 | ||||
|
71 | config.add_route( | |||
|
72 | name='my_account_notifications', | |||
|
73 | pattern=ADMIN_PREFIX + '/my_account/notifications') | |||
|
74 | ||||
|
75 | config.add_route( | |||
|
76 | name='my_account_notifications_toggle_visibility', | |||
|
77 | pattern=ADMIN_PREFIX + '/my_account/toggle_visibility') | |||
|
78 | ||||
|
79 | # channelstream test | |||
|
80 | config.add_route( | |||
|
81 | name='my_account_notifications_test_channelstream', | |||
|
82 | pattern=ADMIN_PREFIX + '/my_account/test_channelstream') | |||
50 |
|
83 | |||
51 | # Scan module for configuration decorators. |
|
84 | # Scan module for configuration decorators. | |
52 | config.scan() |
|
85 | config.scan() |
@@ -103,7 +103,7 b' class TestMyAccountAuthTokens(TestContro' | |||||
103 |
|
103 | |||
104 | response = self.app.post( |
|
104 | response = self.app.post( | |
105 | route_path('my_account_auth_tokens_delete'), |
|
105 | route_path('my_account_auth_tokens_delete'), | |
106 | {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token}) |
|
106 | {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token}) | |
107 | assert_session_flash(response, 'Auth token successfully deleted') |
|
107 | assert_session_flash(response, 'Auth token successfully deleted') | |
108 |
|
108 | |||
109 | user = User.get(user_id) |
|
109 | user = User.get(user_id) |
@@ -132,6 +132,7 b' class TestMyAccountPassword(TestControll' | |||||
132 | self.app.post(route_path('my_account_password'), form_data) |
|
132 | self.app.post(route_path('my_account_password'), form_data) | |
133 |
|
133 | |||
134 | response = self.app.get(route_path('home')) |
|
134 | response = self.app.get(route_path('home')) | |
135 | new_password_hash = response.session['rhodecode_user']['password'] |
|
135 | session = response.get_session_from_response() | |
|
136 | new_password_hash = session['rhodecode_user']['password'] | |||
136 |
|
137 | |||
137 | assert old_password_hash != new_password_hash No newline at end of file |
|
138 | assert old_password_hash != new_password_hash |
@@ -19,18 +19,28 b'' | |||||
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
21 | import logging |
|
21 | import logging | |
|
22 | import datetime | |||
22 |
|
23 | |||
|
24 | import formencode | |||
23 | from pyramid.httpexceptions import HTTPFound |
|
25 | from pyramid.httpexceptions import HTTPFound | |
24 | from pyramid.view import view_config |
|
26 | from pyramid.view import view_config | |
25 |
|
27 | |||
26 | from rhodecode.apps._base import BaseAppView |
|
28 | from rhodecode.apps._base import BaseAppView | |
27 | from rhodecode import forms |
|
29 | from rhodecode import forms | |
|
30 | from rhodecode.lib import helpers as h | |||
|
31 | from rhodecode.lib import audit_logger | |||
|
32 | from rhodecode.lib.ext_json import json | |||
28 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired |
|
33 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired | |
29 |
from rhodecode.lib import |
|
34 | from rhodecode.lib.channelstream import channelstream_request, \ | |
|
35 | ChannelstreamException | |||
30 | from rhodecode.lib.utils2 import safe_int, md5 |
|
36 | from rhodecode.lib.utils2 import safe_int, md5 | |
31 | from rhodecode.model.auth_token import AuthTokenModel |
|
37 | from rhodecode.model.auth_token import AuthTokenModel | |
|
38 | from rhodecode.model.db import ( | |||
|
39 | Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload) | |||
32 | from rhodecode.model.meta import Session |
|
40 | from rhodecode.model.meta import Session | |
|
41 | from rhodecode.model.scm import RepoList | |||
33 | from rhodecode.model.user import UserModel |
|
42 | from rhodecode.model.user import UserModel | |
|
43 | from rhodecode.model.repo import RepoModel | |||
34 | from rhodecode.model.validation_schema.schemas import user_schema |
|
44 | from rhodecode.model.validation_schema.schemas import user_schema | |
35 |
|
45 | |||
36 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
@@ -158,7 +168,7 b' class MyAccountView(BaseAppView):' | |||||
158 | @NotAnonymous() |
|
168 | @NotAnonymous() | |
159 | @CSRFRequired() |
|
169 | @CSRFRequired() | |
160 | @view_config( |
|
170 | @view_config( | |
161 | route_name='my_account_auth_tokens_add', request_method='POST') |
|
171 | route_name='my_account_auth_tokens_add', request_method='POST',) | |
162 | def my_account_auth_tokens_add(self): |
|
172 | def my_account_auth_tokens_add(self): | |
163 | _ = self.request.translate |
|
173 | _ = self.request.translate | |
164 | c = self.load_default_context() |
|
174 | c = self.load_default_context() | |
@@ -169,7 +179,13 b' class MyAccountView(BaseAppView):' | |||||
169 |
|
179 | |||
170 | token = AuthTokenModel().create( |
|
180 | token = AuthTokenModel().create( | |
171 | c.user.user_id, description, lifetime, role) |
|
181 | c.user.user_id, description, lifetime, role) | |
|
182 | token_data = token.get_api_data() | |||
|
183 | ||||
172 | self.maybe_attach_token_scope(token) |
|
184 | self.maybe_attach_token_scope(token) | |
|
185 | audit_logger.store_web( | |||
|
186 | 'user.edit.token.add', action_data={ | |||
|
187 | 'data': {'token': token_data, 'user': 'self'}}, | |||
|
188 | user=self._rhodecode_user, ) | |||
173 | Session().commit() |
|
189 | Session().commit() | |
174 |
|
190 | |||
175 | h.flash(_("Auth token successfully created"), category='success') |
|
191 | h.flash(_("Auth token successfully created"), category='success') | |
@@ -187,8 +203,197 b' class MyAccountView(BaseAppView):' | |||||
187 | del_auth_token = self.request.POST.get('del_auth_token') |
|
203 | del_auth_token = self.request.POST.get('del_auth_token') | |
188 |
|
204 | |||
189 | if del_auth_token: |
|
205 | if del_auth_token: | |
|
206 | token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True) | |||
|
207 | token_data = token.get_api_data() | |||
|
208 | ||||
190 | AuthTokenModel().delete(del_auth_token, c.user.user_id) |
|
209 | AuthTokenModel().delete(del_auth_token, c.user.user_id) | |
|
210 | audit_logger.store_web( | |||
|
211 | 'user.edit.token.delete', action_data={ | |||
|
212 | 'data': {'token': token_data, 'user': 'self'}}, | |||
|
213 | user=self._rhodecode_user,) | |||
191 | Session().commit() |
|
214 | Session().commit() | |
192 | h.flash(_("Auth token successfully deleted"), category='success') |
|
215 | h.flash(_("Auth token successfully deleted"), category='success') | |
193 |
|
216 | |||
194 | return HTTPFound(h.route_path('my_account_auth_tokens')) |
|
217 | return HTTPFound(h.route_path('my_account_auth_tokens')) | |
|
218 | ||||
|
219 | @LoginRequired() | |||
|
220 | @NotAnonymous() | |||
|
221 | @view_config( | |||
|
222 | route_name='my_account_emails', request_method='GET', | |||
|
223 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |||
|
224 | def my_account_emails(self): | |||
|
225 | _ = self.request.translate | |||
|
226 | ||||
|
227 | c = self.load_default_context() | |||
|
228 | c.active = 'emails' | |||
|
229 | ||||
|
230 | c.user_email_map = UserEmailMap.query()\ | |||
|
231 | .filter(UserEmailMap.user == c.user).all() | |||
|
232 | return self._get_template_context(c) | |||
|
233 | ||||
|
234 | @LoginRequired() | |||
|
235 | @NotAnonymous() | |||
|
236 | @CSRFRequired() | |||
|
237 | @view_config( | |||
|
238 | route_name='my_account_emails_add', request_method='POST') | |||
|
239 | def my_account_emails_add(self): | |||
|
240 | _ = self.request.translate | |||
|
241 | c = self.load_default_context() | |||
|
242 | ||||
|
243 | email = self.request.POST.get('new_email') | |||
|
244 | ||||
|
245 | try: | |||
|
246 | UserModel().add_extra_email(c.user.user_id, email) | |||
|
247 | audit_logger.store_web( | |||
|
248 | 'user.edit.email.add', action_data={ | |||
|
249 | 'data': {'email': email, 'user': 'self'}}, | |||
|
250 | user=self._rhodecode_user,) | |||
|
251 | ||||
|
252 | Session().commit() | |||
|
253 | h.flash(_("Added new email address `%s` for user account") % email, | |||
|
254 | category='success') | |||
|
255 | except formencode.Invalid as error: | |||
|
256 | h.flash(h.escape(error.error_dict['email']), category='error') | |||
|
257 | except Exception: | |||
|
258 | log.exception("Exception in my_account_emails") | |||
|
259 | h.flash(_('An error occurred during email saving'), | |||
|
260 | category='error') | |||
|
261 | return HTTPFound(h.route_path('my_account_emails')) | |||
|
262 | ||||
|
263 | @LoginRequired() | |||
|
264 | @NotAnonymous() | |||
|
265 | @CSRFRequired() | |||
|
266 | @view_config( | |||
|
267 | route_name='my_account_emails_delete', request_method='POST') | |||
|
268 | def my_account_emails_delete(self): | |||
|
269 | _ = self.request.translate | |||
|
270 | c = self.load_default_context() | |||
|
271 | ||||
|
272 | del_email_id = self.request.POST.get('del_email_id') | |||
|
273 | if del_email_id: | |||
|
274 | email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email | |||
|
275 | UserModel().delete_extra_email(c.user.user_id, del_email_id) | |||
|
276 | audit_logger.store_web( | |||
|
277 | 'user.edit.email.delete', action_data={ | |||
|
278 | 'data': {'email': email, 'user': 'self'}}, | |||
|
279 | user=self._rhodecode_user,) | |||
|
280 | Session().commit() | |||
|
281 | h.flash(_("Email successfully deleted"), | |||
|
282 | category='success') | |||
|
283 | return HTTPFound(h.route_path('my_account_emails')) | |||
|
284 | ||||
|
285 | @LoginRequired() | |||
|
286 | @NotAnonymous() | |||
|
287 | @CSRFRequired() | |||
|
288 | @view_config( | |||
|
289 | route_name='my_account_notifications_test_channelstream', | |||
|
290 | request_method='POST', renderer='json_ext') | |||
|
291 | def my_account_notifications_test_channelstream(self): | |||
|
292 | message = 'Test message sent via Channelstream by user: {}, on {}'.format( | |||
|
293 | self._rhodecode_user.username, datetime.datetime.now()) | |||
|
294 | payload = { | |||
|
295 | # 'channel': 'broadcast', | |||
|
296 | 'type': 'message', | |||
|
297 | 'timestamp': datetime.datetime.utcnow(), | |||
|
298 | 'user': 'system', | |||
|
299 | 'pm_users': [self._rhodecode_user.username], | |||
|
300 | 'message': { | |||
|
301 | 'message': message, | |||
|
302 | 'level': 'info', | |||
|
303 | 'topic': '/notifications' | |||
|
304 | } | |||
|
305 | } | |||
|
306 | ||||
|
307 | registry = self.request.registry | |||
|
308 | rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) | |||
|
309 | channelstream_config = rhodecode_plugins.get('channelstream', {}) | |||
|
310 | ||||
|
311 | try: | |||
|
312 | channelstream_request(channelstream_config, [payload], '/message') | |||
|
313 | except ChannelstreamException as e: | |||
|
314 | log.exception('Failed to send channelstream data') | |||
|
315 | return {"response": 'ERROR: {}'.format(e.__class__.__name__)} | |||
|
316 | return {"response": 'Channelstream data sent. ' | |||
|
317 | 'You should see a new live message now.'} | |||
|
318 | ||||
|
319 | def _load_my_repos_data(self, watched=False): | |||
|
320 | if watched: | |||
|
321 | admin = False | |||
|
322 | follows_repos = Session().query(UserFollowing)\ | |||
|
323 | .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ | |||
|
324 | .options(joinedload(UserFollowing.follows_repository))\ | |||
|
325 | .all() | |||
|
326 | repo_list = [x.follows_repository for x in follows_repos] | |||
|
327 | else: | |||
|
328 | admin = True | |||
|
329 | repo_list = Repository.get_all_repos( | |||
|
330 | user_id=self._rhodecode_user.user_id) | |||
|
331 | repo_list = RepoList(repo_list, perm_set=[ | |||
|
332 | 'repository.read', 'repository.write', 'repository.admin']) | |||
|
333 | ||||
|
334 | repos_data = RepoModel().get_repos_as_dict( | |||
|
335 | repo_list=repo_list, admin=admin) | |||
|
336 | # json used to render the grid | |||
|
337 | return json.dumps(repos_data) | |||
|
338 | ||||
|
339 | @LoginRequired() | |||
|
340 | @NotAnonymous() | |||
|
341 | @view_config( | |||
|
342 | route_name='my_account_repos', request_method='GET', | |||
|
343 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |||
|
344 | def my_account_repos(self): | |||
|
345 | c = self.load_default_context() | |||
|
346 | c.active = 'repos' | |||
|
347 | ||||
|
348 | # json used to render the grid | |||
|
349 | c.data = self._load_my_repos_data() | |||
|
350 | return self._get_template_context(c) | |||
|
351 | ||||
|
352 | @LoginRequired() | |||
|
353 | @NotAnonymous() | |||
|
354 | @view_config( | |||
|
355 | route_name='my_account_watched', request_method='GET', | |||
|
356 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |||
|
357 | def my_account_watched(self): | |||
|
358 | c = self.load_default_context() | |||
|
359 | c.active = 'watched' | |||
|
360 | ||||
|
361 | # json used to render the grid | |||
|
362 | c.data = self._load_my_repos_data(watched=True) | |||
|
363 | return self._get_template_context(c) | |||
|
364 | ||||
|
365 | @LoginRequired() | |||
|
366 | @NotAnonymous() | |||
|
367 | @view_config( | |||
|
368 | route_name='my_account_perms', request_method='GET', | |||
|
369 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |||
|
370 | def my_account_perms(self): | |||
|
371 | c = self.load_default_context() | |||
|
372 | c.active = 'perms' | |||
|
373 | ||||
|
374 | c.perm_user = c.auth_user | |||
|
375 | return self._get_template_context(c) | |||
|
376 | ||||
|
377 | @LoginRequired() | |||
|
378 | @NotAnonymous() | |||
|
379 | @view_config( | |||
|
380 | route_name='my_account_notifications', request_method='GET', | |||
|
381 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |||
|
382 | def my_notifications(self): | |||
|
383 | c = self.load_default_context() | |||
|
384 | c.active = 'notifications' | |||
|
385 | ||||
|
386 | return self._get_template_context(c) | |||
|
387 | ||||
|
388 | @LoginRequired() | |||
|
389 | @NotAnonymous() | |||
|
390 | @CSRFRequired() | |||
|
391 | @view_config( | |||
|
392 | route_name='my_account_notifications_toggle_visibility', | |||
|
393 | request_method='POST', renderer='json_ext') | |||
|
394 | def my_notifications_toggle_visibility(self): | |||
|
395 | user = self._rhodecode_db_user | |||
|
396 | new_status = not user.user_data.get('notification_status', True) | |||
|
397 | user.update_userdata(notification_status=new_status) | |||
|
398 | Session().commit() | |||
|
399 | return user.user_data['notification_status'] No newline at end of file |
@@ -17,30 +17,137 b'' | |||||
17 | # This program is dual-licensed. If you wish to learn more about the |
|
17 | # This program is dual-licensed. If you wish to learn more about the | |
18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
20 | from rhodecode.apps._base import add_route_with_slash | |||
20 |
|
21 | |||
21 |
|
22 | |||
22 | def includeme(config): |
|
23 | def includeme(config): | |
23 |
|
24 | |||
|
25 | # Summary | |||
|
26 | # NOTE(marcink): one additional route is defined in very bottom, catch | |||
|
27 | # all pattern | |||
|
28 | config.add_route( | |||
|
29 | name='repo_summary_explicit', | |||
|
30 | pattern='/{repo_name:.*?[^/]}/summary', repo_route=True) | |||
|
31 | config.add_route( | |||
|
32 | name='repo_summary_commits', | |||
|
33 | pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True) | |||
|
34 | ||||
|
35 | # repo commits | |||
|
36 | config.add_route( | |||
|
37 | name='repo_commit', | |||
|
38 | pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True) | |||
|
39 | ||||
|
40 | # refs data | |||
|
41 | config.add_route( | |||
|
42 | name='repo_refs_data', | |||
|
43 | pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True) | |||
|
44 | ||||
|
45 | config.add_route( | |||
|
46 | name='repo_refs_changelog_data', | |||
|
47 | pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True) | |||
|
48 | ||||
|
49 | config.add_route( | |||
|
50 | name='repo_stats', | |||
|
51 | pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True) | |||
|
52 | ||||
|
53 | # Tags | |||
|
54 | config.add_route( | |||
|
55 | name='tags_home', | |||
|
56 | pattern='/{repo_name:.*?[^/]}/tags', repo_route=True) | |||
|
57 | ||||
|
58 | # Branches | |||
|
59 | config.add_route( | |||
|
60 | name='branches_home', | |||
|
61 | pattern='/{repo_name:.*?[^/]}/branches', repo_route=True) | |||
|
62 | ||||
|
63 | config.add_route( | |||
|
64 | name='bookmarks_home', | |||
|
65 | pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True) | |||
|
66 | ||||
|
67 | # Pull Requests | |||
|
68 | config.add_route( | |||
|
69 | name='pullrequest_show', | |||
|
70 | pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}', | |||
|
71 | repo_route=True) | |||
|
72 | ||||
|
73 | config.add_route( | |||
|
74 | name='pullrequest_show_all', | |||
|
75 | pattern='/{repo_name:.*?[^/]}/pull-request', | |||
|
76 | repo_route=True, repo_accepted_types=['hg', 'git']) | |||
|
77 | ||||
|
78 | config.add_route( | |||
|
79 | name='pullrequest_show_all_data', | |||
|
80 | pattern='/{repo_name:.*?[^/]}/pull-request-data', | |||
|
81 | repo_route=True, repo_accepted_types=['hg', 'git']) | |||
|
82 | ||||
|
83 | # Settings | |||
|
84 | config.add_route( | |||
|
85 | name='edit_repo', | |||
|
86 | pattern='/{repo_name:.*?[^/]}/settings', repo_route=True) | |||
|
87 | ||||
|
88 | # Settings advanced | |||
|
89 | config.add_route( | |||
|
90 | name='edit_repo_advanced', | |||
|
91 | pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True) | |||
|
92 | config.add_route( | |||
|
93 | name='edit_repo_advanced_delete', | |||
|
94 | pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True) | |||
|
95 | config.add_route( | |||
|
96 | name='edit_repo_advanced_locking', | |||
|
97 | pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True) | |||
|
98 | config.add_route( | |||
|
99 | name='edit_repo_advanced_journal', | |||
|
100 | pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True) | |||
|
101 | config.add_route( | |||
|
102 | name='edit_repo_advanced_fork', | |||
|
103 | pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True) | |||
|
104 | ||||
|
105 | # Caches | |||
|
106 | config.add_route( | |||
|
107 | name='edit_repo_caches', | |||
|
108 | pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True) | |||
|
109 | ||||
|
110 | # Permissions | |||
|
111 | config.add_route( | |||
|
112 | name='edit_repo_perms', | |||
|
113 | pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True) | |||
|
114 | ||||
|
115 | # Repo Review Rules | |||
|
116 | config.add_route( | |||
|
117 | name='repo_reviewers', | |||
|
118 | pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True) | |||
|
119 | ||||
|
120 | config.add_route( | |||
|
121 | name='repo_default_reviewers_data', | |||
|
122 | pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True) | |||
|
123 | ||||
|
124 | # Maintenance | |||
24 | config.add_route( |
|
125 | config.add_route( | |
25 | name='repo_maintenance', |
|
126 | name='repo_maintenance', | |
26 | pattern='/{repo_name:.*?[^/]}/maintenance', repo_route=True) |
|
127 | pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True) | |
27 |
|
128 | |||
28 | config.add_route( |
|
129 | config.add_route( | |
29 | name='repo_maintenance_execute', |
|
130 | name='repo_maintenance_execute', | |
30 | pattern='/{repo_name:.*?[^/]}/maintenance/execute', repo_route=True) |
|
131 | pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True) | |
31 |
|
||||
32 |
|
132 | |||
33 | # Strip |
|
133 | # Strip | |
34 | config.add_route( |
|
134 | config.add_route( | |
35 | name='strip', |
|
135 | name='strip', | |
36 | pattern='/{repo_name:.*?[^/]}/strip', repo_route=True) |
|
136 | pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True) | |
37 |
|
137 | |||
38 | config.add_route( |
|
138 | config.add_route( | |
39 | name='strip_check', |
|
139 | name='strip_check', | |
40 | pattern='/{repo_name:.*?[^/]}/strip_check', repo_route=True) |
|
140 | pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True) | |
41 |
|
141 | |||
42 | config.add_route( |
|
142 | config.add_route( | |
43 | name='strip_execute', |
|
143 | name='strip_execute', | |
44 | pattern='/{repo_name:.*?[^/]}/strip_execute', repo_route=True) |
|
144 | pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True) | |
|
145 | ||||
|
146 | # NOTE(marcink): needs to be at the end for catch-all | |||
|
147 | add_route_with_slash( | |||
|
148 | config, | |||
|
149 | name='repo_summary', | |||
|
150 | pattern='/{repo_name:.*?[^/]}', repo_route=True) | |||
|
151 | ||||
45 | # Scan module for configuration decorators. |
|
152 | # Scan module for configuration decorators. | |
46 | config.scan() |
|
153 | config.scan() |
@@ -18,24 +18,35 b'' | |||||
18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
|
21 | import pytest | |||
21 | from rhodecode.model.db import Repository |
|
22 | from rhodecode.model.db import Repository | |
22 | from rhodecode.tests import * |
|
|||
23 |
|
23 | |||
24 |
|
24 | |||
25 | class TestBookmarksController(TestController): |
|
25 | def route_path(name, params=None, **kwargs): | |
|
26 | import urllib | |||
|
27 | ||||
|
28 | base_url = { | |||
|
29 | 'bookmarks_home': '/{repo_name}/bookmarks', | |||
|
30 | }[name].format(**kwargs) | |||
|
31 | ||||
|
32 | if params: | |||
|
33 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
34 | return base_url | |||
|
35 | ||||
|
36 | ||||
|
37 | @pytest.mark.usefixtures('autologin_user', 'app') | |||
|
38 | class TestBookmarks(object): | |||
26 |
|
39 | |||
27 | def test_index(self, backend): |
|
40 | def test_index(self, backend): | |
28 | self.log_user() |
|
|||
29 | if backend.alias == 'hg': |
|
41 | if backend.alias == 'hg': | |
30 |
response = self.app.get( |
|
42 | response = self.app.get( | |
31 | action='index', |
|
43 | route_path('bookmarks_home', repo_name=backend.repo_name)) | |
32 | repo_name=backend.repo_name)) |
|
|||
33 |
|
44 | |||
34 | repo = Repository.get_by_repo_name(backend.repo_name) |
|
45 | repo = Repository.get_by_repo_name(backend.repo_name) | |
35 | for commit_id, obj_name in repo.scm_instance().bookmarks.items(): |
|
46 | for commit_id, obj_name in repo.scm_instance().bookmarks.items(): | |
36 | assert commit_id in response |
|
47 | assert commit_id in response | |
37 | assert obj_name in response |
|
48 | assert obj_name in response | |
38 | else: |
|
49 | else: | |
39 |
self.app.get( |
|
50 | self.app.get( | |
40 | action='index', |
|
51 | route_path('bookmarks_home', repo_name=backend.repo_name), | |
41 | repo_name=backend.repo_name), status=404) No newline at end of file |
|
52 | status=404) |
@@ -18,17 +18,28 b'' | |||||
18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
|
21 | import pytest | |||
21 | from rhodecode.model.db import Repository |
|
22 | from rhodecode.model.db import Repository | |
22 | from rhodecode.tests import * |
|
|||
23 |
|
23 | |||
24 |
|
24 | |||
25 | class TestBranchesController(TestController): |
|
25 | def route_path(name, params=None, **kwargs): | |
|
26 | import urllib | |||
|
27 | ||||
|
28 | base_url = { | |||
|
29 | 'branches_home': '/{repo_name}/branches', | |||
|
30 | }[name].format(**kwargs) | |||
|
31 | ||||
|
32 | if params: | |||
|
33 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
34 | return base_url | |||
|
35 | ||||
|
36 | ||||
|
37 | @pytest.mark.usefixtures('autologin_user', 'app') | |||
|
38 | class TestBranchesController(object): | |||
26 |
|
39 | |||
27 | def test_index(self, backend): |
|
40 | def test_index(self, backend): | |
28 | self.log_user() |
|
41 | response = self.app.get( | |
29 | response = self.app.get(url(controller='branches', |
|
42 | route_path('branches_home', repo_name=backend.repo_name)) | |
30 | action='index', |
|
|||
31 | repo_name=backend.repo_name)) |
|
|||
32 |
|
43 | |||
33 | repo = Repository.get_by_repo_name(backend.repo_name) |
|
44 | repo = Repository.get_by_repo_name(backend.repo_name) | |
34 |
|
45 |
@@ -23,16 +23,16 b' import re' | |||||
23 | import mock |
|
23 | import mock | |
24 | import pytest |
|
24 | import pytest | |
25 |
|
25 | |||
26 |
from rhodecode. |
|
26 | from rhodecode.apps.repository.views.repo_summary import RepoSummaryView | |
27 | from rhodecode.lib import helpers as h |
|
27 | from rhodecode.lib import helpers as h | |
28 | from rhodecode.lib.compat import OrderedDict |
|
28 | from rhodecode.lib.compat import OrderedDict | |
|
29 | from rhodecode.lib.utils2 import AttributeDict | |||
29 | from rhodecode.lib.vcs.exceptions import RepositoryRequirementError |
|
30 | from rhodecode.lib.vcs.exceptions import RepositoryRequirementError | |
30 | from rhodecode.model.db import Repository |
|
31 | from rhodecode.model.db import Repository | |
31 | from rhodecode.model.meta import Session |
|
32 | from rhodecode.model.meta import Session | |
32 | from rhodecode.model.repo import RepoModel |
|
33 | from rhodecode.model.repo import RepoModel | |
33 | from rhodecode.model.scm import ScmModel |
|
34 | from rhodecode.model.scm import ScmModel | |
34 |
from rhodecode.tests import |
|
35 | from rhodecode.tests import assert_session_flash | |
35 | TestController, url, HG_REPO, assert_session_flash) |
|
|||
36 | from rhodecode.tests.fixture import Fixture |
|
36 | from rhodecode.tests.fixture import Fixture | |
37 | from rhodecode.tests.utils import AssertResponse, repo_on_filesystem |
|
37 | from rhodecode.tests.utils import AssertResponse, repo_on_filesystem | |
38 |
|
38 | |||
@@ -40,14 +40,31 b' from rhodecode.tests.utils import Assert' | |||||
40 | fixture = Fixture() |
|
40 | fixture = Fixture() | |
41 |
|
41 | |||
42 |
|
42 | |||
43 | class TestSummaryController(TestController): |
|
43 | def route_path(name, params=None, **kwargs): | |
44 | def test_index(self, backend): |
|
44 | import urllib | |
45 | self.log_user() |
|
45 | ||
|
46 | base_url = { | |||
|
47 | 'repo_summary': '/{repo_name}', | |||
|
48 | 'repo_stats': '/{repo_name}/repo_stats/{commit_id}', | |||
|
49 | 'repo_refs_data': '/{repo_name}/refs-data', | |||
|
50 | 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog' | |||
|
51 | ||||
|
52 | }[name].format(**kwargs) | |||
|
53 | ||||
|
54 | if params: | |||
|
55 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
56 | return base_url | |||
|
57 | ||||
|
58 | ||||
|
59 | @pytest.mark.usefixtures('app') | |||
|
60 | class TestSummaryView(object): | |||
|
61 | def test_index(self, autologin_user, backend, http_host_only_stub): | |||
46 | repo_id = backend.repo.repo_id |
|
62 | repo_id = backend.repo.repo_id | |
47 | repo_name = backend.repo_name |
|
63 | repo_name = backend.repo_name | |
48 | with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', |
|
64 | with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', | |
49 | return_value=False): |
|
65 | return_value=False): | |
50 |
response = self.app.get( |
|
66 | response = self.app.get( | |
|
67 | route_path('repo_summary', repo_name=repo_name)) | |||
51 |
|
68 | |||
52 | # repo type |
|
69 | # repo type | |
53 | response.mustcontain( |
|
70 | response.mustcontain( | |
@@ -61,46 +78,47 b' class TestSummaryController(TestControll' | |||||
61 | # clone url... |
|
78 | # clone url... | |
62 | response.mustcontain( |
|
79 | response.mustcontain( | |
63 | 'id="clone_url" readonly="readonly"' |
|
80 | 'id="clone_url" readonly="readonly"' | |
64 |
' value="http://test_admin@ |
|
81 | ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, )) | |
65 | response.mustcontain( |
|
82 | response.mustcontain( | |
66 | 'id="clone_url_id" readonly="readonly"' |
|
83 | 'id="clone_url_id" readonly="readonly"' | |
67 |
' value="http://test_admin@ |
|
84 | ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, )) | |
68 |
|
85 | |||
69 |
def test_index_svn_without_proxy( |
|
86 | def test_index_svn_without_proxy( | |
70 | self.log_user() |
|
87 | self, autologin_user, backend_svn, http_host_only_stub): | |
71 | repo_id = backend_svn.repo.repo_id |
|
88 | repo_id = backend_svn.repo.repo_id | |
72 | repo_name = backend_svn.repo_name |
|
89 | repo_name = backend_svn.repo_name | |
73 |
response = self.app.get( |
|
90 | response = self.app.get(route_path('repo_summary', repo_name=repo_name)) | |
74 | # clone url... |
|
91 | # clone url... | |
75 | response.mustcontain( |
|
92 | response.mustcontain( | |
76 | 'id="clone_url" disabled' |
|
93 | 'id="clone_url" disabled' | |
77 |
' value="http://test_admin@ |
|
94 | ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, )) | |
78 | response.mustcontain( |
|
95 | response.mustcontain( | |
79 | 'id="clone_url_id" disabled' |
|
96 | 'id="clone_url_id" disabled' | |
80 |
' value="http://test_admin@ |
|
97 | ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, )) | |
81 |
|
98 | |||
82 |
def test_index_with_trailing_slash( |
|
99 | def test_index_with_trailing_slash( | |
|
100 | self, autologin_user, backend, http_host_only_stub): | |||
|
101 | ||||
83 | repo_id = backend.repo.repo_id |
|
102 | repo_id = backend.repo.repo_id | |
84 | repo_name = backend.repo_name |
|
103 | repo_name = backend.repo_name | |
85 | with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', |
|
104 | with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', | |
86 | return_value=False): |
|
105 | return_value=False): | |
87 | response = self.app.get( |
|
106 | response = self.app.get( | |
88 |
|
|
107 | route_path('repo_summary', repo_name=repo_name) + '/', | |
89 | status=200) |
|
108 | status=200) | |
90 |
|
109 | |||
91 | # clone url... |
|
110 | # clone url... | |
92 | response.mustcontain( |
|
111 | response.mustcontain( | |
93 | 'id="clone_url" readonly="readonly"' |
|
112 | 'id="clone_url" readonly="readonly"' | |
94 |
' value="http://test_admin@ |
|
113 | ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, )) | |
95 | response.mustcontain( |
|
114 | response.mustcontain( | |
96 | 'id="clone_url_id" readonly="readonly"' |
|
115 | 'id="clone_url_id" readonly="readonly"' | |
97 |
' value="http://test_admin@ |
|
116 | ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, )) | |
98 |
|
117 | |||
99 | def test_index_by_id(self, backend): |
|
118 | def test_index_by_id(self, autologin_user, backend): | |
100 | self.log_user() |
|
|||
101 | repo_id = backend.repo.repo_id |
|
119 | repo_id = backend.repo.repo_id | |
102 |
response = self.app.get( |
|
120 | response = self.app.get( | |
103 |
'summary |
|
121 | route_path('repo_summary', repo_name='_%s' % (repo_id,))) | |
104 |
|
122 | |||
105 | # repo type |
|
123 | # repo type | |
106 | response.mustcontain( |
|
124 | response.mustcontain( | |
@@ -111,10 +129,9 b' class TestSummaryController(TestControll' | |||||
111 | """<i class="icon-unlock-alt">""" |
|
129 | """<i class="icon-unlock-alt">""" | |
112 | ) |
|
130 | ) | |
113 |
|
131 | |||
114 | def test_index_by_repo_having_id_path_in_name_hg(self): |
|
132 | def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user): | |
115 | self.log_user() |
|
|||
116 | fixture.create_repo(name='repo_1') |
|
133 | fixture.create_repo(name='repo_1') | |
117 |
response = self.app.get( |
|
134 | response = self.app.get(route_path('repo_summary', repo_name='repo_1')) | |
118 |
|
135 | |||
119 | try: |
|
136 | try: | |
120 | response.mustcontain("repo_1") |
|
137 | response.mustcontain("repo_1") | |
@@ -122,11 +139,11 b' class TestSummaryController(TestControll' | |||||
122 | RepoModel().delete(Repository.get_by_repo_name('repo_1')) |
|
139 | RepoModel().delete(Repository.get_by_repo_name('repo_1')) | |
123 | Session().commit() |
|
140 | Session().commit() | |
124 |
|
141 | |||
125 |
def test_index_with_anonymous_access_disabled( |
|
142 | def test_index_with_anonymous_access_disabled( | |
126 | with fixture.anon_access(False): |
|
143 | self, backend, disable_anonymous_user): | |
127 | response = self.app.get(url('summary_home', repo_name=HG_REPO), |
|
144 | response = self.app.get( | |
128 | status=302) |
|
145 | route_path('repo_summary', repo_name=backend.repo_name), status=302) | |
129 |
|
|
146 | assert 'login' in response.location | |
130 |
|
147 | |||
131 | def _enable_stats(self, repo): |
|
148 | def _enable_stats(self, repo): | |
132 | r = Repository.get_by_repo_name(repo) |
|
149 | r = Repository.get_by_repo_name(repo) | |
@@ -173,17 +190,15 b' class TestSummaryController(TestControll' | |||||
173 | }, |
|
190 | }, | |
174 | } |
|
191 | } | |
175 |
|
192 | |||
176 | def test_repo_stats(self, backend, xhr_header): |
|
193 | def test_repo_stats(self, autologin_user, backend, xhr_header): | |
177 | self.log_user() |
|
|||
178 | response = self.app.get( |
|
194 | response = self.app.get( | |
179 |
|
|
195 | route_path( | |
180 | repo_name=backend.repo_name, commit_id='tip'), |
|
196 | 'repo_stats', repo_name=backend.repo_name, commit_id='tip'), | |
181 | extra_environ=xhr_header, |
|
197 | extra_environ=xhr_header, | |
182 | status=200) |
|
198 | status=200) | |
183 | assert re.match(r'6[\d\.]+ KiB', response.json['size']) |
|
199 | assert re.match(r'6[\d\.]+ KiB', response.json['size']) | |
184 |
|
200 | |||
185 | def test_repo_stats_code_stats_enabled(self, backend, xhr_header): |
|
201 | def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header): | |
186 | self.log_user() |
|
|||
187 | repo_name = backend.repo_name |
|
202 | repo_name = backend.repo_name | |
188 |
|
203 | |||
189 | # codes stats |
|
204 | # codes stats | |
@@ -191,8 +206,8 b' class TestSummaryController(TestControll' | |||||
191 | ScmModel().mark_for_invalidation(repo_name) |
|
206 | ScmModel().mark_for_invalidation(repo_name) | |
192 |
|
207 | |||
193 | response = self.app.get( |
|
208 | response = self.app.get( | |
194 |
|
|
209 | route_path( | |
195 | repo_name=backend.repo_name, commit_id='tip'), |
|
210 | 'repo_stats', repo_name=backend.repo_name, commit_id='tip'), | |
196 | extra_environ=xhr_header, |
|
211 | extra_environ=xhr_header, | |
197 | status=200) |
|
212 | status=200) | |
198 |
|
213 | |||
@@ -203,7 +218,7 b' class TestSummaryController(TestControll' | |||||
203 |
|
218 | |||
204 | def test_repo_refs_data(self, backend): |
|
219 | def test_repo_refs_data(self, backend): | |
205 | response = self.app.get( |
|
220 | response = self.app.get( | |
206 |
|
|
221 | route_path('repo_refs_data', repo_name=backend.repo_name), | |
207 | status=200) |
|
222 | status=200) | |
208 |
|
223 | |||
209 | # Ensure that there is the correct amount of items in the result |
|
224 | # Ensure that there is the correct amount of items in the result | |
@@ -220,72 +235,68 b' class TestSummaryController(TestControll' | |||||
220 | Repository, 'scm_instance', side_effect=RepositoryRequirementError) |
|
235 | Repository, 'scm_instance', side_effect=RepositoryRequirementError) | |
221 |
|
236 | |||
222 | with scm_patcher: |
|
237 | with scm_patcher: | |
223 |
response = self.app.get( |
|
238 | response = self.app.get(route_path('repo_summary', repo_name=repo_name)) | |
224 | assert_response = AssertResponse(response) |
|
239 | assert_response = AssertResponse(response) | |
225 | assert_response.element_contains( |
|
240 | assert_response.element_contains( | |
226 | '.main .alert-warning strong', 'Missing requirements') |
|
241 | '.main .alert-warning strong', 'Missing requirements') | |
227 | assert_response.element_contains( |
|
242 | assert_response.element_contains( | |
228 | '.main .alert-warning', |
|
243 | '.main .alert-warning', | |
229 |
' |
|
244 | 'Commits cannot be displayed, because this repository ' | |
230 |
' |
|
245 | 'uses one or more extensions, which was not enabled.') | |
231 |
|
246 | |||
232 | def test_missing_requirements_page_does_not_contains_switch_to( |
|
247 | def test_missing_requirements_page_does_not_contains_switch_to( | |
233 | self, backend): |
|
248 | self, autologin_user, backend): | |
234 | self.log_user() |
|
|||
235 | repo_name = backend.repo_name |
|
249 | repo_name = backend.repo_name | |
236 | scm_patcher = mock.patch.object( |
|
250 | scm_patcher = mock.patch.object( | |
237 | Repository, 'scm_instance', side_effect=RepositoryRequirementError) |
|
251 | Repository, 'scm_instance', side_effect=RepositoryRequirementError) | |
238 |
|
252 | |||
239 | with scm_patcher: |
|
253 | with scm_patcher: | |
240 |
response = self.app.get( |
|
254 | response = self.app.get(route_path('repo_summary', repo_name=repo_name)) | |
241 | response.mustcontain(no='Switch To') |
|
255 | response.mustcontain(no='Switch To') | |
242 |
|
256 | |||
243 |
|
257 | |||
244 |
@pytest.mark.usefixtures(' |
|
258 | @pytest.mark.usefixtures('app') | |
245 | class TestSwitcherReferenceData: |
|
259 | class TestRepoLocation(object): | |
246 |
|
260 | |||
247 | def test_creates_reference_urls_based_on_name(self): |
|
261 | @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii']) | |
248 | references = { |
|
262 | def test_manual_delete(self, autologin_user, backend, suffix, csrf_token): | |
249 | 'name': 'commit_id', |
|
263 | repo = backend.create_repo(name_suffix=suffix) | |
250 | } |
|
264 | repo_name = repo.repo_name | |
251 | controller = summary.SummaryController() |
|
265 | ||
252 | is_svn = False |
|
266 | # delete from file system | |
253 | result = controller._switcher_reference_data( |
|
267 | RepoModel()._delete_filesystem_repo(repo) | |
254 | 'repo_name', references, is_svn) |
|
|||
255 | expected_url = h.url( |
|
|||
256 | 'files_home', repo_name='repo_name', revision='name', |
|
|||
257 | at='name') |
|
|||
258 | assert result[0]['files_url'] == expected_url |
|
|||
259 |
|
268 | |||
260 | def test_urls_contain_commit_id_if_slash_in_name(self): |
|
269 | # test if the repo is still in the database | |
261 | references = { |
|
270 | new_repo = RepoModel().get_by_repo_name(repo_name) | |
262 | 'name/with/slash': 'commit_id', |
|
271 | assert new_repo.repo_name == repo_name | |
263 | } |
|
|||
264 | controller = summary.SummaryController() |
|
|||
265 | is_svn = False |
|
|||
266 | result = controller._switcher_reference_data( |
|
|||
267 | 'repo_name', references, is_svn) |
|
|||
268 | expected_url = h.url( |
|
|||
269 | 'files_home', repo_name='repo_name', revision='commit_id', |
|
|||
270 | at='name/with/slash') |
|
|||
271 | assert result[0]['files_url'] == expected_url |
|
|||
272 |
|
272 | |||
273 | def test_adds_reference_to_path_for_svn(self): |
|
273 | # check if repo is not in the filesystem | |
274 | references = { |
|
274 | assert not repo_on_filesystem(repo_name) | |
275 | 'name/with/slash': 'commit_id', |
|
275 | self.assert_repo_not_found_redirect(repo_name) | |
276 | } |
|
276 | ||
277 | controller = summary.SummaryController() |
|
277 | def assert_repo_not_found_redirect(self, repo_name): | |
278 | is_svn = True |
|
278 | # run the check page that triggers the other flash message | |
279 | result = controller._switcher_reference_data( |
|
279 | response = self.app.get(h.url('repo_check_home', repo_name=repo_name)) | |
280 | 'repo_name', references, is_svn) |
|
280 | assert_session_flash( | |
281 | expected_url = h.url( |
|
281 | response, 'The repository at %s cannot be located.' % repo_name) | |
282 | 'files_home', repo_name='repo_name', f_path='name/with/slash', |
|
|||
283 | revision='commit_id', at='name/with/slash') |
|
|||
284 | assert result[0]['files_url'] == expected_url |
|
|||
285 |
|
282 | |||
286 |
|
283 | |||
287 |
@pytest. |
|
284 | @pytest.fixture() | |
288 | class TestCreateReferenceData: |
|
285 | def summary_view(context_stub, request_stub, user_util): | |
|
286 | """ | |||
|
287 | Bootstrap view to test the view functions | |||
|
288 | """ | |||
|
289 | request_stub.matched_route = AttributeDict(name='test_view') | |||
|
290 | ||||
|
291 | request_stub.user = user_util.create_user().AuthUser | |||
|
292 | request_stub.db_repo = user_util.create_repo() | |||
|
293 | ||||
|
294 | view = RepoSummaryView(context=context_stub, request=request_stub) | |||
|
295 | return view | |||
|
296 | ||||
|
297 | ||||
|
298 | @pytest.mark.usefixtures('app') | |||
|
299 | class TestCreateReferenceData(object): | |||
289 |
|
300 | |||
290 | @pytest.fixture |
|
301 | @pytest.fixture | |
291 | def example_refs(self): |
|
302 | def example_refs(self): | |
@@ -296,14 +307,13 b' class TestCreateReferenceData:' | |||||
296 | ] |
|
307 | ] | |
297 | return example_refs |
|
308 | return example_refs | |
298 |
|
309 | |||
299 | def test_generates_refs_based_on_commit_ids(self, example_refs): |
|
310 | def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view): | |
300 | repo = mock.Mock() |
|
311 | repo = mock.Mock() | |
301 | repo.name = 'test-repo' |
|
312 | repo.name = 'test-repo' | |
302 | repo.alias = 'git' |
|
313 | repo.alias = 'git' | |
303 | full_repo_name = 'pytest-repo-group/' + repo.name |
|
314 | full_repo_name = 'pytest-repo-group/' + repo.name | |
304 | controller = summary.SummaryController() |
|
|||
305 |
|
315 | |||
306 |
result = |
|
316 | result = summary_view._create_reference_data( | |
307 | repo, full_repo_name, example_refs) |
|
317 | repo, full_repo_name, example_refs) | |
308 |
|
318 | |||
309 | expected_files_url = '/{}/files/'.format(full_repo_name) |
|
319 | expected_files_url = '/{}/files/'.format(full_repo_name) | |
@@ -332,13 +342,13 b' class TestCreateReferenceData:' | |||||
332 | }] |
|
342 | }] | |
333 | assert result == expected_result |
|
343 | assert result == expected_result | |
334 |
|
344 | |||
335 | def test_generates_refs_with_path_for_svn(self, example_refs): |
|
345 | def test_generates_refs_with_path_for_svn(self, example_refs, summary_view): | |
336 | repo = mock.Mock() |
|
346 | repo = mock.Mock() | |
337 | repo.name = 'test-repo' |
|
347 | repo.name = 'test-repo' | |
338 | repo.alias = 'svn' |
|
348 | repo.alias = 'svn' | |
339 | full_repo_name = 'pytest-repo-group/' + repo.name |
|
349 | full_repo_name = 'pytest-repo-group/' + repo.name | |
340 | controller = summary.SummaryController() |
|
350 | ||
341 |
result = |
|
351 | result = summary_view._create_reference_data( | |
342 | repo, full_repo_name, example_refs) |
|
352 | repo, full_repo_name, example_refs) | |
343 |
|
353 | |||
344 | expected_files_url = '/{}/files/'.format(full_repo_name) |
|
354 | expected_files_url = '/{}/files/'.format(full_repo_name) | |
@@ -372,35 +382,9 b' class TestCreateReferenceData:' | |||||
372 | assert result == expected_result |
|
382 | assert result == expected_result | |
373 |
|
383 | |||
374 |
|
384 | |||
375 | @pytest.mark.usefixtures("app") |
|
385 | class TestCreateFilesUrl(object): | |
376 | class TestRepoLocation: |
|
|||
377 |
|
||||
378 | @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii']) |
|
|||
379 | def test_manual_delete(self, autologin_user, backend, suffix, csrf_token): |
|
|||
380 | repo = backend.create_repo(name_suffix=suffix) |
|
|||
381 | repo_name = repo.repo_name |
|
|||
382 |
|
||||
383 | # delete from file system |
|
|||
384 | RepoModel()._delete_filesystem_repo(repo) |
|
|||
385 |
|
||||
386 | # test if the repo is still in the database |
|
|||
387 | new_repo = RepoModel().get_by_repo_name(repo_name) |
|
|||
388 | assert new_repo.repo_name == repo_name |
|
|||
389 |
|
386 | |||
390 | # check if repo is not in the filesystem |
|
387 | def test_creates_non_svn_url(self, summary_view): | |
391 | assert not repo_on_filesystem(repo_name) |
|
|||
392 | self.assert_repo_not_found_redirect(repo_name) |
|
|||
393 |
|
||||
394 | def assert_repo_not_found_redirect(self, repo_name): |
|
|||
395 | # run the check page that triggers the other flash message |
|
|||
396 | response = self.app.get(url('repo_check_home', repo_name=repo_name)) |
|
|||
397 | assert_session_flash( |
|
|||
398 | response, 'The repository at %s cannot be located.' % repo_name) |
|
|||
399 |
|
||||
400 |
|
||||
401 | class TestCreateFilesUrl(object): |
|
|||
402 | def test_creates_non_svn_url(self): |
|
|||
403 | controller = summary.SummaryController() |
|
|||
404 | repo = mock.Mock() |
|
388 | repo = mock.Mock() | |
405 | repo.name = 'abcde' |
|
389 | repo.name = 'abcde' | |
406 | full_repo_name = 'test-repo-group/' + repo.name |
|
390 | full_repo_name = 'test-repo-group/' + repo.name | |
@@ -408,16 +392,15 b' class TestCreateFilesUrl(object):' | |||||
408 | raw_id = 'deadbeef0123456789' |
|
392 | raw_id = 'deadbeef0123456789' | |
409 | is_svn = False |
|
393 | is_svn = False | |
410 |
|
394 | |||
411 |
with mock.patch |
|
395 | with mock.patch('rhodecode.lib.helpers.url') as url_mock: | |
412 |
result = |
|
396 | result = summary_view._create_files_url( | |
413 | repo, full_repo_name, ref_name, raw_id, is_svn) |
|
397 | repo, full_repo_name, ref_name, raw_id, is_svn) | |
414 | url_mock.assert_called_once_with( |
|
398 | url_mock.assert_called_once_with( | |
415 | 'files_home', repo_name=full_repo_name, f_path='', |
|
399 | 'files_home', repo_name=full_repo_name, f_path='', | |
416 | revision=ref_name, at=ref_name) |
|
400 | revision=ref_name, at=ref_name) | |
417 | assert result == url_mock.return_value |
|
401 | assert result == url_mock.return_value | |
418 |
|
402 | |||
419 | def test_creates_svn_url(self): |
|
403 | def test_creates_svn_url(self, summary_view): | |
420 | controller = summary.SummaryController() |
|
|||
421 | repo = mock.Mock() |
|
404 | repo = mock.Mock() | |
422 | repo.name = 'abcde' |
|
405 | repo.name = 'abcde' | |
423 | full_repo_name = 'test-repo-group/' + repo.name |
|
406 | full_repo_name = 'test-repo-group/' + repo.name | |
@@ -425,16 +408,15 b' class TestCreateFilesUrl(object):' | |||||
425 | raw_id = 'deadbeef0123456789' |
|
408 | raw_id = 'deadbeef0123456789' | |
426 | is_svn = True |
|
409 | is_svn = True | |
427 |
|
410 | |||
428 |
with mock.patch |
|
411 | with mock.patch('rhodecode.lib.helpers.url') as url_mock: | |
429 |
result = |
|
412 | result = summary_view._create_files_url( | |
430 | repo, full_repo_name, ref_name, raw_id, is_svn) |
|
413 | repo, full_repo_name, ref_name, raw_id, is_svn) | |
431 | url_mock.assert_called_once_with( |
|
414 | url_mock.assert_called_once_with( | |
432 | 'files_home', repo_name=full_repo_name, f_path=ref_name, |
|
415 | 'files_home', repo_name=full_repo_name, f_path=ref_name, | |
433 | revision=raw_id, at=ref_name) |
|
416 | revision=raw_id, at=ref_name) | |
434 | assert result == url_mock.return_value |
|
417 | assert result == url_mock.return_value | |
435 |
|
418 | |||
436 | def test_name_has_slashes(self): |
|
419 | def test_name_has_slashes(self, summary_view): | |
437 | controller = summary.SummaryController() |
|
|||
438 | repo = mock.Mock() |
|
420 | repo = mock.Mock() | |
439 | repo.name = 'abcde' |
|
421 | repo.name = 'abcde' | |
440 | full_repo_name = 'test-repo-group/' + repo.name |
|
422 | full_repo_name = 'test-repo-group/' + repo.name | |
@@ -442,8 +424,8 b' class TestCreateFilesUrl(object):' | |||||
442 | raw_id = 'deadbeef0123456789' |
|
424 | raw_id = 'deadbeef0123456789' | |
443 | is_svn = False |
|
425 | is_svn = False | |
444 |
|
426 | |||
445 |
with mock.patch |
|
427 | with mock.patch('rhodecode.lib.helpers.url') as url_mock: | |
446 |
result = |
|
428 | result = summary_view._create_files_url( | |
447 | repo, full_repo_name, ref_name, raw_id, is_svn) |
|
429 | repo, full_repo_name, ref_name, raw_id, is_svn) | |
448 | url_mock.assert_called_once_with( |
|
430 | url_mock.assert_called_once_with( | |
449 | 'files_home', repo_name=full_repo_name, f_path='', revision=raw_id, |
|
431 | 'files_home', repo_name=full_repo_name, f_path='', revision=raw_id, | |
@@ -462,42 +444,39 b' class TestReferenceItems(object):' | |||||
462 | def _format_function(name, id_): |
|
444 | def _format_function(name, id_): | |
463 | return 'format_function_{}_{}'.format(name, id_) |
|
445 | return 'format_function_{}_{}'.format(name, id_) | |
464 |
|
446 | |||
465 | def test_creates_required_amount_of_items(self): |
|
447 | def test_creates_required_amount_of_items(self, summary_view): | |
466 | amount = 100 |
|
448 | amount = 100 | |
467 | refs = { |
|
449 | refs = { | |
468 | 'ref{}'.format(i): '{0:040d}'.format(i) |
|
450 | 'ref{}'.format(i): '{0:040d}'.format(i) | |
469 | for i in range(amount) |
|
451 | for i in range(amount) | |
470 | } |
|
452 | } | |
471 |
|
453 | |||
472 | controller = summary.SummaryController() |
|
454 | url_patcher = mock.patch.object(summary_view, '_create_files_url') | |
473 |
|
455 | svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn', | ||
474 | url_patcher = mock.patch.object( |
|
456 | return_value=False) | |
475 | controller, '_create_files_url') |
|
|||
476 | svn_patcher = mock.patch.object( |
|
|||
477 | summary.h, 'is_svn', return_value=False) |
|
|||
478 |
|
457 | |||
479 | with url_patcher as url_mock, svn_patcher: |
|
458 | with url_patcher as url_mock, svn_patcher: | |
480 |
result = |
|
459 | result = summary_view._create_reference_items( | |
481 | self.repo, self.repo_full_name, refs, self.ref_type, |
|
460 | self.repo, self.repo_full_name, refs, self.ref_type, | |
482 | self._format_function) |
|
461 | self._format_function) | |
483 | assert len(result) == amount |
|
462 | assert len(result) == amount | |
484 | assert url_mock.call_count == amount |
|
463 | assert url_mock.call_count == amount | |
485 |
|
464 | |||
486 | def test_single_item_details(self): |
|
465 | def test_single_item_details(self, summary_view): | |
487 | ref_name = 'ref1' |
|
466 | ref_name = 'ref1' | |
488 | ref_id = 'deadbeef' |
|
467 | ref_id = 'deadbeef' | |
489 | refs = { |
|
468 | refs = { | |
490 | ref_name: ref_id |
|
469 | ref_name: ref_id | |
491 | } |
|
470 | } | |
492 |
|
471 | |||
493 | controller = summary.SummaryController() |
|
472 | svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn', | |
|
473 | return_value=False) | |||
|
474 | ||||
494 | url_patcher = mock.patch.object( |
|
475 | url_patcher = mock.patch.object( | |
495 |
|
|
476 | summary_view, '_create_files_url', return_value=self.fake_url) | |
496 | svn_patcher = mock.patch.object( |
|
|||
497 | summary.h, 'is_svn', return_value=False) |
|
|||
498 |
|
477 | |||
499 | with url_patcher as url_mock, svn_patcher: |
|
478 | with url_patcher as url_mock, svn_patcher: | |
500 |
result = |
|
479 | result = summary_view._create_reference_items( | |
501 | self.repo, self.repo_full_name, refs, self.ref_type, |
|
480 | self.repo, self.repo_full_name, refs, self.ref_type, | |
502 | self._format_function) |
|
481 | self._format_function) | |
503 |
|
482 |
@@ -18,16 +18,27 b'' | |||||
18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
|
21 | import pytest | |||
21 | from rhodecode.model.db import Repository |
|
22 | from rhodecode.model.db import Repository | |
22 | from rhodecode.tests import * |
|
|||
23 |
|
23 | |||
24 |
|
24 | |||
25 | class TestTagsController(TestController): |
|
25 | def route_path(name, params=None, **kwargs): | |
|
26 | import urllib | |||
|
27 | ||||
|
28 | base_url = { | |||
|
29 | 'tags_home': '/{repo_name}/tags', | |||
|
30 | }[name].format(**kwargs) | |||
|
31 | ||||
|
32 | if params: | |||
|
33 | base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) | |||
|
34 | return base_url | |||
|
35 | ||||
|
36 | ||||
|
37 | @pytest.mark.usefixtures('autologin_user', 'app') | |||
|
38 | class TestTagsController(object): | |||
26 | def test_index(self, backend): |
|
39 | def test_index(self, backend): | |
27 | self.log_user() |
|
40 | response = self.app.get( | |
28 | response = self.app.get(url(controller='tags', |
|
41 | route_path('tags_home', repo_name=backend.repo_name)) | |
29 | action='index', |
|
|||
30 | repo_name=backend.repo_name)) |
|
|||
31 |
|
42 | |||
32 | repo = Repository.get_by_repo_name(backend.repo_name) |
|
43 | repo = Repository.get_by_repo_name(backend.repo_name) | |
33 |
|
44 |
@@ -41,7 +41,6 b' class RepoMaintenanceView(RepoAppView):' | |||||
41 | return c |
|
41 | return c | |
42 |
|
42 | |||
43 | @LoginRequired() |
|
43 | @LoginRequired() | |
44 | @NotAnonymous() |
|
|||
45 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
44 | @HasRepoPermissionAnyDecorator('repository.admin') | |
46 | @view_config( |
|
45 | @view_config( | |
47 | route_name='repo_maintenance', request_method='GET', |
|
46 | route_name='repo_maintenance', request_method='GET', | |
@@ -54,7 +53,6 b' class RepoMaintenanceView(RepoAppView):' | |||||
54 | return self._get_template_context(c) |
|
53 | return self._get_template_context(c) | |
55 |
|
54 | |||
56 | @LoginRequired() |
|
55 | @LoginRequired() | |
57 | @NotAnonymous() |
|
|||
58 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
56 | @HasRepoPermissionAnyDecorator('repository.admin') | |
59 | @view_config( |
|
57 | @view_config( | |
60 | route_name='repo_maintenance_execute', request_method='GET', |
|
58 | route_name='repo_maintenance_execute', request_method='GET', |
@@ -1,6 +1,6 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 |
|
2 | |||
3 |
# Copyright (C) 201 |
|
3 | # Copyright (C) 2017-2017 RhodeCode GmbH | |
4 | # |
|
4 | # | |
5 | # This program is free software: you can redistribute it and/or modify |
|
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 |
|
6 | # it under the terms of the GNU Affero General Public License, version 3 | |
@@ -22,9 +22,11 b' import logging' | |||||
22 | from pyramid.view import view_config |
|
22 | from pyramid.view import view_config | |
23 |
|
23 | |||
24 | from rhodecode.apps._base import RepoAppView |
|
24 | from rhodecode.apps._base import RepoAppView | |
|
25 | from rhodecode.lib import audit_logger | |||
|
26 | from rhodecode.lib import helpers as h | |||
25 | from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator, |
|
27 | from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator, | |
26 | NotAnonymous) |
|
28 | NotAnonymous, CSRFRequired) | |
27 |
|
29 | from rhodecode.lib.ext_json import json | ||
28 |
|
30 | |||
29 | log = logging.getLogger(__name__) |
|
31 | log = logging.getLogger(__name__) | |
30 |
|
32 | |||
@@ -40,7 +42,6 b' class StripView(RepoAppView):' | |||||
40 | return c |
|
42 | return c | |
41 |
|
43 | |||
42 | @LoginRequired() |
|
44 | @LoginRequired() | |
43 | @NotAnonymous() |
|
|||
44 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
45 | @HasRepoPermissionAnyDecorator('repository.admin') | |
45 | @view_config( |
|
46 | @view_config( | |
46 | route_name='strip', request_method='GET', |
|
47 | route_name='strip', request_method='GET', | |
@@ -53,41 +54,39 b' class StripView(RepoAppView):' | |||||
53 | return self._get_template_context(c) |
|
54 | return self._get_template_context(c) | |
54 |
|
55 | |||
55 | @LoginRequired() |
|
56 | @LoginRequired() | |
56 | @NotAnonymous() |
|
|||
57 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
57 | @HasRepoPermissionAnyDecorator('repository.admin') | |
|
58 | @CSRFRequired() | |||
58 | @view_config( |
|
59 | @view_config( | |
59 | route_name='strip_check', request_method='POST', |
|
60 | route_name='strip_check', request_method='POST', | |
60 | renderer='json', xhr=True |
|
61 | renderer='json', xhr=True) | |
61 | ) |
|
|||
62 | def strip_check(self): |
|
62 | def strip_check(self): | |
63 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
63 | from rhodecode.lib.vcs.backends.base import EmptyCommit | |
64 | data = {} |
|
64 | data = {} | |
65 | rp = self.request.POST |
|
65 | rp = self.request.POST | |
66 | for i in range(1, 11): |
|
66 | for i in range(1, 11): | |
67 | chset = 'changeset_id-%d'%(i,) |
|
67 | chset = 'changeset_id-%d' % (i,) | |
68 | check = rp.get(chset) |
|
68 | check = rp.get(chset) | |
|
69 | ||||
69 | if check: |
|
70 | if check: | |
70 | data[i] = self.db_repo.get_changeset(rp[chset]) |
|
71 | data[i] = self.db_repo.get_changeset(rp[chset]) | |
71 | if isinstance(data[i], EmptyCommit): |
|
72 | if isinstance(data[i], EmptyCommit): | |
72 | data[i] = {'rev': None, 'commit': rp[chset]} |
|
73 | data[i] = {'rev': None, 'commit': h.escape(rp[chset])} | |
73 | else: |
|
74 | else: | |
74 |
data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, |
|
75 | data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, | |
|
76 | 'author': data[i].author, | |||
75 | 'comment': data[i].message} |
|
77 | 'comment': data[i].message} | |
76 | else: |
|
78 | else: | |
77 | break |
|
79 | break | |
78 | return data |
|
80 | return data | |
79 |
|
81 | |||
80 | @LoginRequired() |
|
82 | @LoginRequired() | |
81 | @NotAnonymous() |
|
|||
82 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
83 | @HasRepoPermissionAnyDecorator('repository.admin') | |
|
84 | @CSRFRequired() | |||
83 | @view_config( |
|
85 | @view_config( | |
84 | route_name='strip_execute', request_method='POST', |
|
86 | route_name='strip_execute', request_method='POST', | |
85 | renderer='json', xhr=True |
|
87 | renderer='json', xhr=True) | |
86 | ) |
|
|||
87 | def strip_execute(self): |
|
88 | def strip_execute(self): | |
88 |
|
||||
89 | from rhodecode.model.scm import ScmModel |
|
89 | from rhodecode.model.scm import ScmModel | |
90 | from rhodecode.lib.ext_json import json |
|
|||
91 |
|
90 | |||
92 | c = self.load_default_context() |
|
91 | c = self.load_default_context() | |
93 | user = self._rhodecode_user |
|
92 | user = self._rhodecode_user | |
@@ -95,16 +94,23 b' class StripView(RepoAppView):' | |||||
95 | data = {} |
|
94 | data = {} | |
96 | for idx in rp: |
|
95 | for idx in rp: | |
97 | commit = json.loads(rp[idx]) |
|
96 | commit = json.loads(rp[idx]) | |
98 | #If someone put two times the same branch |
|
97 | # If someone put two times the same branch | |
99 | if commit['branch'] in data.keys(): |
|
98 | if commit['branch'] in data.keys(): | |
100 | continue |
|
99 | continue | |
101 | try: |
|
100 | try: | |
102 |
ScmModel().strip( |
|
101 | ScmModel().strip( | |
103 | commit_id=commit['rev'], branch=commit['branch']) |
|
102 | repo=c.repo_info, | |
104 | log.info('Stripped commit %s from repo `%s` by %s' % (commit['rev'], c.repo_info.repo_name, user)) |
|
103 | commit_id=commit['rev'], branch=commit['branch']) | |
|
104 | log.info('Stripped commit %s from repo `%s` by %s' % ( | |||
|
105 | commit['rev'], c.repo_info.repo_name, user)) | |||
105 | data[commit['rev']] = True |
|
106 | data[commit['rev']] = True | |
106 | except Exception, e: |
|
107 | ||
|
108 | audit_logger.store_web( | |||
|
109 | 'repo.commit.strip', action_data={'commit_id': commit['rev']}, | |||
|
110 | repo=self.db_repo, user=self._rhodecode_user, commit=True) | |||
|
111 | ||||
|
112 | except Exception as e: | |||
107 | data[commit['rev']] = False |
|
113 | data[commit['rev']] = False | |
108 |
log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % ( |
|
114 | log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % ( | |
109 | c.repo_info.repo_name, user, e.message)) |
|
115 | commit['rev'], self.db_repo_name, user, e.message)) | |
110 | return data |
|
116 | return data |
@@ -564,8 +564,6 b' def authenticate(username, password, env' | |||||
564 | for plugin in authn_registry.get_plugins_for_authentication(): |
|
564 | for plugin in authn_registry.get_plugins_for_authentication(): | |
565 | plugin.set_auth_type(auth_type) |
|
565 | plugin.set_auth_type(auth_type) | |
566 | plugin.set_calling_scope_repo(acl_repo_name) |
|
566 | plugin.set_calling_scope_repo(acl_repo_name) | |
567 | user = plugin.get_user(username) |
|
|||
568 | display_user = user.username if user else username |
|
|||
569 |
|
567 | |||
570 | if headers_only and not plugin.is_headers_auth: |
|
568 | if headers_only and not plugin.is_headers_auth: | |
571 | log.debug('Auth type is for headers only and plugin `%s` is not ' |
|
569 | log.debug('Auth type is for headers only and plugin `%s` is not ' |
@@ -71,15 +71,17 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
71 | host = colander.SchemaNode( |
|
71 | host = colander.SchemaNode( | |
72 | colander.String(), |
|
72 | colander.String(), | |
73 | default='', |
|
73 | default='', | |
74 | description=_('Host of the LDAP Server \n' |
|
74 | description=_('Host[s] of the LDAP Server \n' | |
75 |
'(e.g., 192.168.2.154, or ldap-server.domain.com' |
|
75 | '(e.g., 192.168.2.154, or ldap-server.domain.com.\n ' | |
|
76 | 'Multiple servers can be specified using commas'), | |||
76 | preparer=strip_whitespace, |
|
77 | preparer=strip_whitespace, | |
77 | title=_('LDAP Host'), |
|
78 | title=_('LDAP Host'), | |
78 | widget='string') |
|
79 | widget='string') | |
79 | port = colander.SchemaNode( |
|
80 | port = colander.SchemaNode( | |
80 | colander.Int(), |
|
81 | colander.Int(), | |
81 | default=389, |
|
82 | default=389, | |
82 |
description=_('Custom port that the LDAP server is listening on. |
|
83 | description=_('Custom port that the LDAP server is listening on. ' | |
|
84 | 'Default value is: 389'), | |||
83 | preparer=strip_whitespace, |
|
85 | preparer=strip_whitespace, | |
84 | title=_('Port'), |
|
86 | title=_('Port'), | |
85 | validator=colander.Range(min=0, max=65536), |
|
87 | validator=colander.Range(min=0, max=65536), | |
@@ -112,7 +114,9 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
112 | tls_reqcert = colander.SchemaNode( |
|
114 | tls_reqcert = colander.SchemaNode( | |
113 | colander.String(), |
|
115 | colander.String(), | |
114 | default=tls_reqcert_choices[0], |
|
116 | default=tls_reqcert_choices[0], | |
115 |
description=_('Require Cert over TLS?' |
|
117 | description=_('Require Cert over TLS?. Self-signed and custom ' | |
|
118 | 'certificates can be used when\n `RhodeCode Certificate` ' | |||
|
119 | 'found in admin > settings > system info page is extended.'), | |||
116 | title=_('Certificate Checks'), |
|
120 | title=_('Certificate Checks'), | |
117 | validator=colander.OneOf(tls_reqcert_choices), |
|
121 | validator=colander.OneOf(tls_reqcert_choices), | |
118 | widget='select') |
|
122 | widget='select') |
@@ -1,11 +1,11 b'' | |||||
1 | { |
|
1 | { | |
2 | "nodejs-4.3.1": { |
|
2 | "libnghttp2-1.7.1": { | |
3 | "MIT License": "http://spdx.org/licenses/MIT" |
|
3 | "MIT License": "http://spdx.org/licenses/MIT" | |
4 | }, |
|
4 | }, | |
5 |
" |
|
5 | "nodejs-4.3.1": { | |
6 |
" |
|
6 | "MIT License": "http://spdx.org/licenses/MIT" | |
7 |
}, |
|
7 | }, | |
8 |
"python-2.7.1 |
|
8 | "python-2.7.12": { | |
9 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" |
|
9 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" | |
10 | }, |
|
10 | }, | |
11 | "python2.7-Babel-1.3": { |
|
11 | "python2.7-Babel-1.3": { | |
@@ -14,19 +14,25 b'' | |||||
14 | "python2.7-Beaker-1.7.0": { |
|
14 | "python2.7-Beaker-1.7.0": { | |
15 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
15 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
16 | }, |
|
16 | }, | |
|
17 | "python2.7-Chameleon-2.24": { | |||
|
18 | "BSD-like": "http://repoze.org/license.html" | |||
|
19 | }, | |||
17 | "python2.7-FormEncode-1.2.4": { |
|
20 | "python2.7-FormEncode-1.2.4": { | |
18 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" |
|
21 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" | |
19 | }, |
|
22 | }, | |
20 |
"python2.7- |
|
23 | "python2.7-Jinja2-2.7.3": { | |
|
24 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
25 | }, | |||
|
26 | "python2.7-Mako-1.0.6": { | |||
21 | "MIT License": "http://spdx.org/licenses/MIT" |
|
27 | "MIT License": "http://spdx.org/licenses/MIT" | |
22 | }, |
|
28 | }, | |
23 |
"python2.7-Markdown-2.6. |
|
29 | "python2.7-Markdown-2.6.7": { | |
24 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
30 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
25 | }, |
|
31 | }, | |
26 | "python2.7-MarkupSafe-0.23": { |
|
32 | "python2.7-MarkupSafe-0.23": { | |
27 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
33 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
28 | }, |
|
34 | }, | |
29 |
"python2.7-Paste-2.0. |
|
35 | "python2.7-Paste-2.0.3": { | |
30 | "MIT License": "http://spdx.org/licenses/MIT" |
|
36 | "MIT License": "http://spdx.org/licenses/MIT" | |
31 | }, |
|
37 | }, | |
32 | "python2.7-PasteDeploy-1.5.2": { |
|
38 | "python2.7-PasteDeploy-1.5.2": { | |
@@ -35,12 +41,12 b'' | |||||
35 | "python2.7-PasteScript-1.7.5": { |
|
41 | "python2.7-PasteScript-1.7.5": { | |
36 | "MIT License": "http://spdx.org/licenses/MIT" |
|
42 | "MIT License": "http://spdx.org/licenses/MIT" | |
37 | }, |
|
43 | }, | |
38 |
"python2.7-Pygments-2. |
|
44 | "python2.7-Pygments-2.2.0": { | |
39 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
45 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
40 | }, |
|
46 | }, | |
41 |
"python2.7-Pylons-1.0. |
|
47 | "python2.7-Pylons-1.0.2.rhodecode-patch1": { | |
42 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
48 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
43 | }, |
|
49 | }, | |
44 | "python2.7-Routes-1.13": { |
|
50 | "python2.7-Routes-1.13": { | |
45 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
51 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
46 | }, |
|
52 | }, | |
@@ -65,7 +71,7 b'' | |||||
65 | "python2.7-WebOb-1.3.1": { |
|
71 | "python2.7-WebOb-1.3.1": { | |
66 | "MIT License": "http://spdx.org/licenses/MIT" |
|
72 | "MIT License": "http://spdx.org/licenses/MIT" | |
67 | }, |
|
73 | }, | |
68 |
"python2.7-Whoosh-2.7. |
|
74 | "python2.7-Whoosh-2.7.4": { | |
69 | "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause", |
|
75 | "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause", | |
70 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
76 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
71 | }, |
|
77 | }, | |
@@ -87,9 +93,18 b'' | |||||
87 | "python2.7-backport-ipaddress-0.1": { |
|
93 | "python2.7-backport-ipaddress-0.1": { | |
88 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" |
|
94 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" | |
89 | }, |
|
95 | }, | |
|
96 | "python2.7-backports.shutil-get-terminal-size-1.0.0": { | |||
|
97 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
98 | }, | |||
|
99 | "python2.7-bleach-1.5.0": { | |||
|
100 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |||
|
101 | }, | |||
90 | "python2.7-celery-2.2.10": { |
|
102 | "python2.7-celery-2.2.10": { | |
91 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
103 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
92 | }, |
|
104 | }, | |
|
105 | "python2.7-channelstream-0.5.2": { | |||
|
106 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
107 | }, | |||
93 | "python2.7-click-5.1": { |
|
108 | "python2.7-click-5.1": { | |
94 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
109 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
95 | }, |
|
110 | }, | |
@@ -99,82 +114,169 b'' | |||||
99 | "python2.7-configobj-5.0.6": { |
|
114 | "python2.7-configobj-5.0.6": { | |
100 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
115 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
101 | }, |
|
116 | }, | |
102 |
"python2.7-c |
|
117 | "python2.7-configparser-3.5.0": { | |
|
118 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
119 | }, | |||
|
120 | "python2.7-cssselect-1.0.1": { | |||
103 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
121 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
104 | }, |
|
122 | }, | |
105 |
"python2.7-decorator- |
|
123 | "python2.7-decorator-4.0.11": { | |
106 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
124 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
107 | }, |
|
125 | }, | |
|
126 | "python2.7-deform-2.0a2": { | |||
|
127 | "BSD-derived": "http://www.repoze.org/LICENSE.txt" | |||
|
128 | }, | |||
108 | "python2.7-docutils-0.12": { |
|
129 | "python2.7-docutils-0.12": { | |
109 | "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause" |
|
130 | "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause" | |
110 | }, |
|
131 | }, | |
|
132 | "python2.7-dogpile.cache-0.6.1": { | |||
|
133 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
134 | }, | |||
|
135 | "python2.7-dogpile.core-0.4.1": { | |||
|
136 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
137 | }, | |||
111 | "python2.7-elasticsearch-2.3.0": { |
|
138 | "python2.7-elasticsearch-2.3.0": { | |
112 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" |
|
139 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |
113 | }, |
|
140 | }, | |
114 |
"python2.7-elasticsearch-dsl-2. |
|
141 | "python2.7-elasticsearch-dsl-2.2.0": { | |
115 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" |
|
142 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |
116 | }, |
|
143 | }, | |
|
144 | "python2.7-entrypoints-0.2.2": { | |||
|
145 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
146 | }, | |||
|
147 | "python2.7-enum34-1.1.6": { | |||
|
148 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
149 | }, | |||
|
150 | "python2.7-functools32-3.2.3.post2": { | |||
|
151 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" | |||
|
152 | }, | |||
117 | "python2.7-future-0.14.3": { |
|
153 | "python2.7-future-0.14.3": { | |
118 | "MIT License": "http://spdx.org/licenses/MIT" |
|
154 | "MIT License": "http://spdx.org/licenses/MIT" | |
119 | }, |
|
155 | }, | |
120 | "python2.7-futures-3.0.2": { |
|
156 | "python2.7-futures-3.0.2": { | |
121 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
157 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
122 | }, |
|
158 | }, | |
|
159 | "python2.7-gevent-1.1.2": { | |||
|
160 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
161 | }, | |||
123 | "python2.7-gnureadline-6.3.3": { |
|
162 | "python2.7-gnureadline-6.3.3": { | |
124 | "GNU General Public License v1.0 only": "http://spdx.org/licenses/GPL-1.0" |
|
163 | "GNU General Public License v1.0 only": "http://spdx.org/licenses/GPL-1.0" | |
125 | }, |
|
164 | }, | |
|
165 | "python2.7-gprof2dot-2016.10.13": { | |||
|
166 | "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+" | |||
|
167 | }, | |||
|
168 | "python2.7-greenlet-0.4.10": { | |||
|
169 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
170 | }, | |||
126 | "python2.7-gunicorn-19.6.0": { |
|
171 | "python2.7-gunicorn-19.6.0": { | |
127 | "MIT License": "http://spdx.org/licenses/MIT" |
|
172 | "MIT License": "http://spdx.org/licenses/MIT" | |
128 | }, |
|
173 | }, | |
|
174 | "python2.7-html5lib-0.9999999": { | |||
|
175 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
176 | }, | |||
129 | "python2.7-infrae.cache-1.0.1": { |
|
177 | "python2.7-infrae.cache-1.0.1": { | |
130 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" |
|
178 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" | |
131 | }, |
|
179 | }, | |
132 |
"python2.7-ipython- |
|
180 | "python2.7-ipython-5.1.0": { | |
|
181 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
182 | }, | |||
|
183 | "python2.7-ipython-genutils-0.2.0": { | |||
133 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
184 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
134 | }, |
|
185 | }, | |
135 | "python2.7-iso8601-0.1.11": { |
|
186 | "python2.7-iso8601-0.1.11": { | |
136 | "MIT License": "http://spdx.org/licenses/MIT" |
|
187 | "MIT License": "http://spdx.org/licenses/MIT" | |
137 | }, |
|
188 | }, | |
|
189 | "python2.7-itsdangerous-0.24": { | |||
|
190 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
191 | }, | |||
|
192 | "python2.7-jsonschema-2.6.0": { | |||
|
193 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
194 | }, | |||
|
195 | "python2.7-jupyter-client-5.0.0": { | |||
|
196 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
197 | }, | |||
|
198 | "python2.7-jupyter-core-4.3.0": { | |||
|
199 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
200 | }, | |||
138 | "python2.7-kombu-1.5.1": { |
|
201 | "python2.7-kombu-1.5.1": { | |
139 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
202 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
140 | }, |
|
203 | }, | |
141 |
"python2.7-m |
|
204 | "python2.7-mistune-0.7.4": { | |
|
205 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
206 | }, | |||
|
207 | "python2.7-msgpack-python-0.4.8": { | |||
142 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" |
|
208 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |
143 | }, |
|
209 | }, | |
|
210 | "python2.7-nbconvert-5.1.1": { | |||
|
211 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
212 | }, | |||
|
213 | "python2.7-nbformat-4.3.0": { | |||
|
214 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
215 | }, | |||
144 | "python2.7-packaging-15.2": { |
|
216 | "python2.7-packaging-15.2": { | |
145 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" |
|
217 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |
146 | }, |
|
218 | }, | |
147 |
"python2.7-p |
|
219 | "python2.7-pandocfilters-1.4.1": { | |
148 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
220 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
149 | }, |
|
221 | }, | |
150 |
"python2.7-p |
|
222 | "python2.7-pathlib2-2.1.0": { | |
|
223 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
224 | }, | |||
|
225 | "python2.7-peppercorn-0.5": { | |||
|
226 | "BSD-derived": "http://www.repoze.org/LICENSE.txt" | |||
|
227 | }, | |||
|
228 | "python2.7-pexpect-4.2.1": { | |||
|
229 | "ISC License": "http://spdx.org/licenses/ISC" | |||
|
230 | }, | |||
|
231 | "python2.7-pickleshare-0.7.4": { | |||
|
232 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
233 | }, | |||
|
234 | "python2.7-prompt-toolkit-1.0.14": { | |||
|
235 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
236 | }, | |||
|
237 | "python2.7-psutil-4.3.1": { | |||
|
238 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
239 | }, | |||
|
240 | "python2.7-psycopg2-2.6.1": { | |||
151 | "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+" |
|
241 | "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+" | |
152 | }, |
|
242 | }, | |
153 |
"python2.7-p |
|
243 | "python2.7-ptyprocess-0.5.1": { | |
|
244 | "ISC License": "http://opensource.org/licenses/ISC" | |||
|
245 | }, | |||
|
246 | "python2.7-py-1.4.31": { | |||
154 | "MIT License": "http://spdx.org/licenses/MIT" |
|
247 | "MIT License": "http://spdx.org/licenses/MIT" | |
155 | }, |
|
248 | }, | |
156 | "python2.7-py-bcrypt-0.4": { |
|
249 | "python2.7-py-bcrypt-0.4": { | |
157 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" |
|
250 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |
158 | }, |
|
251 | }, | |
|
252 | "python2.7-py-gfm-0.1.3.rhodecode-upstream1": { | |||
|
253 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
254 | }, | |||
159 | "python2.7-pycrypto-2.6.1": { |
|
255 | "python2.7-pycrypto-2.6.1": { | |
160 | "Public Domain": null |
|
256 | "Public Domain": null | |
161 | }, |
|
257 | }, | |
162 | "python2.7-pycurl-7.19.5": { |
|
258 | "python2.7-pycurl-7.19.5": { | |
163 | "MIT License": "http://spdx.org/licenses/MIT" |
|
259 | "MIT License": "http://spdx.org/licenses/MIT" | |
164 | }, |
|
260 | }, | |
|
261 | "python2.7-pygments-markdown-lexer-0.1.0.dev39": { | |||
|
262 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |||
|
263 | }, | |||
165 | "python2.7-pyparsing-1.5.7": { |
|
264 | "python2.7-pyparsing-1.5.7": { | |
166 | "MIT License": "http://spdx.org/licenses/MIT" |
|
265 | "MIT License": "http://spdx.org/licenses/MIT" | |
167 | }, |
|
266 | }, | |
168 |
"python2.7-pyramid-1. |
|
267 | "python2.7-pyramid-1.7.4": { | |
169 | "Repoze License": "http://www.repoze.org/LICENSE.txt" |
|
268 | "Repoze License": "http://www.repoze.org/LICENSE.txt" | |
170 | }, |
|
269 | }, | |
171 | "python2.7-pyramid-beaker-0.8": { |
|
270 | "python2.7-pyramid-beaker-0.8": { | |
172 | "Repoze License": "http://www.repoze.org/LICENSE.txt" |
|
271 | "Repoze License": "http://www.repoze.org/LICENSE.txt" | |
173 | }, |
|
272 | }, | |
174 |
"python2.7-pyramid-debugtoolbar- |
|
273 | "python2.7-pyramid-debugtoolbar-3.0.5": { | |
175 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause", |
|
274 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause", | |
176 | "Repoze License": "http://www.repoze.org/LICENSE.txt" |
|
275 | "Repoze License": "http://www.repoze.org/LICENSE.txt" | |
177 | }, |
|
276 | }, | |
|
277 | "python2.7-pyramid-jinja2-2.5": { | |||
|
278 | "BSD-derived": "http://www.repoze.org/LICENSE.txt" | |||
|
279 | }, | |||
178 | "python2.7-pyramid-mako-1.0.2": { |
|
280 | "python2.7-pyramid-mako-1.0.2": { | |
179 | "Repoze License": "http://www.repoze.org/LICENSE.txt" |
|
281 | "Repoze License": "http://www.repoze.org/LICENSE.txt" | |
180 | }, |
|
282 | }, | |
@@ -182,16 +284,25 b'' | |||||
182 | "libpng License": "http://spdx.org/licenses/Libpng", |
|
284 | "libpng License": "http://spdx.org/licenses/Libpng", | |
183 | "zlib License": "http://spdx.org/licenses/Zlib" |
|
285 | "zlib License": "http://spdx.org/licenses/Zlib" | |
184 | }, |
|
286 | }, | |
185 |
"python2.7-pytest- |
|
287 | "python2.7-pytest-3.0.5": { | |
|
288 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
289 | }, | |||
|
290 | "python2.7-pytest-profiling-1.2.2": { | |||
|
291 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
292 | }, | |||
|
293 | "python2.7-pytest-runner-2.9": { | |||
186 | "MIT License": "http://spdx.org/licenses/MIT" |
|
294 | "MIT License": "http://spdx.org/licenses/MIT" | |
187 | }, |
|
295 | }, | |
188 |
"python2.7-pytest- |
|
296 | "python2.7-pytest-sugar-0.7.1": { | |
|
297 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
298 | }, | |||
|
299 | "python2.7-pytest-timeout-1.2.0": { | |||
189 | "MIT License": "http://spdx.org/licenses/MIT" |
|
300 | "MIT License": "http://spdx.org/licenses/MIT" | |
190 | }, |
|
301 | }, | |
191 |
"python2.7-python-dateutil- |
|
302 | "python2.7-python-dateutil-2.1": { | |
192 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" |
|
303 | "Simplified BSD": null | |
193 | }, |
|
304 | }, | |
194 |
"python2.7-python-editor-1.0. |
|
305 | "python2.7-python-editor-1.0.3": { | |
195 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" |
|
306 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |
196 | }, |
|
307 | }, | |
197 | "python2.7-python-ldap-2.4.19": { |
|
308 | "python2.7-python-ldap-2.4.19": { | |
@@ -203,6 +314,9 b'' | |||||
203 | "python2.7-pytz-2015.4": { |
|
314 | "python2.7-pytz-2015.4": { | |
204 | "MIT License": "http://spdx.org/licenses/MIT" |
|
315 | "MIT License": "http://spdx.org/licenses/MIT" | |
205 | }, |
|
316 | }, | |
|
317 | "python2.7-pyzmq-14.6.0": { | |||
|
318 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
319 | }, | |||
206 | "python2.7-recaptcha-client-1.0.6": { |
|
320 | "python2.7-recaptcha-client-1.0.6": { | |
207 | "MIT License": "http://spdx.org/licenses/MIT" |
|
321 | "MIT License": "http://spdx.org/licenses/MIT" | |
208 | }, |
|
322 | }, | |
@@ -211,21 +325,35 b'' | |||||
211 | }, |
|
325 | }, | |
212 | "python2.7-requests-2.9.1": { |
|
326 | "python2.7-requests-2.9.1": { | |
213 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" |
|
327 | "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" | |
214 | }, |
|
328 | }, | |
215 | "python2.7-setuptools-19.4": { |
|
329 | "python2.7-setuptools-19.4": { | |
216 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0", |
|
330 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0", | |
217 | "Zope Public License 2.0": "http://spdx.org/licenses/ZPL-2.0" |
|
331 | "Zope Public License 2.0": "http://spdx.org/licenses/ZPL-2.0" | |
218 | }, |
|
332 | }, | |
219 |
"python2.7-setuptools-scm-1.1 |
|
333 | "python2.7-setuptools-scm-1.15.0": { | |
220 | "MIT License": "http://spdx.org/licenses/MIT" |
|
334 | "MIT License": "http://spdx.org/licenses/MIT" | |
221 | }, |
|
335 | }, | |
|
336 | "python2.7-simplegeneric-0.8.1": { | |||
|
337 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" | |||
|
338 | }, | |||
222 | "python2.7-simplejson-3.7.2": { |
|
339 | "python2.7-simplejson-3.7.2": { | |
223 | "Academic Free License": "http://spdx.org/licenses/AFL-2.1", |
|
|||
224 | "MIT License": "http://spdx.org/licenses/MIT" |
|
340 | "MIT License": "http://spdx.org/licenses/MIT" | |
225 | }, |
|
341 | }, | |
226 | "python2.7-six-1.9.0": { |
|
342 | "python2.7-six-1.9.0": { | |
227 | "MIT License": "http://spdx.org/licenses/MIT" |
|
343 | "MIT License": "http://spdx.org/licenses/MIT" | |
228 | }, |
|
344 | }, | |
|
345 | "python2.7-subprocess32-3.2.6": { | |||
|
346 | "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" | |||
|
347 | }, | |||
|
348 | "python2.7-termcolor-1.1.0": { | |||
|
349 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
350 | }, | |||
|
351 | "python2.7-testpath-0.1": { | |||
|
352 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
353 | }, | |||
|
354 | "python2.7-traitlets-4.3.2": { | |||
|
355 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
356 | }, | |||
229 | "python2.7-translationstring-1.3": { |
|
357 | "python2.7-translationstring-1.3": { | |
230 | "Repoze License": "http://www.repoze.org/LICENSE.txt" |
|
358 | "Repoze License": "http://www.repoze.org/LICENSE.txt" | |
231 | }, |
|
359 | }, | |
@@ -235,9 +363,15 b'' | |||||
235 | "python2.7-venusian-1.0": { |
|
363 | "python2.7-venusian-1.0": { | |
236 | "Repoze License": "http://www.repoze.org/LICENSE.txt" |
|
364 | "Repoze License": "http://www.repoze.org/LICENSE.txt" | |
237 | }, |
|
365 | }, | |
238 |
"python2.7-waitress- |
|
366 | "python2.7-waitress-1.0.1": { | |
239 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" |
|
367 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" | |
240 | }, |
|
368 | }, | |
|
369 | "python2.7-wcwidth-0.1.7": { | |||
|
370 | "MIT License": "http://spdx.org/licenses/MIT" | |||
|
371 | }, | |||
|
372 | "python2.7-ws4py-0.3.5": { | |||
|
373 | "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" | |||
|
374 | }, | |||
241 | "python2.7-zope.cachedescriptors-4.0.0": { |
|
375 | "python2.7-zope.cachedescriptors-4.0.0": { | |
242 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" |
|
376 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" | |
243 | }, |
|
377 | }, | |
@@ -246,5 +380,9 b'' | |||||
246 | }, |
|
380 | }, | |
247 | "python2.7-zope.interface-4.1.3": { |
|
381 | "python2.7-zope.interface-4.1.3": { | |
248 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" |
|
382 | "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" | |
|
383 | }, | |||
|
384 | "xz-5.2.2": { | |||
|
385 | "GNU General Public License v2.0 or later": "http://spdx.org/licenses/GPL-2.0+", | |||
|
386 | "GNU Library General Public License v2.1 or later": "http://spdx.org/licenses/LGPL-2.1+" | |||
249 | } |
|
387 | } | |
250 | } No newline at end of file |
|
388 | } |
@@ -39,11 +39,15 b' from routes.middleware import RoutesMidd' | |||||
39 | import routes.util |
|
39 | import routes.util | |
40 |
|
40 | |||
41 | import rhodecode |
|
41 | import rhodecode | |
|
42 | ||||
42 | from rhodecode.model import meta |
|
43 | from rhodecode.model import meta | |
43 | from rhodecode.config import patches |
|
44 | from rhodecode.config import patches | |
44 | from rhodecode.config.routing import STATIC_FILE_PREFIX |
|
45 | from rhodecode.config.routing import STATIC_FILE_PREFIX | |
45 | from rhodecode.config.environment import ( |
|
46 | from rhodecode.config.environment import ( | |
46 | load_environment, load_pyramid_environment) |
|
47 | load_environment, load_pyramid_environment) | |
|
48 | ||||
|
49 | from rhodecode.lib.vcs import VCSCommunicationError | |||
|
50 | from rhodecode.lib.exceptions import VCSServerUnavailable | |||
47 | from rhodecode.lib.middleware import csrf |
|
51 | from rhodecode.lib.middleware import csrf | |
48 | from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled |
|
52 | from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled | |
49 | from rhodecode.lib.middleware.error_handling import ( |
|
53 | from rhodecode.lib.middleware.error_handling import ( | |
@@ -51,10 +55,10 b' from rhodecode.lib.middleware.error_hand' | |||||
51 | from rhodecode.lib.middleware.https_fixup import HttpsFixup |
|
55 | from rhodecode.lib.middleware.https_fixup import HttpsFixup | |
52 | from rhodecode.lib.middleware.vcs import VCSMiddleware |
|
56 | from rhodecode.lib.middleware.vcs import VCSMiddleware | |
53 | from rhodecode.lib.plugins.utils import register_rhodecode_plugin |
|
57 | from rhodecode.lib.plugins.utils import register_rhodecode_plugin | |
54 | from rhodecode.lib.utils2 import aslist as rhodecode_aslist |
|
58 | from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict | |
55 | from rhodecode.subscribers import ( |
|
59 | from rhodecode.subscribers import ( | |
56 |
scan_repositories_if_enabled, write_ |
|
60 | scan_repositories_if_enabled, write_js_routes_if_enabled, | |
57 |
write_ |
|
61 | write_metadata_if_needed) | |
58 |
|
62 | |||
59 |
|
63 | |||
60 | log = logging.getLogger(__name__) |
|
64 | log = logging.getLogger(__name__) | |
@@ -221,7 +225,7 b' def add_pylons_compat_data(registry, glo' | |||||
221 |
|
225 | |||
222 | def error_handler(exception, request): |
|
226 | def error_handler(exception, request): | |
223 | import rhodecode |
|
227 | import rhodecode | |
224 |
from rhodecode.lib |
|
228 | from rhodecode.lib import helpers | |
225 |
|
229 | |||
226 | rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode' |
|
230 | rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode' | |
227 |
|
231 | |||
@@ -229,6 +233,8 b' def error_handler(exception, request):' | |||||
229 | # prefer original exception for the response since it may have headers set |
|
233 | # prefer original exception for the response since it may have headers set | |
230 | if isinstance(exception, HTTPException): |
|
234 | if isinstance(exception, HTTPException): | |
231 | base_response = exception |
|
235 | base_response = exception | |
|
236 | elif isinstance(exception, VCSCommunicationError): | |||
|
237 | base_response = VCSServerUnavailable() | |||
232 |
|
238 | |||
233 | def is_http_error(response): |
|
239 | def is_http_error(response): | |
234 | # error which should have traceback |
|
240 | # error which should have traceback | |
@@ -255,9 +261,10 b' def error_handler(exception, request):' | |||||
255 | c.causes = [] |
|
261 | c.causes = [] | |
256 | if hasattr(base_response, 'causes'): |
|
262 | if hasattr(base_response, 'causes'): | |
257 | c.causes = base_response.causes |
|
263 | c.causes = base_response.causes | |
|
264 | c.messages = helpers.flash.pop_messages() | |||
258 |
|
265 | |||
259 | response = render_to_response( |
|
266 | response = render_to_response( | |
260 | '/errors/error_document.mako', {'c': c}, request=request, |
|
267 | '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request, | |
261 | response=base_response) |
|
268 | response=base_response) | |
262 |
|
269 | |||
263 | return response |
|
270 | return response | |
@@ -284,11 +291,15 b' def includeme(config):' | |||||
284 |
|
291 | |||
285 | # apps |
|
292 | # apps | |
286 | config.include('rhodecode.apps._base') |
|
293 | config.include('rhodecode.apps._base') | |
|
294 | config.include('rhodecode.apps.ops') | |||
287 |
|
295 | |||
288 | config.include('rhodecode.apps.admin') |
|
296 | config.include('rhodecode.apps.admin') | |
289 | config.include('rhodecode.apps.channelstream') |
|
297 | config.include('rhodecode.apps.channelstream') | |
290 | config.include('rhodecode.apps.login') |
|
298 | config.include('rhodecode.apps.login') | |
|
299 | config.include('rhodecode.apps.home') | |||
291 | config.include('rhodecode.apps.repository') |
|
300 | config.include('rhodecode.apps.repository') | |
|
301 | config.include('rhodecode.apps.repo_group') | |||
|
302 | config.include('rhodecode.apps.search') | |||
292 | config.include('rhodecode.apps.user_profile') |
|
303 | config.include('rhodecode.apps.user_profile') | |
293 | config.include('rhodecode.apps.my_account') |
|
304 | config.include('rhodecode.apps.my_account') | |
294 | config.include('rhodecode.apps.svn_support') |
|
305 | config.include('rhodecode.apps.svn_support') | |
@@ -307,6 +318,12 b' def includeme(config):' | |||||
307 | config.add_subscriber(write_metadata_if_needed, ApplicationCreated) |
|
318 | config.add_subscriber(write_metadata_if_needed, ApplicationCreated) | |
308 | config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated) |
|
319 | config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated) | |
309 |
|
320 | |||
|
321 | # events | |||
|
322 | # TODO(marcink): this should be done when pyramid migration is finished | |||
|
323 | # config.add_subscriber( | |||
|
324 | # 'rhodecode.integrations.integrations_event_handler', | |||
|
325 | # 'rhodecode.events.RhodecodeEvent') | |||
|
326 | ||||
310 | # Set the authorization policy. |
|
327 | # Set the authorization policy. | |
311 | authz_policy = ACLAuthorizationPolicy() |
|
328 | authz_policy = ACLAuthorizationPolicy() | |
312 | config.set_authorization_policy(authz_policy) |
|
329 | config.set_authorization_policy(authz_policy) | |
@@ -314,6 +331,10 b' def includeme(config):' | |||||
314 | # Set the default renderer for HTML templates to mako. |
|
331 | # Set the default renderer for HTML templates to mako. | |
315 | config.add_mako_renderer('.html') |
|
332 | config.add_mako_renderer('.html') | |
316 |
|
333 | |||
|
334 | config.add_renderer( | |||
|
335 | name='json_ext', | |||
|
336 | factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json') | |||
|
337 | ||||
317 | # include RhodeCode plugins |
|
338 | # include RhodeCode plugins | |
318 | includes = aslist(settings.get('rhodecode.includes', [])) |
|
339 | includes = aslist(settings.get('rhodecode.includes', [])) | |
319 | for inc in includes: |
|
340 | for inc in includes: | |
@@ -395,7 +416,6 b' def wrap_app_in_wsgi_middlewares(pyramid' | |||||
395 | pool = meta.Base.metadata.bind.engine.pool |
|
416 | pool = meta.Base.metadata.bind.engine.pool | |
396 | log.debug('sa pool status: %s', pool.status()) |
|
417 | log.debug('sa pool status: %s', pool.status()) | |
397 |
|
418 | |||
398 |
|
||||
399 | return pyramid_app_with_cleanup |
|
419 | return pyramid_app_with_cleanup | |
400 |
|
420 | |||
401 |
|
421 |
@@ -32,8 +32,6 b' import os' | |||||
32 | import re |
|
32 | import re | |
33 | from routes import Mapper |
|
33 | from routes import Mapper | |
34 |
|
34 | |||
35 | from rhodecode.config import routing_links |
|
|||
36 |
|
||||
37 | # prefix for non repository related links needs to be prefixed with `/` |
|
35 | # prefix for non repository related links needs to be prefixed with `/` | |
38 | ADMIN_PREFIX = '/_admin' |
|
36 | ADMIN_PREFIX = '/_admin' | |
39 | STATIC_FILE_PREFIX = '/_static' |
|
37 | STATIC_FILE_PREFIX = '/_static' | |
@@ -119,8 +117,9 b' class JSRoutesMapper(Mapper):' | |||||
119 |
|
117 | |||
120 | def make_map(config): |
|
118 | def make_map(config): | |
121 | """Create, configure and return the routes Mapper""" |
|
119 | """Create, configure and return the routes Mapper""" | |
122 | rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'], |
|
120 | rmap = JSRoutesMapper( | |
123 | always_scan=config['debug']) |
|
121 | directory=config['pylons.paths']['controllers'], | |
|
122 | always_scan=config['debug']) | |||
124 | rmap.minimization = False |
|
123 | rmap.minimization = False | |
125 | rmap.explicit = False |
|
124 | rmap.explicit = False | |
126 |
|
125 | |||
@@ -186,36 +185,7 b' def make_map(config):' | |||||
186 | # CUSTOM ROUTES HERE |
|
185 | # CUSTOM ROUTES HERE | |
187 | #========================================================================== |
|
186 | #========================================================================== | |
188 |
|
187 | |||
189 | # MAIN PAGE |
|
188 | # ping and pylons error test | |
190 | rmap.connect('home', '/', controller='home', action='index', jsroute=True) |
|
|||
191 | rmap.connect('goto_switcher_data', '/_goto_data', controller='home', |
|
|||
192 | action='goto_switcher_data') |
|
|||
193 | rmap.connect('repo_list_data', '/_repos', controller='home', |
|
|||
194 | action='repo_list_data') |
|
|||
195 |
|
||||
196 | rmap.connect('user_autocomplete_data', '/_users', controller='home', |
|
|||
197 | action='user_autocomplete_data', jsroute=True) |
|
|||
198 | rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home', |
|
|||
199 | action='user_group_autocomplete_data', jsroute=True) |
|
|||
200 |
|
||||
201 | # TODO: johbo: Static links, to be replaced by our redirection mechanism |
|
|||
202 | rmap.connect('rst_help', |
|
|||
203 | 'http://docutils.sourceforge.net/docs/user/rst/quickref.html', |
|
|||
204 | _static=True) |
|
|||
205 | rmap.connect('markdown_help', |
|
|||
206 | 'http://daringfireball.net/projects/markdown/syntax', |
|
|||
207 | _static=True) |
|
|||
208 | rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True) |
|
|||
209 | rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True) |
|
|||
210 | rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True) |
|
|||
211 | # TODO: anderson - making this a static link since redirect won't play |
|
|||
212 | # nice with POST requests |
|
|||
213 | rmap.connect('enterprise_license_convert_from_old', |
|
|||
214 | 'https://rhodecode.com/u/license-upgrade', |
|
|||
215 | _static=True) |
|
|||
216 |
|
||||
217 | routing_links.connect_redirection_links(rmap) |
|
|||
218 |
|
||||
219 | rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping') |
|
189 | rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping') | |
220 | rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test') |
|
190 | rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test') | |
221 |
|
191 | |||
@@ -228,10 +198,6 b' def make_map(config):' | |||||
228 | action='index', conditions={'method': ['GET']}) |
|
198 | action='index', conditions={'method': ['GET']}) | |
229 | m.connect('new_repo', '/create_repository', jsroute=True, |
|
199 | m.connect('new_repo', '/create_repository', jsroute=True, | |
230 | action='create_repository', conditions={'method': ['GET']}) |
|
200 | action='create_repository', conditions={'method': ['GET']}) | |
231 | m.connect('/repos/{repo_name}', |
|
|||
232 | action='update', conditions={'method': ['PUT'], |
|
|||
233 | 'function': check_repo}, |
|
|||
234 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
235 | m.connect('delete_repo', '/repos/{repo_name}', |
|
201 | m.connect('delete_repo', '/repos/{repo_name}', | |
236 | action='delete', conditions={'method': ['DELETE']}, |
|
202 | action='delete', conditions={'method': ['DELETE']}, | |
237 | requirements=URL_NAME_REQUIREMENTS) |
|
203 | requirements=URL_NAME_REQUIREMENTS) | |
@@ -321,19 +287,6 b' def make_map(config):' | |||||
321 | m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary', |
|
287 | m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary', | |
322 | action='edit_perms_summary', conditions={'method': ['GET']}) |
|
288 | action='edit_perms_summary', conditions={'method': ['GET']}) | |
323 |
|
289 | |||
324 | m.connect('edit_user_emails', '/users/{user_id}/edit/emails', |
|
|||
325 | action='edit_emails', conditions={'method': ['GET']}) |
|
|||
326 | m.connect('edit_user_emails', '/users/{user_id}/edit/emails', |
|
|||
327 | action='add_email', conditions={'method': ['PUT']}) |
|
|||
328 | m.connect('edit_user_emails', '/users/{user_id}/edit/emails', |
|
|||
329 | action='delete_email', conditions={'method': ['DELETE']}) |
|
|||
330 |
|
||||
331 | m.connect('edit_user_ips', '/users/{user_id}/edit/ips', |
|
|||
332 | action='edit_ips', conditions={'method': ['GET']}) |
|
|||
333 | m.connect('edit_user_ips', '/users/{user_id}/edit/ips', |
|
|||
334 | action='add_ip', conditions={'method': ['PUT']}) |
|
|||
335 | m.connect('edit_user_ips', '/users/{user_id}/edit/ips', |
|
|||
336 | action='delete_ip', conditions={'method': ['DELETE']}) |
|
|||
337 |
|
290 | |||
338 | # ADMIN USER GROUPS REST ROUTES |
|
291 | # ADMIN USER GROUPS REST ROUTES | |
339 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
292 | with rmap.submapper(path_prefix=ADMIN_PREFIX, | |
@@ -519,37 +472,9 b' def make_map(config):' | |||||
519 | m.connect('my_account_password', '/my_account/password', |
|
472 | m.connect('my_account_password', '/my_account/password', | |
520 | action='my_account_password', conditions={'method': ['GET']}) |
|
473 | action='my_account_password', conditions={'method': ['GET']}) | |
521 |
|
474 | |||
522 | m.connect('my_account_repos', '/my_account/repos', |
|
|||
523 | action='my_account_repos', conditions={'method': ['GET']}) |
|
|||
524 |
|
||||
525 | m.connect('my_account_watched', '/my_account/watched', |
|
|||
526 | action='my_account_watched', conditions={'method': ['GET']}) |
|
|||
527 |
|
||||
528 | m.connect('my_account_pullrequests', '/my_account/pull_requests', |
|
475 | m.connect('my_account_pullrequests', '/my_account/pull_requests', | |
529 | action='my_account_pullrequests', conditions={'method': ['GET']}) |
|
476 | action='my_account_pullrequests', conditions={'method': ['GET']}) | |
530 |
|
477 | |||
531 | m.connect('my_account_perms', '/my_account/perms', |
|
|||
532 | action='my_account_perms', conditions={'method': ['GET']}) |
|
|||
533 |
|
||||
534 | m.connect('my_account_emails', '/my_account/emails', |
|
|||
535 | action='my_account_emails', conditions={'method': ['GET']}) |
|
|||
536 | m.connect('my_account_emails', '/my_account/emails', |
|
|||
537 | action='my_account_emails_add', conditions={'method': ['POST']}) |
|
|||
538 | m.connect('my_account_emails', '/my_account/emails', |
|
|||
539 | action='my_account_emails_delete', conditions={'method': ['DELETE']}) |
|
|||
540 |
|
||||
541 | m.connect('my_account_notifications', '/my_account/notifications', |
|
|||
542 | action='my_notifications', |
|
|||
543 | conditions={'method': ['GET']}) |
|
|||
544 | m.connect('my_account_notifications_toggle_visibility', |
|
|||
545 | '/my_account/toggle_visibility', |
|
|||
546 | action='my_notifications_toggle_visibility', |
|
|||
547 | conditions={'method': ['POST']}) |
|
|||
548 | m.connect('my_account_notifications_test_channelstream', |
|
|||
549 | '/my_account/test_channelstream', |
|
|||
550 | action='my_account_notifications_test_channelstream', |
|
|||
551 | conditions={'method': ['POST']}) |
|
|||
552 |
|
||||
553 | # NOTIFICATION REST ROUTES |
|
478 | # NOTIFICATION REST ROUTES | |
554 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
479 | with rmap.submapper(path_prefix=ADMIN_PREFIX, | |
555 | controller='admin/notifications') as m: |
|
480 | controller='admin/notifications') as m: | |
@@ -597,22 +522,6 b' def make_map(config):' | |||||
597 | action='show', conditions={'method': ['GET']}, |
|
522 | action='show', conditions={'method': ['GET']}, | |
598 | requirements=URL_NAME_REQUIREMENTS) |
|
523 | requirements=URL_NAME_REQUIREMENTS) | |
599 |
|
524 | |||
600 | # ADMIN MAIN PAGES |
|
|||
601 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
|||
602 | controller='admin/admin') as m: |
|
|||
603 | m.connect('admin_home', '', action='index') |
|
|||
604 | m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', |
|
|||
605 | action='add_repo') |
|
|||
606 | m.connect( |
|
|||
607 | 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}', |
|
|||
608 | action='pull_requests') |
|
|||
609 | m.connect( |
|
|||
610 | 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}', |
|
|||
611 | action='pull_requests') |
|
|||
612 | m.connect( |
|
|||
613 | 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}', |
|
|||
614 | action='pull_requests') |
|
|||
615 |
|
||||
616 | # USER JOURNAL |
|
525 | # USER JOURNAL | |
617 | rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), |
|
526 | rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), | |
618 | controller='journal', action='index') |
|
527 | controller='journal', action='index') | |
@@ -642,15 +551,6 b' def make_map(config):' | |||||
642 | controller='journal', action='toggle_following', jsroute=True, |
|
551 | controller='journal', action='toggle_following', jsroute=True, | |
643 | conditions={'method': ['POST']}) |
|
552 | conditions={'method': ['POST']}) | |
644 |
|
553 | |||
645 | # FULL TEXT SEARCH |
|
|||
646 | rmap.connect('search', '%s/search' % (ADMIN_PREFIX,), |
|
|||
647 | controller='search') |
|
|||
648 | rmap.connect('search_repo_home', '/{repo_name}/search', |
|
|||
649 | controller='search', |
|
|||
650 | action='index', |
|
|||
651 | conditions={'function': check_repo}, |
|
|||
652 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
653 |
|
||||
654 | # FEEDS |
|
554 | # FEEDS | |
655 | rmap.connect('rss_feed_home', '/{repo_name}/feed/rss', |
|
555 | rmap.connect('rss_feed_home', '/{repo_name}/feed/rss', | |
656 | controller='feed', action='rss', |
|
556 | controller='feed', action='rss', | |
@@ -673,21 +573,6 b' def make_map(config):' | |||||
673 | controller='admin/repos', action='repo_check', |
|
573 | controller='admin/repos', action='repo_check', | |
674 | requirements=URL_NAME_REQUIREMENTS) |
|
574 | requirements=URL_NAME_REQUIREMENTS) | |
675 |
|
575 | |||
676 | rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}', |
|
|||
677 | controller='summary', action='repo_stats', |
|
|||
678 | conditions={'function': check_repo}, |
|
|||
679 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) |
|
|||
680 |
|
||||
681 | rmap.connect('repo_refs_data', '/{repo_name}/refs-data', |
|
|||
682 | controller='summary', action='repo_refs_data', |
|
|||
683 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) |
|
|||
684 | rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog', |
|
|||
685 | controller='summary', action='repo_refs_changelog_data', |
|
|||
686 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) |
|
|||
687 | rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers', |
|
|||
688 | controller='summary', action='repo_default_reviewers_data', |
|
|||
689 | jsroute=True, requirements=URL_NAME_REQUIREMENTS) |
|
|||
690 |
|
||||
691 | rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}', |
|
576 | rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}', | |
692 | controller='changeset', revision='tip', |
|
577 | controller='changeset', revision='tip', | |
693 | conditions={'function': check_repo}, |
|
578 | conditions={'function': check_repo}, | |
@@ -702,21 +587,6 b' def make_map(config):' | |||||
702 | requirements=URL_NAME_REQUIREMENTS) |
|
587 | requirements=URL_NAME_REQUIREMENTS) | |
703 |
|
588 | |||
704 | # repo edit options |
|
589 | # repo edit options | |
705 | rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True, |
|
|||
706 | controller='admin/repos', action='edit', |
|
|||
707 | conditions={'method': ['GET'], 'function': check_repo}, |
|
|||
708 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
709 |
|
||||
710 | rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions', |
|
|||
711 | jsroute=True, |
|
|||
712 | controller='admin/repos', action='edit_permissions', |
|
|||
713 | conditions={'method': ['GET'], 'function': check_repo}, |
|
|||
714 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
715 | rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions', |
|
|||
716 | controller='admin/repos', action='edit_permissions_update', |
|
|||
717 | conditions={'method': ['PUT'], 'function': check_repo}, |
|
|||
718 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
719 |
|
||||
720 | rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields', |
|
590 | rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields', | |
721 | controller='admin/repos', action='edit_fields', |
|
591 | controller='admin/repos', action='edit_fields', | |
722 | conditions={'method': ['GET'], 'function': check_repo}, |
|
592 | conditions={'method': ['GET'], 'function': check_repo}, | |
@@ -730,39 +600,11 b' def make_map(config):' | |||||
730 | conditions={'method': ['DELETE'], 'function': check_repo}, |
|
600 | conditions={'method': ['DELETE'], 'function': check_repo}, | |
731 | requirements=URL_NAME_REQUIREMENTS) |
|
601 | requirements=URL_NAME_REQUIREMENTS) | |
732 |
|
602 | |||
733 | rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced', |
|
|||
734 | controller='admin/repos', action='edit_advanced', |
|
|||
735 | conditions={'method': ['GET'], 'function': check_repo}, |
|
|||
736 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
737 |
|
||||
738 | rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking', |
|
|||
739 | controller='admin/repos', action='edit_advanced_locking', |
|
|||
740 | conditions={'method': ['PUT'], 'function': check_repo}, |
|
|||
741 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
742 | rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle', |
|
603 | rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle', | |
743 | controller='admin/repos', action='toggle_locking', |
|
604 | controller='admin/repos', action='toggle_locking', | |
744 | conditions={'method': ['GET'], 'function': check_repo}, |
|
605 | conditions={'method': ['GET'], 'function': check_repo}, | |
745 | requirements=URL_NAME_REQUIREMENTS) |
|
606 | requirements=URL_NAME_REQUIREMENTS) | |
746 |
|
607 | |||
747 | rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal', |
|
|||
748 | controller='admin/repos', action='edit_advanced_journal', |
|
|||
749 | conditions={'method': ['PUT'], 'function': check_repo}, |
|
|||
750 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
751 |
|
||||
752 | rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork', |
|
|||
753 | controller='admin/repos', action='edit_advanced_fork', |
|
|||
754 | conditions={'method': ['PUT'], 'function': check_repo}, |
|
|||
755 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
756 |
|
||||
757 | rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches', |
|
|||
758 | controller='admin/repos', action='edit_caches_form', |
|
|||
759 | conditions={'method': ['GET'], 'function': check_repo}, |
|
|||
760 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
761 | rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches', |
|
|||
762 | controller='admin/repos', action='edit_caches', |
|
|||
763 | conditions={'method': ['PUT'], 'function': check_repo}, |
|
|||
764 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
765 |
|
||||
766 | rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote', |
|
608 | rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote', | |
767 | controller='admin/repos', action='edit_remote_form', |
|
609 | controller='admin/repos', action='edit_remote_form', | |
768 | conditions={'method': ['GET'], 'function': check_repo}, |
|
610 | conditions={'method': ['GET'], 'function': check_repo}, | |
@@ -931,13 +773,6 b' def make_map(config):' | |||||
931 | 'method': ['DELETE']}, |
|
773 | 'method': ['DELETE']}, | |
932 | requirements=URL_NAME_REQUIREMENTS) |
|
774 | requirements=URL_NAME_REQUIREMENTS) | |
933 |
|
775 | |||
934 | rmap.connect('pullrequest_show_all', |
|
|||
935 | '/{repo_name}/pull-request', |
|
|||
936 | controller='pullrequests', |
|
|||
937 | action='show_all', conditions={'function': check_repo, |
|
|||
938 | 'method': ['GET']}, |
|
|||
939 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) |
|
|||
940 |
|
||||
941 | rmap.connect('pullrequest_comment', |
|
776 | rmap.connect('pullrequest_comment', | |
942 | '/{repo_name}/pull-request-comment/{pull_request_id}', |
|
777 | '/{repo_name}/pull-request-comment/{pull_request_id}', | |
943 | controller='pullrequests', |
|
778 | controller='pullrequests', | |
@@ -951,31 +786,10 b' def make_map(config):' | |||||
951 | conditions={'function': check_repo, 'method': ['DELETE']}, |
|
786 | conditions={'function': check_repo, 'method': ['DELETE']}, | |
952 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) |
|
787 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
953 |
|
788 | |||
954 | rmap.connect('summary_home_explicit', '/{repo_name}/summary', |
|
|||
955 | controller='summary', conditions={'function': check_repo}, |
|
|||
956 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
957 |
|
||||
958 | rmap.connect('branches_home', '/{repo_name}/branches', |
|
|||
959 | controller='branches', conditions={'function': check_repo}, |
|
|||
960 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
961 |
|
||||
962 | rmap.connect('tags_home', '/{repo_name}/tags', |
|
|||
963 | controller='tags', conditions={'function': check_repo}, |
|
|||
964 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
965 |
|
||||
966 | rmap.connect('bookmarks_home', '/{repo_name}/bookmarks', |
|
|||
967 | controller='bookmarks', conditions={'function': check_repo}, |
|
|||
968 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
969 |
|
||||
970 | rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True, |
|
789 | rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True, | |
971 | controller='changelog', conditions={'function': check_repo}, |
|
790 | controller='changelog', conditions={'function': check_repo}, | |
972 | requirements=URL_NAME_REQUIREMENTS) |
|
791 | requirements=URL_NAME_REQUIREMENTS) | |
973 |
|
792 | |||
974 | rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary', |
|
|||
975 | controller='changelog', action='changelog_summary', |
|
|||
976 | conditions={'function': check_repo}, |
|
|||
977 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
978 |
|
||||
979 | rmap.connect('changelog_file_home', |
|
793 | rmap.connect('changelog_file_home', | |
980 | '/{repo_name}/changelog/{revision}/{f_path}', |
|
794 | '/{repo_name}/changelog/{revision}/{f_path}', | |
981 | controller='changelog', f_path=None, |
|
795 | controller='changelog', f_path=None, | |
@@ -1128,26 +942,4 b' def make_map(config):' | |||||
1128 | conditions={'function': check_repo}, |
|
942 | conditions={'function': check_repo}, | |
1129 | requirements=URL_NAME_REQUIREMENTS) |
|
943 | requirements=URL_NAME_REQUIREMENTS) | |
1130 |
|
944 | |||
1131 | # must be here for proper group/repo catching pattern |
|
|||
1132 | _connect_with_slash( |
|
|||
1133 | rmap, 'repo_group_home', '/{group_name}', |
|
|||
1134 | controller='home', action='index_repo_group', |
|
|||
1135 | conditions={'function': check_group}, |
|
|||
1136 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
1137 |
|
||||
1138 | # catch all, at the end |
|
|||
1139 | _connect_with_slash( |
|
|||
1140 | rmap, 'summary_home', '/{repo_name}', jsroute=True, |
|
|||
1141 | controller='summary', action='index', |
|
|||
1142 | conditions={'function': check_repo}, |
|
|||
1143 | requirements=URL_NAME_REQUIREMENTS) |
|
|||
1144 |
|
||||
1145 | return rmap |
|
945 | return rmap | |
1146 |
|
||||
1147 |
|
||||
1148 | def _connect_with_slash(mapper, name, path, *args, **kwargs): |
|
|||
1149 | """ |
|
|||
1150 | Connect a route with an optional trailing slash in `path`. |
|
|||
1151 | """ |
|
|||
1152 | mapper.connect(name + '_slash', path + '/', *args, **kwargs) |
|
|||
1153 | mapper.connect(name, path, *args, **kwargs) |
|
@@ -46,27 +46,56 b' you can see it working.' | |||||
46 | # flake8: noqa |
|
46 | # flake8: noqa | |
47 | from __future__ import unicode_literals |
|
47 | from __future__ import unicode_literals | |
48 |
|
48 | |||
|
49 | link_config = [ | |||
|
50 | { | |||
|
51 | "name": "enterprise_docs", | |||
|
52 | "target": "https://rhodecode.com/r1/enterprise/docs/", | |||
|
53 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/", | |||
|
54 | }, | |||
|
55 | { | |||
|
56 | "name": "enterprise_log_file_locations", | |||
|
57 | "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/", | |||
|
58 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files", | |||
|
59 | }, | |||
|
60 | { | |||
|
61 | "name": "enterprise_issue_tracker_settings", | |||
|
62 | "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/", | |||
|
63 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html", | |||
|
64 | }, | |||
|
65 | { | |||
|
66 | "name": "enterprise_svn_setup", | |||
|
67 | "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/", | |||
|
68 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html", | |||
|
69 | }, | |||
|
70 | { | |||
|
71 | "name": "rst_help", | |||
|
72 | "target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html", | |||
|
73 | "external_target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html", | |||
|
74 | }, | |||
|
75 | { | |||
|
76 | "name": "markdown_help", | |||
|
77 | "target": "https://daringfireball.net/projects/markdown/syntax", | |||
|
78 | "external_target": "https://daringfireball.net/projects/markdown/syntax", | |||
|
79 | }, | |||
|
80 | { | |||
|
81 | "name": "rhodecode_official", | |||
|
82 | "target": "https://rhodecode.com", | |||
|
83 | "external_target": "https://rhodecode.com/", | |||
|
84 | }, | |||
|
85 | { | |||
|
86 | "name": "rhodecode_support", | |||
|
87 | "target": "https://rhodecode.com/help/", | |||
|
88 | "external_target": "https://rhodecode.com/support", | |||
|
89 | }, | |||
|
90 | { | |||
|
91 | "name": "rhodecode_translations", | |||
|
92 | "target": "https://rhodecode.com/translate/enterprise", | |||
|
93 | "external_target": "https://www.transifex.com/rhodecode/RhodeCode/", | |||
|
94 | }, | |||
49 |
|
95 | |||
50 | link_config = [ |
|
|||
51 | {"name": "enterprise_docs", |
|
|||
52 | "target": "https://rhodecode.com/r1/enterprise/docs/", |
|
|||
53 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/", |
|
|||
54 | }, |
|
|||
55 | {"name": "enterprise_log_file_locations", |
|
|||
56 | "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/", |
|
|||
57 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files", |
|
|||
58 | }, |
|
|||
59 | {"name": "enterprise_issue_tracker_settings", |
|
|||
60 | "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/", |
|
|||
61 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html", |
|
|||
62 | }, |
|
|||
63 | {"name": "enterprise_svn_setup", |
|
|||
64 | "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/", |
|
|||
65 | "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html", |
|
|||
66 | }, |
|
|||
67 | ] |
|
96 | ] | |
68 |
|
97 | |||
69 |
|
98 | |||
70 |
def connect_redirection_links( |
|
99 | def connect_redirection_links(config): | |
71 | for link in link_config: |
|
100 | for link in link_config: | |
72 |
|
|
101 | config.add_route(link['name'], link['target'], static=True) |
@@ -24,35 +24,27 b' my account controller for RhodeCode admi' | |||||
24 | """ |
|
24 | """ | |
25 |
|
25 | |||
26 | import logging |
|
26 | import logging | |
27 | import datetime |
|
|||
28 |
|
27 | |||
29 | import formencode |
|
28 | import formencode | |
30 | from formencode import htmlfill |
|
29 | from formencode import htmlfill | |
31 | from pyramid.threadlocal import get_current_registry |
|
|||
32 | from pyramid.httpexceptions import HTTPFound |
|
30 | from pyramid.httpexceptions import HTTPFound | |
33 |
|
31 | |||
34 |
from pylons import request, tmpl_context as c |
|
32 | from pylons import request, tmpl_context as c | |
35 | from pylons.controllers.util import redirect |
|
33 | from pylons.controllers.util import redirect | |
36 | from pylons.i18n.translation import _ |
|
34 | from pylons.i18n.translation import _ | |
37 | from sqlalchemy.orm import joinedload |
|
|||
38 |
|
35 | |||
39 | from rhodecode.lib import helpers as h |
|
36 | from rhodecode.lib import helpers as h | |
40 | from rhodecode.lib import auth |
|
37 | from rhodecode.lib import auth | |
41 | from rhodecode.lib.auth import ( |
|
38 | from rhodecode.lib.auth import ( | |
42 | LoginRequired, NotAnonymous, AuthUser) |
|
39 | LoginRequired, NotAnonymous, AuthUser) | |
43 | from rhodecode.lib.base import BaseController, render |
|
40 | from rhodecode.lib.base import BaseController, render | |
44 | from rhodecode.lib.utils import jsonify |
|
|||
45 | from rhodecode.lib.utils2 import safe_int, str2bool |
|
41 | from rhodecode.lib.utils2 import safe_int, str2bool | |
46 | from rhodecode.lib.ext_json import json |
|
42 | from rhodecode.lib.ext_json import json | |
47 | from rhodecode.lib.channelstream import channelstream_request, \ |
|
|||
48 | ChannelstreamException |
|
|||
49 |
|
43 | |||
50 | from rhodecode.model.db import ( |
|
44 | from rhodecode.model.db import ( | |
51 | Repository, PullRequest, UserEmailMap, User, UserFollowing) |
|
45 | Repository, PullRequest, UserEmailMap, User, UserFollowing) | |
52 | from rhodecode.model.forms import UserForm |
|
46 | from rhodecode.model.forms import UserForm | |
53 | from rhodecode.model.scm import RepoList |
|
|||
54 | from rhodecode.model.user import UserModel |
|
47 | from rhodecode.model.user import UserModel | |
55 | from rhodecode.model.repo import RepoModel |
|
|||
56 | from rhodecode.model.meta import Session |
|
48 | from rhodecode.model.meta import Session | |
57 | from rhodecode.model.pull_request import PullRequestModel |
|
49 | from rhodecode.model.pull_request import PullRequestModel | |
58 | from rhodecode.model.comment import CommentsModel |
|
50 | from rhodecode.model.comment import CommentsModel | |
@@ -82,26 +74,6 b' class MyAccountController(BaseController' | |||||
82 | c.auth_user = AuthUser( |
|
74 | c.auth_user = AuthUser( | |
83 | user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) |
|
75 | user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) | |
84 |
|
76 | |||
85 | def _load_my_repos_data(self, watched=False): |
|
|||
86 | if watched: |
|
|||
87 | admin = False |
|
|||
88 | follows_repos = Session().query(UserFollowing)\ |
|
|||
89 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
|||
90 | .options(joinedload(UserFollowing.follows_repository))\ |
|
|||
91 | .all() |
|
|||
92 | repo_list = [x.follows_repository for x in follows_repos] |
|
|||
93 | else: |
|
|||
94 | admin = True |
|
|||
95 | repo_list = Repository.get_all_repos( |
|
|||
96 | user_id=c.rhodecode_user.user_id) |
|
|||
97 | repo_list = RepoList(repo_list, perm_set=[ |
|
|||
98 | 'repository.read', 'repository.write', 'repository.admin']) |
|
|||
99 |
|
||||
100 | repos_data = RepoModel().get_repos_as_dict( |
|
|||
101 | repo_list=repo_list, admin=admin) |
|
|||
102 | # json used to render the grid |
|
|||
103 | return json.dumps(repos_data) |
|
|||
104 |
|
||||
105 | @auth.CSRFRequired() |
|
77 | @auth.CSRFRequired() | |
106 | def my_account_update(self): |
|
78 | def my_account_update(self): | |
107 | """ |
|
79 | """ | |
@@ -181,65 +153,6 b' class MyAccountController(BaseController' | |||||
181 | force_defaults=False |
|
153 | force_defaults=False | |
182 | ) |
|
154 | ) | |
183 |
|
155 | |||
184 | def my_account_repos(self): |
|
|||
185 | c.active = 'repos' |
|
|||
186 | self.__load_data() |
|
|||
187 |
|
||||
188 | # json used to render the grid |
|
|||
189 | c.data = self._load_my_repos_data() |
|
|||
190 | return render('admin/my_account/my_account.mako') |
|
|||
191 |
|
||||
192 | def my_account_watched(self): |
|
|||
193 | c.active = 'watched' |
|
|||
194 | self.__load_data() |
|
|||
195 |
|
||||
196 | # json used to render the grid |
|
|||
197 | c.data = self._load_my_repos_data(watched=True) |
|
|||
198 | return render('admin/my_account/my_account.mako') |
|
|||
199 |
|
||||
200 | def my_account_perms(self): |
|
|||
201 | c.active = 'perms' |
|
|||
202 | self.__load_data() |
|
|||
203 | c.perm_user = c.auth_user |
|
|||
204 |
|
||||
205 | return render('admin/my_account/my_account.mako') |
|
|||
206 |
|
||||
207 | def my_account_emails(self): |
|
|||
208 | c.active = 'emails' |
|
|||
209 | self.__load_data() |
|
|||
210 |
|
||||
211 | c.user_email_map = UserEmailMap.query()\ |
|
|||
212 | .filter(UserEmailMap.user == c.user).all() |
|
|||
213 | return render('admin/my_account/my_account.mako') |
|
|||
214 |
|
||||
215 | @auth.CSRFRequired() |
|
|||
216 | def my_account_emails_add(self): |
|
|||
217 | email = request.POST.get('new_email') |
|
|||
218 |
|
||||
219 | try: |
|
|||
220 | UserModel().add_extra_email(c.rhodecode_user.user_id, email) |
|
|||
221 | Session().commit() |
|
|||
222 | h.flash(_("Added new email address `%s` for user account") % email, |
|
|||
223 | category='success') |
|
|||
224 | except formencode.Invalid as error: |
|
|||
225 | msg = error.error_dict['email'] |
|
|||
226 | h.flash(msg, category='error') |
|
|||
227 | except Exception: |
|
|||
228 | log.exception("Exception in my_account_emails") |
|
|||
229 | h.flash(_('An error occurred during email saving'), |
|
|||
230 | category='error') |
|
|||
231 | return redirect(url('my_account_emails')) |
|
|||
232 |
|
||||
233 | @auth.CSRFRequired() |
|
|||
234 | def my_account_emails_delete(self): |
|
|||
235 | email_id = request.POST.get('del_email_id') |
|
|||
236 | user_model = UserModel() |
|
|||
237 | user_model.delete_extra_email(c.rhodecode_user.user_id, email_id) |
|
|||
238 | Session().commit() |
|
|||
239 | h.flash(_("Removed email address from user account"), |
|
|||
240 | category='success') |
|
|||
241 | return redirect(url('my_account_emails')) |
|
|||
242 |
|
||||
243 | def _extract_ordering(self, request): |
|
156 | def _extract_ordering(self, request): | |
244 | column_index = safe_int(request.GET.get('order[0][column]')) |
|
157 | column_index = safe_int(request.GET.get('order[0][column]')) | |
245 | order_dir = request.GET.get('order[0][dir]', 'desc') |
|
158 | order_dir = request.GET.get('order[0][dir]', 'desc') | |
@@ -320,45 +233,4 b' class MyAccountController(BaseController' | |||||
320 | else: |
|
233 | else: | |
321 | return json.dumps(data) |
|
234 | return json.dumps(data) | |
322 |
|
235 | |||
323 | def my_notifications(self): |
|
|||
324 | c.active = 'notifications' |
|
|||
325 | return render('admin/my_account/my_account.mako') |
|
|||
326 |
|
236 | |||
327 | @auth.CSRFRequired() |
|
|||
328 | @jsonify |
|
|||
329 | def my_notifications_toggle_visibility(self): |
|
|||
330 | user = c.rhodecode_user.get_instance() |
|
|||
331 | new_status = not user.user_data.get('notification_status', True) |
|
|||
332 | user.update_userdata(notification_status=new_status) |
|
|||
333 | Session().commit() |
|
|||
334 | return user.user_data['notification_status'] |
|
|||
335 |
|
||||
336 | @auth.CSRFRequired() |
|
|||
337 | @jsonify |
|
|||
338 | def my_account_notifications_test_channelstream(self): |
|
|||
339 | message = 'Test message sent via Channelstream by user: {}, on {}'.format( |
|
|||
340 | c.rhodecode_user.username, datetime.datetime.now()) |
|
|||
341 | payload = { |
|
|||
342 | 'type': 'message', |
|
|||
343 | 'timestamp': datetime.datetime.utcnow(), |
|
|||
344 | 'user': 'system', |
|
|||
345 | #'channel': 'broadcast', |
|
|||
346 | 'pm_users': [c.rhodecode_user.username], |
|
|||
347 | 'message': { |
|
|||
348 | 'message': message, |
|
|||
349 | 'level': 'info', |
|
|||
350 | 'topic': '/notifications' |
|
|||
351 | } |
|
|||
352 | } |
|
|||
353 |
|
||||
354 | registry = get_current_registry() |
|
|||
355 | rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) |
|
|||
356 | channelstream_config = rhodecode_plugins.get('channelstream', {}) |
|
|||
357 |
|
||||
358 | try: |
|
|||
359 | channelstream_request(channelstream_config, [payload], '/message') |
|
|||
360 | except ChannelstreamException as e: |
|
|||
361 | log.exception('Failed to send channelstream data') |
|
|||
362 | return {"response": 'ERROR: {}'.format(e.__class__.__name__)} |
|
|||
363 | return {"response": 'Channelstream data sent. ' |
|
|||
364 | 'You should see a new live message now.'} |
|
@@ -48,10 +48,6 b' log = logging.getLogger(__name__)' | |||||
48 |
|
48 | |||
49 | class NotificationsController(BaseController): |
|
49 | class NotificationsController(BaseController): | |
50 | """REST Controller styled on the Atom Publishing Protocol""" |
|
50 | """REST Controller styled on the Atom Publishing Protocol""" | |
51 | # To properly map this controller, ensure your config/routing.py |
|
|||
52 | # file has a resource setup: |
|
|||
53 | # map.resource('notification', 'notifications', controller='_admin/notifications', |
|
|||
54 | # path_prefix='/_admin', name_prefix='_admin_') |
|
|||
55 |
|
51 | |||
56 | @LoginRequired() |
|
52 | @LoginRequired() | |
57 | @NotAnonymous() |
|
53 | @NotAnonymous() | |
@@ -62,8 +58,8 b' class NotificationsController(BaseContro' | |||||
62 | """GET /_admin/notifications: All items in the collection""" |
|
58 | """GET /_admin/notifications: All items in the collection""" | |
63 | # url('notifications') |
|
59 | # url('notifications') | |
64 | c.user = c.rhodecode_user |
|
60 | c.user = c.rhodecode_user | |
65 |
notif = NotificationModel().get_for_user( |
|
61 | notif = NotificationModel().get_for_user( | |
66 |
|
|
62 | c.rhodecode_user.user_id, filter_=request.GET.getall('type')) | |
67 |
|
63 | |||
68 | p = safe_int(request.GET.get('page', 1), 1) |
|
64 | p = safe_int(request.GET.get('page', 1), 1) | |
69 | notifications_url = webhelpers.paginate.PageURL( |
|
65 | notifications_url = webhelpers.paginate.PageURL( | |
@@ -86,7 +82,6 b' class NotificationsController(BaseContro' | |||||
86 |
|
82 | |||
87 | return render('admin/notifications/notifications.mako') |
|
83 | return render('admin/notifications/notifications.mako') | |
88 |
|
84 | |||
89 |
|
||||
90 | @auth.CSRFRequired() |
|
85 | @auth.CSRFRequired() | |
91 | def mark_all_read(self): |
|
86 | def mark_all_read(self): | |
92 | if request.is_xhr: |
|
87 | if request.is_xhr: | |
@@ -115,15 +110,8 b' class NotificationsController(BaseContro' | |||||
115 |
|
110 | |||
116 | @auth.CSRFRequired() |
|
111 | @auth.CSRFRequired() | |
117 | def update(self, notification_id): |
|
112 | def update(self, notification_id): | |
118 | """PUT /_admin/notifications/id: Update an existing item""" |
|
113 | no = Notification.get_or_404(notification_id) | |
119 | # Forms posted to this method should contain a hidden field: |
|
|||
120 | # <input type="hidden" name="_method" value="PUT" /> |
|
|||
121 | # Or using helpers: |
|
|||
122 | # h.form(url('notification', notification_id=ID), |
|
|||
123 | # method='put') |
|
|||
124 | # url('notification', notification_id=ID) |
|
|||
125 | try: |
|
114 | try: | |
126 | no = Notification.get(notification_id) |
|
|||
127 | if self._has_permissions(no): |
|
115 | if self._has_permissions(no): | |
128 | # deletes only notification2user |
|
116 | # deletes only notification2user | |
129 | NotificationModel().mark_read(c.rhodecode_user.user_id, no) |
|
117 | NotificationModel().mark_read(c.rhodecode_user.user_id, no) | |
@@ -136,15 +124,8 b' class NotificationsController(BaseContro' | |||||
136 |
|
124 | |||
137 | @auth.CSRFRequired() |
|
125 | @auth.CSRFRequired() | |
138 | def delete(self, notification_id): |
|
126 | def delete(self, notification_id): | |
139 | """DELETE /_admin/notifications/id: Delete an existing item""" |
|
127 | no = Notification.get_or_404(notification_id) | |
140 | # Forms posted to this method should contain a hidden field: |
|
|||
141 | # <input type="hidden" name="_method" value="DELETE" /> |
|
|||
142 | # Or using helpers: |
|
|||
143 | # h.form(url('notification', notification_id=ID), |
|
|||
144 | # method='delete') |
|
|||
145 | # url('notification', notification_id=ID) |
|
|||
146 | try: |
|
128 | try: | |
147 | no = Notification.get(notification_id) |
|
|||
148 | if self._has_permissions(no): |
|
129 | if self._has_permissions(no): | |
149 | # deletes only notification2user |
|
130 | # deletes only notification2user | |
150 | NotificationModel().delete(c.rhodecode_user.user_id, no) |
|
131 | NotificationModel().delete(c.rhodecode_user.user_id, no) | |
@@ -156,10 +137,8 b' class NotificationsController(BaseContro' | |||||
156 | raise HTTPBadRequest() |
|
137 | raise HTTPBadRequest() | |
157 |
|
138 | |||
158 | def show(self, notification_id): |
|
139 | def show(self, notification_id): | |
159 | """GET /_admin/notifications/id: Show a specific item""" |
|
|||
160 | # url('notification', notification_id=ID) |
|
|||
161 | c.user = c.rhodecode_user |
|
140 | c.user = c.rhodecode_user | |
162 | no = Notification.get(notification_id) |
|
141 | no = Notification.get_or_404(notification_id) | |
163 |
|
142 | |||
164 | if no and self._has_permissions(no): |
|
143 | if no and self._has_permissions(no): | |
165 | unotification = NotificationModel()\ |
|
144 | unotification = NotificationModel()\ |
@@ -64,12 +64,7 b' class PermissionsController(BaseControll' | |||||
64 | c.active = 'application' |
|
64 | c.active = 'application' | |
65 | self.__load_data() |
|
65 | self.__load_data() | |
66 |
|
66 | |||
67 | c.user = User.get_default_user() |
|
67 | c.user = User.get_default_user(refresh=True) | |
68 |
|
||||
69 | # TODO: johbo: The default user might be based on outdated state which |
|
|||
70 | # has been loaded from the cache. A call to refresh() ensures that the |
|
|||
71 | # latest state from the database is used. |
|
|||
72 | Session().refresh(c.user) |
|
|||
73 |
|
68 | |||
74 | app_settings = SettingsModel().get_all_settings() |
|
69 | app_settings = SettingsModel().get_all_settings() | |
75 | defaults = { |
|
70 | defaults = { |
@@ -34,17 +34,18 b' from pylons.i18n.translation import _, u' | |||||
34 |
|
34 | |||
35 | from rhodecode.lib import auth |
|
35 | from rhodecode.lib import auth | |
36 | from rhodecode.lib import helpers as h |
|
36 | from rhodecode.lib import helpers as h | |
|
37 | from rhodecode.lib import audit_logger | |||
37 | from rhodecode.lib.ext_json import json |
|
38 | from rhodecode.lib.ext_json import json | |
38 | from rhodecode.lib.auth import ( |
|
39 | from rhodecode.lib.auth import ( | |
39 | LoginRequired, NotAnonymous, HasPermissionAll, |
|
40 | LoginRequired, NotAnonymous, HasPermissionAll, | |
40 | HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator) |
|
41 | HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator) | |
41 | from rhodecode.lib.base import BaseController, render |
|
42 | from rhodecode.lib.base import BaseController, render | |
|
43 | from rhodecode.lib.utils2 import safe_int | |||
42 | from rhodecode.model.db import RepoGroup, User |
|
44 | from rhodecode.model.db import RepoGroup, User | |
43 | from rhodecode.model.scm import RepoGroupList |
|
45 | from rhodecode.model.scm import RepoGroupList | |
44 | from rhodecode.model.repo_group import RepoGroupModel |
|
46 | from rhodecode.model.repo_group import RepoGroupModel | |
45 | from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm |
|
47 | from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm | |
46 | from rhodecode.model.meta import Session |
|
48 | from rhodecode.model.meta import Session | |
47 | from rhodecode.lib.utils2 import safe_int |
|
|||
48 |
|
49 | |||
49 |
|
50 | |||
50 | log = logging.getLogger(__name__) |
|
51 | log = logging.getLogger(__name__) | |
@@ -153,9 +154,6 b' class RepoGroupsController(BaseControlle' | |||||
153 |
|
154 | |||
154 | @NotAnonymous() |
|
155 | @NotAnonymous() | |
155 | def index(self): |
|
156 | def index(self): | |
156 | """GET /repo_groups: All items in the collection""" |
|
|||
157 | # url('repo_groups') |
|
|||
158 |
|
||||
159 | repo_group_list = RepoGroup.get_all_repo_groups() |
|
157 | repo_group_list = RepoGroup.get_all_repo_groups() | |
160 | _perms = ['group.admin'] |
|
158 | _perms = ['group.admin'] | |
161 | repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) |
|
159 | repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) | |
@@ -168,8 +166,6 b' class RepoGroupsController(BaseControlle' | |||||
168 | @NotAnonymous() |
|
166 | @NotAnonymous() | |
169 | @auth.CSRFRequired() |
|
167 | @auth.CSRFRequired() | |
170 | def create(self): |
|
168 | def create(self): | |
171 | """POST /repo_groups: Create a new item""" |
|
|||
172 | # url('repo_groups') |
|
|||
173 |
|
169 | |||
174 | parent_group_id = safe_int(request.POST.get('group_parent_id')) |
|
170 | parent_group_id = safe_int(request.POST.get('group_parent_id')) | |
175 | can_create = self._can_create_repo_group(parent_group_id) |
|
171 | can_create = self._can_create_repo_group(parent_group_id) | |
@@ -183,20 +179,29 b' class RepoGroupsController(BaseControlle' | |||||
183 | try: |
|
179 | try: | |
184 | owner = c.rhodecode_user |
|
180 | owner = c.rhodecode_user | |
185 | form_result = repo_group_form.to_python(dict(request.POST)) |
|
181 | form_result = repo_group_form.to_python(dict(request.POST)) | |
186 | RepoGroupModel().create( |
|
182 | repo_group = RepoGroupModel().create( | |
187 | group_name=form_result['group_name_full'], |
|
183 | group_name=form_result['group_name_full'], | |
188 | group_description=form_result['group_description'], |
|
184 | group_description=form_result['group_description'], | |
189 | owner=owner.user_id, |
|
185 | owner=owner.user_id, | |
190 | copy_permissions=form_result['group_copy_permissions'] |
|
186 | copy_permissions=form_result['group_copy_permissions'] | |
191 | ) |
|
187 | ) | |
|
188 | Session().flush() | |||
|
189 | ||||
|
190 | repo_group_data = repo_group.get_api_data() | |||
|
191 | audit_logger.store_web( | |||
|
192 | 'repo_group.create', action_data={'data': repo_group_data}, | |||
|
193 | user=c.rhodecode_user) | |||
|
194 | ||||
192 | Session().commit() |
|
195 | Session().commit() | |
|
196 | ||||
193 | _new_group_name = form_result['group_name_full'] |
|
197 | _new_group_name = form_result['group_name_full'] | |
|
198 | ||||
194 | repo_group_url = h.link_to( |
|
199 | repo_group_url = h.link_to( | |
195 | _new_group_name, |
|
200 | _new_group_name, | |
196 |
h. |
|
201 | h.route_path('repo_group_home', repo_group_name=_new_group_name)) | |
197 | h.flash(h.literal(_('Created repository group %s') |
|
202 | h.flash(h.literal(_('Created repository group %s') | |
198 | % repo_group_url), category='success') |
|
203 | % repo_group_url), category='success') | |
199 | # TODO: in futureaction_logger(, '', '', '', self.sa) |
|
204 | ||
200 | except formencode.Invalid as errors: |
|
205 | except formencode.Invalid as errors: | |
201 | return htmlfill.render( |
|
206 | return htmlfill.render( | |
202 | render('admin/repo_groups/repo_group_add.mako'), |
|
207 | render('admin/repo_groups/repo_group_add.mako'), | |
@@ -216,8 +221,6 b' class RepoGroupsController(BaseControlle' | |||||
216 | # perm checks inside |
|
221 | # perm checks inside | |
217 | @NotAnonymous() |
|
222 | @NotAnonymous() | |
218 | def new(self): |
|
223 | def new(self): | |
219 | """GET /repo_groups/new: Form to create a new item""" |
|
|||
220 | # url('new_repo_group') |
|
|||
221 | # perm check for admin, create_group perm or admin of parent_group |
|
224 | # perm check for admin, create_group perm or admin of parent_group | |
222 | parent_group_id = safe_int(request.GET.get('parent_group')) |
|
225 | parent_group_id = safe_int(request.GET.get('parent_group')) | |
223 | if not self._can_create_repo_group(parent_group_id): |
|
226 | if not self._can_create_repo_group(parent_group_id): | |
@@ -229,12 +232,6 b' class RepoGroupsController(BaseControlle' | |||||
229 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
232 | @HasRepoGroupPermissionAnyDecorator('group.admin') | |
230 | @auth.CSRFRequired() |
|
233 | @auth.CSRFRequired() | |
231 | def update(self, group_name): |
|
234 | def update(self, group_name): | |
232 | """PUT /repo_groups/group_name: Update an existing item""" |
|
|||
233 | # Forms posted to this method should contain a hidden field: |
|
|||
234 | # <input type="hidden" name="_method" value="PUT" /> |
|
|||
235 | # Or using helpers: |
|
|||
236 | # h.form(url('repos_group', group_name=GROUP_NAME), method='put') |
|
|||
237 | # url('repo_group_home', group_name=GROUP_NAME) |
|
|||
238 |
|
235 | |||
239 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
236 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) | |
240 | can_create_in_root = self._can_create_repo_group() |
|
237 | can_create_in_root = self._can_create_repo_group() | |
@@ -250,16 +247,21 b' class RepoGroupsController(BaseControlle' | |||||
250 | available_groups=c.repo_groups_choices, |
|
247 | available_groups=c.repo_groups_choices, | |
251 | can_create_in_root=can_create_in_root, allow_disabled=True)() |
|
248 | can_create_in_root=can_create_in_root, allow_disabled=True)() | |
252 |
|
249 | |||
|
250 | old_values = c.repo_group.get_api_data() | |||
253 | try: |
|
251 | try: | |
254 | form_result = repo_group_form.to_python(dict(request.POST)) |
|
252 | form_result = repo_group_form.to_python(dict(request.POST)) | |
255 | gr_name = form_result['group_name'] |
|
253 | gr_name = form_result['group_name'] | |
256 | new_gr = RepoGroupModel().update(group_name, form_result) |
|
254 | new_gr = RepoGroupModel().update(group_name, form_result) | |
|
255 | ||||
|
256 | audit_logger.store_web( | |||
|
257 | 'repo_group.edit', action_data={'old_data': old_values}, | |||
|
258 | user=c.rhodecode_user) | |||
|
259 | ||||
257 | Session().commit() |
|
260 | Session().commit() | |
258 | h.flash(_('Updated repository group %s') % (gr_name,), |
|
261 | h.flash(_('Updated repository group %s') % (gr_name,), | |
259 | category='success') |
|
262 | category='success') | |
260 | # we now have new name ! |
|
263 | # we now have new name ! | |
261 | group_name = new_gr.group_name |
|
264 | group_name = new_gr.group_name | |
262 | # TODO: in future action_logger(, '', '', '', self.sa) |
|
|||
263 | except formencode.Invalid as errors: |
|
265 | except formencode.Invalid as errors: | |
264 | c.active = 'settings' |
|
266 | c.active = 'settings' | |
265 | return htmlfill.render( |
|
267 | return htmlfill.render( | |
@@ -279,13 +281,6 b' class RepoGroupsController(BaseControlle' | |||||
279 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
281 | @HasRepoGroupPermissionAnyDecorator('group.admin') | |
280 | @auth.CSRFRequired() |
|
282 | @auth.CSRFRequired() | |
281 | def delete(self, group_name): |
|
283 | def delete(self, group_name): | |
282 | """DELETE /repo_groups/group_name: Delete an existing item""" |
|
|||
283 | # Forms posted to this method should contain a hidden field: |
|
|||
284 | # <input type="hidden" name="_method" value="DELETE" /> |
|
|||
285 | # Or using helpers: |
|
|||
286 | # h.form(url('repos_group', group_name=GROUP_NAME), method='delete') |
|
|||
287 | # url('repo_group_home', group_name=GROUP_NAME) |
|
|||
288 |
|
||||
289 | gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
284 | gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name) | |
290 | repos = gr.repositories.all() |
|
285 | repos = gr.repositories.all() | |
291 | if repos: |
|
286 | if repos: | |
@@ -307,11 +302,16 b' class RepoGroupsController(BaseControlle' | |||||
307 | return redirect(url('repo_groups')) |
|
302 | return redirect(url('repo_groups')) | |
308 |
|
303 | |||
309 | try: |
|
304 | try: | |
|
305 | old_values = gr.get_api_data() | |||
310 | RepoGroupModel().delete(group_name) |
|
306 | RepoGroupModel().delete(group_name) | |
|
307 | ||||
|
308 | audit_logger.store_web( | |||
|
309 | 'repo_group.delete', action_data={'old_data': old_values}, | |||
|
310 | user=c.rhodecode_user) | |||
|
311 | ||||
311 | Session().commit() |
|
312 | Session().commit() | |
312 | h.flash(_('Removed repository group %s') % group_name, |
|
313 | h.flash(_('Removed repository group %s') % group_name, | |
313 | category='success') |
|
314 | category='success') | |
314 | # TODO: in future action_logger(, '', '', '', self.sa) |
|
|||
315 | except Exception: |
|
315 | except Exception: | |
316 | log.exception("Exception during deletion of repository group") |
|
316 | log.exception("Exception during deletion of repository group") | |
317 | h.flash(_('Error occurred during deletion of repository group %s') |
|
317 | h.flash(_('Error occurred during deletion of repository group %s') | |
@@ -321,8 +321,7 b' class RepoGroupsController(BaseControlle' | |||||
321 |
|
321 | |||
322 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
322 | @HasRepoGroupPermissionAnyDecorator('group.admin') | |
323 | def edit(self, group_name): |
|
323 | def edit(self, group_name): | |
324 | """GET /repo_groups/group_name/edit: Form to edit an existing item""" |
|
324 | ||
325 | # url('edit_repo_group', group_name=GROUP_NAME) |
|
|||
326 | c.active = 'settings' |
|
325 | c.active = 'settings' | |
327 |
|
326 | |||
328 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
327 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) | |
@@ -346,8 +345,6 b' class RepoGroupsController(BaseControlle' | |||||
346 |
|
345 | |||
347 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
346 | @HasRepoGroupPermissionAnyDecorator('group.admin') | |
348 | def edit_repo_group_advanced(self, group_name): |
|
347 | def edit_repo_group_advanced(self, group_name): | |
349 | """GET /repo_groups/group_name/edit: Form to edit an existing item""" |
|
|||
350 | # url('edit_repo_group', group_name=GROUP_NAME) |
|
|||
351 | c.active = 'advanced' |
|
348 | c.active = 'advanced' | |
352 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
349 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) | |
353 |
|
350 | |||
@@ -355,8 +352,6 b' class RepoGroupsController(BaseControlle' | |||||
355 |
|
352 | |||
356 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
353 | @HasRepoGroupPermissionAnyDecorator('group.admin') | |
357 | def edit_repo_group_perms(self, group_name): |
|
354 | def edit_repo_group_perms(self, group_name): | |
358 | """GET /repo_groups/group_name/edit: Form to edit an existing item""" |
|
|||
359 | # url('edit_repo_group', group_name=GROUP_NAME) |
|
|||
360 | c.active = 'perms' |
|
355 | c.active = 'perms' | |
361 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
356 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) | |
362 | self.__load_defaults() |
|
357 | self.__load_defaults() | |
@@ -374,8 +369,6 b' class RepoGroupsController(BaseControlle' | |||||
374 | def update_perms(self, group_name): |
|
369 | def update_perms(self, group_name): | |
375 | """ |
|
370 | """ | |
376 | Update permissions for given repository group |
|
371 | Update permissions for given repository group | |
377 |
|
||||
378 | :param group_name: |
|
|||
379 | """ |
|
372 | """ | |
380 |
|
373 | |||
381 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
374 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) | |
@@ -393,14 +386,20 b' class RepoGroupsController(BaseControlle' | |||||
393 | # iterate over all members(if in recursive mode) of this groups and |
|
386 | # iterate over all members(if in recursive mode) of this groups and | |
394 | # set the permissions ! |
|
387 | # set the permissions ! | |
395 | # this can be potentially heavy operation |
|
388 | # this can be potentially heavy operation | |
396 | RepoGroupModel().update_permissions( |
|
389 | changes = RepoGroupModel().update_permissions( | |
397 | c.repo_group, |
|
390 | c.repo_group, | |
398 | form['perm_additions'], form['perm_updates'], |
|
391 | form['perm_additions'], form['perm_updates'], form['perm_deletions'], | |
399 |
|
|
392 | form['recursive']) | |
400 |
|
393 | |||
401 | # TODO: implement this |
|
394 | action_data = { | |
402 | # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', |
|
395 | 'added': changes['added'], | |
403 | # repo_name, self.ip_addr, self.sa) |
|
396 | 'updated': changes['updated'], | |
|
397 | 'deleted': changes['deleted'], | |||
|
398 | } | |||
|
399 | audit_logger.store_web( | |||
|
400 | 'repo_group.edit.permissions', action_data=action_data, | |||
|
401 | user=c.rhodecode_user) | |||
|
402 | ||||
404 | Session().commit() |
|
403 | Session().commit() | |
405 | h.flash(_('Repository Group permissions updated'), category='success') |
|
404 | h.flash(_('Repository Group permissions updated'), category='success') | |
406 | return redirect(url('edit_repo_group_perms', group_name=group_name)) |
|
405 | return redirect(url('edit_repo_group_perms', group_name=group_name)) |
@@ -41,15 +41,11 b' from rhodecode.lib.auth import (' | |||||
41 | HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator) |
|
41 | HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator) | |
42 | from rhodecode.lib.base import BaseRepoController, render |
|
42 | from rhodecode.lib.base import BaseRepoController, render | |
43 | from rhodecode.lib.ext_json import json |
|
43 | from rhodecode.lib.ext_json import json | |
44 |
from rhodecode.lib. |
|
44 | from rhodecode.lib.utils import repo_name_slug, jsonify | |
45 | from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify |
|
|||
46 | from rhodecode.lib.utils2 import safe_int, str2bool |
|
45 | from rhodecode.lib.utils2 import safe_int, str2bool | |
47 |
from rhodecode.l |
|
46 | from rhodecode.model.db import (Repository, RepoGroup, RepositoryField) | |
48 | from rhodecode.model.db import ( |
|
|||
49 | User, Repository, UserFollowing, RepoGroup, RepositoryField) |
|
|||
50 | from rhodecode.model.forms import ( |
|
47 | from rhodecode.model.forms import ( | |
51 |
RepoForm, RepoFieldForm, Repo |
|
48 | RepoForm, RepoFieldForm, RepoVcsSettingsForm, IssueTrackerPatternsForm) | |
52 | IssueTrackerPatternsForm) |
|
|||
53 | from rhodecode.model.meta import Session |
|
49 | from rhodecode.model.meta import Session | |
54 | from rhodecode.model.repo import RepoModel |
|
50 | from rhodecode.model.repo import RepoModel | |
55 | from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList |
|
51 | from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList | |
@@ -185,7 +181,7 b' class ReposController(BaseRepoController' | |||||
185 | except Exception as e: |
|
181 | except Exception as e: | |
186 | msg = self._log_creation_exception(e, form_result.get('repo_name')) |
|
182 | msg = self._log_creation_exception(e, form_result.get('repo_name')) | |
187 | h.flash(msg, category='error') |
|
183 | h.flash(msg, category='error') | |
188 |
return redirect( |
|
184 | return redirect(h.route_path('home')) | |
189 |
|
185 | |||
190 | return redirect(h.url('repo_creating_home', |
|
186 | return redirect(h.url('repo_creating_home', | |
191 | repo_name=form_result['repo_name_full'], |
|
187 | repo_name=form_result['repo_name_full'], | |
@@ -265,7 +261,7 b' class ReposController(BaseRepoController' | |||||
265 | if task.failed(): |
|
261 | if task.failed(): | |
266 | msg = self._log_creation_exception(task.result, c.repo) |
|
262 | msg = self._log_creation_exception(task.result, c.repo) | |
267 | h.flash(msg, category='error') |
|
263 | h.flash(msg, category='error') | |
268 |
return redirect( |
|
264 | return redirect(h.route_path('home'), code=501) | |
269 |
|
265 | |||
270 | repo = Repository.get_by_repo_name(repo_name) |
|
266 | repo = Repository.get_by_repo_name(repo_name) | |
271 | if repo and repo.repo_state == Repository.STATE_CREATED: |
|
267 | if repo and repo.repo_state == Repository.STATE_CREATED: | |
@@ -274,9 +270,9 b' class ReposController(BaseRepoController' | |||||
274 | h.flash(_('Created repository %s from %s') |
|
270 | h.flash(_('Created repository %s from %s') | |
275 | % (repo.repo_name, clone_uri), category='success') |
|
271 | % (repo.repo_name, clone_uri), category='success') | |
276 | else: |
|
272 | else: | |
277 |
repo_url = h.link_to( |
|
273 | repo_url = h.link_to( | |
278 | h.url('summary_home', |
|
274 | repo.repo_name, | |
279 |
|
|
275 | h.route_path('repo_summary',repo_name=repo.repo_name)) | |
280 | fork = repo.fork |
|
276 | fork = repo.fork | |
281 | if fork: |
|
277 | if fork: | |
282 | fork_name = fork.repo_name |
|
278 | fork_name = fork.repo_name | |
@@ -288,165 +284,14 b' class ReposController(BaseRepoController' | |||||
288 | return {'result': True} |
|
284 | return {'result': True} | |
289 | return {'result': False} |
|
285 | return {'result': False} | |
290 |
|
286 | |||
291 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
292 | @auth.CSRFRequired() |
|
|||
293 | def update(self, repo_name): |
|
|||
294 | """ |
|
|||
295 | PUT /repos/repo_name: Update an existing item""" |
|
|||
296 | # Forms posted to this method should contain a hidden field: |
|
|||
297 | # <input type="hidden" name="_method" value="PUT" /> |
|
|||
298 | # Or using helpers: |
|
|||
299 | # h.form(url('repo', repo_name=ID), |
|
|||
300 | # method='put') |
|
|||
301 | # url('repo', repo_name=ID) |
|
|||
302 |
|
||||
303 | self.__load_data(repo_name) |
|
|||
304 | c.active = 'settings' |
|
|||
305 | c.repo_fields = RepositoryField.query()\ |
|
|||
306 | .filter(RepositoryField.repository == c.repo_info).all() |
|
|||
307 |
|
||||
308 | repo_model = RepoModel() |
|
|||
309 | changed_name = repo_name |
|
|||
310 |
|
||||
311 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
|||
312 | # override the choices with extracted revisions ! |
|
|||
313 | repo = Repository.get_by_repo_name(repo_name) |
|
|||
314 | old_data = { |
|
|||
315 | 'repo_name': repo_name, |
|
|||
316 | 'repo_group': repo.group.get_dict() if repo.group else {}, |
|
|||
317 | 'repo_type': repo.repo_type, |
|
|||
318 | } |
|
|||
319 | _form = RepoForm( |
|
|||
320 | edit=True, old_data=old_data, repo_groups=c.repo_groups_choices, |
|
|||
321 | landing_revs=c.landing_revs_choices, allow_disabled=True)() |
|
|||
322 |
|
||||
323 | try: |
|
|||
324 | form_result = _form.to_python(dict(request.POST)) |
|
|||
325 | repo = repo_model.update(repo_name, **form_result) |
|
|||
326 | ScmModel().mark_for_invalidation(repo_name) |
|
|||
327 | h.flash(_('Repository %s updated successfully') % repo_name, |
|
|||
328 | category='success') |
|
|||
329 | changed_name = repo.repo_name |
|
|||
330 | action_logger(c.rhodecode_user, 'admin_updated_repo', |
|
|||
331 | changed_name, self.ip_addr, self.sa) |
|
|||
332 | Session().commit() |
|
|||
333 | except formencode.Invalid as errors: |
|
|||
334 | defaults = self.__load_data(repo_name) |
|
|||
335 | defaults.update(errors.value) |
|
|||
336 | return htmlfill.render( |
|
|||
337 | render('admin/repos/repo_edit.mako'), |
|
|||
338 | defaults=defaults, |
|
|||
339 | errors=errors.error_dict or {}, |
|
|||
340 | prefix_error=False, |
|
|||
341 | encoding="UTF-8", |
|
|||
342 | force_defaults=False) |
|
|||
343 |
|
||||
344 | except Exception: |
|
|||
345 | log.exception("Exception during update of repository") |
|
|||
346 | h.flash(_('Error occurred during update of repository %s') \ |
|
|||
347 | % repo_name, category='error') |
|
|||
348 | return redirect(url('edit_repo', repo_name=changed_name)) |
|
|||
349 |
|
||||
350 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
351 | @auth.CSRFRequired() |
|
|||
352 | def delete(self, repo_name): |
|
|||
353 | """ |
|
|||
354 | DELETE /repos/repo_name: Delete an existing item""" |
|
|||
355 | # Forms posted to this method should contain a hidden field: |
|
|||
356 | # <input type="hidden" name="_method" value="DELETE" /> |
|
|||
357 | # Or using helpers: |
|
|||
358 | # h.form(url('repo', repo_name=ID), |
|
|||
359 | # method='delete') |
|
|||
360 | # url('repo', repo_name=ID) |
|
|||
361 |
|
||||
362 | repo_model = RepoModel() |
|
|||
363 | repo = repo_model.get_by_repo_name(repo_name) |
|
|||
364 | if not repo: |
|
|||
365 | h.not_mapped_error(repo_name) |
|
|||
366 | return redirect(url('repos')) |
|
|||
367 | try: |
|
|||
368 | _forks = repo.forks.count() |
|
|||
369 | handle_forks = None |
|
|||
370 | if _forks and request.POST.get('forks'): |
|
|||
371 | do = request.POST['forks'] |
|
|||
372 | if do == 'detach_forks': |
|
|||
373 | handle_forks = 'detach' |
|
|||
374 | h.flash(_('Detached %s forks') % _forks, category='success') |
|
|||
375 | elif do == 'delete_forks': |
|
|||
376 | handle_forks = 'delete' |
|
|||
377 | h.flash(_('Deleted %s forks') % _forks, category='success') |
|
|||
378 | repo_model.delete(repo, forks=handle_forks) |
|
|||
379 | action_logger(c.rhodecode_user, 'admin_deleted_repo', |
|
|||
380 | repo_name, self.ip_addr, self.sa) |
|
|||
381 | ScmModel().mark_for_invalidation(repo_name) |
|
|||
382 | h.flash(_('Deleted repository %s') % repo_name, category='success') |
|
|||
383 | Session().commit() |
|
|||
384 | except AttachedForksError: |
|
|||
385 | h.flash(_('Cannot delete %s it still contains attached forks') |
|
|||
386 | % repo_name, category='warning') |
|
|||
387 |
|
||||
388 | except Exception: |
|
|||
389 | log.exception("Exception during deletion of repository") |
|
|||
390 | h.flash(_('An error occurred during deletion of %s') % repo_name, |
|
|||
391 | category='error') |
|
|||
392 |
|
||||
393 | return redirect(url('repos')) |
|
|||
394 |
|
||||
395 | @HasPermissionAllDecorator('hg.admin') |
|
287 | @HasPermissionAllDecorator('hg.admin') | |
396 | def show(self, repo_name, format='html'): |
|
288 | def show(self, repo_name, format='html'): | |
397 | """GET /repos/repo_name: Show a specific item""" |
|
289 | """GET /repos/repo_name: Show a specific item""" | |
398 | # url('repo', repo_name=ID) |
|
290 | # url('repo', repo_name=ID) | |
399 |
|
291 | |||
400 | @HasRepoPermissionAllDecorator('repository.admin') |
|
292 | @HasRepoPermissionAllDecorator('repository.admin') | |
401 | def edit(self, repo_name): |
|
|||
402 | """GET /repo_name/settings: Form to edit an existing item""" |
|
|||
403 | # url('edit_repo', repo_name=ID) |
|
|||
404 | defaults = self.__load_data(repo_name) |
|
|||
405 | if 'clone_uri' in defaults: |
|
|||
406 | del defaults['clone_uri'] |
|
|||
407 |
|
||||
408 | c.repo_fields = RepositoryField.query()\ |
|
|||
409 | .filter(RepositoryField.repository == c.repo_info).all() |
|
|||
410 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
|||
411 | c.active = 'settings' |
|
|||
412 | return htmlfill.render( |
|
|||
413 | render('admin/repos/repo_edit.mako'), |
|
|||
414 | defaults=defaults, |
|
|||
415 | encoding="UTF-8", |
|
|||
416 | force_defaults=False) |
|
|||
417 |
|
||||
418 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
419 | def edit_permissions(self, repo_name): |
|
|||
420 | """GET /repo_name/settings: Form to edit an existing item""" |
|
|||
421 | # url('edit_repo', repo_name=ID) |
|
|||
422 | c.repo_info = self._load_repo(repo_name) |
|
|||
423 | c.active = 'permissions' |
|
|||
424 | defaults = RepoModel()._get_defaults(repo_name) |
|
|||
425 |
|
||||
426 | return htmlfill.render( |
|
|||
427 | render('admin/repos/repo_edit.mako'), |
|
|||
428 | defaults=defaults, |
|
|||
429 | encoding="UTF-8", |
|
|||
430 | force_defaults=False) |
|
|||
431 |
|
||||
432 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
433 | @auth.CSRFRequired() |
|
|||
434 | def edit_permissions_update(self, repo_name): |
|
|||
435 | form = RepoPermsForm()().to_python(request.POST) |
|
|||
436 | RepoModel().update_permissions(repo_name, |
|
|||
437 | form['perm_additions'], form['perm_updates'], form['perm_deletions']) |
|
|||
438 |
|
||||
439 | #TODO: implement this |
|
|||
440 | #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', |
|
|||
441 | # repo_name, self.ip_addr, self.sa) |
|
|||
442 | Session().commit() |
|
|||
443 | h.flash(_('Repository permissions updated'), category='success') |
|
|||
444 | return redirect(url('edit_repo_perms', repo_name=repo_name)) |
|
|||
445 |
|
||||
446 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
447 | def edit_fields(self, repo_name): |
|
293 | def edit_fields(self, repo_name): | |
448 | """GET /repo_name/settings: Form to edit an existing item""" |
|
294 | """GET /repo_name/settings: Form to edit an existing item""" | |
449 | # url('edit_repo', repo_name=ID) |
|
|||
450 | c.repo_info = self._load_repo(repo_name) |
|
295 | c.repo_info = self._load_repo(repo_name) | |
451 | c.repo_fields = RepositoryField.query()\ |
|
296 | c.repo_fields = RepositoryField.query()\ | |
452 | .filter(RepositoryField.repository == c.repo_info).all() |
|
297 | .filter(RepositoryField.repository == c.repo_info).all() | |
@@ -490,106 +335,6 b' class ReposController(BaseRepoController' | |||||
490 | h.flash(msg, category='error') |
|
335 | h.flash(msg, category='error') | |
491 | return redirect(url('edit_repo_fields', repo_name=repo_name)) |
|
336 | return redirect(url('edit_repo_fields', repo_name=repo_name)) | |
492 |
|
337 | |||
493 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
494 | def edit_advanced(self, repo_name): |
|
|||
495 | """GET /repo_name/settings: Form to edit an existing item""" |
|
|||
496 | # url('edit_repo', repo_name=ID) |
|
|||
497 | c.repo_info = self._load_repo(repo_name) |
|
|||
498 | c.default_user_id = User.get_default_user().user_id |
|
|||
499 | c.in_public_journal = UserFollowing.query()\ |
|
|||
500 | .filter(UserFollowing.user_id == c.default_user_id)\ |
|
|||
501 | .filter(UserFollowing.follows_repository == c.repo_info).scalar() |
|
|||
502 |
|
||||
503 | c.active = 'advanced' |
|
|||
504 | c.has_origin_repo_read_perm = False |
|
|||
505 | if c.repo_info.fork: |
|
|||
506 | c.has_origin_repo_read_perm = h.HasRepoPermissionAny( |
|
|||
507 | 'repository.write', 'repository.read', 'repository.admin')( |
|
|||
508 | c.repo_info.fork.repo_name, 'repo set as fork page') |
|
|||
509 |
|
||||
510 | if request.POST: |
|
|||
511 | return redirect(url('repo_edit_advanced')) |
|
|||
512 | return render('admin/repos/repo_edit.mako') |
|
|||
513 |
|
||||
514 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
515 | @auth.CSRFRequired() |
|
|||
516 | def edit_advanced_journal(self, repo_name): |
|
|||
517 | """ |
|
|||
518 | Set's this repository to be visible in public journal, |
|
|||
519 | in other words assing default user to follow this repo |
|
|||
520 |
|
||||
521 | :param repo_name: |
|
|||
522 | """ |
|
|||
523 |
|
||||
524 | try: |
|
|||
525 | repo_id = Repository.get_by_repo_name(repo_name).repo_id |
|
|||
526 | user_id = User.get_default_user().user_id |
|
|||
527 | self.scm_model.toggle_following_repo(repo_id, user_id) |
|
|||
528 | h.flash(_('Updated repository visibility in public journal'), |
|
|||
529 | category='success') |
|
|||
530 | Session().commit() |
|
|||
531 | except Exception: |
|
|||
532 | h.flash(_('An error occurred during setting this' |
|
|||
533 | ' repository in public journal'), |
|
|||
534 | category='error') |
|
|||
535 |
|
||||
536 | return redirect(url('edit_repo_advanced', repo_name=repo_name)) |
|
|||
537 |
|
||||
538 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
539 | @auth.CSRFRequired() |
|
|||
540 | def edit_advanced_fork(self, repo_name): |
|
|||
541 | """ |
|
|||
542 | Mark given repository as a fork of another |
|
|||
543 |
|
||||
544 | :param repo_name: |
|
|||
545 | """ |
|
|||
546 |
|
||||
547 | new_fork_id = request.POST.get('id_fork_of') |
|
|||
548 | try: |
|
|||
549 |
|
||||
550 | if new_fork_id and not new_fork_id.isdigit(): |
|
|||
551 | log.error('Given fork id %s is not an INT', new_fork_id) |
|
|||
552 |
|
||||
553 | fork_id = safe_int(new_fork_id) |
|
|||
554 | repo = ScmModel().mark_as_fork(repo_name, fork_id, |
|
|||
555 | c.rhodecode_user.username) |
|
|||
556 | fork = repo.fork.repo_name if repo.fork else _('Nothing') |
|
|||
557 | Session().commit() |
|
|||
558 | h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork), |
|
|||
559 | category='success') |
|
|||
560 | except RepositoryError as e: |
|
|||
561 | log.exception("Repository Error occurred") |
|
|||
562 | h.flash(str(e), category='error') |
|
|||
563 | except Exception as e: |
|
|||
564 | log.exception("Exception while editing fork") |
|
|||
565 | h.flash(_('An error occurred during this operation'), |
|
|||
566 | category='error') |
|
|||
567 |
|
||||
568 | return redirect(url('edit_repo_advanced', repo_name=repo_name)) |
|
|||
569 |
|
||||
570 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
571 | @auth.CSRFRequired() |
|
|||
572 | def edit_advanced_locking(self, repo_name): |
|
|||
573 | """ |
|
|||
574 | Unlock repository when it is locked ! |
|
|||
575 |
|
||||
576 | :param repo_name: |
|
|||
577 | """ |
|
|||
578 | try: |
|
|||
579 | repo = Repository.get_by_repo_name(repo_name) |
|
|||
580 | if request.POST.get('set_lock'): |
|
|||
581 | Repository.lock(repo, c.rhodecode_user.user_id, |
|
|||
582 | lock_reason=Repository.LOCK_WEB) |
|
|||
583 | h.flash(_('Locked repository'), category='success') |
|
|||
584 | elif request.POST.get('set_unlock'): |
|
|||
585 | Repository.unlock(repo) |
|
|||
586 | h.flash(_('Unlocked repository'), category='success') |
|
|||
587 | except Exception as e: |
|
|||
588 | log.exception("Exception during unlocking") |
|
|||
589 | h.flash(_('An error occurred during unlocking'), |
|
|||
590 | category='error') |
|
|||
591 | return redirect(url('edit_repo_advanced', repo_name=repo_name)) |
|
|||
592 |
|
||||
593 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
338 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') | |
594 | @auth.CSRFRequired() |
|
339 | @auth.CSRFRequired() | |
595 | def toggle_locking(self, repo_name): |
|
340 | def toggle_locking(self, repo_name): | |
@@ -617,32 +362,7 b' class ReposController(BaseRepoController' | |||||
617 | log.exception("Exception during unlocking") |
|
362 | log.exception("Exception during unlocking") | |
618 | h.flash(_('An error occurred during unlocking'), |
|
363 | h.flash(_('An error occurred during unlocking'), | |
619 | category='error') |
|
364 | category='error') | |
620 |
return redirect( |
|
365 | return redirect(h.route_path('repo_summary', repo_name=repo_name)) | |
621 |
|
||||
622 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
623 | @auth.CSRFRequired() |
|
|||
624 | def edit_caches(self, repo_name): |
|
|||
625 | """PUT /{repo_name}/settings/caches: invalidate the repo caches.""" |
|
|||
626 | try: |
|
|||
627 | ScmModel().mark_for_invalidation(repo_name, delete=True) |
|
|||
628 | Session().commit() |
|
|||
629 | h.flash(_('Cache invalidation successful'), |
|
|||
630 | category='success') |
|
|||
631 | except Exception: |
|
|||
632 | log.exception("Exception during cache invalidation") |
|
|||
633 | h.flash(_('An error occurred during cache invalidation'), |
|
|||
634 | category='error') |
|
|||
635 |
|
||||
636 | return redirect(url('edit_repo_caches', repo_name=c.repo_name)) |
|
|||
637 |
|
||||
638 | @HasRepoPermissionAllDecorator('repository.admin') |
|
|||
639 | def edit_caches_form(self, repo_name): |
|
|||
640 | """GET /repo_name/settings: Form to edit an existing item""" |
|
|||
641 | # url('edit_repo', repo_name=ID) |
|
|||
642 | c.repo_info = self._load_repo(repo_name) |
|
|||
643 | c.active = 'caches' |
|
|||
644 |
|
||||
645 | return render('admin/repos/repo_edit.mako') |
|
|||
646 |
|
366 | |||
647 | @HasRepoPermissionAllDecorator('repository.admin') |
|
367 | @HasRepoPermissionAllDecorator('repository.admin') | |
648 | @auth.CSRFRequired() |
|
368 | @auth.CSRFRequired() | |
@@ -660,7 +380,6 b' class ReposController(BaseRepoController' | |||||
660 | @HasRepoPermissionAllDecorator('repository.admin') |
|
380 | @HasRepoPermissionAllDecorator('repository.admin') | |
661 | def edit_remote_form(self, repo_name): |
|
381 | def edit_remote_form(self, repo_name): | |
662 | """GET /repo_name/settings: Form to edit an existing item""" |
|
382 | """GET /repo_name/settings: Form to edit an existing item""" | |
663 | # url('edit_repo', repo_name=ID) |
|
|||
664 | c.repo_info = self._load_repo(repo_name) |
|
383 | c.repo_info = self._load_repo(repo_name) | |
665 | c.active = 'remote' |
|
384 | c.active = 'remote' | |
666 |
|
385 | |||
@@ -682,7 +401,6 b' class ReposController(BaseRepoController' | |||||
682 | @HasRepoPermissionAllDecorator('repository.admin') |
|
401 | @HasRepoPermissionAllDecorator('repository.admin') | |
683 | def edit_statistics_form(self, repo_name): |
|
402 | def edit_statistics_form(self, repo_name): | |
684 | """GET /repo_name/settings: Form to edit an existing item""" |
|
403 | """GET /repo_name/settings: Form to edit an existing item""" | |
685 | # url('edit_repo', repo_name=ID) |
|
|||
686 | c.repo_info = self._load_repo(repo_name) |
|
404 | c.repo_info = self._load_repo(repo_name) | |
687 | repo = c.repo_info.scm_instance() |
|
405 | repo = c.repo_info.scm_instance() | |
688 |
|
406 |
@@ -195,9 +195,12 b' class SettingsController(BaseController)' | |||||
195 | pyramid_settings = get_current_registry().settings |
|
195 | pyramid_settings = get_current_registry().settings | |
196 | c.svn_proxy_generate_config = pyramid_settings[generate_config] |
|
196 | c.svn_proxy_generate_config = pyramid_settings[generate_config] | |
197 |
|
197 | |||
|
198 | defaults = self._form_defaults() | |||
|
199 | ||||
|
200 | model.create_largeobjects_dirs_if_needed(defaults['paths_root_path']) | |||
198 | return htmlfill.render( |
|
201 | return htmlfill.render( | |
199 | render('admin/settings/settings.mako'), |
|
202 | render('admin/settings/settings.mako'), | |
200 |
defaults= |
|
203 | defaults=defaults, | |
201 | encoding="UTF-8", |
|
204 | encoding="UTF-8", | |
202 | force_defaults=False) |
|
205 | force_defaults=False) | |
203 |
|
206 |
@@ -35,9 +35,11 b' from sqlalchemy.orm import joinedload' | |||||
35 |
|
35 | |||
36 | from rhodecode.lib import auth |
|
36 | from rhodecode.lib import auth | |
37 | from rhodecode.lib import helpers as h |
|
37 | from rhodecode.lib import helpers as h | |
|
38 | from rhodecode.lib import audit_logger | |||
|
39 | from rhodecode.lib.ext_json import json | |||
38 | from rhodecode.lib.exceptions import UserGroupAssignedException,\ |
|
40 | from rhodecode.lib.exceptions import UserGroupAssignedException,\ | |
39 | RepoGroupAssignmentError |
|
41 | RepoGroupAssignmentError | |
40 |
from rhodecode.lib.utils import jsonify |
|
42 | from rhodecode.lib.utils import jsonify | |
41 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int |
|
43 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int | |
42 | from rhodecode.lib.auth import ( |
|
44 | from rhodecode.lib.auth import ( | |
43 | LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, |
|
45 | LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, | |
@@ -52,8 +54,7 b' from rhodecode.model.forms import (' | |||||
52 | UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm, |
|
54 | UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm, | |
53 | UserPermissionsForm) |
|
55 | UserPermissionsForm) | |
54 | from rhodecode.model.meta import Session |
|
56 | from rhodecode.model.meta import Session | |
55 | from rhodecode.lib.utils import action_logger |
|
57 | ||
56 | from rhodecode.lib.ext_json import json |
|
|||
57 |
|
58 | |||
58 | log = logging.getLogger(__name__) |
|
59 | log = logging.getLogger(__name__) | |
59 |
|
60 | |||
@@ -105,8 +106,6 b' class UserGroupsController(BaseControlle' | |||||
105 | # permission check inside |
|
106 | # permission check inside | |
106 | @NotAnonymous() |
|
107 | @NotAnonymous() | |
107 | def index(self): |
|
108 | def index(self): | |
108 | """GET /users_groups: All items in the collection""" |
|
|||
109 | # url('users_groups') |
|
|||
110 |
|
109 | |||
111 | from rhodecode.lib.utils import PartialRenderer |
|
110 | from rhodecode.lib.utils import PartialRenderer | |
112 | _render = PartialRenderer('data_table/_dt_elements.mako') |
|
111 | _render = PartialRenderer('data_table/_dt_elements.mako') | |
@@ -142,8 +141,6 b' class UserGroupsController(BaseControlle' | |||||
142 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
141 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') | |
143 | @auth.CSRFRequired() |
|
142 | @auth.CSRFRequired() | |
144 | def create(self): |
|
143 | def create(self): | |
145 | """POST /users_groups: Create a new item""" |
|
|||
146 | # url('users_groups') |
|
|||
147 |
|
144 | |||
148 | users_group_form = UserGroupForm()() |
|
145 | users_group_form = UserGroupForm()() | |
149 | try: |
|
146 | try: | |
@@ -154,14 +151,16 b' class UserGroupsController(BaseControlle' | |||||
154 | owner=c.rhodecode_user.user_id, |
|
151 | owner=c.rhodecode_user.user_id, | |
155 | active=form_result['users_group_active']) |
|
152 | active=form_result['users_group_active']) | |
156 | Session().flush() |
|
153 | Session().flush() | |
157 |
|
154 | creation_data = user_group.get_api_data() | ||
158 | user_group_name = form_result['users_group_name'] |
|
155 | user_group_name = form_result['users_group_name'] | |
159 | action_logger(c.rhodecode_user, |
|
156 | ||
160 | 'admin_created_users_group:%s' % user_group_name, |
|
157 | audit_logger.store_web( | |
161 | None, self.ip_addr, self.sa) |
|
158 | 'user_group.create', action_data={'data': creation_data}, | |
162 | user_group_link = h.link_to(h.escape(user_group_name), |
|
159 | user=c.rhodecode_user) | |
163 | url('edit_users_group', |
|
160 | ||
164 | user_group_id=user_group.users_group_id)) |
|
161 | user_group_link = h.link_to( | |
|
162 | h.escape(user_group_name), | |||
|
163 | url('edit_users_group', user_group_id=user_group.users_group_id)) | |||
165 | h.flash(h.literal(_('Created user group %(user_group_link)s') |
|
164 | h.flash(h.literal(_('Created user group %(user_group_link)s') | |
166 | % {'user_group_link': user_group_link}), |
|
165 | % {'user_group_link': user_group_link}), | |
167 | category='success') |
|
166 | category='success') | |
@@ -191,13 +190,6 b' class UserGroupsController(BaseControlle' | |||||
191 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
190 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') | |
192 | @auth.CSRFRequired() |
|
191 | @auth.CSRFRequired() | |
193 | def update(self, user_group_id): |
|
192 | def update(self, user_group_id): | |
194 | """PUT /user_groups/user_group_id: Update an existing item""" |
|
|||
195 | # Forms posted to this method should contain a hidden field: |
|
|||
196 | # <input type="hidden" name="_method" value="PUT" /> |
|
|||
197 | # Or using helpers: |
|
|||
198 | # h.form(url('users_group', user_group_id=ID), |
|
|||
199 | # method='put') |
|
|||
200 | # url('users_group', user_group_id=ID) |
|
|||
201 |
|
193 | |||
202 | user_group_id = safe_int(user_group_id) |
|
194 | user_group_id = safe_int(user_group_id) | |
203 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
195 | c.user_group = UserGroup.get_or_404(user_group_id) | |
@@ -207,16 +199,22 b' class UserGroupsController(BaseControlle' | |||||
207 | users_group_form = UserGroupForm( |
|
199 | users_group_form = UserGroupForm( | |
208 | edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)() |
|
200 | edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)() | |
209 |
|
201 | |||
|
202 | old_values = c.user_group.get_api_data() | |||
210 | try: |
|
203 | try: | |
211 | form_result = users_group_form.to_python(request.POST) |
|
204 | form_result = users_group_form.to_python(request.POST) | |
212 | pstruct = peppercorn.parse(request.POST.items()) |
|
205 | pstruct = peppercorn.parse(request.POST.items()) | |
213 | form_result['users_group_members'] = pstruct['user_group_members'] |
|
206 | form_result['users_group_members'] = pstruct['user_group_members'] | |
214 |
|
207 | |||
215 | UserGroupModel().update(c.user_group, form_result) |
|
208 | user_group, added_members, removed_members = \ | |
|
209 | UserGroupModel().update(c.user_group, form_result) | |||
216 | updated_user_group = form_result['users_group_name'] |
|
210 | updated_user_group = form_result['users_group_name'] | |
217 | action_logger(c.rhodecode_user, |
|
211 | ||
218 | 'admin_updated_users_group:%s' % updated_user_group, |
|
212 | audit_logger.store_web( | |
219 | None, self.ip_addr, self.sa) |
|
213 | 'user_group.edit', action_data={'old_data': old_values}, | |
|
214 | user=c.rhodecode_user) | |||
|
215 | ||||
|
216 | # TODO(marcink): use added/removed to set user_group.edit.member.add | |||
|
217 | ||||
220 | h.flash(_('Updated user group %s') % updated_user_group, |
|
218 | h.flash(_('Updated user group %s') % updated_user_group, | |
221 | category='success') |
|
219 | category='success') | |
222 | Session().commit() |
|
220 | Session().commit() | |
@@ -241,19 +239,16 b' class UserGroupsController(BaseControlle' | |||||
241 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
239 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') | |
242 | @auth.CSRFRequired() |
|
240 | @auth.CSRFRequired() | |
243 | def delete(self, user_group_id): |
|
241 | def delete(self, user_group_id): | |
244 | """DELETE /user_groups/user_group_id: Delete an existing item""" |
|
|||
245 | # Forms posted to this method should contain a hidden field: |
|
|||
246 | # <input type="hidden" name="_method" value="DELETE" /> |
|
|||
247 | # Or using helpers: |
|
|||
248 | # h.form(url('users_group', user_group_id=ID), |
|
|||
249 | # method='delete') |
|
|||
250 | # url('users_group', user_group_id=ID) |
|
|||
251 | user_group_id = safe_int(user_group_id) |
|
242 | user_group_id = safe_int(user_group_id) | |
252 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
243 | c.user_group = UserGroup.get_or_404(user_group_id) | |
253 | force = str2bool(request.POST.get('force')) |
|
244 | force = str2bool(request.POST.get('force')) | |
254 |
|
245 | |||
|
246 | old_values = c.user_group.get_api_data() | |||
255 | try: |
|
247 | try: | |
256 | UserGroupModel().delete(c.user_group, force=force) |
|
248 | UserGroupModel().delete(c.user_group, force=force) | |
|
249 | audit_logger.store_web( | |||
|
250 | 'user.delete', action_data={'old_data': old_values}, | |||
|
251 | user=c.rhodecode_user) | |||
257 | Session().commit() |
|
252 | Session().commit() | |
258 | h.flash(_('Successfully deleted user group'), category='success') |
|
253 | h.flash(_('Successfully deleted user group'), category='success') | |
259 | except UserGroupAssignedException as e: |
|
254 | except UserGroupAssignedException as e: | |
@@ -330,9 +325,9 b' class UserGroupsController(BaseControlle' | |||||
330 | except RepoGroupAssignmentError: |
|
325 | except RepoGroupAssignmentError: | |
331 | h.flash(_('Target group cannot be the same'), category='error') |
|
326 | h.flash(_('Target group cannot be the same'), category='error') | |
332 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) |
|
327 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) | |
333 | #TODO: implement this |
|
328 | ||
334 | #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', |
|
329 | # TODO(marcink): implement global permissions | |
335 | # repo_name, self.ip_addr, self.sa) |
|
330 | # audit_log.store_web('user_group.edit.permissions') | |
336 | Session().commit() |
|
331 | Session().commit() | |
337 | h.flash(_('User Group permissions updated'), category='success') |
|
332 | h.flash(_('User Group permissions updated'), category='success') | |
338 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) |
|
333 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) | |
@@ -389,8 +384,6 b' class UserGroupsController(BaseControlle' | |||||
389 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
384 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') | |
390 | @auth.CSRFRequired() |
|
385 | @auth.CSRFRequired() | |
391 | def update_global_perms(self, user_group_id): |
|
386 | def update_global_perms(self, user_group_id): | |
392 | """PUT /users_perm/user_group_id: Update an existing item""" |
|
|||
393 | # url('users_group_perm', user_group_id=ID, method='put') |
|
|||
394 | user_group_id = safe_int(user_group_id) |
|
387 | user_group_id = safe_int(user_group_id) | |
395 | user_group = UserGroup.get_or_404(user_group_id) |
|
388 | user_group = UserGroup.get_or_404(user_group_id) | |
396 | c.active = 'global_perms' |
|
389 | c.active = 'global_perms' | |
@@ -492,6 +485,9 b' class UserGroupsController(BaseControlle' | |||||
492 | @XHRRequired() |
|
485 | @XHRRequired() | |
493 | @jsonify |
|
486 | @jsonify | |
494 | def user_group_members(self, user_group_id): |
|
487 | def user_group_members(self, user_group_id): | |
|
488 | """ | |||
|
489 | Return members of given user group | |||
|
490 | """ | |||
495 | user_group_id = safe_int(user_group_id) |
|
491 | user_group_id = safe_int(user_group_id) | |
496 | user_group = UserGroup.get_or_404(user_group_id) |
|
492 | user_group = UserGroup.get_or_404(user_group_id) | |
497 | group_members_obj = sorted((x.user for x in user_group.members), |
|
493 | group_members_obj = sorted((x.user for x in user_group.members), | |
@@ -500,8 +496,8 b' class UserGroupsController(BaseControlle' | |||||
500 | group_members = [ |
|
496 | group_members = [ | |
501 | { |
|
497 | { | |
502 | 'id': user.user_id, |
|
498 | 'id': user.user_id, | |
503 | 'first_name': user.name, |
|
499 | 'first_name': user.first_name, | |
504 | 'last_name': user.lastname, |
|
500 | 'last_name': user.last_name, | |
505 | 'username': user.username, |
|
501 | 'username': user.username, | |
506 | 'icon_link': h.gravatar_url(user.email, 30), |
|
502 | 'icon_link': h.gravatar_url(user.email, 30), | |
507 | 'value_display': h.person(user.email), |
|
503 | 'value_display': h.person(user.email), |
@@ -31,15 +31,17 b' from pylons.controllers.util import redi' | |||||
31 | from pylons.i18n.translation import _ |
|
31 | from pylons.i18n.translation import _ | |
32 |
|
32 | |||
33 | from rhodecode.authentication.plugins import auth_rhodecode |
|
33 | from rhodecode.authentication.plugins import auth_rhodecode | |
|
34 | ||||
|
35 | from rhodecode.lib import helpers as h | |||
|
36 | from rhodecode.lib import auth | |||
|
37 | from rhodecode.lib import audit_logger | |||
|
38 | from rhodecode.lib.auth import ( | |||
|
39 | LoginRequired, HasPermissionAllDecorator, AuthUser) | |||
|
40 | from rhodecode.lib.base import BaseController, render | |||
34 | from rhodecode.lib.exceptions import ( |
|
41 | from rhodecode.lib.exceptions import ( | |
35 | DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, |
|
42 | DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, | |
36 | UserOwnsUserGroupsException, UserCreationError) |
|
43 | UserOwnsUserGroupsException, UserCreationError) | |
37 | from rhodecode.lib import helpers as h |
|
44 | from rhodecode.lib.utils2 import safe_int, AttributeDict | |
38 | from rhodecode.lib import auth |
|
|||
39 | from rhodecode.lib.auth import ( |
|
|||
40 | LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token) |
|
|||
41 | from rhodecode.lib.base import BaseController, render |
|
|||
42 | from rhodecode.model.auth_token import AuthTokenModel |
|
|||
43 |
|
45 | |||
44 | from rhodecode.model.db import ( |
|
46 | from rhodecode.model.db import ( | |
45 | PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) |
|
47 | PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) | |
@@ -49,8 +51,6 b' from rhodecode.model.repo_group import R' | |||||
49 | from rhodecode.model.user import UserModel |
|
51 | from rhodecode.model.user import UserModel | |
50 | from rhodecode.model.meta import Session |
|
52 | from rhodecode.model.meta import Session | |
51 | from rhodecode.model.permission import PermissionModel |
|
53 | from rhodecode.model.permission import PermissionModel | |
52 | from rhodecode.lib.utils import action_logger |
|
|||
53 | from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict |
|
|||
54 |
|
54 | |||
55 | log = logging.getLogger(__name__) |
|
55 | log = logging.getLogger(__name__) | |
56 |
|
56 | |||
@@ -88,7 +88,6 b' class UsersController(BaseController):' | |||||
88 | @HasPermissionAllDecorator('hg.admin') |
|
88 | @HasPermissionAllDecorator('hg.admin') | |
89 | @auth.CSRFRequired() |
|
89 | @auth.CSRFRequired() | |
90 | def create(self): |
|
90 | def create(self): | |
91 | """POST /users: Create a new item""" |
|
|||
92 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name |
|
91 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name | |
93 | user_model = UserModel() |
|
92 | user_model = UserModel() | |
94 | user_form = UserForm()() |
|
93 | user_form = UserForm()() | |
@@ -96,9 +95,12 b' class UsersController(BaseController):' | |||||
96 | form_result = user_form.to_python(dict(request.POST)) |
|
95 | form_result = user_form.to_python(dict(request.POST)) | |
97 | user = user_model.create(form_result) |
|
96 | user = user_model.create(form_result) | |
98 | Session().flush() |
|
97 | Session().flush() | |
|
98 | creation_data = user.get_api_data() | |||
99 | username = form_result['username'] |
|
99 | username = form_result['username'] | |
100 | action_logger(c.rhodecode_user, 'admin_created_user:%s' % username, |
|
100 | ||
101 | None, self.ip_addr, self.sa) |
|
101 | audit_logger.store_web( | |
|
102 | 'user.create', action_data={'data': creation_data}, | |||
|
103 | user=c.rhodecode_user) | |||
102 |
|
104 | |||
103 | user_link = h.link_to(h.escape(username), |
|
105 | user_link = h.link_to(h.escape(username), | |
104 | url('edit_user', |
|
106 | url('edit_user', | |
@@ -125,8 +127,6 b' class UsersController(BaseController):' | |||||
125 |
|
127 | |||
126 | @HasPermissionAllDecorator('hg.admin') |
|
128 | @HasPermissionAllDecorator('hg.admin') | |
127 | def new(self): |
|
129 | def new(self): | |
128 | """GET /users/new: Form to create a new item""" |
|
|||
129 | # url('new_user') |
|
|||
130 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name |
|
130 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name | |
131 | self._get_personal_repo_group_template_vars() |
|
131 | self._get_personal_repo_group_template_vars() | |
132 | return render('admin/users/user_add.mako') |
|
132 | return render('admin/users/user_add.mako') | |
@@ -134,13 +134,7 b' class UsersController(BaseController):' | |||||
134 | @HasPermissionAllDecorator('hg.admin') |
|
134 | @HasPermissionAllDecorator('hg.admin') | |
135 | @auth.CSRFRequired() |
|
135 | @auth.CSRFRequired() | |
136 | def update(self, user_id): |
|
136 | def update(self, user_id): | |
137 | """PUT /users/user_id: Update an existing item""" |
|
137 | ||
138 | # Forms posted to this method should contain a hidden field: |
|
|||
139 | # <input type="hidden" name="_method" value="PUT" /> |
|
|||
140 | # Or using helpers: |
|
|||
141 | # h.form(url('update_user', user_id=ID), |
|
|||
142 | # method='put') |
|
|||
143 | # url('user', user_id=ID) |
|
|||
144 | user_id = safe_int(user_id) |
|
138 | user_id = safe_int(user_id) | |
145 | c.user = User.get_or_404(user_id) |
|
139 | c.user = User.get_or_404(user_id) | |
146 | c.active = 'profile' |
|
140 | c.active = 'profile' | |
@@ -152,6 +146,7 b' class UsersController(BaseController):' | |||||
152 | old_data={'user_id': user_id, |
|
146 | old_data={'user_id': user_id, | |
153 | 'email': c.user.email})() |
|
147 | 'email': c.user.email})() | |
154 | form_result = {} |
|
148 | form_result = {} | |
|
149 | old_values = c.user.get_api_data() | |||
155 | try: |
|
150 | try: | |
156 | form_result = _form.to_python(dict(request.POST)) |
|
151 | form_result = _form.to_python(dict(request.POST)) | |
157 | skip_attrs = ['extern_type', 'extern_name'] |
|
152 | skip_attrs = ['extern_type', 'extern_name'] | |
@@ -160,12 +155,15 b' class UsersController(BaseController):' | |||||
160 | # forbid updating username for external accounts |
|
155 | # forbid updating username for external accounts | |
161 | skip_attrs.append('username') |
|
156 | skip_attrs.append('username') | |
162 |
|
157 | |||
163 |
UserModel().update_user( |
|
158 | UserModel().update_user( | |
164 | usr = form_result['username'] |
|
159 | user_id, skip_attrs=skip_attrs, **form_result) | |
165 | action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr, |
|
160 | ||
166 | None, self.ip_addr, self.sa) |
|
161 | audit_logger.store_web( | |
|
162 | 'user.edit', action_data={'old_data': old_values}, | |||
|
163 | user=c.rhodecode_user) | |||
|
164 | ||||
|
165 | Session().commit() | |||
167 | h.flash(_('User updated successfully'), category='success') |
|
166 | h.flash(_('User updated successfully'), category='success') | |
168 | Session().commit() |
|
|||
169 | except formencode.Invalid as errors: |
|
167 | except formencode.Invalid as errors: | |
170 | defaults = errors.value |
|
168 | defaults = errors.value | |
171 | e = errors.error_dict or {} |
|
169 | e = errors.error_dict or {} | |
@@ -188,13 +186,6 b' class UsersController(BaseController):' | |||||
188 | @HasPermissionAllDecorator('hg.admin') |
|
186 | @HasPermissionAllDecorator('hg.admin') | |
189 | @auth.CSRFRequired() |
|
187 | @auth.CSRFRequired() | |
190 | def delete(self, user_id): |
|
188 | def delete(self, user_id): | |
191 | """DELETE /users/user_id: Delete an existing item""" |
|
|||
192 | # Forms posted to this method should contain a hidden field: |
|
|||
193 | # <input type="hidden" name="_method" value="DELETE" /> |
|
|||
194 | # Or using helpers: |
|
|||
195 | # h.form(url('delete_user', user_id=ID), |
|
|||
196 | # method='delete') |
|
|||
197 | # url('user', user_id=ID) |
|
|||
198 | user_id = safe_int(user_id) |
|
189 | user_id = safe_int(user_id) | |
199 | c.user = User.get_or_404(user_id) |
|
190 | c.user = User.get_or_404(user_id) | |
200 |
|
191 | |||
@@ -249,10 +240,16 b' class UsersController(BaseController):' | |||||
249 | _('Deleted %s user groups') % len(_user_groups), |
|
240 | _('Deleted %s user groups') % len(_user_groups), | |
250 | category='success') |
|
241 | category='success') | |
251 |
|
242 | |||
|
243 | old_values = c.user.get_api_data() | |||
252 | try: |
|
244 | try: | |
253 | UserModel().delete(c.user, handle_repos=handle_repos, |
|
245 | UserModel().delete(c.user, handle_repos=handle_repos, | |
254 | handle_repo_groups=handle_repo_groups, |
|
246 | handle_repo_groups=handle_repo_groups, | |
255 | handle_user_groups=handle_user_groups) |
|
247 | handle_user_groups=handle_user_groups) | |
|
248 | ||||
|
249 | audit_logger.store_web( | |||
|
250 | 'user.delete', action_data={'old_data': old_values}, | |||
|
251 | user=c.rhodecode_user) | |||
|
252 | ||||
256 | Session().commit() |
|
253 | Session().commit() | |
257 | set_handle_flash_repos() |
|
254 | set_handle_flash_repos() | |
258 | set_handle_flash_repo_groups() |
|
255 | set_handle_flash_repo_groups() | |
@@ -272,19 +269,25 b' class UsersController(BaseController):' | |||||
272 | def reset_password(self, user_id): |
|
269 | def reset_password(self, user_id): | |
273 | """ |
|
270 | """ | |
274 | toggle reset password flag for this user |
|
271 | toggle reset password flag for this user | |
275 |
|
||||
276 | :param user_id: |
|
|||
277 | """ |
|
272 | """ | |
278 | user_id = safe_int(user_id) |
|
273 | user_id = safe_int(user_id) | |
279 | c.user = User.get_or_404(user_id) |
|
274 | c.user = User.get_or_404(user_id) | |
280 | try: |
|
275 | try: | |
281 | old_value = c.user.user_data.get('force_password_change') |
|
276 | old_value = c.user.user_data.get('force_password_change') | |
282 | c.user.update_userdata(force_password_change=not old_value) |
|
277 | c.user.update_userdata(force_password_change=not old_value) | |
283 | Session().commit() |
|
278 | ||
284 | if old_value: |
|
279 | if old_value: | |
285 | msg = _('Force password change disabled for user') |
|
280 | msg = _('Force password change disabled for user') | |
|
281 | audit_logger.store_web( | |||
|
282 | 'user.edit.password_reset.disabled', | |||
|
283 | user=c.rhodecode_user) | |||
286 | else: |
|
284 | else: | |
287 | msg = _('Force password change enabled for user') |
|
285 | msg = _('Force password change enabled for user') | |
|
286 | audit_logger.store_web( | |||
|
287 | 'user.edit.password_reset.enabled', | |||
|
288 | user=c.rhodecode_user) | |||
|
289 | ||||
|
290 | Session().commit() | |||
288 | h.flash(msg, category='success') |
|
291 | h.flash(msg, category='success') | |
289 | except Exception: |
|
292 | except Exception: | |
290 | log.exception("Exception during password reset for user") |
|
293 | log.exception("Exception during password reset for user") | |
@@ -298,8 +301,6 b' class UsersController(BaseController):' | |||||
298 | def create_personal_repo_group(self, user_id): |
|
301 | def create_personal_repo_group(self, user_id): | |
299 | """ |
|
302 | """ | |
300 | Create personal repository group for this user |
|
303 | Create personal repository group for this user | |
301 |
|
||||
302 | :param user_id: |
|
|||
303 | """ |
|
304 | """ | |
304 | from rhodecode.model.repo_group import RepoGroupModel |
|
305 | from rhodecode.model.repo_group import RepoGroupModel | |
305 |
|
306 | |||
@@ -381,8 +382,7 b' class UsersController(BaseController):' | |||||
381 | return redirect(h.route_path('users')) |
|
382 | return redirect(h.route_path('users')) | |
382 |
|
383 | |||
383 | c.active = 'advanced' |
|
384 | c.active = 'advanced' | |
384 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) |
|
385 | c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id) | |
385 | c.personal_repo_group = c.perm_user.personal_repo_group |
|
|||
386 | c.personal_repo_group_name = RepoGroupModel()\ |
|
386 | c.personal_repo_group_name = RepoGroupModel()\ | |
387 | .get_personal_group_name(user) |
|
387 | .get_personal_group_name(user) | |
388 | c.first_admin = User.get_first_super_admin() |
|
388 | c.first_admin = User.get_first_super_admin() | |
@@ -429,8 +429,6 b' class UsersController(BaseController):' | |||||
429 | @HasPermissionAllDecorator('hg.admin') |
|
429 | @HasPermissionAllDecorator('hg.admin') | |
430 | @auth.CSRFRequired() |
|
430 | @auth.CSRFRequired() | |
431 | def update_global_perms(self, user_id): |
|
431 | def update_global_perms(self, user_id): | |
432 | """PUT /users_perm/user_id: Update an existing item""" |
|
|||
433 | # url('user_perm', user_id=ID, method='put') |
|
|||
434 | user_id = safe_int(user_id) |
|
432 | user_id = safe_int(user_id) | |
435 | user = User.get_or_404(user_id) |
|
433 | user = User.get_or_404(user_id) | |
436 | c.active = 'global_perms' |
|
434 | c.active = 'global_perms' | |
@@ -457,11 +455,13 b' class UsersController(BaseController):' | |||||
457 |
|
455 | |||
458 | PermissionModel().update_user_permissions(form_result) |
|
456 | PermissionModel().update_user_permissions(form_result) | |
459 |
|
457 | |||
|
458 | # TODO(marcink): implement global permissions | |||
|
459 | # audit_log.store_web('user.edit.permissions') | |||
|
460 | ||||
460 | Session().commit() |
|
461 | Session().commit() | |
461 | h.flash(_('User global permissions updated successfully'), |
|
462 | h.flash(_('User global permissions updated successfully'), | |
462 | category='success') |
|
463 | category='success') | |
463 |
|
464 | |||
464 | Session().commit() |
|
|||
465 | except formencode.Invalid as errors: |
|
465 | except formencode.Invalid as errors: | |
466 | defaults = errors.value |
|
466 | defaults = errors.value | |
467 | c.user = user |
|
467 | c.user = user | |
@@ -491,140 +491,3 b' class UsersController(BaseController):' | |||||
491 |
|
491 | |||
492 | return render('admin/users/user_edit.mako') |
|
492 | return render('admin/users/user_edit.mako') | |
493 |
|
493 | |||
494 | @HasPermissionAllDecorator('hg.admin') |
|
|||
495 | def edit_emails(self, user_id): |
|
|||
496 | user_id = safe_int(user_id) |
|
|||
497 | c.user = User.get_or_404(user_id) |
|
|||
498 | if c.user.username == User.DEFAULT_USER: |
|
|||
499 | h.flash(_("You can't edit this user"), category='warning') |
|
|||
500 | return redirect(h.route_path('users')) |
|
|||
501 |
|
||||
502 | c.active = 'emails' |
|
|||
503 | c.user_email_map = UserEmailMap.query() \ |
|
|||
504 | .filter(UserEmailMap.user == c.user).all() |
|
|||
505 |
|
||||
506 | defaults = c.user.get_dict() |
|
|||
507 | return htmlfill.render( |
|
|||
508 | render('admin/users/user_edit.mako'), |
|
|||
509 | defaults=defaults, |
|
|||
510 | encoding="UTF-8", |
|
|||
511 | force_defaults=False) |
|
|||
512 |
|
||||
513 | @HasPermissionAllDecorator('hg.admin') |
|
|||
514 | @auth.CSRFRequired() |
|
|||
515 | def add_email(self, user_id): |
|
|||
516 | """POST /user_emails:Add an existing item""" |
|
|||
517 | # url('user_emails', user_id=ID, method='put') |
|
|||
518 | user_id = safe_int(user_id) |
|
|||
519 | c.user = User.get_or_404(user_id) |
|
|||
520 |
|
||||
521 | email = request.POST.get('new_email') |
|
|||
522 | user_model = UserModel() |
|
|||
523 |
|
||||
524 | try: |
|
|||
525 | user_model.add_extra_email(user_id, email) |
|
|||
526 | Session().commit() |
|
|||
527 | h.flash(_("Added new email address `%s` for user account") % email, |
|
|||
528 | category='success') |
|
|||
529 | except formencode.Invalid as error: |
|
|||
530 | msg = error.error_dict['email'] |
|
|||
531 | h.flash(msg, category='error') |
|
|||
532 | except Exception: |
|
|||
533 | log.exception("Exception during email saving") |
|
|||
534 | h.flash(_('An error occurred during email saving'), |
|
|||
535 | category='error') |
|
|||
536 | return redirect(url('edit_user_emails', user_id=user_id)) |
|
|||
537 |
|
||||
538 | @HasPermissionAllDecorator('hg.admin') |
|
|||
539 | @auth.CSRFRequired() |
|
|||
540 | def delete_email(self, user_id): |
|
|||
541 | """DELETE /user_emails_delete/user_id: Delete an existing item""" |
|
|||
542 | # url('user_emails_delete', user_id=ID, method='delete') |
|
|||
543 | user_id = safe_int(user_id) |
|
|||
544 | c.user = User.get_or_404(user_id) |
|
|||
545 | email_id = request.POST.get('del_email_id') |
|
|||
546 | user_model = UserModel() |
|
|||
547 | user_model.delete_extra_email(user_id, email_id) |
|
|||
548 | Session().commit() |
|
|||
549 | h.flash(_("Removed email address from user account"), category='success') |
|
|||
550 | return redirect(url('edit_user_emails', user_id=user_id)) |
|
|||
551 |
|
||||
552 | @HasPermissionAllDecorator('hg.admin') |
|
|||
553 | def edit_ips(self, user_id): |
|
|||
554 | user_id = safe_int(user_id) |
|
|||
555 | c.user = User.get_or_404(user_id) |
|
|||
556 | if c.user.username == User.DEFAULT_USER: |
|
|||
557 | h.flash(_("You can't edit this user"), category='warning') |
|
|||
558 | return redirect(h.route_path('users')) |
|
|||
559 |
|
||||
560 | c.active = 'ips' |
|
|||
561 | c.user_ip_map = UserIpMap.query() \ |
|
|||
562 | .filter(UserIpMap.user == c.user).all() |
|
|||
563 |
|
||||
564 | c.inherit_default_ips = c.user.inherit_default_permissions |
|
|||
565 | c.default_user_ip_map = UserIpMap.query() \ |
|
|||
566 | .filter(UserIpMap.user == User.get_default_user()).all() |
|
|||
567 |
|
||||
568 | defaults = c.user.get_dict() |
|
|||
569 | return htmlfill.render( |
|
|||
570 | render('admin/users/user_edit.mako'), |
|
|||
571 | defaults=defaults, |
|
|||
572 | encoding="UTF-8", |
|
|||
573 | force_defaults=False) |
|
|||
574 |
|
||||
575 | @HasPermissionAllDecorator('hg.admin') |
|
|||
576 | @auth.CSRFRequired() |
|
|||
577 | def add_ip(self, user_id): |
|
|||
578 | """POST /user_ips:Add an existing item""" |
|
|||
579 | # url('user_ips', user_id=ID, method='put') |
|
|||
580 |
|
||||
581 | user_id = safe_int(user_id) |
|
|||
582 | c.user = User.get_or_404(user_id) |
|
|||
583 | user_model = UserModel() |
|
|||
584 | try: |
|
|||
585 | ip_list = user_model.parse_ip_range(request.POST.get('new_ip')) |
|
|||
586 | except Exception as e: |
|
|||
587 | ip_list = [] |
|
|||
588 | log.exception("Exception during ip saving") |
|
|||
589 | h.flash(_('An error occurred during ip saving:%s' % (e,)), |
|
|||
590 | category='error') |
|
|||
591 |
|
||||
592 | desc = request.POST.get('description') |
|
|||
593 | added = [] |
|
|||
594 | for ip in ip_list: |
|
|||
595 | try: |
|
|||
596 | user_model.add_extra_ip(user_id, ip, desc) |
|
|||
597 | Session().commit() |
|
|||
598 | added.append(ip) |
|
|||
599 | except formencode.Invalid as error: |
|
|||
600 | msg = error.error_dict['ip'] |
|
|||
601 | h.flash(msg, category='error') |
|
|||
602 | except Exception: |
|
|||
603 | log.exception("Exception during ip saving") |
|
|||
604 | h.flash(_('An error occurred during ip saving'), |
|
|||
605 | category='error') |
|
|||
606 | if added: |
|
|||
607 | h.flash( |
|
|||
608 | _("Added ips %s to user whitelist") % (', '.join(ip_list), ), |
|
|||
609 | category='success') |
|
|||
610 | if 'default_user' in request.POST: |
|
|||
611 | return redirect(url('admin_permissions_ips')) |
|
|||
612 | return redirect(url('edit_user_ips', user_id=user_id)) |
|
|||
613 |
|
||||
614 | @HasPermissionAllDecorator('hg.admin') |
|
|||
615 | @auth.CSRFRequired() |
|
|||
616 | def delete_ip(self, user_id): |
|
|||
617 | """DELETE /user_ips_delete/user_id: Delete an existing item""" |
|
|||
618 | # url('user_ips_delete', user_id=ID, method='delete') |
|
|||
619 | user_id = safe_int(user_id) |
|
|||
620 | c.user = User.get_or_404(user_id) |
|
|||
621 |
|
||||
622 | ip_id = request.POST.get('del_ip_id') |
|
|||
623 | user_model = UserModel() |
|
|||
624 | user_model.delete_extra_ip(user_id, ip_id) |
|
|||
625 | Session().commit() |
|
|||
626 | h.flash(_("Removed ip address from user whitelist"), category='success') |
|
|||
627 |
|
||||
628 | if 'default_user' in request.POST: |
|
|||
629 | return redirect(url('admin_permissions_ips')) |
|
|||
630 | return redirect(url('edit_user_ips', user_id=user_id)) |
|
@@ -46,27 +46,6 b' log = logging.getLogger(__name__)' | |||||
46 | DEFAULT_CHANGELOG_SIZE = 20 |
|
46 | DEFAULT_CHANGELOG_SIZE = 20 | |
47 |
|
47 | |||
48 |
|
48 | |||
49 | def _load_changelog_summary(): |
|
|||
50 | p = safe_int(request.GET.get('page'), 1) |
|
|||
51 | size = safe_int(request.GET.get('size'), 10) |
|
|||
52 |
|
||||
53 | def url_generator(**kw): |
|
|||
54 | return url('summary_home', |
|
|||
55 | repo_name=c.rhodecode_db_repo.repo_name, size=size, **kw) |
|
|||
56 |
|
||||
57 | pre_load = ['author', 'branch', 'date', 'message'] |
|
|||
58 | try: |
|
|||
59 | collection = c.rhodecode_repo.get_commits(pre_load=pre_load) |
|
|||
60 | except EmptyRepositoryError: |
|
|||
61 | collection = c.rhodecode_repo |
|
|||
62 |
|
||||
63 | c.repo_commits = RepoPage( |
|
|||
64 | collection, page=p, items_per_page=size, url=url_generator) |
|
|||
65 | page_ids = [x.raw_id for x in c.repo_commits] |
|
|||
66 | c.comments = c.rhodecode_db_repo.get_comments(page_ids) |
|
|||
67 | c.statuses = c.rhodecode_db_repo.statuses(page_ids) |
|
|||
68 |
|
||||
69 |
|
||||
70 | class ChangelogController(BaseRepoController): |
|
49 | class ChangelogController(BaseRepoController): | |
71 |
|
50 | |||
72 | def __before__(self): |
|
51 | def __before__(self): | |
@@ -88,13 +67,11 b' class ChangelogController(BaseRepoContro' | |||||
88 | except EmptyRepositoryError: |
|
67 | except EmptyRepositoryError: | |
89 | if not redirect_after: |
|
68 | if not redirect_after: | |
90 | return None |
|
69 | return None | |
91 |
h.flash |
|
70 | h.flash(_('There are no commits yet'), category='warning') | |
92 | category='warning') |
|
|||
93 | redirect(url('changelog_home', repo_name=repo.repo_name)) |
|
71 | redirect(url('changelog_home', repo_name=repo.repo_name)) | |
94 | except RepositoryError as e: |
|
72 | except RepositoryError as e: | |
95 |
|
|
73 | log.exception(safe_str(e)) | |
96 | log.exception(msg) |
|
74 | h.flash(safe_str(h.escape(e)), category='warning') | |
97 | h.flash(msg, category='warning') |
|
|||
98 | if not partial: |
|
75 | if not partial: | |
99 | redirect(h.url('changelog_home', repo_name=repo.repo_name)) |
|
76 | redirect(h.url('changelog_home', repo_name=repo.repo_name)) | |
100 | raise HTTPBadRequest() |
|
77 | raise HTTPBadRequest() | |
@@ -134,7 +111,7 b' class ChangelogController(BaseRepoContro' | |||||
134 |
|
111 | |||
135 | def _check_if_valid_branch(self, branch_name, repo_name, f_path): |
|
112 | def _check_if_valid_branch(self, branch_name, repo_name, f_path): | |
136 | if branch_name not in c.rhodecode_repo.branches_all: |
|
113 | if branch_name not in c.rhodecode_repo.branches_all: | |
137 | h.flash('Branch {} is not found.'.format(branch_name), |
|
114 | h.flash('Branch {} is not found.'.format(h.escape(branch_name)), | |
138 | category='warning') |
|
115 | category='warning') | |
139 | redirect(url('changelog_file_home', repo_name=repo_name, |
|
116 | redirect(url('changelog_file_home', repo_name=repo_name, | |
140 | revision=branch_name, f_path=f_path or '')) |
|
117 | revision=branch_name, f_path=f_path or '')) | |
@@ -210,12 +187,11 b' class ChangelogController(BaseRepoContro' | |||||
210 | collection, p, chunk_size, c.branch_name, dynamic=f_path) |
|
187 | collection, p, chunk_size, c.branch_name, dynamic=f_path) | |
211 |
|
188 | |||
212 | except EmptyRepositoryError as e: |
|
189 | except EmptyRepositoryError as e: | |
213 | h.flash(safe_str(e), category='warning') |
|
190 | h.flash(safe_str(h.escape(e)), category='warning') | |
214 |
return redirect( |
|
191 | return redirect(h.route_path('repo_summary', repo_name=repo_name)) | |
215 | except (RepositoryError, CommitDoesNotExistError, Exception) as e: |
|
192 | except (RepositoryError, CommitDoesNotExistError, Exception) as e: | |
216 |
|
|
193 | log.exception(safe_str(e)) | |
217 | log.exception(msg) |
|
194 | h.flash(safe_str(h.escape(e)), category='error') | |
218 | h.flash(msg, category='error') |
|
|||
219 | return redirect(url('changelog_home', repo_name=repo_name)) |
|
195 | return redirect(url('changelog_home', repo_name=repo_name)) | |
220 |
|
196 | |||
221 | if (request.environ.get('HTTP_X_PARTIAL_XHR') |
|
197 | if (request.environ.get('HTTP_X_PARTIAL_XHR') | |
@@ -279,12 +255,3 b' class ChangelogController(BaseRepoContro' | |||||
279 | c.rhodecode_repo, c.pagination, |
|
255 | c.rhodecode_repo, c.pagination, | |
280 | prev_data=prev_data, next_data=next_data) |
|
256 | prev_data=prev_data, next_data=next_data) | |
281 | return render('changelog/changelog_elements.mako') |
|
257 | return render('changelog/changelog_elements.mako') | |
282 |
|
||||
283 | @LoginRequired() |
|
|||
284 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
|||
285 | 'repository.admin') |
|
|||
286 | def changelog_summary(self, repo_name): |
|
|||
287 | if request.environ.get('HTTP_X_PJAX'): |
|
|||
288 | _load_changelog_summary() |
|
|||
289 | return render('changelog/changelog_summary_data.mako') |
|
|||
290 | raise HTTPNotFound() |
|
@@ -39,8 +39,8 b' from rhodecode.lib.base import BaseRepoC' | |||||
39 | from rhodecode.lib.compat import OrderedDict |
|
39 | from rhodecode.lib.compat import OrderedDict | |
40 | from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError |
|
40 | from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError | |
41 | import rhodecode.lib.helpers as h |
|
41 | import rhodecode.lib.helpers as h | |
42 |
from rhodecode.lib.utils import |
|
42 | from rhodecode.lib.utils import jsonify | |
43 | from rhodecode.lib.utils2 import safe_unicode |
|
43 | from rhodecode.lib.utils2 import safe_unicode, safe_int | |
44 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
44 | from rhodecode.lib.vcs.backends.base import EmptyCommit | |
45 | from rhodecode.lib.vcs.exceptions import ( |
|
45 | from rhodecode.lib.vcs.exceptions import ( | |
46 | RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError) |
|
46 | RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError) | |
@@ -48,7 +48,6 b' from rhodecode.model.db import Changeset' | |||||
48 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
48 | from rhodecode.model.changeset_status import ChangesetStatusModel | |
49 | from rhodecode.model.comment import CommentsModel |
|
49 | from rhodecode.model.comment import CommentsModel | |
50 | from rhodecode.model.meta import Session |
|
50 | from rhodecode.model.meta import Session | |
51 | from rhodecode.model.repo import RepoModel |
|
|||
52 |
|
51 | |||
53 |
|
52 | |||
54 | log = logging.getLogger(__name__) |
|
53 | log = logging.getLogger(__name__) | |
@@ -268,8 +267,10 b' class ChangesetController(BaseRepoContro' | |||||
268 | repo_name=c.repo_name, |
|
267 | repo_name=c.repo_name, | |
269 | source_node_getter=_node_getter(commit1), |
|
268 | source_node_getter=_node_getter(commit1), | |
270 | target_node_getter=_node_getter(commit2), |
|
269 | target_node_getter=_node_getter(commit2), | |
271 | comments=inline_comments |
|
270 | comments=inline_comments) | |
272 | ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id) |
|
271 | diffset = diffset.render_patchset( | |
|
272 | _parsed, commit1.raw_id, commit2.raw_id) | |||
|
273 | ||||
273 | c.changes[commit.raw_id] = diffset |
|
274 | c.changes[commit.raw_id] = diffset | |
274 | else: |
|
275 | else: | |
275 | # downloads/raw we only need RAW diff nothing else |
|
276 | # downloads/raw we only need RAW diff nothing else | |
@@ -368,7 +369,6 b' class ChangesetController(BaseRepoContro' | |||||
368 | comment_type=comment_type, |
|
369 | comment_type=comment_type, | |
369 | resolves_comment_id=resolves_comment_id |
|
370 | resolves_comment_id=resolves_comment_id | |
370 | ) |
|
371 | ) | |
371 | c.inline_comment = True if comment.line_no else False |
|
|||
372 |
|
372 | |||
373 | # get status if set ! |
|
373 | # get status if set ! | |
374 | if status: |
|
374 | if status: | |
@@ -433,20 +433,26 b' class ChangesetController(BaseRepoContro' | |||||
433 | @auth.CSRFRequired() |
|
433 | @auth.CSRFRequired() | |
434 | @jsonify |
|
434 | @jsonify | |
435 | def delete_comment(self, repo_name, comment_id): |
|
435 | def delete_comment(self, repo_name, comment_id): | |
436 | comment = ChangesetComment.get(comment_id) |
|
436 | comment = ChangesetComment.get_or_404(safe_int(comment_id)) | |
437 | if not comment: |
|
437 | if not comment: | |
438 | log.debug('Comment with id:%s not found, skipping', comment_id) |
|
438 | log.debug('Comment with id:%s not found, skipping', comment_id) | |
439 | # comment already deleted in another call probably |
|
439 | # comment already deleted in another call probably | |
440 | return True |
|
440 | return True | |
441 |
|
441 | |||
442 | owner = (comment.author.user_id == c.rhodecode_user.user_id) |
|
|||
443 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) |
|
442 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) | |
444 |
|
|
443 | super_admin = h.HasPermissionAny('hg.admin')() | |
445 | CommentsModel().delete(comment=comment) |
|
444 | comment_owner = (comment.author.user_id == c.rhodecode_user.user_id) | |
|
445 | is_repo_comment = comment.repo.repo_name == c.repo_name | |||
|
446 | comment_repo_admin = is_repo_admin and is_repo_comment | |||
|
447 | ||||
|
448 | if super_admin or comment_owner or comment_repo_admin: | |||
|
449 | CommentsModel().delete(comment=comment, user=c.rhodecode_user) | |||
446 | Session().commit() |
|
450 | Session().commit() | |
447 | return True |
|
451 | return True | |
448 | else: |
|
452 | else: | |
449 | raise HTTPForbidden() |
|
453 | log.warning('No permissions for user %s to delete comment_id: %s', | |
|
454 | c.rhodecode_user, comment_id) | |||
|
455 | raise HTTPNotFound() | |||
450 |
|
456 | |||
451 | @LoginRequired() |
|
457 | @LoginRequired() | |
452 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
458 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
@@ -24,7 +24,7 b' Compare controller for showing differenc' | |||||
24 |
|
24 | |||
25 | import logging |
|
25 | import logging | |
26 |
|
26 | |||
27 | from webob.exc import HTTPBadRequest |
|
27 | from webob.exc import HTTPBadRequest, HTTPNotFound | |
28 | from pylons import request, tmpl_context as c, url |
|
28 | from pylons import request, tmpl_context as c, url | |
29 | from pylons.controllers.util import redirect |
|
29 | from pylons.controllers.util import redirect | |
30 | from pylons.i18n.translation import _ |
|
30 | from pylons.i18n.translation import _ | |
@@ -63,14 +63,13 b' class CompareController(BaseRepoControll' | |||||
63 | return repo.scm_instance().EMPTY_COMMIT |
|
63 | return repo.scm_instance().EMPTY_COMMIT | |
64 | h.flash(h.literal(_('There are no commits yet')), |
|
64 | h.flash(h.literal(_('There are no commits yet')), | |
65 | category='warning') |
|
65 | category='warning') | |
66 |
redirect( |
|
66 | redirect(h.route_path('repo_summary', repo_name=repo.repo_name)) | |
67 |
|
67 | |||
68 | except RepositoryError as e: |
|
68 | except RepositoryError as e: | |
69 |
|
|
69 | log.exception(safe_str(e)) | |
70 | log.exception(msg) |
|
70 | h.flash(safe_str(h.escape(e)), category='warning') | |
71 | h.flash(msg, category='warning') |
|
|||
72 | if not partial: |
|
71 | if not partial: | |
73 |
redirect(h. |
|
72 | redirect(h.route_path('repo_summary', repo_name=repo.repo_name)) | |
74 | raise HTTPBadRequest() |
|
73 | raise HTTPBadRequest() | |
75 |
|
74 | |||
76 | @LoginRequired() |
|
75 | @LoginRequired() | |
@@ -86,6 +85,10 b' class CompareController(BaseRepoControll' | |||||
86 | target_repo = request.GET.get('target_repo', source_repo) |
|
85 | target_repo = request.GET.get('target_repo', source_repo) | |
87 | c.source_repo = Repository.get_by_repo_name(source_repo) |
|
86 | c.source_repo = Repository.get_by_repo_name(source_repo) | |
88 | c.target_repo = Repository.get_by_repo_name(target_repo) |
|
87 | c.target_repo = Repository.get_by_repo_name(target_repo) | |
|
88 | ||||
|
89 | if c.source_repo is None or c.target_repo is None: | |||
|
90 | raise HTTPNotFound() | |||
|
91 | ||||
89 | c.source_ref = c.target_ref = _('Select commit') |
|
92 | c.source_ref = c.target_ref = _('Select commit') | |
90 | c.source_ref_type = "" |
|
93 | c.source_ref_type = "" | |
91 | c.target_ref_type = "" |
|
94 | c.target_ref_type = "" | |
@@ -141,18 +144,17 b' class CompareController(BaseRepoControll' | |||||
141 | target_repo = Repository.get_by_repo_name(target_repo_name) |
|
144 | target_repo = Repository.get_by_repo_name(target_repo_name) | |
142 |
|
145 | |||
143 | if source_repo is None: |
|
146 | if source_repo is None: | |
144 |
|
|
147 | log.error('Could not find the source repo: {}' | |
145 |
|
|
148 | .format(source_repo_name)) | |
146 |
|
149 | h.flash(_('Could not find the source repo: `{}`') | ||
147 | log.error(msg) |
|
150 | .format(h.escape(source_repo_name)), category='error') | |
148 | h.flash(msg, category='error') |
|
|||
149 | return redirect(url('compare_home', repo_name=c.repo_name)) |
|
151 | return redirect(url('compare_home', repo_name=c.repo_name)) | |
150 |
|
152 | |||
151 | if target_repo is None: |
|
153 | if target_repo is None: | |
152 |
|
|
154 | log.error('Could not find the target repo: {}' | |
153 |
|
|
155 | .format(source_repo_name)) | |
154 | log.error(msg) |
|
156 | h.flash(_('Could not find the target repo: `{}`') | |
155 |
h. |
|
157 | .format(h.escape(target_repo_name)), category='error') | |
156 | return redirect(url('compare_home', repo_name=c.repo_name)) |
|
158 | return redirect(url('compare_home', repo_name=c.repo_name)) | |
157 |
|
159 | |||
158 | source_scm = source_repo.scm_instance() |
|
160 | source_scm = source_repo.scm_instance() | |
@@ -269,11 +271,13 b' class CompareController(BaseRepoControll' | |||||
269 | return None |
|
271 | return None | |
270 | return get_node |
|
272 | return get_node | |
271 |
|
273 | |||
272 |
|
|
274 | diffset = codeblocks.DiffSet( | |
273 | repo_name=source_repo.repo_name, |
|
275 | repo_name=source_repo.repo_name, | |
274 | source_node_getter=_node_getter(source_commit), |
|
276 | source_node_getter=_node_getter(source_commit), | |
275 | target_node_getter=_node_getter(target_commit), |
|
277 | target_node_getter=_node_getter(target_commit), | |
276 | ).render_patchset(_parsed, source_ref, target_ref) |
|
278 | ) | |
|
279 | c.diffset = diffset.render_patchset( | |||
|
280 | _parsed, source_ref, target_ref) | |||
277 |
|
281 | |||
278 | c.preview_mode = merge |
|
282 | c.preview_mode = merge | |
279 | c.source_commit = source_commit |
|
283 | c.source_commit = source_commit |
@@ -113,7 +113,7 b' class FeedController(BaseRepoController)' | |||||
113 | def _generate_feed(cache_key): |
|
113 | def _generate_feed(cache_key): | |
114 | feed = Atom1Feed( |
|
114 | feed = Atom1Feed( | |
115 | title=self.title % repo_name, |
|
115 | title=self.title % repo_name, | |
116 |
link=url('summary |
|
116 | link=h.route_url('repo_summary', repo_name=repo_name), | |
117 | description=self.description % repo_name, |
|
117 | description=self.description % repo_name, | |
118 | language=self.language, |
|
118 | language=self.language, | |
119 | ttl=self.ttl |
|
119 | ttl=self.ttl | |
@@ -150,8 +150,7 b' class FeedController(BaseRepoController)' | |||||
150 | def _generate_feed(cache_key): |
|
150 | def _generate_feed(cache_key): | |
151 | feed = Rss201rev2Feed( |
|
151 | feed = Rss201rev2Feed( | |
152 | title=self.title % repo_name, |
|
152 | title=self.title % repo_name, | |
153 |
link=url('summary |
|
153 | link=h.route_url('repo_summary', repo_name=repo_name), | |
154 | qualified=True), |
|
|||
155 | description=self.description % repo_name, |
|
154 | description=self.description % repo_name, | |
156 | language=self.language, |
|
155 | language=self.language, | |
157 | ttl=self.ttl |
|
156 | ttl=self.ttl |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from rhodecode/templates/admin/admin.mako to rhodecode/templates/admin/admin_audit_logs.mako |
|
NO CONTENT: file renamed from rhodecode/templates/admin/admin.mako to rhodecode/templates/admin/admin_audit_logs.mako | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from rhodecode/tests/controllers/test_search.py to rhodecode/tests/lib/test_search.py |
|
NO CONTENT: file renamed from rhodecode/tests/controllers/test_search.py to rhodecode/tests/lib/test_search.py |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
General Comments 0
You need to be logged in to leave comments.
Login now