##// END OF EJS Templates
fixed file history tests
marcink -
r3009:f0e19116 beta
parent child Browse files
Show More
@@ -1,598 +1,594 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 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib import diffs
37 37 from rhodecode.lib import helpers as h
38 38
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
41 41 str2bool
42 42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 43 from rhodecode.lib.base import BaseRepoController, render
44 44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 45 from rhodecode.lib.vcs.conf import settings
46 46 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 47 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 48 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
49 49 NodeDoesNotExistError, ChangesetError
50 50 from rhodecode.lib.vcs.nodes import FileNode
51 51
52 52 from rhodecode.model.repo import RepoModel
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.db import Repository
55 55
56 56 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
57 57 _context_url, get_line_ctx, get_ignore_ws
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class FilesController(BaseRepoController):
64 64
65 65 def __before__(self):
66 66 super(FilesController, self).__before__()
67 67 c.cut_off_limit = self.cut_off_limit
68 68
69 69 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 70 """
71 71 Safe way to get changeset if error occur it redirects to tip with
72 72 proper message
73 73
74 74 :param rev: revision to fetch
75 75 :param repo_name: repo name to redirect after
76 76 """
77 77
78 78 try:
79 79 return c.rhodecode_repo.get_changeset(rev)
80 80 except EmptyRepositoryError, e:
81 81 if not redirect_after:
82 82 return None
83 83 url_ = url('files_add_home',
84 84 repo_name=c.repo_name,
85 85 revision=0, f_path='')
86 86 add_new = '<a href="%s">[%s]</a>' % (url_, _('click here to add new file'))
87 87 h.flash(h.literal(_('There are no files yet %s') % add_new),
88 88 category='warning')
89 89 redirect(h.url('summary_home', repo_name=repo_name))
90 90
91 91 except RepositoryError, e:
92 92 h.flash(str(e), category='warning')
93 93 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
94 94
95 95 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 96 """
97 97 Returns file_node, if error occurs or given path is directory,
98 98 it'll redirect to top level path
99 99
100 100 :param repo_name: repo_name
101 101 :param cs: given changeset
102 102 :param path: path to lookup
103 103 """
104 104
105 105 try:
106 106 file_node = cs.get_node(path)
107 107 if file_node.is_dir():
108 108 raise RepositoryError('given path is a directory')
109 109 except RepositoryError, e:
110 110 h.flash(str(e), category='warning')
111 111 redirect(h.url('files_home', repo_name=repo_name,
112 112 revision=cs.raw_id))
113 113
114 114 return file_node
115 115
116 116 @LoginRequired()
117 117 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 118 'repository.admin')
119 119 def index(self, repo_name, revision, f_path, annotate=False):
120 120 # redirect to given revision from form if given
121 121 post_revision = request.POST.get('at_rev', None)
122 122 if post_revision:
123 123 cs = self.__get_cs_or_redirect(post_revision, repo_name)
124 124 redirect(url('files_home', repo_name=c.repo_name,
125 125 revision=cs.raw_id, f_path=f_path))
126 126
127 127 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 128 c.branch = request.GET.get('branch', None)
129 129 c.f_path = f_path
130 130 c.annotate = annotate
131 131 cur_rev = c.changeset.revision
132 132
133 133 # prev link
134 134 try:
135 135 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 136 c.url_prev = url('files_home', repo_name=c.repo_name,
137 137 revision=prev_rev.raw_id, f_path=f_path)
138 138 if c.branch:
139 139 c.url_prev += '?branch=%s' % c.branch
140 140 except (ChangesetDoesNotExistError, VCSError):
141 141 c.url_prev = '#'
142 142
143 143 # next link
144 144 try:
145 145 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 146 c.url_next = url('files_home', repo_name=c.repo_name,
147 147 revision=next_rev.raw_id, f_path=f_path)
148 148 if c.branch:
149 149 c.url_next += '?branch=%s' % c.branch
150 150 except (ChangesetDoesNotExistError, VCSError):
151 151 c.url_next = '#'
152 152
153 153 # files or dirs
154 154 try:
155 155 c.file = c.changeset.get_node(f_path)
156 156
157 157 if c.file.is_file():
158 158 c.load_full_history = False
159 159 file_last_cs = c.file.last_changeset
160 160 c.file_changeset = (c.changeset
161 161 if c.changeset.revision < file_last_cs.revision
162 162 else file_last_cs)
163 163 _hist = []
164 164 c.file_history = []
165 165 if c.load_full_history:
166 166 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
167 167
168 168 c.authors = []
169 169 for a in set([x.author for x in _hist]):
170 170 c.authors.append((h.email(a), h.person(a)))
171 171 else:
172 172 c.authors = c.file_history = []
173 173 except RepositoryError, e:
174 174 h.flash(str(e), category='warning')
175 175 redirect(h.url('files_home', repo_name=repo_name,
176 176 revision='tip'))
177 177
178 178 if request.environ.get('HTTP_X_PARTIAL_XHR'):
179 179 return render('files/files_ypjax.html')
180 180
181 181 return render('files/files.html')
182 182
183 183 def history(self, repo_name, revision, f_path, annotate=False):
184 184 if request.environ.get('HTTP_X_PARTIAL_XHR'):
185 185 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
186 186 c.f_path = f_path
187 187 c.annotate = annotate
188 188 c.file = c.changeset.get_node(f_path)
189 189 if c.file.is_file():
190 190 file_last_cs = c.file.last_changeset
191 191 c.file_changeset = (c.changeset
192 192 if c.changeset.revision < file_last_cs.revision
193 193 else file_last_cs)
194 194 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
195 195 c.authors = []
196 196 for a in set([x.author for x in _hist]):
197 197 c.authors.append((h.email(a), h.person(a)))
198 198 return render('files/files_history_box.html')
199 199
200 200 @LoginRequired()
201 201 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
202 202 'repository.admin')
203 203 def rawfile(self, repo_name, revision, f_path):
204 204 cs = self.__get_cs_or_redirect(revision, repo_name)
205 205 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
206 206
207 207 response.content_disposition = 'attachment; filename=%s' % \
208 208 safe_str(f_path.split(Repository.url_sep())[-1])
209 209
210 210 response.content_type = file_node.mimetype
211 211 return file_node.content
212 212
213 213 @LoginRequired()
214 214 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
215 215 'repository.admin')
216 216 def raw(self, repo_name, revision, f_path):
217 217 cs = self.__get_cs_or_redirect(revision, repo_name)
218 218 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
219 219
220 220 raw_mimetype_mapping = {
221 221 # map original mimetype to a mimetype used for "show as raw"
222 222 # you can also provide a content-disposition to override the
223 223 # default "attachment" disposition.
224 224 # orig_type: (new_type, new_dispo)
225 225
226 226 # show images inline:
227 227 'image/x-icon': ('image/x-icon', 'inline'),
228 228 'image/png': ('image/png', 'inline'),
229 229 'image/gif': ('image/gif', 'inline'),
230 230 'image/jpeg': ('image/jpeg', 'inline'),
231 231 'image/svg+xml': ('image/svg+xml', 'inline'),
232 232 }
233 233
234 234 mimetype = file_node.mimetype
235 235 try:
236 236 mimetype, dispo = raw_mimetype_mapping[mimetype]
237 237 except KeyError:
238 238 # we don't know anything special about this, handle it safely
239 239 if file_node.is_binary:
240 240 # do same as download raw for binary files
241 241 mimetype, dispo = 'application/octet-stream', 'attachment'
242 242 else:
243 243 # do not just use the original mimetype, but force text/plain,
244 244 # otherwise it would serve text/html and that might be unsafe.
245 245 # Note: underlying vcs library fakes text/plain mimetype if the
246 246 # mimetype can not be determined and it thinks it is not
247 247 # binary.This might lead to erroneous text display in some
248 248 # cases, but helps in other cases, like with text files
249 249 # without extension.
250 250 mimetype, dispo = 'text/plain', 'inline'
251 251
252 252 if dispo == 'attachment':
253 253 dispo = 'attachment; filename=%s' % \
254 254 safe_str(f_path.split(os.sep)[-1])
255 255
256 256 response.content_disposition = dispo
257 257 response.content_type = mimetype
258 258 return file_node.content
259 259
260 260 @LoginRequired()
261 261 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
262 262 def edit(self, repo_name, revision, f_path):
263 263 repo = Repository.get_by_repo_name(repo_name)
264 264 if repo.enable_locking and repo.locked[0]:
265 265 h.flash(_('This repository is has been locked by %s on %s')
266 266 % (h.person_by_id(repo.locked[0]),
267 267 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
268 268 'warning')
269 269 return redirect(h.url('files_home',
270 270 repo_name=repo_name, revision='tip'))
271 271
272 272 r_post = request.POST
273 273
274 274 c.cs = self.__get_cs_or_redirect(revision, repo_name)
275 275 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
276 276
277 277 if c.file.is_binary:
278 278 return redirect(url('files_home', repo_name=c.repo_name,
279 279 revision=c.cs.raw_id, f_path=f_path))
280 280
281 281 c.f_path = f_path
282 282
283 283 if r_post:
284 284
285 285 old_content = c.file.content
286 286 sl = old_content.splitlines(1)
287 287 first_line = sl[0] if sl else ''
288 288 # modes: 0 - Unix, 1 - Mac, 2 - DOS
289 289 mode = detect_mode(first_line, 0)
290 290 content = convert_line_endings(r_post.get('content'), mode)
291 291
292 292 message = r_post.get('message') or (_('Edited %s via RhodeCode')
293 293 % (f_path))
294 294 author = self.rhodecode_user.full_contact
295 295
296 296 if content == old_content:
297 297 h.flash(_('No changes'),
298 298 category='warning')
299 299 return redirect(url('changeset_home', repo_name=c.repo_name,
300 300 revision='tip'))
301 301
302 302 try:
303 303 self.scm_model.commit_change(repo=c.rhodecode_repo,
304 304 repo_name=repo_name, cs=c.cs,
305 305 user=self.rhodecode_user,
306 306 author=author, message=message,
307 307 content=content, f_path=f_path)
308 308 h.flash(_('Successfully committed to %s') % f_path,
309 309 category='success')
310 310
311 311 except Exception:
312 312 log.error(traceback.format_exc())
313 313 h.flash(_('Error occurred during commit'), category='error')
314 314 return redirect(url('changeset_home',
315 315 repo_name=c.repo_name, revision='tip'))
316 316
317 317 return render('files/files_edit.html')
318 318
319 319 @LoginRequired()
320 320 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
321 321 def add(self, repo_name, revision, f_path):
322 322
323 323 repo = Repository.get_by_repo_name(repo_name)
324 324 if repo.enable_locking and repo.locked[0]:
325 325 h.flash(_('This repository is has been locked by %s on %s')
326 326 % (h.person_by_id(repo.locked[0]),
327 327 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
328 328 'warning')
329 329 return redirect(h.url('files_home',
330 330 repo_name=repo_name, revision='tip'))
331 331
332 332 r_post = request.POST
333 333 c.cs = self.__get_cs_or_redirect(revision, repo_name,
334 334 redirect_after=False)
335 335 if c.cs is None:
336 336 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
337 337
338 338 c.f_path = f_path
339 339
340 340 if r_post:
341 341 unix_mode = 0
342 342 content = convert_line_endings(r_post.get('content'), unix_mode)
343 343
344 344 message = r_post.get('message') or (_('Added %s via RhodeCode')
345 345 % (f_path))
346 346 location = r_post.get('location')
347 347 filename = r_post.get('filename')
348 348 file_obj = r_post.get('upload_file', None)
349 349
350 350 if file_obj is not None and hasattr(file_obj, 'filename'):
351 351 filename = file_obj.filename
352 352 content = file_obj.file
353 353
354 354 node_path = os.path.join(location, filename)
355 355 author = self.rhodecode_user.full_contact
356 356
357 357 if not content:
358 358 h.flash(_('No content'), category='warning')
359 359 return redirect(url('changeset_home', repo_name=c.repo_name,
360 360 revision='tip'))
361 361 if not filename:
362 362 h.flash(_('No filename'), category='warning')
363 363 return redirect(url('changeset_home', repo_name=c.repo_name,
364 364 revision='tip'))
365 365
366 366 try:
367 367 self.scm_model.create_node(repo=c.rhodecode_repo,
368 368 repo_name=repo_name, cs=c.cs,
369 369 user=self.rhodecode_user,
370 370 author=author, message=message,
371 371 content=content, f_path=node_path)
372 372 h.flash(_('Successfully committed to %s') % node_path,
373 373 category='success')
374 374 except NodeAlreadyExistsError, e:
375 375 h.flash(_(e), category='error')
376 376 except Exception:
377 377 log.error(traceback.format_exc())
378 378 h.flash(_('Error occurred during commit'), category='error')
379 379 return redirect(url('changeset_home',
380 380 repo_name=c.repo_name, revision='tip'))
381 381
382 382 return render('files/files_add.html')
383 383
384 384 @LoginRequired()
385 385 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 386 'repository.admin')
387 387 def archivefile(self, repo_name, fname):
388 388
389 389 fileformat = None
390 390 revision = None
391 391 ext = None
392 392 subrepos = request.GET.get('subrepos') == 'true'
393 393
394 394 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
395 395 archive_spec = fname.split(ext_data[1])
396 396 if len(archive_spec) == 2 and archive_spec[1] == '':
397 397 fileformat = a_type or ext_data[1]
398 398 revision = archive_spec[0]
399 399 ext = ext_data[1]
400 400
401 401 try:
402 402 dbrepo = RepoModel().get_by_repo_name(repo_name)
403 403 if dbrepo.enable_downloads is False:
404 404 return _('downloads disabled')
405 405
406 406 if c.rhodecode_repo.alias == 'hg':
407 407 # patch and reset hooks section of UI config to not run any
408 408 # hooks on fetching archives with subrepos
409 409 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
410 410 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
411 411
412 412 cs = c.rhodecode_repo.get_changeset(revision)
413 413 content_type = settings.ARCHIVE_SPECS[fileformat][0]
414 414 except ChangesetDoesNotExistError:
415 415 return _('Unknown revision %s') % revision
416 416 except EmptyRepositoryError:
417 417 return _('Empty repository')
418 418 except (ImproperArchiveTypeError, KeyError):
419 419 return _('Unknown archive type')
420 420
421 421 fd, archive = tempfile.mkstemp()
422 422 t = open(archive, 'wb')
423 423 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
424 424 t.close()
425 425
426 426 def get_chunked_archive(archive):
427 427 stream = open(archive, 'rb')
428 428 while True:
429 429 data = stream.read(16 * 1024)
430 430 if not data:
431 431 stream.close()
432 432 os.close(fd)
433 433 os.remove(archive)
434 434 break
435 435 yield data
436 436
437 437 response.content_disposition = str('attachment; filename=%s-%s%s' \
438 438 % (repo_name, revision[:12], ext))
439 439 response.content_type = str(content_type)
440 440 return get_chunked_archive(archive)
441 441
442 442 @LoginRequired()
443 443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 444 'repository.admin')
445 445 def diff(self, repo_name, f_path):
446 446 ignore_whitespace = request.GET.get('ignorews') == '1'
447 447 line_context = request.GET.get('context', 3)
448 448 diff1 = request.GET.get('diff1', '')
449 449 diff2 = request.GET.get('diff2', '')
450 450 c.action = request.GET.get('diff')
451 451 c.no_changes = diff1 == diff2
452 452 c.f_path = f_path
453 453 c.big_diff = False
454 454 c.anchor_url = anchor_url
455 455 c.ignorews_url = _ignorews_url
456 456 c.context_url = _context_url
457 457 c.changes = OrderedDict()
458 458 c.changes[diff2] = []
459 459
460 460 #special case if we want a show rev only, it's impl here
461 461 #to reduce JS and callbacks
462 462
463 463 if request.GET.get('show_rev'):
464 464 if str2bool(request.GET.get('annotate', 'False')):
465 465 _url = url('files_annotate_home', repo_name=c.repo_name,
466 466 revision=diff1, f_path=c.f_path)
467 467 else:
468 468 _url = url('files_home', repo_name=c.repo_name,
469 469 revision=diff1, f_path=c.f_path)
470 470
471 471 return redirect(_url)
472 472 try:
473 473 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
474 474 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
475 475 try:
476 476 node1 = c.changeset_1.get_node(f_path)
477 477 except NodeDoesNotExistError:
478 478 c.changeset_1 = EmptyChangeset(cs=diff1,
479 479 revision=c.changeset_1.revision,
480 480 repo=c.rhodecode_repo)
481 481 node1 = FileNode(f_path, '', changeset=c.changeset_1)
482 482 else:
483 483 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
484 484 node1 = FileNode(f_path, '', changeset=c.changeset_1)
485 485
486 486 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
487 487 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
488 488 try:
489 489 node2 = c.changeset_2.get_node(f_path)
490 490 except NodeDoesNotExistError:
491 491 c.changeset_2 = EmptyChangeset(cs=diff2,
492 492 revision=c.changeset_2.revision,
493 493 repo=c.rhodecode_repo)
494 494 node2 = FileNode(f_path, '', changeset=c.changeset_2)
495 495 else:
496 496 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
497 497 node2 = FileNode(f_path, '', changeset=c.changeset_2)
498 498 except RepositoryError:
499 499 log.error(traceback.format_exc())
500 500 return redirect(url('files_home', repo_name=c.repo_name,
501 501 f_path=f_path))
502 502
503 503 if c.action == 'download':
504 504 _diff = diffs.get_gitdiff(node1, node2,
505 505 ignore_whitespace=ignore_whitespace,
506 506 context=line_context)
507 507 diff = diffs.DiffProcessor(_diff, format='gitdiff')
508 508
509 509 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
510 510 response.content_type = 'text/plain'
511 511 response.content_disposition = (
512 512 'attachment; filename=%s' % diff_name
513 513 )
514 514 return diff.as_raw()
515 515
516 516 elif c.action == 'raw':
517 517 _diff = diffs.get_gitdiff(node1, node2,
518 518 ignore_whitespace=ignore_whitespace,
519 519 context=line_context)
520 520 diff = diffs.DiffProcessor(_diff, format='gitdiff')
521 521 response.content_type = 'text/plain'
522 522 return diff.as_raw()
523 523
524 524 else:
525 525 fid = h.FID(diff2, node2.path)
526 526 line_context_lcl = get_line_ctx(fid, request.GET)
527 527 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
528 528
529 529 lim = request.GET.get('fulldiff') or self.cut_off_limit
530 530 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
531 531 filenode_new=node2,
532 532 cut_off_limit=lim,
533 533 ignore_whitespace=ign_whitespace_lcl,
534 534 line_context=line_context_lcl,
535 535 enable_comments=False)
536 536 op = ''
537 537 filename = node1.path
538 538 cs_changes = {
539 539 'fid': [cs1, cs2, op, filename, diff, st]
540 540 }
541 541 c.changes = cs_changes
542 542
543 543 return render('files/file_diff.html')
544 544
545 545 def _get_node_history(self, cs, f_path, changesets=None):
546 546 """
547 547 get changesets history for given node
548 548
549 549 :param cs: changeset to calculate history
550 550 :param f_path: path for node to calculate history for
551 551 :param changesets: if passed don't calculate history and take
552 552 changesets defined in this list
553 553 """
554 import time
555 s = time.time()
556 554 # calculate history based on tip
557 555 tip_cs = c.rhodecode_repo.get_changeset()
558 556 if changesets is None:
559 557 try:
560 558 changesets = tip_cs.get_file_history(f_path)
561 559 except (NodeDoesNotExistError, ChangesetError):
562 560 #this node is not present at tip !
563 561 changesets = cs.get_file_history(f_path)
564 print time.time()-s
565 562 hist_l = []
566 563
567 564 changesets_group = ([], _("Changesets"))
568 565 branches_group = ([], _("Branches"))
569 566 tags_group = ([], _("Tags"))
570 567 _hg = cs.repository.alias == 'hg'
571 568 for chs in changesets:
572 569 #_branch = '(%s)' % chs.branch if _hg else ''
573 570 _branch = chs.branch
574 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
571 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
575 572 changesets_group[0].append((chs.raw_id, n_desc,))
576 print time.time()-s
577 573 hist_l.append(changesets_group)
578 574
579 575 for name, chs in c.rhodecode_repo.branches.items():
580 576 branches_group[0].append((chs, name),)
581 577 hist_l.append(branches_group)
582 578
583 579 for name, chs in c.rhodecode_repo.tags.items():
584 580 tags_group[0].append((chs, name),)
585 581 hist_l.append(tags_group)
586 print time.time()-s
582
587 583 return hist_l, changesets
588 584
589 585 @LoginRequired()
590 586 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
591 587 'repository.admin')
592 588 @jsonify
593 589 def nodelist(self, repo_name, revision, f_path):
594 590 if request.environ.get('HTTP_X_PARTIAL_XHR'):
595 591 cs = self.__get_cs_or_redirect(revision, repo_name)
596 592 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
597 593 flat=False)
598 594 return {'nodes': _d + _f}
@@ -1,333 +1,348 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 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://myissueserver.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.
77 In addition some other __str__ are unicode as well
78 Added test for unicode
79 Improved test to clone into uniq repository.
80 removed extra unicode conversion in diff.</div>
81 """)
82
83 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
84
85 def test_file_source_history(self):
86 self.log_user()
87 response = self.app.get(url(controller='files', action='history',
88 repo_name=HG_REPO,
89 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
90 f_path='vcs/nodes.py'),
91 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
76 92 #test or history
77 93 response.mustcontain("""<optgroup label="Changesets">
78 94 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
79 95 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
80 96 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
81 97 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
82 98 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
83 99 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
84 100 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
85 101 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
86 102 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
87 103 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
88 104 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
89 105 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
90 106 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
91 107 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
92 108 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
93 109 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
94 110 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
95 111 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
96 112 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
97 113 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
98 114 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
99 115 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
100 116 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
101 117 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
102 118 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
103 119 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
104 120 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
105 121 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
106 122 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
107 123 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
108 124 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
109 125 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
110 126 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
111 127 </optgroup>
112 128 <optgroup label="Branches">
113 129 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
114 130 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
115 131 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
116 132 </optgroup>
117 133 <optgroup label="Tags">
118 134 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
119 135 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
120 136 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
121 137 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
122 138 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
123 139 </optgroup>
124 140 """)
125 141
126 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://myissueserver.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.
127 In addition some other __str__ are unicode as well
128 Added test for unicode
129 Improved test to clone into uniq repository.
130 removed extra unicode conversion in diff.</div>
131 """)
132
133 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
134
135 142 def test_file_annotation(self):
136 143 self.log_user()
137 144 response = self.app.get(url(controller='files', action='index',
138 145 repo_name=HG_REPO,
139 146 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
140 147 f_path='vcs/nodes.py',
141 148 annotate=True))
142 149
150 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
151
152 def test_file_annotation_history(self):
153 self.log_user()
154 response = self.app.get(url(controller='files', action='history',
155 repo_name=HG_REPO,
156 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
157 f_path='vcs/nodes.py',
158 annotate=True),
159 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
143 160
144 161 response.mustcontain("""<optgroup label="Changesets">
145 162 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
146 163 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
147 164 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
148 165 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
149 166 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
150 167 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
151 168 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
152 169 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
153 170 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
154 171 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
155 172 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
156 173 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
157 174 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
158 175 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
159 176 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
160 177 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
161 178 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
162 179 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
163 180 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
164 181 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
165 182 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
166 183 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
167 184 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
168 185 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
169 186 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
170 187 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
171 188 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
172 189 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
173 190 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
174 191 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
175 192 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
176 193 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
177 194 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
178 195 </optgroup>
179 196 <optgroup label="Branches">
180 197 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
181 198 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
182 199 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
183 200 </optgroup>
184 201 <optgroup label="Tags">
185 202 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
186 203 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
187 204 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
188 205 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
189 206 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
190 207 </optgroup>""")
191 208
192 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
193
194 209 def test_file_annotation_git(self):
195 210 self.log_user()
196 211 response = self.app.get(url(controller='files', action='index',
197 212 repo_name=GIT_REPO,
198 213 revision='master',
199 214 f_path='vcs/nodes.py',
200 215 annotate=True))
201 216
202 217 def test_archival(self):
203 218 self.log_user()
204 219
205 220 for arch_ext, info in ARCHIVE_SPECS.items():
206 221 short = '27cd5cce30c9%s' % arch_ext
207 222 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
208 223 filename = '%s-%s' % (HG_REPO, short)
209 224 response = self.app.get(url(controller='files',
210 225 action='archivefile',
211 226 repo_name=HG_REPO,
212 227 fname=fname))
213 228
214 229 self.assertEqual(response.status, '200 OK')
215 230 heads = [
216 231 ('Pragma', 'no-cache'),
217 232 ('Cache-Control', 'no-cache'),
218 233 ('Content-Disposition', 'attachment; filename=%s' % filename),
219 234 ('Content-Type', '%s; charset=utf-8' % info[0]),
220 235 ]
221 236 self.assertEqual(response.response._headers.items(), heads)
222 237
223 238 def test_archival_wrong_ext(self):
224 239 self.log_user()
225 240
226 241 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
227 242 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
228 243
229 244 response = self.app.get(url(controller='files',
230 245 action='archivefile',
231 246 repo_name=HG_REPO,
232 247 fname=fname))
233 248 response.mustcontain('Unknown archive type')
234 249
235 250 def test_archival_wrong_revision(self):
236 251 self.log_user()
237 252
238 253 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
239 254 fname = '%s.zip' % rev
240 255
241 256 response = self.app.get(url(controller='files',
242 257 action='archivefile',
243 258 repo_name=HG_REPO,
244 259 fname=fname))
245 260 response.mustcontain('Unknown revision')
246 261
247 262 #==========================================================================
248 263 # RAW FILE
249 264 #==========================================================================
250 265 def test_raw_file_ok(self):
251 266 self.log_user()
252 267 response = self.app.get(url(controller='files', action='rawfile',
253 268 repo_name=HG_REPO,
254 269 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
255 270 f_path='vcs/nodes.py'))
256 271
257 272 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
258 273 self.assertEqual(response.content_type, "text/x-python")
259 274
260 275 def test_raw_file_wrong_cs(self):
261 276 self.log_user()
262 277 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
263 278 f_path = 'vcs/nodes.py'
264 279
265 280 response = self.app.get(url(controller='files', action='rawfile',
266 281 repo_name=HG_REPO,
267 282 revision=rev,
268 283 f_path=f_path))
269 284
270 285 msg = """Revision %r does not exist for this repository""" % (rev)
271 286 self.checkSessionFlash(response, msg)
272 287
273 288 msg = """%s""" % (HG_REPO)
274 289 self.checkSessionFlash(response, msg)
275 290
276 291 def test_raw_file_wrong_f_path(self):
277 292 self.log_user()
278 293 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
279 294 f_path = 'vcs/ERRORnodes.py'
280 295 response = self.app.get(url(controller='files', action='rawfile',
281 296 repo_name=HG_REPO,
282 297 revision=rev,
283 298 f_path=f_path))
284 299
285 300 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
286 301 self.checkSessionFlash(response, msg)
287 302
288 303 #==========================================================================
289 304 # RAW RESPONSE - PLAIN
290 305 #==========================================================================
291 306 def test_raw_ok(self):
292 307 self.log_user()
293 308 response = self.app.get(url(controller='files', action='raw',
294 309 repo_name=HG_REPO,
295 310 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
296 311 f_path='vcs/nodes.py'))
297 312
298 313 self.assertEqual(response.content_type, "text/plain")
299 314
300 315 def test_raw_wrong_cs(self):
301 316 self.log_user()
302 317 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
303 318 f_path = 'vcs/nodes.py'
304 319
305 320 response = self.app.get(url(controller='files', action='raw',
306 321 repo_name=HG_REPO,
307 322 revision=rev,
308 323 f_path=f_path))
309 324 msg = """Revision %r does not exist for this repository""" % (rev)
310 325 self.checkSessionFlash(response, msg)
311 326
312 327 msg = """%s""" % (HG_REPO)
313 328 self.checkSessionFlash(response, msg)
314 329
315 330 def test_raw_wrong_f_path(self):
316 331 self.log_user()
317 332 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
318 333 f_path = 'vcs/ERRORnodes.py'
319 334 response = self.app.get(url(controller='files', action='raw',
320 335 repo_name=HG_REPO,
321 336 revision=rev,
322 337 f_path=f_path))
323 338 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
324 339 self.checkSessionFlash(response, msg)
325 340
326 341 def test_ajaxed_files_list(self):
327 342 self.log_user()
328 343 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
329 344 response = self.app.get(
330 345 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
331 346 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
332 347 )
333 348 response.mustcontain("vcs/web/simplevcs/views/repository.py")
General Comments 0
You need to be logged in to leave comments. Login now