##// END OF EJS Templates
fixes for tests on Windows
marcink -
r2255:95800dad beta
parent child Browse files
Show More
@@ -1,486 +1,487 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import traceback
29 29
30 30 from pylons import request, response, tmpl_context as c, url
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33 from pylons.decorators import jsonify
34 34
35 35 from rhodecode.lib import diffs
36 36 from rhodecode.lib import helpers as h
37 37
38 38 from rhodecode.lib.compat import OrderedDict
39 39 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
40 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 41 from rhodecode.lib.base import BaseRepoController, render
42 42 from rhodecode.lib.utils import EmptyChangeset
43 43 from rhodecode.lib.vcs.conf import settings
44 44 from rhodecode.lib.vcs.exceptions import RepositoryError, \
45 45 ChangesetDoesNotExistError, EmptyRepositoryError, \
46 46 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
47 47 from rhodecode.lib.vcs.nodes import FileNode
48 48
49 49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.db import Repository
51 52
52 53 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
53 54 _context_url, get_line_ctx, get_ignore_ws
54 55
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 class FilesController(BaseRepoController):
60 61
61 62 @LoginRequired()
62 63 def __before__(self):
63 64 super(FilesController, self).__before__()
64 65 c.cut_off_limit = self.cut_off_limit
65 66
66 67 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
67 68 """
68 69 Safe way to get changeset if error occur it redirects to tip with
69 70 proper message
70 71
71 72 :param rev: revision to fetch
72 73 :param repo_name: repo name to redirect after
73 74 """
74 75
75 76 try:
76 77 return c.rhodecode_repo.get_changeset(rev)
77 78 except EmptyRepositoryError, e:
78 79 if not redirect_after:
79 80 return None
80 81 url_ = url('files_add_home',
81 82 repo_name=c.repo_name,
82 83 revision=0, f_path='')
83 84 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
84 85 h.flash(h.literal(_('There are no files yet %s' % add_new)),
85 86 category='warning')
86 87 redirect(h.url('summary_home', repo_name=repo_name))
87 88
88 89 except RepositoryError, e:
89 90 h.flash(str(e), category='warning')
90 91 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
91 92
92 93 def __get_filenode_or_redirect(self, repo_name, cs, path):
93 94 """
94 95 Returns file_node, if error occurs or given path is directory,
95 96 it'll redirect to top level path
96 97
97 98 :param repo_name: repo_name
98 99 :param cs: given changeset
99 100 :param path: path to lookup
100 101 """
101 102
102 103 try:
103 104 file_node = cs.get_node(path)
104 105 if file_node.is_dir():
105 106 raise RepositoryError('given path is a directory')
106 107 except RepositoryError, e:
107 108 h.flash(str(e), category='warning')
108 109 redirect(h.url('files_home', repo_name=repo_name,
109 110 revision=cs.raw_id))
110 111
111 112 return file_node
112 113
113 114 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
114 115 'repository.admin')
115 116 def index(self, repo_name, revision, f_path, annotate=False):
116 117 # redirect to given revision from form if given
117 118 post_revision = request.POST.get('at_rev', None)
118 119 if post_revision:
119 120 cs = self.__get_cs_or_redirect(post_revision, repo_name)
120 121 redirect(url('files_home', repo_name=c.repo_name,
121 122 revision=cs.raw_id, f_path=f_path))
122 123
123 124 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
124 125 c.branch = request.GET.get('branch', None)
125 126 c.f_path = f_path
126 127 c.annotate = annotate
127 128 cur_rev = c.changeset.revision
128 129
129 130 # prev link
130 131 try:
131 132 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
132 133 c.url_prev = url('files_home', repo_name=c.repo_name,
133 134 revision=prev_rev.raw_id, f_path=f_path)
134 135 if c.branch:
135 136 c.url_prev += '?branch=%s' % c.branch
136 137 except (ChangesetDoesNotExistError, VCSError):
137 138 c.url_prev = '#'
138 139
139 140 # next link
140 141 try:
141 142 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
142 143 c.url_next = url('files_home', repo_name=c.repo_name,
143 144 revision=next_rev.raw_id, f_path=f_path)
144 145 if c.branch:
145 146 c.url_next += '?branch=%s' % c.branch
146 147 except (ChangesetDoesNotExistError, VCSError):
147 148 c.url_next = '#'
148 149
149 150 # files or dirs
150 151 try:
151 152 c.file = c.changeset.get_node(f_path)
152 153
153 154 if c.file.is_file():
154 155 c.file_history = self._get_node_history(c.changeset, f_path)
155 156 else:
156 157 c.file_history = []
157 158 except RepositoryError, e:
158 159 h.flash(str(e), category='warning')
159 160 redirect(h.url('files_home', repo_name=repo_name,
160 161 revision=revision))
161 162
162 163 return render('files/files.html')
163 164
164 165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
165 166 'repository.admin')
166 167 def rawfile(self, repo_name, revision, f_path):
167 168 cs = self.__get_cs_or_redirect(revision, repo_name)
168 169 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
169 170
170 171 response.content_disposition = 'attachment; filename=%s' % \
171 safe_str(f_path.split(os.sep)[-1])
172 safe_str(f_path.split(Repository.url_sep())[-1])
172 173
173 174 response.content_type = file_node.mimetype
174 175 return file_node.content
175 176
176 177 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
177 178 'repository.admin')
178 179 def raw(self, repo_name, revision, f_path):
179 180 cs = self.__get_cs_or_redirect(revision, repo_name)
180 181 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
181 182
182 183 raw_mimetype_mapping = {
183 184 # map original mimetype to a mimetype used for "show as raw"
184 185 # you can also provide a content-disposition to override the
185 186 # default "attachment" disposition.
186 187 # orig_type: (new_type, new_dispo)
187 188
188 189 # show images inline:
189 190 'image/x-icon': ('image/x-icon', 'inline'),
190 191 'image/png': ('image/png', 'inline'),
191 192 'image/gif': ('image/gif', 'inline'),
192 193 'image/jpeg': ('image/jpeg', 'inline'),
193 194 'image/svg+xml': ('image/svg+xml', 'inline'),
194 195 }
195 196
196 197 mimetype = file_node.mimetype
197 198 try:
198 199 mimetype, dispo = raw_mimetype_mapping[mimetype]
199 200 except KeyError:
200 201 # we don't know anything special about this, handle it safely
201 202 if file_node.is_binary:
202 203 # do same as download raw for binary files
203 204 mimetype, dispo = 'application/octet-stream', 'attachment'
204 205 else:
205 206 # do not just use the original mimetype, but force text/plain,
206 207 # otherwise it would serve text/html and that might be unsafe.
207 208 # Note: underlying vcs library fakes text/plain mimetype if the
208 209 # mimetype can not be determined and it thinks it is not
209 210 # binary.This might lead to erroneous text display in some
210 211 # cases, but helps in other cases, like with text files
211 212 # without extension.
212 213 mimetype, dispo = 'text/plain', 'inline'
213 214
214 215 if dispo == 'attachment':
215 216 dispo = 'attachment; filename=%s' % \
216 217 safe_str(f_path.split(os.sep)[-1])
217 218
218 219 response.content_disposition = dispo
219 220 response.content_type = mimetype
220 221 return file_node.content
221 222
222 223 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
223 224 def edit(self, repo_name, revision, f_path):
224 225 r_post = request.POST
225 226
226 227 c.cs = self.__get_cs_or_redirect(revision, repo_name)
227 228 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
228 229
229 230 if c.file.is_binary:
230 231 return redirect(url('files_home', repo_name=c.repo_name,
231 232 revision=c.cs.raw_id, f_path=f_path))
232 233
233 234 c.f_path = f_path
234 235
235 236 if r_post:
236 237
237 238 old_content = c.file.content
238 239 sl = old_content.splitlines(1)
239 240 first_line = sl[0] if sl else ''
240 241 # modes: 0 - Unix, 1 - Mac, 2 - DOS
241 242 mode = detect_mode(first_line, 0)
242 243 content = convert_line_endings(r_post.get('content'), mode)
243 244
244 245 message = r_post.get('message') or (_('Edited %s via RhodeCode')
245 246 % (f_path))
246 247 author = self.rhodecode_user.full_contact
247 248
248 249 if content == old_content:
249 250 h.flash(_('No changes'),
250 251 category='warning')
251 252 return redirect(url('changeset_home', repo_name=c.repo_name,
252 253 revision='tip'))
253 254
254 255 try:
255 256 self.scm_model.commit_change(repo=c.rhodecode_repo,
256 257 repo_name=repo_name, cs=c.cs,
257 258 user=self.rhodecode_user,
258 259 author=author, message=message,
259 260 content=content, f_path=f_path)
260 261 h.flash(_('Successfully committed to %s' % f_path),
261 262 category='success')
262 263
263 264 except Exception:
264 265 log.error(traceback.format_exc())
265 266 h.flash(_('Error occurred during commit'), category='error')
266 267 return redirect(url('changeset_home',
267 268 repo_name=c.repo_name, revision='tip'))
268 269
269 270 return render('files/files_edit.html')
270 271
271 272 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
272 273 def add(self, repo_name, revision, f_path):
273 274 r_post = request.POST
274 275 c.cs = self.__get_cs_or_redirect(revision, repo_name,
275 276 redirect_after=False)
276 277 if c.cs is None:
277 278 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
278 279
279 280 c.f_path = f_path
280 281
281 282 if r_post:
282 283 unix_mode = 0
283 284 content = convert_line_endings(r_post.get('content'), unix_mode)
284 285
285 286 message = r_post.get('message') or (_('Added %s via RhodeCode')
286 287 % (f_path))
287 288 location = r_post.get('location')
288 289 filename = r_post.get('filename')
289 290 file_obj = r_post.get('upload_file', None)
290 291
291 292 if file_obj is not None and hasattr(file_obj, 'filename'):
292 293 filename = file_obj.filename
293 294 content = file_obj.file
294 295
295 296 node_path = os.path.join(location, filename)
296 297 author = self.rhodecode_user.full_contact
297 298
298 299 if not content:
299 300 h.flash(_('No content'), category='warning')
300 301 return redirect(url('changeset_home', repo_name=c.repo_name,
301 302 revision='tip'))
302 303 if not filename:
303 304 h.flash(_('No filename'), category='warning')
304 305 return redirect(url('changeset_home', repo_name=c.repo_name,
305 306 revision='tip'))
306 307
307 308 try:
308 309 self.scm_model.create_node(repo=c.rhodecode_repo,
309 310 repo_name=repo_name, cs=c.cs,
310 311 user=self.rhodecode_user,
311 312 author=author, message=message,
312 313 content=content, f_path=node_path)
313 314 h.flash(_('Successfully committed to %s' % node_path),
314 315 category='success')
315 316 except NodeAlreadyExistsError, e:
316 317 h.flash(_(e), category='error')
317 318 except Exception:
318 319 log.error(traceback.format_exc())
319 320 h.flash(_('Error occurred during commit'), category='error')
320 321 return redirect(url('changeset_home',
321 322 repo_name=c.repo_name, revision='tip'))
322 323
323 324 return render('files/files_add.html')
324 325
325 326 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 327 'repository.admin')
327 328 def archivefile(self, repo_name, fname):
328 329
329 330 fileformat = None
330 331 revision = None
331 332 ext = None
332 333 subrepos = request.GET.get('subrepos') == 'true'
333 334
334 335 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
335 336 archive_spec = fname.split(ext_data[1])
336 337 if len(archive_spec) == 2 and archive_spec[1] == '':
337 338 fileformat = a_type or ext_data[1]
338 339 revision = archive_spec[0]
339 340 ext = ext_data[1]
340 341
341 342 try:
342 343 dbrepo = RepoModel().get_by_repo_name(repo_name)
343 344 if dbrepo.enable_downloads is False:
344 345 return _('downloads disabled')
345 346
346 347 if c.rhodecode_repo.alias == 'hg':
347 348 # patch and reset hooks section of UI config to not run any
348 349 # hooks on fetching archives with subrepos
349 350 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
350 351 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
351 352
352 353 cs = c.rhodecode_repo.get_changeset(revision)
353 354 content_type = settings.ARCHIVE_SPECS[fileformat][0]
354 355 except ChangesetDoesNotExistError:
355 356 return _('Unknown revision %s') % revision
356 357 except EmptyRepositoryError:
357 358 return _('Empty repository')
358 359 except (ImproperArchiveTypeError, KeyError):
359 360 return _('Unknown archive type')
360 361
361 362 response.content_type = content_type
362 363 response.content_disposition = 'attachment; filename=%s-%s%s' \
363 364 % (repo_name, revision, ext)
364 365
365 366 import tempfile
366 367 archive = tempfile.mkstemp()[1]
367 368 t = open(archive, 'wb')
368 369 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
369 370
370 371 def get_chunked_archive(archive):
371 372 stream = open(archive, 'rb')
372 373 while True:
373 374 data = stream.read(4096)
374 375 if not data:
375 376 os.remove(archive)
376 377 break
377 378 yield data
378 379
379 380 return get_chunked_archive(archive)
380 381
381 382 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
382 383 'repository.admin')
383 384 def diff(self, repo_name, f_path):
384 385 ignore_whitespace = request.GET.get('ignorews') == '1'
385 386 line_context = request.GET.get('context', 3)
386 387 diff1 = request.GET.get('diff1', '')
387 388 diff2 = request.GET.get('diff2', '')
388 389 c.action = request.GET.get('diff')
389 390 c.no_changes = diff1 == diff2
390 391 c.f_path = f_path
391 392 c.big_diff = False
392 393 c.anchor_url = anchor_url
393 394 c.ignorews_url = _ignorews_url
394 395 c.context_url = _context_url
395 396 c.changes = OrderedDict()
396 397 c.changes[diff2] = []
397 398 try:
398 399 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
399 400 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
400 401 node1 = c.changeset_1.get_node(f_path)
401 402 else:
402 403 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
403 404 node1 = FileNode('.', '', changeset=c.changeset_1)
404 405
405 406 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
406 407 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
407 408 node2 = c.changeset_2.get_node(f_path)
408 409 else:
409 410 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
410 411 node2 = FileNode('.', '', changeset=c.changeset_2)
411 412 except RepositoryError:
412 413 return redirect(url('files_home', repo_name=c.repo_name,
413 414 f_path=f_path))
414 415
415 416 if c.action == 'download':
416 417 _diff = diffs.get_gitdiff(node1, node2,
417 418 ignore_whitespace=ignore_whitespace,
418 419 context=line_context)
419 420 diff = diffs.DiffProcessor(_diff, format='gitdiff')
420 421
421 422 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
422 423 response.content_type = 'text/plain'
423 424 response.content_disposition = (
424 425 'attachment; filename=%s' % diff_name
425 426 )
426 427 return diff.raw_diff()
427 428
428 429 elif c.action == 'raw':
429 430 _diff = diffs.get_gitdiff(node1, node2,
430 431 ignore_whitespace=ignore_whitespace,
431 432 context=line_context)
432 433 diff = diffs.DiffProcessor(_diff, format='gitdiff')
433 434 response.content_type = 'text/plain'
434 435 return diff.raw_diff()
435 436
436 437 else:
437 438 fid = h.FID(diff2, node2.path)
438 439 line_context_lcl = get_line_ctx(fid, request.GET)
439 440 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
440 441
441 442 lim = request.GET.get('fulldiff') or self.cut_off_limit
442 443 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
443 444 filenode_new=node2,
444 445 cut_off_limit=lim,
445 446 ignore_whitespace=ign_whitespace_lcl,
446 447 line_context=line_context_lcl,
447 448 enable_comments=False)
448 449
449 450 c.changes = [('', node2, diff, cs1, cs2, st,)]
450 451
451 452 return render('files/file_diff.html')
452 453
453 454 def _get_node_history(self, cs, f_path):
454 455 changesets = cs.get_file_history(f_path)
455 456 hist_l = []
456 457
457 458 changesets_group = ([], _("Changesets"))
458 459 branches_group = ([], _("Branches"))
459 460 tags_group = ([], _("Tags"))
460 461 _hg = cs.repository.alias == 'hg'
461 462 for chs in changesets:
462 463 _branch = '(%s)' % chs.branch if _hg else ''
463 464 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
464 465 changesets_group[0].append((chs.raw_id, n_desc,))
465 466
466 467 hist_l.append(changesets_group)
467 468
468 469 for name, chs in c.rhodecode_repo.branches.items():
469 470 branches_group[0].append((chs, name),)
470 471 hist_l.append(branches_group)
471 472
472 473 for name, chs in c.rhodecode_repo.tags.items():
473 474 tags_group[0].append((chs, name),)
474 475 hist_l.append(tags_group)
475 476
476 477 return hist_l
477 478
478 479 @jsonify
479 480 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
480 481 'repository.admin')
481 482 def nodelist(self, repo_name, revision, f_path):
482 483 if request.environ.get('HTTP_X_PARTIAL_XHR'):
483 484 cs = self.__get_cs_or_redirect(revision, repo_name)
484 485 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
485 486 flat=False)
486 487 return _d + _f
@@ -1,315 +1,316 b''
1 1 from rhodecode.tests import *
2 2
3 3 ARCHIVE_SPECS = {
4 4 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
5 5 '.tar.gz': ('application/x-gzip', 'tgz', ''),
6 6 '.zip': ('application/zip', 'zip', ''),
7 7 }
8 8
9 9
10 10 class TestFilesController(TestController):
11 11
12 12 def test_index(self):
13 13 self.log_user()
14 14 response = self.app.get(url(controller='files', action='index',
15 15 repo_name=HG_REPO,
16 16 revision='tip',
17 17 f_path='/'))
18 18 # Test response...
19 19 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
20 20 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
21 21 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
22 22 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
23 23 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
24 24
25 25 def test_index_revision(self):
26 26 self.log_user()
27 27
28 28 response = self.app.get(
29 29 url(controller='files', action='index',
30 30 repo_name=HG_REPO,
31 31 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
32 32 f_path='/')
33 33 )
34 34
35 35 #Test response...
36 36
37 37 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
38 38 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
39 39 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
40 40 response.mustcontain('1.1 KiB')
41 41 response.mustcontain('text/x-python')
42 42
43 43 def test_index_different_branch(self):
44 44 self.log_user()
45 45
46 46 response = self.app.get(url(controller='files', action='index',
47 47 repo_name=HG_REPO,
48 48 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
49 49 f_path='/'))
50 50
51 51 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
52 52
53 53 def test_index_paging(self):
54 54 self.log_user()
55 55
56 56 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
57 57 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
58 58 (109, '75feb4c33e81186c87eac740cee2447330288412'),
59 59 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
60 60 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
61 61
62 62 response = self.app.get(url(controller='files', action='index',
63 63 repo_name=HG_REPO,
64 64 revision=r[1],
65 65 f_path='/'))
66 66
67 67 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
68 68
69 69 def test_file_source(self):
70 70 self.log_user()
71 71 response = self.app.get(url(controller='files', action='index',
72 72 repo_name=HG_REPO,
73 73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
74 74 f_path='vcs/nodes.py'))
75 75
76 76 #test or history
77 77 response.mustcontain("""<optgroup label="Changesets">
78 78 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
79 79 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
80 80 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
81 81 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
82 82 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
83 83 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
84 84 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
85 85 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
86 86 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
87 87 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
88 88 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
89 89 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
90 90 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
91 91 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
92 92 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
93 93 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
94 94 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
95 95 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
96 96 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
97 97 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
98 98 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
99 99 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
100 100 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
101 101 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
102 102 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
103 103 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
104 104 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
105 105 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
106 106 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
107 107 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
108 108 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
109 109 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
110 110 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
111 111 </optgroup>
112 112 <optgroup label="Branches">
113 113 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
114 114 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
115 115 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
116 116 </optgroup>
117 117 <optgroup label="Tags">
118 118 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
119 119 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
120 120 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
121 121 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
122 122 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
123 123 </optgroup>
124 124 """)
125 125
126 126 response.mustcontain("""<div class="commit">merge</div>""")
127 127
128 128 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
129 129
130 130 def test_file_annotation(self):
131 131 self.log_user()
132 132 response = self.app.get(url(controller='files', action='index',
133 133 repo_name=HG_REPO,
134 134 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
135 135 f_path='vcs/nodes.py',
136 136 annotate=True))
137 137
138 138
139 139 response.mustcontain("""<optgroup label="Changesets">
140 140 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
141 141 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
142 142 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
143 143 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
144 144 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
145 145 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
146 146 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
147 147 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
148 148 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
149 149 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
150 150 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
151 151 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
152 152 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
153 153 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
154 154 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
155 155 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
156 156 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
157 157 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
158 158 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
159 159 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
160 160 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
161 161 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
162 162 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
163 163 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
164 164 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
165 165 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
166 166 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
167 167 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
168 168 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
169 169 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
170 170 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
171 171 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
172 172 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
173 173 </optgroup>
174 174 <optgroup label="Branches">
175 175 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
176 176 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
177 177 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
178 178 </optgroup>
179 179 <optgroup label="Tags">
180 180 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
181 181 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
182 182 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
183 183 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
184 184 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
185 185 </optgroup>""")
186 186
187 187 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
188 188
189 189 def test_archival(self):
190 190 self.log_user()
191 191
192 192 for arch_ext, info in ARCHIVE_SPECS.items():
193 193 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
194 194 filename = '%s-%s' % (HG_REPO, fname)
195 195
196 196 response = self.app.get(url(controller='files', action='archivefile',
197 197 repo_name=HG_REPO,
198 198 fname=fname))
199 199
200 assert response.status == '200 OK', 'wrong response code'
201 assert response.response._headers.items() == [('Pragma', 'no-cache'),
200 self.assertEqual(response.status, '200 OK')
201 self.assertEqual(response.response._headers.items(),
202 [('Pragma', 'no-cache'),
202 203 ('Cache-Control', 'no-cache'),
203 204 ('Content-Type', '%s; charset=utf-8' % info[0]),
204 ('Content-Disposition', 'attachment; filename=%s' % filename), ], 'wrong headers'
205 ('Content-Disposition', 'attachment; filename=%s' % filename),]
206 )
205 207
206 208 def test_archival_wrong_ext(self):
207 209 self.log_user()
208 210
209 211 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
210 212 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
211 213
212 214 response = self.app.get(url(controller='files', action='archivefile',
213 215 repo_name=HG_REPO,
214 216 fname=fname))
215 assert 'Unknown archive type' in response.body
216
217 response.mustcontain('Unknown archive type')
217 218
218 219 def test_archival_wrong_revision(self):
219 220 self.log_user()
220 221
221 222 for rev in ['00x000000', 'tar', 'wrong', '@##$@$424213232', '232dffcd']:
222 223 fname = '%s.zip' % rev
223 224
224 225 response = self.app.get(url(controller='files', action='archivefile',
225 226 repo_name=HG_REPO,
226 227 fname=fname))
227 assert 'Unknown revision' in response.body
228 response.mustcontain('Unknown revision')
228 229
229 230 #==========================================================================
230 231 # RAW FILE
231 232 #==========================================================================
232 233 def test_raw_file_ok(self):
233 234 self.log_user()
234 235 response = self.app.get(url(controller='files', action='rawfile',
235 236 repo_name=HG_REPO,
236 237 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
237 238 f_path='vcs/nodes.py'))
238 239
239 assert response.content_disposition == "attachment; filename=nodes.py"
240 assert response.content_type == "text/x-python"
240 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
241 self.assertEqual(response.content_type, "text/x-python")
241 242
242 243 def test_raw_file_wrong_cs(self):
243 244 self.log_user()
244 245 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
245 246 f_path = 'vcs/nodes.py'
246 247
247 248 response = self.app.get(url(controller='files', action='rawfile',
248 249 repo_name=HG_REPO,
249 250 revision=rev,
250 251 f_path=f_path))
251 252
252 253 msg = """Revision %r does not exist for this repository""" % (rev)
253 254 self.checkSessionFlash(response, msg)
254 255
255 256 msg = """%s""" % (HG_REPO)
256 257 self.checkSessionFlash(response, msg)
257 258
258 259 def test_raw_file_wrong_f_path(self):
259 260 self.log_user()
260 261 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
261 262 f_path = 'vcs/ERRORnodes.py'
262 263 response = self.app.get(url(controller='files', action='rawfile',
263 264 repo_name=HG_REPO,
264 265 revision=rev,
265 266 f_path=f_path))
266 267
267 268 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
268 269 self.checkSessionFlash(response, msg)
269 270
270 271 #==========================================================================
271 272 # RAW RESPONSE - PLAIN
272 273 #==========================================================================
273 274 def test_raw_ok(self):
274 275 self.log_user()
275 276 response = self.app.get(url(controller='files', action='raw',
276 277 repo_name=HG_REPO,
277 278 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
278 279 f_path='vcs/nodes.py'))
279 280
280 assert response.content_type == "text/plain"
281 self.assertEqual(response.content_type, "text/plain")
281 282
282 283 def test_raw_wrong_cs(self):
283 284 self.log_user()
284 285 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
285 286 f_path = 'vcs/nodes.py'
286 287
287 288 response = self.app.get(url(controller='files', action='raw',
288 289 repo_name=HG_REPO,
289 290 revision=rev,
290 291 f_path=f_path))
291 292 msg = """Revision %r does not exist for this repository""" % (rev)
292 293 self.checkSessionFlash(response, msg)
293 294
294 295 msg = """%s""" % (HG_REPO)
295 296 self.checkSessionFlash(response, msg)
296 297
297 298 def test_raw_wrong_f_path(self):
298 299 self.log_user()
299 300 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
300 301 f_path = 'vcs/ERRORnodes.py'
301 302 response = self.app.get(url(controller='files', action='raw',
302 303 repo_name=HG_REPO,
303 304 revision=rev,
304 305 f_path=f_path))
305 306 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
306 307 self.checkSessionFlash(response, msg)
307 308
308 309 def test_ajaxed_files_list(self):
309 310 self.log_user()
310 311 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
311 312 response = self.app.get(
312 313 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
313 314 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
314 315 )
315 316 response.mustcontain("vcs/web/simplevcs/views/repository.py")
@@ -1,715 +1,715 b''
1 1 import os
2 2 import unittest
3 3 from rhodecode.tests import *
4 4
5 5 from rhodecode.model.repos_group import ReposGroupModel
6 6 from rhodecode.model.repo import RepoModel
7 7 from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
8 UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm
8 UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm,\
9 Repository
9 10 from sqlalchemy.exc import IntegrityError
10 11 from rhodecode.model.user import UserModel
11 12
12 13 from rhodecode.model.meta import Session
13 14 from rhodecode.model.notification import NotificationModel
14 15 from rhodecode.model.users_group import UsersGroupModel
15 16 from rhodecode.lib.auth import AuthUser
16 17
17 18
18 19 def _make_group(path, desc='desc', parent_id=None,
19 20 skip_if_exists=False):
20 21
21 22 gr = RepoGroup.get_by_group_name(path)
22 23 if gr and skip_if_exists:
23 24 return gr
24 25
25 26 gr = ReposGroupModel().create(path, desc, parent_id)
26 27 return gr
27 28
28 29
29 30 class TestReposGroups(unittest.TestCase):
30 31
31 32 def setUp(self):
32 33 self.g1 = _make_group('test1', skip_if_exists=True)
33 34 Session.commit()
34 35 self.g2 = _make_group('test2', skip_if_exists=True)
35 36 Session.commit()
36 37 self.g3 = _make_group('test3', skip_if_exists=True)
37 38 Session.commit()
38 39
39 40 def tearDown(self):
40 41 print 'out'
41 42
42 43 def __check_path(self, *path):
43 44 """
44 45 Checks the path for existance !
45 46 """
46 47 path = [TESTS_TMP_PATH] + list(path)
47 48 path = os.path.join(*path)
48 49 return os.path.isdir(path)
49 50
50 51 def _check_folders(self):
51 52 print os.listdir(TESTS_TMP_PATH)
52 53
53 54 def __delete_group(self, id_):
54 55 ReposGroupModel().delete(id_)
55 56
56 57 def __update_group(self, id_, path, desc='desc', parent_id=None):
57 58 form_data = dict(
58 59 group_name=path,
59 60 group_description=desc,
60 61 group_parent_id=parent_id,
61 62 perms_updates=[],
62 63 perms_new=[]
63 64 )
64 65 gr = ReposGroupModel().update(id_, form_data)
65 66 return gr
66 67
67 68 def test_create_group(self):
68 69 g = _make_group('newGroup')
69 70 self.assertEqual(g.full_path, 'newGroup')
70 71
71 72 self.assertTrue(self.__check_path('newGroup'))
72 73
73 74 def test_create_same_name_group(self):
74 75 self.assertRaises(IntegrityError, lambda:_make_group('newGroup'))
75 76 Session.rollback()
76 77
77 78 def test_same_subgroup(self):
78 79 sg1 = _make_group('sub1', parent_id=self.g1.group_id)
79 80 self.assertEqual(sg1.parent_group, self.g1)
80 81 self.assertEqual(sg1.full_path, 'test1/sub1')
81 82 self.assertTrue(self.__check_path('test1', 'sub1'))
82 83
83 84 ssg1 = _make_group('subsub1', parent_id=sg1.group_id)
84 85 self.assertEqual(ssg1.parent_group, sg1)
85 86 self.assertEqual(ssg1.full_path, 'test1/sub1/subsub1')
86 87 self.assertTrue(self.__check_path('test1', 'sub1', 'subsub1'))
87 88
88 89 def test_remove_group(self):
89 90 sg1 = _make_group('deleteme')
90 91 self.__delete_group(sg1.group_id)
91 92
92 93 self.assertEqual(RepoGroup.get(sg1.group_id), None)
93 94 self.assertFalse(self.__check_path('deteteme'))
94 95
95 96 sg1 = _make_group('deleteme', parent_id=self.g1.group_id)
96 97 self.__delete_group(sg1.group_id)
97 98
98 99 self.assertEqual(RepoGroup.get(sg1.group_id), None)
99 100 self.assertFalse(self.__check_path('test1', 'deteteme'))
100 101
101 102 def test_rename_single_group(self):
102 103 sg1 = _make_group('initial')
103 104
104 105 new_sg1 = self.__update_group(sg1.group_id, 'after')
105 106 self.assertTrue(self.__check_path('after'))
106 107 self.assertEqual(RepoGroup.get_by_group_name('initial'), None)
107 108
108 109 def test_update_group_parent(self):
109 110
110 111 sg1 = _make_group('initial', parent_id=self.g1.group_id)
111 112
112 113 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g1.group_id)
113 114 self.assertTrue(self.__check_path('test1', 'after'))
114 115 self.assertEqual(RepoGroup.get_by_group_name('test1/initial'), None)
115 116
116 117 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g3.group_id)
117 118 self.assertTrue(self.__check_path('test3', 'after'))
118 119 self.assertEqual(RepoGroup.get_by_group_name('test3/initial'), None)
119 120
120 121 new_sg1 = self.__update_group(sg1.group_id, 'hello')
121 122 self.assertTrue(self.__check_path('hello'))
122 123
123 124 self.assertEqual(RepoGroup.get_by_group_name('hello'), new_sg1)
124 125
125 126 def test_subgrouping_with_repo(self):
126 127
127 128 g1 = _make_group('g1')
128 129 g2 = _make_group('g2')
129 130
130 131 # create new repo
131 132 form_data = dict(repo_name='john',
132 133 repo_name_full='john',
133 134 fork_name=None,
134 135 description=None,
135 136 repo_group=None,
136 137 private=False,
137 138 repo_type='hg',
138 139 clone_uri=None)
139 140 cur_user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
140 141 r = RepoModel().create(form_data, cur_user)
141 142
142 143 self.assertEqual(r.repo_name, 'john')
143 144
144 145 # put repo into group
145 146 form_data = form_data
146 147 form_data['repo_group'] = g1.group_id
147 148 form_data['perms_new'] = []
148 149 form_data['perms_updates'] = []
149 150 RepoModel().update(r.repo_name, form_data)
150 151 self.assertEqual(r.repo_name, 'g1/john')
151 152
152 153 self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id)
153 154 self.assertTrue(self.__check_path('g2', 'g1'))
154 155
155 156 # test repo
156 self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
157
157 self.assertEqual(r.repo_name, RepoGroup.url_sep().join(['g2', 'g1', r.just_name]))
158 158
159 159 def test_move_to_root(self):
160 160 g1 = _make_group('t11')
161 161 Session.commit()
162 162 g2 = _make_group('t22',parent_id=g1.group_id)
163 163 Session.commit()
164 164
165 165 self.assertEqual(g2.full_path,'t11/t22')
166 166 self.assertTrue(self.__check_path('t11', 't22'))
167 167
168 168 g2 = self.__update_group(g2.group_id, 'g22', parent_id=None)
169 169 Session.commit()
170 170
171 171 self.assertEqual(g2.group_name,'g22')
172 172 # we moved out group from t1 to '' so it's full path should be 'g2'
173 173 self.assertEqual(g2.full_path,'g22')
174 174 self.assertFalse(self.__check_path('t11', 't22'))
175 175 self.assertTrue(self.__check_path('g22'))
176 176
177 177
178 178 class TestUser(unittest.TestCase):
179 179 def __init__(self, methodName='runTest'):
180 180 Session.remove()
181 181 super(TestUser, self).__init__(methodName=methodName)
182 182
183 183 def test_create_and_remove(self):
184 184 usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe',
185 185 email=u'u232@rhodecode.org',
186 186 name=u'u1', lastname=u'u1')
187 187 Session.commit()
188 188 self.assertEqual(User.get_by_username(u'test_user'), usr)
189 189
190 190 # make users group
191 191 users_group = UsersGroupModel().create('some_example_group')
192 192 Session.commit()
193 193
194 194 UsersGroupModel().add_user_to_group(users_group, usr)
195 195 Session.commit()
196 196
197 197 self.assertEqual(UsersGroup.get(users_group.users_group_id), users_group)
198 198 self.assertEqual(UsersGroupMember.query().count(), 1)
199 199 UserModel().delete(usr.user_id)
200 200 Session.commit()
201 201
202 202 self.assertEqual(UsersGroupMember.query().all(), [])
203 203
204 204
205 205 class TestNotifications(unittest.TestCase):
206 206
207 207 def __init__(self, methodName='runTest'):
208 208 Session.remove()
209 209 self.u1 = UserModel().create_or_update(username=u'u1',
210 210 password=u'qweqwe',
211 211 email=u'u1@rhodecode.org',
212 212 name=u'u1', lastname=u'u1')
213 213 Session.commit()
214 214 self.u1 = self.u1.user_id
215 215
216 216 self.u2 = UserModel().create_or_update(username=u'u2',
217 217 password=u'qweqwe',
218 218 email=u'u2@rhodecode.org',
219 219 name=u'u2', lastname=u'u3')
220 220 Session.commit()
221 221 self.u2 = self.u2.user_id
222 222
223 223 self.u3 = UserModel().create_or_update(username=u'u3',
224 224 password=u'qweqwe',
225 225 email=u'u3@rhodecode.org',
226 226 name=u'u3', lastname=u'u3')
227 227 Session.commit()
228 228 self.u3 = self.u3.user_id
229 229
230 230 super(TestNotifications, self).__init__(methodName=methodName)
231 231
232 232 def _clean_notifications(self):
233 233 for n in Notification.query().all():
234 234 Session.delete(n)
235 235
236 236 Session.commit()
237 237 self.assertEqual(Notification.query().all(), [])
238 238
239 239 def tearDown(self):
240 240 self._clean_notifications()
241 241
242 242 def test_create_notification(self):
243 243 self.assertEqual([], Notification.query().all())
244 244 self.assertEqual([], UserNotification.query().all())
245 245
246 246 usrs = [self.u1, self.u2]
247 247 notification = NotificationModel().create(created_by=self.u1,
248 248 subject=u'subj', body=u'hi there',
249 249 recipients=usrs)
250 250 Session.commit()
251 251 u1 = User.get(self.u1)
252 252 u2 = User.get(self.u2)
253 253 u3 = User.get(self.u3)
254 254 notifications = Notification.query().all()
255 255 self.assertEqual(len(notifications), 1)
256 256
257 257 unotification = UserNotification.query()\
258 258 .filter(UserNotification.notification == notification).all()
259 259
260 260 self.assertEqual(notifications[0].recipients, [u1, u2])
261 261 self.assertEqual(notification.notification_id,
262 262 notifications[0].notification_id)
263 263 self.assertEqual(len(unotification), len(usrs))
264 264 self.assertEqual([x.user.user_id for x in unotification], usrs)
265 265
266 266 def test_user_notifications(self):
267 267 self.assertEqual([], Notification.query().all())
268 268 self.assertEqual([], UserNotification.query().all())
269 269
270 270 notification1 = NotificationModel().create(created_by=self.u1,
271 271 subject=u'subj', body=u'hi there1',
272 272 recipients=[self.u3])
273 273 Session.commit()
274 274 notification2 = NotificationModel().create(created_by=self.u1,
275 275 subject=u'subj', body=u'hi there2',
276 276 recipients=[self.u3])
277 277 Session.commit()
278 278 u3 = Session.query(User).get(self.u3)
279 279
280 280 self.assertEqual(sorted([x.notification for x in u3.notifications]),
281 281 sorted([notification2, notification1]))
282 282
283 283 def test_delete_notifications(self):
284 284 self.assertEqual([], Notification.query().all())
285 285 self.assertEqual([], UserNotification.query().all())
286 286
287 287 notification = NotificationModel().create(created_by=self.u1,
288 288 subject=u'title', body=u'hi there3',
289 289 recipients=[self.u3, self.u1, self.u2])
290 290 Session.commit()
291 291 notifications = Notification.query().all()
292 292 self.assertTrue(notification in notifications)
293 293
294 294 Notification.delete(notification.notification_id)
295 295 Session.commit()
296 296
297 297 notifications = Notification.query().all()
298 298 self.assertFalse(notification in notifications)
299 299
300 300 un = UserNotification.query().filter(UserNotification.notification
301 301 == notification).all()
302 302 self.assertEqual(un, [])
303 303
304 304 def test_delete_association(self):
305 305
306 306 self.assertEqual([], Notification.query().all())
307 307 self.assertEqual([], UserNotification.query().all())
308 308
309 309 notification = NotificationModel().create(created_by=self.u1,
310 310 subject=u'title', body=u'hi there3',
311 311 recipients=[self.u3, self.u1, self.u2])
312 312 Session.commit()
313 313
314 314 unotification = UserNotification.query()\
315 315 .filter(UserNotification.notification ==
316 316 notification)\
317 317 .filter(UserNotification.user_id == self.u3)\
318 318 .scalar()
319 319
320 320 self.assertEqual(unotification.user_id, self.u3)
321 321
322 322 NotificationModel().delete(self.u3,
323 323 notification.notification_id)
324 324 Session.commit()
325 325
326 326 u3notification = UserNotification.query()\
327 327 .filter(UserNotification.notification ==
328 328 notification)\
329 329 .filter(UserNotification.user_id == self.u3)\
330 330 .scalar()
331 331
332 332 self.assertEqual(u3notification, None)
333 333
334 334 # notification object is still there
335 335 self.assertEqual(Notification.query().all(), [notification])
336 336
337 337 #u1 and u2 still have assignments
338 338 u1notification = UserNotification.query()\
339 339 .filter(UserNotification.notification ==
340 340 notification)\
341 341 .filter(UserNotification.user_id == self.u1)\
342 342 .scalar()
343 343 self.assertNotEqual(u1notification, None)
344 344 u2notification = UserNotification.query()\
345 345 .filter(UserNotification.notification ==
346 346 notification)\
347 347 .filter(UserNotification.user_id == self.u2)\
348 348 .scalar()
349 349 self.assertNotEqual(u2notification, None)
350 350
351 351 def test_notification_counter(self):
352 352 self._clean_notifications()
353 353 self.assertEqual([], Notification.query().all())
354 354 self.assertEqual([], UserNotification.query().all())
355 355
356 356 NotificationModel().create(created_by=self.u1,
357 357 subject=u'title', body=u'hi there_delete',
358 358 recipients=[self.u3, self.u1])
359 359 Session.commit()
360 360
361 361 self.assertEqual(NotificationModel()
362 362 .get_unread_cnt_for_user(self.u1), 1)
363 363 self.assertEqual(NotificationModel()
364 364 .get_unread_cnt_for_user(self.u2), 0)
365 365 self.assertEqual(NotificationModel()
366 366 .get_unread_cnt_for_user(self.u3), 1)
367 367
368 368 notification = NotificationModel().create(created_by=self.u1,
369 369 subject=u'title', body=u'hi there3',
370 370 recipients=[self.u3, self.u1, self.u2])
371 371 Session.commit()
372 372
373 373 self.assertEqual(NotificationModel()
374 374 .get_unread_cnt_for_user(self.u1), 2)
375 375 self.assertEqual(NotificationModel()
376 376 .get_unread_cnt_for_user(self.u2), 1)
377 377 self.assertEqual(NotificationModel()
378 378 .get_unread_cnt_for_user(self.u3), 2)
379 379
380 380
381 381 class TestUsers(unittest.TestCase):
382 382
383 383 def __init__(self, methodName='runTest'):
384 384 super(TestUsers, self).__init__(methodName=methodName)
385 385
386 386 def setUp(self):
387 387 self.u1 = UserModel().create_or_update(username=u'u1',
388 388 password=u'qweqwe',
389 389 email=u'u1@rhodecode.org',
390 390 name=u'u1', lastname=u'u1')
391 391
392 392 def tearDown(self):
393 393 perm = Permission.query().all()
394 394 for p in perm:
395 395 UserModel().revoke_perm(self.u1, p)
396 396
397 397 UserModel().delete(self.u1)
398 398 Session.commit()
399 399
400 400 def test_add_perm(self):
401 401 perm = Permission.query().all()[0]
402 402 UserModel().grant_perm(self.u1, perm)
403 403 Session.commit()
404 404 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
405 405
406 406 def test_has_perm(self):
407 407 perm = Permission.query().all()
408 408 for p in perm:
409 409 has_p = UserModel().has_perm(self.u1, p)
410 410 self.assertEqual(False, has_p)
411 411
412 412 def test_revoke_perm(self):
413 413 perm = Permission.query().all()[0]
414 414 UserModel().grant_perm(self.u1, perm)
415 415 Session.commit()
416 416 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
417 417
418 418 #revoke
419 419 UserModel().revoke_perm(self.u1, perm)
420 420 Session.commit()
421 421 self.assertEqual(UserModel().has_perm(self.u1, perm), False)
422 422
423 423
424 424 class TestPermissions(unittest.TestCase):
425 425 def __init__(self, methodName='runTest'):
426 426 super(TestPermissions, self).__init__(methodName=methodName)
427 427
428 428 def setUp(self):
429 429 self.u1 = UserModel().create_or_update(
430 430 username=u'u1', password=u'qweqwe',
431 431 email=u'u1@rhodecode.org', name=u'u1', lastname=u'u1'
432 432 )
433 433 self.u2 = UserModel().create_or_update(
434 434 username=u'u2', password=u'qweqwe',
435 435 email=u'u2@rhodecode.org', name=u'u2', lastname=u'u2'
436 436 )
437 437 self.anon = User.get_by_username('default')
438 438 self.a1 = UserModel().create_or_update(
439 439 username=u'a1', password=u'qweqwe',
440 440 email=u'a1@rhodecode.org', name=u'a1', lastname=u'a1', admin=True
441 441 )
442 442 Session.commit()
443 443
444 444 def tearDown(self):
445 445 if hasattr(self, 'test_repo'):
446 446 RepoModel().delete(repo=self.test_repo)
447 447 UserModel().delete(self.u1)
448 448 UserModel().delete(self.u2)
449 449 UserModel().delete(self.a1)
450 450 if hasattr(self, 'g1'):
451 451 ReposGroupModel().delete(self.g1.group_id)
452 452 if hasattr(self, 'g2'):
453 453 ReposGroupModel().delete(self.g2.group_id)
454 454
455 455 if hasattr(self, 'ug1'):
456 456 UsersGroupModel().delete(self.ug1, force=True)
457 457
458 458 Session.commit()
459 459
460 460 def test_default_perms_set(self):
461 461 u1_auth = AuthUser(user_id=self.u1.user_id)
462 462 perms = {
463 463 'repositories_groups': {},
464 464 'global': set([u'hg.create.repository', u'repository.read',
465 465 u'hg.register.manual_activate']),
466 466 'repositories': {u'vcs_test_hg': u'repository.read'}
467 467 }
468 468 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
469 469 perms['repositories'][HG_REPO])
470 470 new_perm = 'repository.write'
471 471 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
472 472 Session.commit()
473 473
474 474 u1_auth = AuthUser(user_id=self.u1.user_id)
475 475 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], new_perm)
476 476
477 477 def test_default_admin_perms_set(self):
478 478 a1_auth = AuthUser(user_id=self.a1.user_id)
479 479 perms = {
480 480 'repositories_groups': {},
481 481 'global': set([u'hg.admin']),
482 482 'repositories': {u'vcs_test_hg': u'repository.admin'}
483 483 }
484 484 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
485 485 perms['repositories'][HG_REPO])
486 486 new_perm = 'repository.write'
487 487 RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1, perm=new_perm)
488 488 Session.commit()
489 489 # cannot really downgrade admins permissions !? they still get's set as
490 490 # admin !
491 491 u1_auth = AuthUser(user_id=self.a1.user_id)
492 492 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
493 493 perms['repositories'][HG_REPO])
494 494
495 495 def test_default_group_perms(self):
496 496 self.g1 = _make_group('test1', skip_if_exists=True)
497 497 self.g2 = _make_group('test2', skip_if_exists=True)
498 498 u1_auth = AuthUser(user_id=self.u1.user_id)
499 499 perms = {
500 500 'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'},
501 501 'global': set([u'hg.create.repository', u'repository.read', u'hg.register.manual_activate']),
502 502 'repositories': {u'vcs_test_hg': u'repository.read'}
503 503 }
504 504 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
505 505 perms['repositories'][HG_REPO])
506 506 self.assertEqual(u1_auth.permissions['repositories_groups'],
507 507 perms['repositories_groups'])
508 508
509 509 def test_default_admin_group_perms(self):
510 510 self.g1 = _make_group('test1', skip_if_exists=True)
511 511 self.g2 = _make_group('test2', skip_if_exists=True)
512 512 a1_auth = AuthUser(user_id=self.a1.user_id)
513 513 perms = {
514 514 'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'},
515 515 'global': set(['hg.admin']),
516 516 'repositories': {u'vcs_test_hg': 'repository.admin'}
517 517 }
518 518
519 519 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
520 520 perms['repositories'][HG_REPO])
521 521 self.assertEqual(a1_auth.permissions['repositories_groups'],
522 522 perms['repositories_groups'])
523 523
524 524 def test_propagated_permission_from_users_group(self):
525 525 # make group
526 526 self.ug1 = UsersGroupModel().create('G1')
527 527 # add user to group
528 528 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
529 529
530 530 # set permission to lower
531 531 new_perm = 'repository.none'
532 532 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
533 533 Session.commit()
534 534 u1_auth = AuthUser(user_id=self.u1.user_id)
535 535 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
536 536 new_perm)
537 537
538 538 # grant perm for group this should override permission from user
539 539 new_perm = 'repository.write'
540 540 RepoModel().grant_users_group_permission(repo=HG_REPO,
541 541 group_name=self.ug1,
542 542 perm=new_perm)
543 543 # check perms
544 544 u1_auth = AuthUser(user_id=self.u1.user_id)
545 545 perms = {
546 546 'repositories_groups': {},
547 547 'global': set([u'hg.create.repository', u'repository.read',
548 548 u'hg.register.manual_activate']),
549 549 'repositories': {u'vcs_test_hg': u'repository.read'}
550 550 }
551 551 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
552 552 new_perm)
553 553 self.assertEqual(u1_auth.permissions['repositories_groups'],
554 554 perms['repositories_groups'])
555 555
556 556 def test_propagated_permission_from_users_group_lower_weight(self):
557 557 # make group
558 558 self.ug1 = UsersGroupModel().create('G1')
559 559 # add user to group
560 560 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
561 561
562 562 # set permission to lower
563 563 new_perm_h = 'repository.write'
564 564 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
565 565 perm=new_perm_h)
566 566 Session.commit()
567 567 u1_auth = AuthUser(user_id=self.u1.user_id)
568 568 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
569 569 new_perm_h)
570 570
571 571 # grant perm for group this should NOT override permission from user
572 572 # since it's lower than granted
573 573 new_perm_l = 'repository.read'
574 574 RepoModel().grant_users_group_permission(repo=HG_REPO,
575 575 group_name=self.ug1,
576 576 perm=new_perm_l)
577 577 # check perms
578 578 u1_auth = AuthUser(user_id=self.u1.user_id)
579 579 perms = {
580 580 'repositories_groups': {},
581 581 'global': set([u'hg.create.repository', u'repository.read',
582 582 u'hg.register.manual_activate']),
583 583 'repositories': {u'vcs_test_hg': u'repository.write'}
584 584 }
585 585 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
586 586 new_perm_h)
587 587 self.assertEqual(u1_auth.permissions['repositories_groups'],
588 588 perms['repositories_groups'])
589 589
590 590 def test_repo_in_group_permissions(self):
591 591 self.g1 = _make_group('group1', skip_if_exists=True)
592 592 self.g2 = _make_group('group2', skip_if_exists=True)
593 593 Session.commit()
594 594 # both perms should be read !
595 595 u1_auth = AuthUser(user_id=self.u1.user_id)
596 596 self.assertEqual(u1_auth.permissions['repositories_groups'],
597 597 {u'group1': u'group.read', u'group2': u'group.read'})
598 598
599 599 a1_auth = AuthUser(user_id=self.anon.user_id)
600 600 self.assertEqual(a1_auth.permissions['repositories_groups'],
601 601 {u'group1': u'group.read', u'group2': u'group.read'})
602 602
603 603 #Change perms to none for both groups
604 604 ReposGroupModel().grant_user_permission(repos_group=self.g1,
605 605 user=self.anon,
606 606 perm='group.none')
607 607 ReposGroupModel().grant_user_permission(repos_group=self.g2,
608 608 user=self.anon,
609 609 perm='group.none')
610 610
611 611
612 612 u1_auth = AuthUser(user_id=self.u1.user_id)
613 613 self.assertEqual(u1_auth.permissions['repositories_groups'],
614 614 {u'group1': u'group.none', u'group2': u'group.none'})
615 615
616 616 a1_auth = AuthUser(user_id=self.anon.user_id)
617 617 self.assertEqual(a1_auth.permissions['repositories_groups'],
618 618 {u'group1': u'group.none', u'group2': u'group.none'})
619 619
620 620 # add repo to group
621 621 form_data = {
622 622 'repo_name':HG_REPO,
623 'repo_name_full':os.path.join(self.g1.group_name,HG_REPO),
623 'repo_name_full':RepoGroup.url_sep().join([self.g1.group_name,HG_REPO]),
624 624 'repo_type':'hg',
625 625 'clone_uri':'',
626 626 'repo_group':self.g1.group_id,
627 627 'description':'desc',
628 628 'private':False
629 629 }
630 630 self.test_repo = RepoModel().create(form_data, cur_user=self.u1)
631 631 Session.commit()
632 632
633 633 u1_auth = AuthUser(user_id=self.u1.user_id)
634 634 self.assertEqual(u1_auth.permissions['repositories_groups'],
635 635 {u'group1': u'group.none', u'group2': u'group.none'})
636 636
637 637 a1_auth = AuthUser(user_id=self.anon.user_id)
638 638 self.assertEqual(a1_auth.permissions['repositories_groups'],
639 639 {u'group1': u'group.none', u'group2': u'group.none'})
640 640
641 641 #grant permission for u2 !
642 642 ReposGroupModel().grant_user_permission(repos_group=self.g1,
643 643 user=self.u2,
644 644 perm='group.read')
645 645 ReposGroupModel().grant_user_permission(repos_group=self.g2,
646 646 user=self.u2,
647 647 perm='group.read')
648 648 Session.commit()
649 649 self.assertNotEqual(self.u1, self.u2)
650 650 #u1 and anon should have not change perms while u2 should !
651 651 u1_auth = AuthUser(user_id=self.u1.user_id)
652 652 self.assertEqual(u1_auth.permissions['repositories_groups'],
653 653 {u'group1': u'group.none', u'group2': u'group.none'})
654 654
655 655 u2_auth = AuthUser(user_id=self.u2.user_id)
656 656 self.assertEqual(u2_auth.permissions['repositories_groups'],
657 657 {u'group1': u'group.read', u'group2': u'group.read'})
658 658
659 659 a1_auth = AuthUser(user_id=self.anon.user_id)
660 660 self.assertEqual(a1_auth.permissions['repositories_groups'],
661 661 {u'group1': u'group.none', u'group2': u'group.none'})
662 662
663 663 def test_repo_group_user_as_user_group_member(self):
664 664 # create Group1
665 665 self.g1 = _make_group('group1', skip_if_exists=True)
666 666 Session.commit()
667 667 a1_auth = AuthUser(user_id=self.anon.user_id)
668 668
669 669 self.assertEqual(a1_auth.permissions['repositories_groups'],
670 670 {u'group1': u'group.read'})
671 671
672 672 # set default permission to none
673 673 ReposGroupModel().grant_user_permission(repos_group=self.g1,
674 674 user=self.anon,
675 675 perm='group.none')
676 676 # make group
677 677 self.ug1 = UsersGroupModel().create('G1')
678 678 # add user to group
679 679 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
680 680 Session.commit()
681 681
682 682 # check if user is in the group
683 683 membrs = [x.user_id for x in UsersGroupModel().get(self.ug1.users_group_id).members]
684 684 self.assertEqual(membrs, [self.u1.user_id])
685 685 # add some user to that group
686 686
687 687 # check his permissions
688 688 a1_auth = AuthUser(user_id=self.anon.user_id)
689 689 self.assertEqual(a1_auth.permissions['repositories_groups'],
690 690 {u'group1': u'group.none'})
691 691
692 692 u1_auth = AuthUser(user_id=self.u1.user_id)
693 693 self.assertEqual(u1_auth.permissions['repositories_groups'],
694 694 {u'group1': u'group.none'})
695 695
696 696 # grant ug1 read permissions for
697 697 ReposGroupModel().grant_users_group_permission(repos_group=self.g1,
698 698 group_name=self.ug1,
699 699 perm='group.read')
700 700 Session.commit()
701 701 # check if the
702 702 obj = Session.query(UsersGroupRepoGroupToPerm)\
703 703 .filter(UsersGroupRepoGroupToPerm.group == self.g1)\
704 704 .filter(UsersGroupRepoGroupToPerm.users_group == self.ug1)\
705 705 .scalar()
706 706 self.assertEqual(obj.permission.permission_name, 'group.read')
707 707
708 708 a1_auth = AuthUser(user_id=self.anon.user_id)
709 709
710 710 self.assertEqual(a1_auth.permissions['repositories_groups'],
711 711 {u'group1': u'group.none'})
712 712
713 713 u1_auth = AuthUser(user_id=self.u1.user_id)
714 714 self.assertEqual(u1_auth.permissions['repositories_groups'],
715 715 {u'group1': u'group.read'})
General Comments 0
You need to be logged in to leave comments. Login now