##// END OF EJS Templates
white space cleanup
marcink -
r3149:68f9c216 beta
parent child Browse files
Show More
@@ -1,486 +1,486 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
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 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 40 NotAnonymous
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib import diffs
43 43 from rhodecode.lib.utils import action_logger, jsonify
44 44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.diffs import LimitedDiffContainer
47 47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
48 48 ChangesetComment
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.comment import ChangesetCommentsModel
53 53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 54 from rhodecode.model.forms import PullRequestForm
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class PullrequestsController(BaseRepoController):
60 60
61 61 @LoginRequired()
62 62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
63 63 'repository.admin')
64 64 def __before__(self):
65 65 super(PullrequestsController, self).__before__()
66 66 repo_model = RepoModel()
67 67 c.users_array = repo_model.get_users_js()
68 68 c.users_groups_array = repo_model.get_users_groups_js()
69 69
70 70 def _get_repo_refs(self, repo):
71 71 hist_l = []
72 72
73 73 branches_group = ([('branch:%s:%s' % (k, v), k) for
74 74 k, v in repo.branches.iteritems()], _("Branches"))
75 75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
76 76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
77 77 tags_group = ([('tag:%s:%s' % (k, v), k) for
78 78 k, v in repo.tags.iteritems()], _("Tags"))
79 79
80 80 hist_l.append(bookmarks_group)
81 81 hist_l.append(branches_group)
82 82 hist_l.append(tags_group)
83 83
84 84 return hist_l
85 85
86 86 def _get_default_rev(self, repo):
87 87 """
88 88 Get's default revision to do compare on pull request
89 89
90 90 :param repo:
91 91 """
92 92 repo = repo.scm_instance
93 93 if 'default' in repo.branches:
94 94 return 'default'
95 95 else:
96 96 #if repo doesn't have default branch return first found
97 97 return repo.branches.keys()[0]
98 98
99 99 def _get_is_allowed_change_status(self, pull_request):
100 owner = self.rhodecode_user.user_id == pull_request.user_id
100 owner = self.rhodecode_user.user_id == pull_request.user_id
101 101 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
102 102 pull_request.reviewers]
103 103 return (self.rhodecode_user.admin or owner or reviewer)
104 104
105 105 def show_all(self, repo_name):
106 106 c.pull_requests = PullRequestModel().get_all(repo_name)
107 107 c.repo_name = repo_name
108 108 return render('/pullrequests/pullrequest_show_all.html')
109 109
110 110 @NotAnonymous()
111 111 def index(self):
112 112 org_repo = c.rhodecode_db_repo
113 113
114 114 if org_repo.scm_instance.alias != 'hg':
115 115 log.error('Review not available for GIT REPOS')
116 116 raise HTTPNotFound
117 117
118 118 try:
119 119 org_repo.scm_instance.get_changeset()
120 120 except EmptyRepositoryError, e:
121 121 h.flash(h.literal(_('There are no changesets yet')),
122 122 category='warning')
123 123 redirect(url('summary_home', repo_name=org_repo.repo_name))
124 124
125 125 other_repos_info = {}
126 126
127 127 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
128 128 c.org_repos = []
129 129 c.other_repos = []
130 130 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
131 131 org_repo.user.username, c.repo_name))
132 132 )
133 133
134 134 # add org repo to other so we can open pull request agains itself
135 135 c.other_repos.extend(c.org_repos)
136 136
137 137 c.default_pull_request = org_repo.repo_name # repo name pre-selected
138 138 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
139 139 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
140 140 #add orginal repo
141 141 other_repos_info[org_repo.repo_name] = {
142 142 'gravatar': h.gravatar_url(org_repo.user.email, 24),
143 143 'description': org_repo.description,
144 144 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
145 145 }
146 146
147 147 #gather forks and add to this list
148 148 for fork in org_repo.forks:
149 149 c.other_repos.append((fork.repo_name, '%s/%s' % (
150 150 fork.user.username, fork.repo_name))
151 151 )
152 152 other_repos_info[fork.repo_name] = {
153 153 'gravatar': h.gravatar_url(fork.user.email, 24),
154 154 'description': fork.description,
155 155 'revs': h.select('other_ref', '',
156 156 self._get_repo_refs(fork.scm_instance),
157 157 class_='refs')
158 158 }
159 159 #add parents of this fork also, but only if it's not empty
160 160 if org_repo.parent and org_repo.parent.scm_instance.revisions:
161 161 c.default_pull_request = org_repo.parent.repo_name
162 162 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
163 163 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
164 164 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
165 165 org_repo.parent.user.username,
166 166 org_repo.parent.repo_name))
167 167 )
168 168 other_repos_info[org_repo.parent.repo_name] = {
169 169 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
170 170 'description': org_repo.parent.description,
171 171 'revs': h.select('other_ref', '',
172 172 self._get_repo_refs(org_repo.parent.scm_instance),
173 173 class_='refs')
174 174 }
175 175
176 176 c.other_repos_info = json.dumps(other_repos_info)
177 177 c.review_members = [org_repo.user]
178 178 return render('/pullrequests/pullrequest.html')
179 179
180 180 @NotAnonymous()
181 181 def create(self, repo_name):
182 182 repo = RepoModel()._get_repo(repo_name)
183 183 try:
184 184 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
185 185 except formencode.Invalid, errors:
186 186 log.error(traceback.format_exc())
187 187 if errors.error_dict.get('revisions'):
188 188 msg = 'Revisions: %s' % errors.error_dict['revisions']
189 189 elif errors.error_dict.get('pullrequest_title'):
190 190 msg = _('Pull request requires a title with min. 3 chars')
191 191 else:
192 192 msg = _('error during creation of pull request')
193 193
194 194 h.flash(msg, 'error')
195 195 return redirect(url('pullrequest_home', repo_name=repo_name))
196 196
197 197 org_repo = _form['org_repo']
198 198 org_ref = _form['org_ref']
199 199 other_repo = _form['other_repo']
200 200 other_ref = _form['other_ref']
201 201 revisions = _form['revisions']
202 202 reviewers = _form['review_members']
203 203
204 204 # if we have cherry picked pull request we don't care what is in
205 205 # org_ref/other_ref
206 206 rev_start = request.POST.get('rev_start')
207 207 rev_end = request.POST.get('rev_end')
208 208
209 209 if rev_start and rev_end:
210 210 # this is swapped to simulate that rev_end is a revision from
211 211 # parent of the fork
212 212 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
213 213 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
214 214
215 215 title = _form['pullrequest_title']
216 216 description = _form['pullrequest_desc']
217 217
218 218 try:
219 219 pull_request = PullRequestModel().create(
220 220 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
221 221 other_ref, revisions, reviewers, title, description
222 222 )
223 223 Session().commit()
224 224 h.flash(_('Successfully opened new pull request'),
225 225 category='success')
226 226 except Exception:
227 227 h.flash(_('Error occurred during sending pull request'),
228 228 category='error')
229 229 log.error(traceback.format_exc())
230 230 return redirect(url('pullrequest_home', repo_name=repo_name))
231 231
232 232 return redirect(url('pullrequest_show', repo_name=other_repo,
233 233 pull_request_id=pull_request.pull_request_id))
234 234
235 235 @NotAnonymous()
236 236 @jsonify
237 237 def update(self, repo_name, pull_request_id):
238 238 pull_request = PullRequest.get_or_404(pull_request_id)
239 239 if pull_request.is_closed():
240 240 raise HTTPForbidden()
241 241 #only owner or admin can update it
242 242 owner = pull_request.author.user_id == c.rhodecode_user.user_id
243 243 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
244 244 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
245 245 request.POST.get('reviewers_ids', '').split(',')))
246 246
247 247 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
248 248 Session().commit()
249 249 return True
250 250 raise HTTPForbidden()
251 251
252 252 @NotAnonymous()
253 253 @jsonify
254 254 def delete(self, repo_name, pull_request_id):
255 255 pull_request = PullRequest.get_or_404(pull_request_id)
256 256 #only owner can delete it !
257 257 if pull_request.author.user_id == c.rhodecode_user.user_id:
258 258 PullRequestModel().delete(pull_request)
259 259 Session().commit()
260 260 h.flash(_('Successfully deleted pull request'),
261 261 category='success')
262 262 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
263 263 raise HTTPForbidden()
264 264
265 265 def _load_compare_data(self, pull_request, enable_comments=True):
266 266 """
267 267 Load context data needed for generating compare diff
268 268
269 269 :param pull_request:
270 270 :type pull_request:
271 271 """
272 272 rev_start = request.GET.get('rev_start')
273 273 rev_end = request.GET.get('rev_end')
274 274
275 275 org_repo = pull_request.org_repo
276 276 (org_ref_type,
277 277 org_ref_name,
278 278 org_ref_rev) = pull_request.org_ref.split(':')
279 279
280 280 other_repo = org_repo
281 281 (other_ref_type,
282 282 other_ref_name,
283 283 other_ref_rev) = pull_request.other_ref.split(':')
284 284
285 285 # despite opening revisions for bookmarks/branches/tags, we always
286 286 # convert this to rev to prevent changes after book or branch change
287 287 org_ref = ('rev', org_ref_rev)
288 288 other_ref = ('rev', other_ref_rev)
289 289
290 290 c.org_repo = org_repo
291 291 c.other_repo = other_repo
292 292
293 293 c.fulldiff = fulldiff = request.GET.get('fulldiff')
294 294
295 295 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
296 296
297 297 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
298 298 if c.cs_ranges[0].parents
299 299 else EmptyChangeset(), 'raw_id'))
300 300
301 301 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
302 302 c.target_repo = other_repo.repo_name
303 303 # defines that we need hidden inputs with changesets
304 304 c.as_form = request.GET.get('as_form', False)
305 305
306 306 c.org_ref = org_ref[1]
307 307 c.other_ref = other_ref[1]
308 308
309 309 diff_limit = self.cut_off_limit if not fulldiff else None
310 310
311 311 #we swap org/other ref since we run a simple diff on one repo
312 312 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
313 313
314 314 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
315 315 diff_limit=diff_limit)
316 316 _parsed = diff_processor.prepare()
317 317
318 318 c.limited_diff = False
319 319 if isinstance(_parsed, LimitedDiffContainer):
320 320 c.limited_diff = True
321 321
322 322 c.files = []
323 323 c.changes = {}
324 324 c.lines_added = 0
325 325 c.lines_deleted = 0
326 326 for f in _parsed:
327 327 st = f['stats']
328 328 if st[0] != 'b':
329 329 c.lines_added += st[0]
330 330 c.lines_deleted += st[1]
331 331 fid = h.FID('', f['filename'])
332 332 c.files.append([fid, f['operation'], f['filename'], f['stats']])
333 333 diff = diff_processor.as_html(enable_comments=enable_comments,
334 334 parsed_lines=[f])
335 335 c.changes[fid] = [f['operation'], f['filename'], diff]
336 336
337 337 def show(self, repo_name, pull_request_id):
338 338 repo_model = RepoModel()
339 339 c.users_array = repo_model.get_users_js()
340 340 c.users_groups_array = repo_model.get_users_groups_js()
341 341 c.pull_request = PullRequest.get_or_404(pull_request_id)
342 342 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
343 343 cc_model = ChangesetCommentsModel()
344 344 cs_model = ChangesetStatusModel()
345 345 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
346 346 pull_request=c.pull_request,
347 347 with_revisions=True)
348 348
349 349 cs_statuses = defaultdict(list)
350 350 for st in _cs_statuses:
351 351 cs_statuses[st.author.username] += [st]
352 352
353 353 c.pull_request_reviewers = []
354 354 c.pull_request_pending_reviewers = []
355 355 for o in c.pull_request.reviewers:
356 356 st = cs_statuses.get(o.user.username, None)
357 357 if st:
358 358 sorter = lambda k: k.version
359 359 st = [(x, list(y)[0])
360 360 for x, y in (groupby(sorted(st, key=sorter), sorter))]
361 361 else:
362 362 c.pull_request_pending_reviewers.append(o.user)
363 363 c.pull_request_reviewers.append([o.user, st])
364 364
365 365 # pull_requests repo_name we opened it against
366 366 # ie. other_repo must match
367 367 if repo_name != c.pull_request.other_repo.repo_name:
368 368 raise HTTPNotFound
369 369
370 370 # load compare data into template context
371 371 enable_comments = not c.pull_request.is_closed()
372 372 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
373 373
374 374 # inline comments
375 375 c.inline_cnt = 0
376 376 c.inline_comments = cc_model.get_inline_comments(
377 377 c.rhodecode_db_repo.repo_id,
378 378 pull_request=pull_request_id)
379 379 # count inline comments
380 380 for __, lines in c.inline_comments:
381 381 for comments in lines.values():
382 382 c.inline_cnt += len(comments)
383 383 # comments
384 384 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
385 385 pull_request=pull_request_id)
386 386
387 387 try:
388 388 cur_status = c.statuses[c.pull_request.revisions[0]][0]
389 389 except:
390 390 log.error(traceback.format_exc())
391 391 cur_status = 'undefined'
392 392 if c.pull_request.is_closed() and 0:
393 393 c.current_changeset_status = cur_status
394 394 else:
395 395 # changeset(pull-request) status calulation based on reviewers
396 396 c.current_changeset_status = cs_model.calculate_status(
397 397 c.pull_request_reviewers,
398 398 )
399 399 c.changeset_statuses = ChangesetStatus.STATUSES
400 400
401 401 return render('/pullrequests/pullrequest_show.html')
402 402
403 403 @NotAnonymous()
404 404 @jsonify
405 405 def comment(self, repo_name, pull_request_id):
406 406 pull_request = PullRequest.get_or_404(pull_request_id)
407 407 if pull_request.is_closed():
408 408 raise HTTPForbidden()
409 409
410 410 status = request.POST.get('changeset_status')
411 411 change_status = request.POST.get('change_changeset_status')
412 412 text = request.POST.get('text')
413 413
414 414 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
415 415 if status and change_status and allowed_to_change_status:
416 416 text = text or (_('Status change -> %s')
417 417 % ChangesetStatus.get_status_lbl(status))
418 418 comm = ChangesetCommentsModel().create(
419 419 text=text,
420 420 repo=c.rhodecode_db_repo.repo_id,
421 421 user=c.rhodecode_user.user_id,
422 422 pull_request=pull_request_id,
423 423 f_path=request.POST.get('f_path'),
424 424 line_no=request.POST.get('line'),
425 425 status_change=(ChangesetStatus.get_status_lbl(status)
426 426 if status and change_status and allowed_to_change_status else None)
427 427 )
428 428
429 429 action_logger(self.rhodecode_user,
430 430 'user_commented_pull_request:%s' % pull_request_id,
431 431 c.rhodecode_db_repo, self.ip_addr, self.sa)
432 432
433 433 if allowed_to_change_status:
434 434 # get status if set !
435 435 if status and change_status:
436 436 ChangesetStatusModel().set_status(
437 437 c.rhodecode_db_repo.repo_id,
438 438 status,
439 439 c.rhodecode_user.user_id,
440 440 comm,
441 441 pull_request=pull_request_id
442 442 )
443 443
444 444 if request.POST.get('save_close'):
445 445 if status in ['rejected', 'approved']:
446 446 PullRequestModel().close_pull_request(pull_request_id)
447 447 action_logger(self.rhodecode_user,
448 448 'user_closed_pull_request:%s' % pull_request_id,
449 449 c.rhodecode_db_repo, self.ip_addr, self.sa)
450 450 else:
451 451 h.flash(_('Closing pull request on other statuses than '
452 452 'rejected or approved forbidden'),
453 453 category='warning')
454 454
455 455 Session().commit()
456 456
457 457 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
458 458 return redirect(h.url('pullrequest_show', repo_name=repo_name,
459 459 pull_request_id=pull_request_id))
460 460
461 461 data = {
462 462 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
463 463 }
464 464 if comm:
465 465 c.co = comm
466 466 data.update(comm.get_dict())
467 467 data.update({'rendered_text':
468 468 render('changeset/changeset_comment_block.html')})
469 469
470 470 return data
471 471
472 472 @NotAnonymous()
473 473 @jsonify
474 474 def delete_comment(self, repo_name, comment_id):
475 475 co = ChangesetComment.get(comment_id)
476 476 if co.pull_request.is_closed():
477 477 #don't allow deleting comments on closed pull request
478 478 raise HTTPForbidden()
479 479
480 480 owner = co.author.user_id == c.rhodecode_user.user_id
481 481 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
482 482 ChangesetCommentsModel().delete(comment=co)
483 483 Session().commit()
484 484 return True
485 485 else:
486 486 raise HTTPForbidden()
@@ -1,1173 +1,1172 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12 import re
13 13 import urlparse
14 14 import textwrap
15 15
16 16 from datetime import datetime
17 17 from pygments.formatters.html import HtmlFormatter
18 18 from pygments import highlight as code_highlight
19 19 from pylons import url, request, config
20 20 from pylons.i18n.translation import _, ungettext
21 21 from hashlib import md5
22 22
23 23 from webhelpers.html import literal, HTML, escape
24 24 from webhelpers.html.tools import *
25 25 from webhelpers.html.builder import make_tag
26 26 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
27 27 end_form, file, form, hidden, image, javascript_link, link_to, \
28 28 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
29 29 submit, text, password, textarea, title, ul, xml_declaration, radio
30 30 from webhelpers.html.tools import auto_link, button_to, highlight, \
31 31 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
32 32 from webhelpers.number import format_byte_size, format_bit_size
33 33 from webhelpers.pylonslib import Flash as _Flash
34 34 from webhelpers.pylonslib.secure_form import secure_form
35 35 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
36 36 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
37 37 replace_whitespace, urlify, truncate, wrap_paragraphs
38 38 from webhelpers.date import time_ago_in_words
39 39 from webhelpers.paginate import Page
40 40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
41 41 convert_boolean_attrs, NotGiven, _make_safe_id_component
42 42
43 43 from rhodecode.lib.annotate import annotate_highlight
44 44 from rhodecode.lib.utils import repo_name_slug
45 45 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
46 46 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
47 47 from rhodecode.lib.markup_renderer import MarkupRenderer
48 48 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
49 49 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
50 50 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.db import URL_SEP, Permission
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 html_escape_table = {
58 58 "&": "&amp;",
59 59 '"': "&quot;",
60 60 "'": "&apos;",
61 61 ">": "&gt;",
62 62 "<": "&lt;",
63 63 }
64 64
65 65
66 66 def html_escape(text):
67 67 """Produce entities within text."""
68 68 return "".join(html_escape_table.get(c, c) for c in text)
69 69
70 70
71 71 def shorter(text, size=20):
72 72 postfix = '...'
73 73 if len(text) > size:
74 74 return text[:size - len(postfix)] + postfix
75 75 return text
76 76
77 77
78 78 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
79 79 """
80 80 Reset button
81 81 """
82 82 _set_input_attrs(attrs, type, name, value)
83 83 _set_id_attr(attrs, id, name)
84 84 convert_boolean_attrs(attrs, ["disabled"])
85 85 return HTML.input(**attrs)
86 86
87 87 reset = _reset
88 88 safeid = _make_safe_id_component
89 89
90 90
91 91 def FID(raw_id, path):
92 92 """
93 93 Creates a uniqe ID for filenode based on it's hash of path and revision
94 94 it's safe to use in urls
95 95
96 96 :param raw_id:
97 97 :param path:
98 98 """
99 99
100 100 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
101 101
102 102
103 103 def get_token():
104 104 """Return the current authentication token, creating one if one doesn't
105 105 already exist.
106 106 """
107 107 token_key = "_authentication_token"
108 108 from pylons import session
109 109 if not token_key in session:
110 110 try:
111 111 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
112 112 except AttributeError: # Python < 2.4
113 113 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
114 114 session[token_key] = token
115 115 if hasattr(session, 'save'):
116 116 session.save()
117 117 return session[token_key]
118 118
119 119
120 120 class _GetError(object):
121 121 """Get error from form_errors, and represent it as span wrapped error
122 122 message
123 123
124 124 :param field_name: field to fetch errors for
125 125 :param form_errors: form errors dict
126 126 """
127 127
128 128 def __call__(self, field_name, form_errors):
129 129 tmpl = """<span class="error_msg">%s</span>"""
130 130 if form_errors and field_name in form_errors:
131 131 return literal(tmpl % form_errors.get(field_name))
132 132
133 133 get_error = _GetError()
134 134
135 135
136 136 class _ToolTip(object):
137 137
138 138 def __call__(self, tooltip_title, trim_at=50):
139 139 """
140 140 Special function just to wrap our text into nice formatted
141 141 autowrapped text
142 142
143 143 :param tooltip_title:
144 144 """
145 145 tooltip_title = escape(tooltip_title)
146 146 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
147 147 return tooltip_title
148 148 tooltip = _ToolTip()
149 149
150 150
151 151 class _FilesBreadCrumbs(object):
152 152
153 153 def __call__(self, repo_name, rev, paths):
154 154 if isinstance(paths, str):
155 155 paths = safe_unicode(paths)
156 156 url_l = [link_to(repo_name, url('files_home',
157 157 repo_name=repo_name,
158 158 revision=rev, f_path=''),
159 159 class_='ypjax-link')]
160 160 paths_l = paths.split('/')
161 161 for cnt, p in enumerate(paths_l):
162 162 if p != '':
163 163 url_l.append(link_to(p,
164 164 url('files_home',
165 165 repo_name=repo_name,
166 166 revision=rev,
167 167 f_path='/'.join(paths_l[:cnt + 1])
168 168 ),
169 169 class_='ypjax-link'
170 170 )
171 171 )
172 172
173 173 return literal('/'.join(url_l))
174 174
175 175 files_breadcrumbs = _FilesBreadCrumbs()
176 176
177 177
178 178 class CodeHtmlFormatter(HtmlFormatter):
179 179 """
180 180 My code Html Formatter for source codes
181 181 """
182 182
183 183 def wrap(self, source, outfile):
184 184 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
185 185
186 186 def _wrap_code(self, source):
187 187 for cnt, it in enumerate(source):
188 188 i, t = it
189 189 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
190 190 yield i, t
191 191
192 192 def _wrap_tablelinenos(self, inner):
193 193 dummyoutfile = StringIO.StringIO()
194 194 lncount = 0
195 195 for t, line in inner:
196 196 if t:
197 197 lncount += 1
198 198 dummyoutfile.write(line)
199 199
200 200 fl = self.linenostart
201 201 mw = len(str(lncount + fl - 1))
202 202 sp = self.linenospecial
203 203 st = self.linenostep
204 204 la = self.lineanchors
205 205 aln = self.anchorlinenos
206 206 nocls = self.noclasses
207 207 if sp:
208 208 lines = []
209 209
210 210 for i in range(fl, fl + lncount):
211 211 if i % st == 0:
212 212 if i % sp == 0:
213 213 if aln:
214 214 lines.append('<a href="#%s%d" class="special">%*d</a>' %
215 215 (la, i, mw, i))
216 216 else:
217 217 lines.append('<span class="special">%*d</span>' % (mw, i))
218 218 else:
219 219 if aln:
220 220 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
221 221 else:
222 222 lines.append('%*d' % (mw, i))
223 223 else:
224 224 lines.append('')
225 225 ls = '\n'.join(lines)
226 226 else:
227 227 lines = []
228 228 for i in range(fl, fl + lncount):
229 229 if i % st == 0:
230 230 if aln:
231 231 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
232 232 else:
233 233 lines.append('%*d' % (mw, i))
234 234 else:
235 235 lines.append('')
236 236 ls = '\n'.join(lines)
237 237
238 238 # in case you wonder about the seemingly redundant <div> here: since the
239 239 # content in the other cell also is wrapped in a div, some browsers in
240 240 # some configurations seem to mess up the formatting...
241 241 if nocls:
242 242 yield 0, ('<table class="%stable">' % self.cssclass +
243 243 '<tr><td><div class="linenodiv" '
244 244 'style="background-color: #f0f0f0; padding-right: 10px">'
245 245 '<pre style="line-height: 125%">' +
246 246 ls + '</pre></div></td><td id="hlcode" class="code">')
247 247 else:
248 248 yield 0, ('<table class="%stable">' % self.cssclass +
249 249 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
250 250 ls + '</pre></div></td><td id="hlcode" class="code">')
251 251 yield 0, dummyoutfile.getvalue()
252 252 yield 0, '</td></tr></table>'
253 253
254 254
255 255 def pygmentize(filenode, **kwargs):
256 256 """pygmentize function using pygments
257 257
258 258 :param filenode:
259 259 """
260 260
261 261 return literal(code_highlight(filenode.content,
262 262 filenode.lexer, CodeHtmlFormatter(**kwargs)))
263 263
264 264
265 265 def pygmentize_annotation(repo_name, filenode, **kwargs):
266 266 """
267 267 pygmentize function for annotation
268 268
269 269 :param filenode:
270 270 """
271 271
272 272 color_dict = {}
273 273
274 274 def gen_color(n=10000):
275 275 """generator for getting n of evenly distributed colors using
276 276 hsv color and golden ratio. It always return same order of colors
277 277
278 278 :returns: RGB tuple
279 279 """
280 280
281 281 def hsv_to_rgb(h, s, v):
282 282 if s == 0.0:
283 283 return v, v, v
284 284 i = int(h * 6.0) # XXX assume int() truncates!
285 285 f = (h * 6.0) - i
286 286 p = v * (1.0 - s)
287 287 q = v * (1.0 - s * f)
288 288 t = v * (1.0 - s * (1.0 - f))
289 289 i = i % 6
290 290 if i == 0:
291 291 return v, t, p
292 292 if i == 1:
293 293 return q, v, p
294 294 if i == 2:
295 295 return p, v, t
296 296 if i == 3:
297 297 return p, q, v
298 298 if i == 4:
299 299 return t, p, v
300 300 if i == 5:
301 301 return v, p, q
302 302
303 303 golden_ratio = 0.618033988749895
304 304 h = 0.22717784590367374
305 305
306 306 for _ in xrange(n):
307 307 h += golden_ratio
308 308 h %= 1
309 309 HSV_tuple = [h, 0.95, 0.95]
310 310 RGB_tuple = hsv_to_rgb(*HSV_tuple)
311 311 yield map(lambda x: str(int(x * 256)), RGB_tuple)
312 312
313 313 cgenerator = gen_color()
314 314
315 315 def get_color_string(cs):
316 316 if cs in color_dict:
317 317 col = color_dict[cs]
318 318 else:
319 319 col = color_dict[cs] = cgenerator.next()
320 320 return "color: rgb(%s)! important;" % (', '.join(col))
321 321
322 322 def url_func(repo_name):
323 323
324 324 def _url_func(changeset):
325 325 author = changeset.author
326 326 date = changeset.date
327 327 message = tooltip(changeset.message)
328 328
329 329 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
330 330 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
331 331 "</b> %s<br/></div>")
332 332
333 333 tooltip_html = tooltip_html % (author, date, message)
334 334 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
335 335 short_id(changeset.raw_id))
336 336 uri = link_to(
337 337 lnk_format,
338 338 url('changeset_home', repo_name=repo_name,
339 339 revision=changeset.raw_id),
340 340 style=get_color_string(changeset.raw_id),
341 341 class_='tooltip',
342 342 title=tooltip_html
343 343 )
344 344
345 345 uri += '\n'
346 346 return uri
347 347 return _url_func
348 348
349 349 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
350 350
351 351
352 352 def is_following_repo(repo_name, user_id):
353 353 from rhodecode.model.scm import ScmModel
354 354 return ScmModel().is_following_repo(repo_name, user_id)
355 355
356 356 flash = _Flash()
357 357
358 358 #==============================================================================
359 359 # SCM FILTERS available via h.
360 360 #==============================================================================
361 361 from rhodecode.lib.vcs.utils import author_name, author_email
362 362 from rhodecode.lib.utils2 import credentials_filter, age as _age
363 363 from rhodecode.model.db import User, ChangesetStatus
364 364
365 365 age = lambda x: _age(x)
366 366 capitalize = lambda x: x.capitalize()
367 367 email = author_email
368 368 short_id = lambda x: x[:12]
369 369 hide_credentials = lambda x: ''.join(credentials_filter(x))
370 370
371 371
372 372 def fmt_date(date):
373 373 if date:
374 374 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
375 375 return date.strftime(_fmt).decode('utf8')
376 376
377 377 return ""
378 378
379 379
380 380 def is_git(repository):
381 381 if hasattr(repository, 'alias'):
382 382 _type = repository.alias
383 383 elif hasattr(repository, 'repo_type'):
384 384 _type = repository.repo_type
385 385 else:
386 386 _type = repository
387 387 return _type == 'git'
388 388
389 389
390 390 def is_hg(repository):
391 391 if hasattr(repository, 'alias'):
392 392 _type = repository.alias
393 393 elif hasattr(repository, 'repo_type'):
394 394 _type = repository.repo_type
395 395 else:
396 396 _type = repository
397 397 return _type == 'hg'
398 398
399 399
400 400 def email_or_none(author):
401 401 # extract email from the commit string
402 402 _email = email(author)
403 403 if _email != '':
404 404 # check it against RhodeCode database, and use the MAIN email for this
405 405 # user
406 406 user = User.get_by_email(_email, case_insensitive=True, cache=True)
407 407 if user is not None:
408 408 return user.email
409 409 return _email
410 410
411 411 # See if it contains a username we can get an email from
412 412 user = User.get_by_username(author_name(author), case_insensitive=True,
413 413 cache=True)
414 414 if user is not None:
415 415 return user.email
416 416
417 417 # No valid email, not a valid user in the system, none!
418 418 return None
419 419
420 420
421 421 def person(author, show_attr="username_and_name"):
422 422 # attr to return from fetched user
423 423 person_getter = lambda usr: getattr(usr, show_attr)
424 424
425 425 # Valid email in the attribute passed, see if they're in the system
426 426 _email = email(author)
427 427 if _email != '':
428 428 user = User.get_by_email(_email, case_insensitive=True, cache=True)
429 429 if user is not None:
430 430 return person_getter(user)
431 431 return _email
432 432
433 433 # Maybe it's a username?
434 434 _author = author_name(author)
435 435 user = User.get_by_username(_author, case_insensitive=True,
436 436 cache=True)
437 437 if user is not None:
438 438 return person_getter(user)
439 439
440 440 # Still nothing? Just pass back the author name then
441 441 return _author
442 442
443 443
444 444 def person_by_id(id_, show_attr="username_and_name"):
445 445 # attr to return from fetched user
446 446 person_getter = lambda usr: getattr(usr, show_attr)
447 447
448 448 #maybe it's an ID ?
449 449 if str(id_).isdigit() or isinstance(id_, int):
450 450 id_ = int(id_)
451 451 user = User.get(id_)
452 452 if user is not None:
453 453 return person_getter(user)
454 454 return id_
455 455
456 456
457 457 def desc_stylize(value):
458 458 """
459 459 converts tags from value into html equivalent
460 460
461 461 :param value:
462 462 """
463 463 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
464 464 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
465 465 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
466 466 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
467 467 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
468 468 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
469 469 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
470 470 '<div class="metatag" tag="lang">\\2</div>', value)
471 471 value = re.sub(r'\[([a-z]+)\]',
472 472 '<div class="metatag" tag="\\1">\\1</div>', value)
473 473
474 474 return value
475 475
476 476
477 477 def bool2icon(value):
478 478 """Returns True/False values represented as small html image of true/false
479 479 icons
480 480
481 481 :param value: bool value
482 482 """
483 483
484 484 if value is True:
485 485 return HTML.tag('img', src=url("/images/icons/accept.png"),
486 486 alt=_('True'))
487 487
488 488 if value is False:
489 489 return HTML.tag('img', src=url("/images/icons/cancel.png"),
490 490 alt=_('False'))
491 491
492 492 return value
493 493
494 494
495 495 def action_parser(user_log, feed=False, parse_cs=False):
496 496 """
497 497 This helper will action_map the specified string action into translated
498 498 fancy names with icons and links
499 499
500 500 :param user_log: user log instance
501 501 :param feed: use output for feeds (no html and fancy icons)
502 502 :param parse_cs: parse Changesets into VCS instances
503 503 """
504 504
505 505 action = user_log.action
506 506 action_params = ' '
507 507
508 508 x = action.split(':')
509 509
510 510 if len(x) > 1:
511 511 action, action_params = x
512 512
513 513 def get_cs_links():
514 514 revs_limit = 3 # display this amount always
515 515 revs_top_limit = 50 # show upto this amount of changesets hidden
516 516 revs_ids = action_params.split(',')
517 517 deleted = user_log.repository is None
518 518 if deleted:
519 519 return ','.join(revs_ids)
520 520
521 521 repo_name = user_log.repository.repo_name
522 522
523 523 def lnk(rev, repo_name):
524 524 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
525 525 lazy_cs = True
526 526 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
527 527 lazy_cs = False
528 528 lbl = '?'
529 529 if rev.op == 'delete_branch':
530 530 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
531 531 title = ''
532 532 elif rev.op == 'tag':
533 533 lbl = '%s' % _('Created tag: %s') % rev.ref_name
534 534 title = ''
535 535 _url = '#'
536 536
537 537 else:
538 538 lbl = '%s' % (rev.short_id[:8])
539 539 _url = url('changeset_home', repo_name=repo_name,
540 540 revision=rev.raw_id)
541 541 title = tooltip(rev.message)
542 542 else:
543 543 ## changeset cannot be found/striped/removed etc.
544 544 lbl = ('%s' % rev)[:12]
545 545 _url = '#'
546 546 title = _('Changeset not found')
547 547 if parse_cs:
548 548 return link_to(lbl, _url, title=title, class_='tooltip')
549 549 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
550 550 class_='lazy-cs' if lazy_cs else '')
551 551
552 552 revs = []
553 553 if len(filter(lambda v: v != '', revs_ids)) > 0:
554 554 repo = None
555 555 for rev in revs_ids[:revs_top_limit]:
556 556 _op = _name = None
557 557 if len(rev.split('=>')) == 2:
558 558 _op, _name = rev.split('=>')
559 559
560 560 # we want parsed changesets, or new log store format is bad
561 561 if parse_cs:
562 562 try:
563 563 if repo is None:
564 564 repo = user_log.repository.scm_instance
565 565 _rev = repo.get_changeset(rev)
566 566 revs.append(_rev)
567 567 except ChangesetDoesNotExistError:
568 568 log.error('cannot find revision %s in this repo' % rev)
569 569 revs.append(rev)
570 570 continue
571 571 else:
572 572 _rev = AttributeDict({
573 573 'short_id': rev[:12],
574 574 'raw_id': rev,
575 575 'message': '',
576 576 'op': _op,
577 577 'ref_name': _name
578 578 })
579 579 revs.append(_rev)
580 580 cs_links = []
581 581 cs_links.append(" " + ', '.join(
582 582 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
583 583 )
584 584 )
585 585
586 586 compare_view = (
587 587 ' <div class="compare_view tooltip" title="%s">'
588 588 '<a href="%s">%s</a> </div>' % (
589 589 _('Show all combined changesets %s->%s') % (
590 590 revs_ids[0][:12], revs_ids[-1][:12]
591 591 ),
592 592 url('changeset_home', repo_name=repo_name,
593 593 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
594 594 ),
595 595 _('compare view')
596 596 )
597 597 )
598 598
599 599 # if we have exactly one more than normally displayed
600 600 # just display it, takes less space than displaying
601 601 # "and 1 more revisions"
602 602 if len(revs_ids) == revs_limit + 1:
603 603 rev = revs[revs_limit]
604 604 cs_links.append(", " + lnk(rev, repo_name))
605 605
606 606 # hidden-by-default ones
607 607 if len(revs_ids) > revs_limit + 1:
608 608 uniq_id = revs_ids[0]
609 609 html_tmpl = (
610 610 '<span> %s <a class="show_more" id="_%s" '
611 611 'href="#more">%s</a> %s</span>'
612 612 )
613 613 if not feed:
614 614 cs_links.append(html_tmpl % (
615 615 _('and'),
616 616 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
617 617 _('revisions')
618 618 )
619 619 )
620 620
621 621 if not feed:
622 622 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
623 623 else:
624 624 html_tmpl = '<span id="%s"> %s </span>'
625 625
626 626 morelinks = ', '.join(
627 627 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
628 628 )
629 629
630 630 if len(revs_ids) > revs_top_limit:
631 631 morelinks += ', ...'
632 632
633 633 cs_links.append(html_tmpl % (uniq_id, morelinks))
634 634 if len(revs) > 1:
635 635 cs_links.append(compare_view)
636 636 return ''.join(cs_links)
637 637
638 638 def get_fork_name():
639 639 repo_name = action_params
640 640 _url = url('summary_home', repo_name=repo_name)
641 641 return _('fork name %s') % link_to(action_params, _url)
642 642
643 643 def get_user_name():
644 644 user_name = action_params
645 645 return user_name
646 646
647 647 def get_users_group():
648 648 group_name = action_params
649 649 return group_name
650 650
651 651 def get_pull_request():
652 652 pull_request_id = action_params
653 653 deleted = user_log.repository is None
654 654 if deleted:
655 655 repo_name = user_log.repository_name
656 656 else:
657 657 repo_name = user_log.repository.repo_name
658 658 return link_to(_('Pull request #%s') % pull_request_id,
659 659 url('pullrequest_show', repo_name=repo_name,
660 660 pull_request_id=pull_request_id))
661 661
662 662 # action : translated str, callback(extractor), icon
663 663 action_map = {
664 664 'user_deleted_repo': (_('[deleted] repository'),
665 665 None, 'database_delete.png'),
666 666 'user_created_repo': (_('[created] repository'),
667 667 None, 'database_add.png'),
668 668 'user_created_fork': (_('[created] repository as fork'),
669 669 None, 'arrow_divide.png'),
670 670 'user_forked_repo': (_('[forked] repository'),
671 671 get_fork_name, 'arrow_divide.png'),
672 672 'user_updated_repo': (_('[updated] repository'),
673 673 None, 'database_edit.png'),
674 674 'admin_deleted_repo': (_('[delete] repository'),
675 675 None, 'database_delete.png'),
676 676 'admin_created_repo': (_('[created] repository'),
677 677 None, 'database_add.png'),
678 678 'admin_forked_repo': (_('[forked] repository'),
679 679 None, 'arrow_divide.png'),
680 680 'admin_updated_repo': (_('[updated] repository'),
681 681 None, 'database_edit.png'),
682 682 'admin_created_user': (_('[created] user'),
683 683 get_user_name, 'user_add.png'),
684 684 'admin_updated_user': (_('[updated] user'),
685 685 get_user_name, 'user_edit.png'),
686 686 'admin_created_users_group': (_('[created] users group'),
687 687 get_users_group, 'group_add.png'),
688 688 'admin_updated_users_group': (_('[updated] users group'),
689 689 get_users_group, 'group_edit.png'),
690 690 'user_commented_revision': (_('[commented] on revision in repository'),
691 691 get_cs_links, 'comment_add.png'),
692 692 'user_commented_pull_request': (_('[commented] on pull request for'),
693 693 get_pull_request, 'comment_add.png'),
694 694 'user_closed_pull_request': (_('[closed] pull request for'),
695 695 get_pull_request, 'tick.png'),
696 696 'push': (_('[pushed] into'),
697 697 get_cs_links, 'script_add.png'),
698 698 'push_local': (_('[committed via RhodeCode] into repository'),
699 699 get_cs_links, 'script_edit.png'),
700 700 'push_remote': (_('[pulled from remote] into repository'),
701 701 get_cs_links, 'connect.png'),
702 702 'pull': (_('[pulled] from'),
703 703 None, 'down_16.png'),
704 704 'started_following_repo': (_('[started following] repository'),
705 705 None, 'heart_add.png'),
706 706 'stopped_following_repo': (_('[stopped following] repository'),
707 707 None, 'heart_delete.png'),
708 708 }
709 709
710 710 action_str = action_map.get(action, action)
711 711 if feed:
712 712 action = action_str[0].replace('[', '').replace(']', '')
713 713 else:
714 714 action = action_str[0]\
715 715 .replace('[', '<span class="journal_highlight">')\
716 716 .replace(']', '</span>')
717 717
718 718 action_params_func = lambda: ""
719 719
720 720 if callable(action_str[1]):
721 721 action_params_func = action_str[1]
722 722
723 723 def action_parser_icon():
724 724 action = user_log.action
725 725 action_params = None
726 726 x = action.split(':')
727 727
728 728 if len(x) > 1:
729 729 action, action_params = x
730 730
731 731 tmpl = """<img src="%s%s" alt="%s"/>"""
732 732 ico = action_map.get(action, ['', '', ''])[2]
733 733 return literal(tmpl % ((url('/images/icons/')), ico, action))
734 734
735 735 # returned callbacks we need to call to get
736 736 return [lambda: literal(action), action_params_func, action_parser_icon]
737 737
738 738
739 739
740 740 #==============================================================================
741 741 # PERMS
742 742 #==============================================================================
743 743 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
744 744 HasRepoPermissionAny, HasRepoPermissionAll
745 745
746 746
747 747 #==============================================================================
748 748 # GRAVATAR URL
749 749 #==============================================================================
750 750
751 751 def gravatar_url(email_address, size=30):
752 752 from pylons import url # doh, we need to re-import url to mock it later
753 753
754 754 if (not str2bool(config['app_conf'].get('use_gravatar')) or
755 755 not email_address or email_address == 'anonymous@rhodecode.org'):
756 756 f = lambda a, l: min(l, key=lambda x: abs(x - a))
757 757 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
758 758
759 759 if(str2bool(config['app_conf'].get('use_gravatar')) and
760 760 config['app_conf'].get('alternative_gravatar_url')):
761 761 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
762 762 parsed_url = urlparse.urlparse(url.current(qualified=True))
763 763 tmpl = tmpl.replace('{email}', email_address)\
764 764 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
765 765 .replace('{netloc}', parsed_url.netloc)\
766 766 .replace('{scheme}', parsed_url.scheme)\
767 767 .replace('{size}', str(size))
768 768 return tmpl
769 769
770 770 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
771 771 default = 'identicon'
772 772 baseurl_nossl = "http://www.gravatar.com/avatar/"
773 773 baseurl_ssl = "https://secure.gravatar.com/avatar/"
774 774 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
775 775
776 776 if isinstance(email_address, unicode):
777 777 #hashlib crashes on unicode items
778 778 email_address = safe_str(email_address)
779 779 # construct the url
780 780 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
781 781 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
782 782
783 783 return gravatar_url
784 784
785 785
786 786 #==============================================================================
787 787 # REPO PAGER, PAGER FOR REPOSITORY
788 788 #==============================================================================
789 789 class RepoPage(Page):
790 790
791 791 def __init__(self, collection, page=1, items_per_page=20,
792 792 item_count=None, url=None, **kwargs):
793 793
794 794 """Create a "RepoPage" instance. special pager for paging
795 795 repository
796 796 """
797 797 self._url_generator = url
798 798
799 799 # Safe the kwargs class-wide so they can be used in the pager() method
800 800 self.kwargs = kwargs
801 801
802 802 # Save a reference to the collection
803 803 self.original_collection = collection
804 804
805 805 self.collection = collection
806 806
807 807 # The self.page is the number of the current page.
808 808 # The first page has the number 1!
809 809 try:
810 810 self.page = int(page) # make it int() if we get it as a string
811 811 except (ValueError, TypeError):
812 812 self.page = 1
813 813
814 814 self.items_per_page = items_per_page
815 815
816 816 # Unless the user tells us how many items the collections has
817 817 # we calculate that ourselves.
818 818 if item_count is not None:
819 819 self.item_count = item_count
820 820 else:
821 821 self.item_count = len(self.collection)
822 822
823 823 # Compute the number of the first and last available page
824 824 if self.item_count > 0:
825 825 self.first_page = 1
826 826 self.page_count = int(math.ceil(float(self.item_count) /
827 827 self.items_per_page))
828 828 self.last_page = self.first_page + self.page_count - 1
829 829
830 830 # Make sure that the requested page number is the range of
831 831 # valid pages
832 832 if self.page > self.last_page:
833 833 self.page = self.last_page
834 834 elif self.page < self.first_page:
835 835 self.page = self.first_page
836 836
837 837 # Note: the number of items on this page can be less than
838 838 # items_per_page if the last page is not full
839 839 self.first_item = max(0, (self.item_count) - (self.page *
840 840 items_per_page))
841 841 self.last_item = ((self.item_count - 1) - items_per_page *
842 842 (self.page - 1))
843 843
844 844 self.items = list(self.collection[self.first_item:self.last_item + 1])
845 845
846 846 # Links to previous and next page
847 847 if self.page > self.first_page:
848 848 self.previous_page = self.page - 1
849 849 else:
850 850 self.previous_page = None
851 851
852 852 if self.page < self.last_page:
853 853 self.next_page = self.page + 1
854 854 else:
855 855 self.next_page = None
856 856
857 857 # No items available
858 858 else:
859 859 self.first_page = None
860 860 self.page_count = 0
861 861 self.last_page = None
862 862 self.first_item = None
863 863 self.last_item = None
864 864 self.previous_page = None
865 865 self.next_page = None
866 866 self.items = []
867 867
868 868 # This is a subclass of the 'list' type. Initialise the list now.
869 869 list.__init__(self, reversed(self.items))
870 870
871 871
872 872 def changed_tooltip(nodes):
873 873 """
874 874 Generates a html string for changed nodes in changeset page.
875 875 It limits the output to 30 entries
876 876
877 877 :param nodes: LazyNodesGenerator
878 878 """
879 879 if nodes:
880 880 pref = ': <br/> '
881 881 suf = ''
882 882 if len(nodes) > 30:
883 883 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
884 884 return literal(pref + '<br/> '.join([safe_unicode(x.path)
885 885 for x in nodes[:30]]) + suf)
886 886 else:
887 887 return ': ' + _('No Files')
888 888
889 889
890 890 def repo_link(groups_and_repos, last_url=None):
891 891 """
892 892 Makes a breadcrumbs link to repo within a group
893 893 joins &raquo; on each group to create a fancy link
894 894
895 895 ex::
896 896 group >> subgroup >> repo
897 897
898 898 :param groups_and_repos:
899 899 :param last_url:
900 900 """
901 901 groups, repo_name = groups_and_repos
902 902 last_link = link_to(repo_name, last_url) if last_url else repo_name
903 903
904 904 if not groups:
905 905 if last_url:
906 906 return last_link
907 907 return repo_name
908 908 else:
909 909 def make_link(group):
910 910 return link_to(group.name,
911 911 url('repos_group_home', group_name=group.group_name))
912 912 return literal(' &raquo; '.join(map(make_link, groups) + [last_link]))
913 913
914 914
915 915 def fancy_file_stats(stats):
916 916 """
917 917 Displays a fancy two colored bar for number of added/deleted
918 918 lines of code on file
919 919
920 920 :param stats: two element list of added/deleted lines of code
921 921 """
922 922 def cgen(l_type, a_v, d_v):
923 923 mapping = {'tr': 'top-right-rounded-corner-mid',
924 924 'tl': 'top-left-rounded-corner-mid',
925 925 'br': 'bottom-right-rounded-corner-mid',
926 926 'bl': 'bottom-left-rounded-corner-mid'}
927 927 map_getter = lambda x: mapping[x]
928 928
929 929 if l_type == 'a' and d_v:
930 930 #case when added and deleted are present
931 931 return ' '.join(map(map_getter, ['tl', 'bl']))
932 932
933 933 if l_type == 'a' and not d_v:
934 934 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
935 935
936 936 if l_type == 'd' and a_v:
937 937 return ' '.join(map(map_getter, ['tr', 'br']))
938 938
939 939 if l_type == 'd' and not a_v:
940 940 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
941 941
942 942 a, d = stats[0], stats[1]
943 943 width = 100
944 944
945 945 if a == 'b':
946 946 #binary mode
947 947 b_d = '<div class="bin%s %s" style="width:100%%">%s</div>' % (d, cgen('a', a_v='', d_v=0), 'bin')
948 948 b_a = '<div class="bin1" style="width:0%%">%s</div>' % ('bin')
949 949 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
950 950
951 951 t = stats[0] + stats[1]
952 952 unit = float(width) / (t or 1)
953 953
954 954 # needs > 9% of width to be visible or 0 to be hidden
955 955 a_p = max(9, unit * a) if a > 0 else 0
956 956 d_p = max(9, unit * d) if d > 0 else 0
957 957 p_sum = a_p + d_p
958 958
959 959 if p_sum > width:
960 960 #adjust the percentage to be == 100% since we adjusted to 9
961 961 if a_p > d_p:
962 962 a_p = a_p - (p_sum - width)
963 963 else:
964 964 d_p = d_p - (p_sum - width)
965 965
966 966 a_v = a if a > 0 else ''
967 967 d_v = d if d > 0 else ''
968 968
969 969 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
970 970 cgen('a', a_v, d_v), a_p, a_v
971 971 )
972 972 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
973 973 cgen('d', a_v, d_v), d_p, d_v
974 974 )
975 975 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
976 976
977 977
978 978 def urlify_text(text_):
979 979
980 980 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
981 981 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
982 982
983 983 def url_func(match_obj):
984 984 url_full = match_obj.groups()[0]
985 985 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
986 986
987 987 return literal(url_pat.sub(url_func, text_))
988 988
989 989
990 990 def urlify_changesets(text_, repository):
991 991 """
992 992 Extract revision ids from changeset and make link from them
993 993
994 994 :param text_:
995 995 :param repository:
996 996 """
997 997
998 998 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
999 999
1000 1000 def url_func(match_obj):
1001 1001 rev = match_obj.groups()[0]
1002 1002 pref = ''
1003 1003 if match_obj.group().startswith(' '):
1004 1004 pref = ' '
1005 1005 tmpl = (
1006 1006 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1007 1007 '%(rev)s'
1008 1008 '</a>'
1009 1009 )
1010 1010 return tmpl % {
1011 1011 'pref': pref,
1012 1012 'cls': 'revision-link',
1013 1013 'url': url('changeset_home', repo_name=repository, revision=rev),
1014 1014 'rev': rev,
1015 1015 }
1016 1016
1017 1017 newtext = URL_PAT.sub(url_func, text_)
1018 1018
1019 1019 return newtext
1020 1020
1021 1021
1022 1022 def urlify_commit(text_, repository=None, link_=None):
1023 1023 """
1024 1024 Parses given text message and makes proper links.
1025 1025 issues are linked to given issue-server, and rest is a changeset link
1026 1026 if link_ is given, in other case it's a plain text
1027 1027
1028 1028 :param text_:
1029 1029 :param repository:
1030 1030 :param link_: changeset link
1031 1031 """
1032 1032 import traceback
1033 1033
1034 1034 def escaper(string):
1035 1035 return string.replace('<', '&lt;').replace('>', '&gt;')
1036 1036
1037 1037 def linkify_others(t, l):
1038 1038 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1039 1039 links = []
1040 1040 for e in urls.split(t):
1041 1041 if not urls.match(e):
1042 1042 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1043 1043 else:
1044 1044 links.append(e)
1045 1045
1046 1046 return ''.join(links)
1047 1047
1048 1048 # urlify changesets - extrac revisions and make link out of them
1049 1049 newtext = urlify_changesets(escaper(text_), repository)
1050 1050
1051 1051 try:
1052 1052 conf = config['app_conf']
1053 1053
1054 1054 # allow multiple issue servers to be used
1055 1055 valid_indices = [
1056 1056 x.group(1)
1057 1057 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1058 1058 if x and 'issue_server_link%s' % x.group(1) in conf
1059 1059 and 'issue_prefix%s' % x.group(1) in conf
1060 1060 ]
1061 1061
1062 1062 log.debug('found issue server suffixes `%s` during valuation of: %s'
1063 1063 % (','.join(valid_indices), newtext))
1064 1064
1065 1065 for pattern_index in valid_indices:
1066 1066 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1067 1067 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1068 1068 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1069 1069
1070 1070 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1071 1071 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1072 1072 ISSUE_PREFIX))
1073 1073
1074 1074 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1075 1075
1076 1076 def url_func(match_obj):
1077 1077 pref = ''
1078 1078 if match_obj.group().startswith(' '):
1079 1079 pref = ' '
1080 1080
1081 1081 issue_id = ''.join(match_obj.groups())
1082 1082 tmpl = (
1083 1083 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1084 1084 '%(issue-prefix)s%(id-repr)s'
1085 1085 '</a>'
1086 1086 )
1087 1087 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1088 1088 if repository:
1089 1089 url = url.replace('{repo}', repository)
1090 1090 repo_name = repository.split(URL_SEP)[-1]
1091 1091 url = url.replace('{repo_name}', repo_name)
1092 1092
1093 1093 return tmpl % {
1094 1094 'pref': pref,
1095 1095 'cls': 'issue-tracker-link',
1096 1096 'url': url,
1097 1097 'id-repr': issue_id,
1098 1098 'issue-prefix': ISSUE_PREFIX,
1099 1099 'serv': ISSUE_SERVER_LNK,
1100 1100 }
1101 1101 newtext = URL_PAT.sub(url_func, newtext)
1102 1102 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1103 1103
1104 1104 # if we actually did something above
1105 1105 if link_:
1106 1106 # wrap not links into final link => link_
1107 1107 newtext = linkify_others(newtext, link_)
1108 1108 except:
1109 1109 log.error(traceback.format_exc())
1110 1110 pass
1111 1111
1112 1112 return literal(newtext)
1113 1113
1114 1114
1115 1115 def rst(source):
1116 1116 return literal('<div class="rst-block">%s</div>' %
1117 1117 MarkupRenderer.rst(source))
1118 1118
1119 1119
1120 1120 def rst_w_mentions(source):
1121 1121 """
1122 1122 Wrapped rst renderer with @mention highlighting
1123 1123
1124 1124 :param source:
1125 1125 """
1126 1126 return literal('<div class="rst-block">%s</div>' %
1127 1127 MarkupRenderer.rst_with_mentions(source))
1128 1128
1129 1129
1130 1130 def changeset_status(repo, revision):
1131 1131 return ChangesetStatusModel().get_status(repo, revision)
1132 1132
1133 1133
1134 1134 def changeset_status_lbl(changeset_status):
1135 1135 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1136 1136
1137 1137
1138 1138 def get_permission_name(key):
1139 1139 return dict(Permission.PERMS).get(key)
1140 1140
1141 1141
1142 1142 def journal_filter_help():
1143 1143 return _(textwrap.dedent('''
1144 1144 Example filter terms:
1145 1145 repository:vcs
1146 1146 username:marcin
1147 1147 action:*push*
1148 1148 ip:127.0.0.1
1149 1149 date:20120101
1150 1150 date:[20120101100000 TO 20120102]
1151 1151
1152 1152 Generate wildcards using '*' character:
1153 1153 "repositroy:vcs*" - search everything starting with 'vcs'
1154 1154 "repository:*vcs*" - search for repository containing 'vcs'
1155 1155
1156 1156 Optional AND / OR operators in queries
1157 1157 "repository:vcs OR repository:test"
1158 1158 "username:test AND repository:test*"
1159 1159 '''))
1160 1160
1161 1161
1162 1162 def not_mapped_error(repo_name):
1163 1163 flash(_('%s repository is not mapped to db perhaps'
1164 1164 ' it was created or renamed from the filesystem'
1165 1165 ' please run the application again'
1166 1166 ' in order to rescan repositories') % repo_name, category='error')
1167 1167
1168 1168
1169 1169 def ip_range(ip_addr):
1170 1170 from rhodecode.model.db import UserIpMap
1171 1171 s, e = UserIpMap._get_ip_range(ip_addr)
1172 1172 return '%s - %s' % (s, e)
1173
@@ -1,754 +1,754 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 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 re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 import decorator
36 36 import warnings
37 37 from os.path import abspath
38 38 from os.path import dirname as dn, join as jn
39 39
40 40 from paste.script.command import Command, BadCommand
41 41
42 42 from mercurial import ui, config
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45
46 46 from rhodecode.lib.vcs import get_backend
47 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model import meta
55 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
56 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.repos_group import ReposGroupModel
59 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
60 60 from rhodecode.lib.vcs.utils.fakemod import create_module
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
65 65
66 66
67 67 def recursive_replace(str_, replace=' '):
68 68 """
69 69 Recursive replace of given sign to just one instance
70 70
71 71 :param str_: given string
72 72 :param replace: char to find and replace multiple instances
73 73
74 74 Examples::
75 75 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
76 76 'Mighty-Mighty-Bo-sstones'
77 77 """
78 78
79 79 if str_.find(replace * 2) == -1:
80 80 return str_
81 81 else:
82 82 str_ = str_.replace(replace * 2, replace)
83 83 return recursive_replace(str_, replace)
84 84
85 85
86 86 def repo_name_slug(value):
87 87 """
88 88 Return slug of name of repository
89 89 This function is called on each creation/modification
90 90 of repository to prevent bad names in repo
91 91 """
92 92
93 93 slug = remove_formatting(value)
94 94 slug = strip_tags(slug)
95 95
96 96 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
97 97 slug = slug.replace(c, '-')
98 98 slug = recursive_replace(slug, '-')
99 99 slug = collapse(slug, '-')
100 100 return slug
101 101
102 102
103 103 def get_repo_slug(request):
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105 if _repo:
106 106 _repo = _repo.rstrip('/')
107 107 return _repo
108 108
109 109
110 110 def get_repos_group_slug(request):
111 111 _group = request.environ['pylons.routes_dict'].get('group_name')
112 112 if _group:
113 113 _group = _group.rstrip('/')
114 114 return _group
115 115
116 116
117 117 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
118 118 """
119 119 Action logger for various actions made by users
120 120
121 121 :param user: user that made this action, can be a unique username string or
122 122 object containing user_id attribute
123 123 :param action: action to log, should be on of predefined unique actions for
124 124 easy translations
125 125 :param repo: string name of repository or object containing repo_id,
126 126 that action was made on
127 127 :param ipaddr: optional ip address from what the action was made
128 128 :param sa: optional sqlalchemy session
129 129
130 130 """
131 131
132 132 if not sa:
133 133 sa = meta.Session()
134 134
135 135 try:
136 136 if hasattr(user, 'user_id'):
137 137 user_obj = User.get(user.user_id)
138 138 elif isinstance(user, basestring):
139 139 user_obj = User.get_by_username(user)
140 140 else:
141 141 raise Exception('You have to provide a user object or a username')
142 142
143 143 if hasattr(repo, 'repo_id'):
144 144 repo_obj = Repository.get(repo.repo_id)
145 145 repo_name = repo_obj.repo_name
146 146 elif isinstance(repo, basestring):
147 147 repo_name = repo.lstrip('/')
148 148 repo_obj = Repository.get_by_repo_name(repo_name)
149 149 else:
150 150 repo_obj = None
151 151 repo_name = ''
152 152
153 153 user_log = UserLog()
154 154 user_log.user_id = user_obj.user_id
155 155 user_log.username = user_obj.username
156 156 user_log.action = safe_unicode(action)
157 157
158 158 user_log.repository = repo_obj
159 159 user_log.repository_name = repo_name
160 160
161 161 user_log.action_date = datetime.datetime.now()
162 162 user_log.user_ip = ipaddr
163 163 sa.add(user_log)
164 164
165 165 log.info('Logging action %s on %s by %s' %
166 166 (action, safe_unicode(repo), user_obj))
167 167 if commit:
168 168 sa.commit()
169 169 except:
170 170 log.error(traceback.format_exc())
171 171 raise
172 172
173 173
174 174 def get_repos(path, recursive=False):
175 175 """
176 176 Scans given path for repos and return (name,(type,path)) tuple
177 177
178 178 :param path: path to scan for repositories
179 179 :param recursive: recursive search and return names with subdirs in front
180 180 """
181 181
182 182 # remove ending slash for better results
183 183 path = path.rstrip(os.sep)
184 184
185 185 def _get_repos(p):
186 186 if not os.access(p, os.W_OK):
187 187 return
188 188 for dirpath in os.listdir(p):
189 189 if os.path.isfile(os.path.join(p, dirpath)):
190 190 continue
191 191 cur_path = os.path.join(p, dirpath)
192 192 try:
193 193 scm_info = get_scm(cur_path)
194 194 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
195 195 except VCSError:
196 196 if not recursive:
197 197 continue
198 198 #check if this dir containts other repos for recursive scan
199 199 rec_path = os.path.join(p, dirpath)
200 200 if os.path.isdir(rec_path):
201 201 for inner_scm in _get_repos(rec_path):
202 202 yield inner_scm
203 203
204 204 return _get_repos(path)
205 205
206 206
207 207 def is_valid_repo(repo_name, base_path, scm=None):
208 208 """
209 209 Returns True if given path is a valid repository False otherwise.
210 210 If scm param is given also compare if given scm is the same as expected
211 211 from scm parameter
212 212
213 213 :param repo_name:
214 214 :param base_path:
215 215 :param scm:
216 216
217 217 :return True: if given path is a valid repository
218 218 """
219 219 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
220 220
221 221 try:
222 222 scm_ = get_scm(full_path)
223 223 if scm:
224 224 return scm_[0] == scm
225 225 return True
226 226 except VCSError:
227 227 return False
228 228
229 229
230 230 def is_valid_repos_group(repos_group_name, base_path):
231 231 """
232 232 Returns True if given path is a repos group False otherwise
233 233
234 234 :param repo_name:
235 235 :param base_path:
236 236 """
237 237 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
238 238
239 239 # check if it's not a repo
240 240 if is_valid_repo(repos_group_name, base_path):
241 241 return False
242 242
243 243 try:
244 244 # we need to check bare git repos at higher level
245 245 # since we might match branches/hooks/info/objects or possible
246 246 # other things inside bare git repo
247 247 get_scm(os.path.dirname(full_path))
248 248 return False
249 249 except VCSError:
250 250 pass
251 251
252 252 # check if it's a valid path
253 253 if os.path.isdir(full_path):
254 254 return True
255 255
256 256 return False
257 257
258 258
259 259 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
260 260 while True:
261 261 ok = raw_input(prompt)
262 262 if ok in ('y', 'ye', 'yes'):
263 263 return True
264 264 if ok in ('n', 'no', 'nop', 'nope'):
265 265 return False
266 266 retries = retries - 1
267 267 if retries < 0:
268 268 raise IOError
269 269 print complaint
270 270
271 271 #propagated from mercurial documentation
272 272 ui_sections = ['alias', 'auth',
273 273 'decode/encode', 'defaults',
274 274 'diff', 'email',
275 275 'extensions', 'format',
276 276 'merge-patterns', 'merge-tools',
277 277 'hooks', 'http_proxy',
278 278 'smtp', 'patch',
279 279 'paths', 'profiling',
280 280 'server', 'trusted',
281 281 'ui', 'web', ]
282 282
283 283
284 284 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
285 285 """
286 286 A function that will read python rc files or database
287 287 and make an mercurial ui object from read options
288 288
289 289 :param path: path to mercurial config file
290 290 :param checkpaths: check the path
291 291 :param read_from: read from 'file' or 'db'
292 292 """
293 293
294 294 baseui = ui.ui()
295 295
296 296 # clean the baseui object
297 297 baseui._ocfg = config.config()
298 298 baseui._ucfg = config.config()
299 299 baseui._tcfg = config.config()
300 300
301 301 if read_from == 'file':
302 302 if not os.path.isfile(path):
303 303 log.debug('hgrc file is not present at %s, skipping...' % path)
304 304 return False
305 305 log.debug('reading hgrc from %s' % path)
306 306 cfg = config.config()
307 307 cfg.read(path)
308 308 for section in ui_sections:
309 309 for k, v in cfg.items(section):
310 310 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
311 311 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
312 312
313 313 elif read_from == 'db':
314 314 sa = meta.Session()
315 315 ret = sa.query(RhodeCodeUi)\
316 316 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
317 317 .all()
318 318
319 319 hg_ui = ret
320 320 for ui_ in hg_ui:
321 321 if ui_.ui_active:
322 322 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
323 323 ui_.ui_key, ui_.ui_value)
324 324 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
325 325 safe_str(ui_.ui_value))
326 326 if ui_.ui_key == 'push_ssl':
327 327 # force set push_ssl requirement to False, rhodecode
328 328 # handles that
329 329 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
330 330 False)
331 331 if clear_session:
332 332 meta.Session.remove()
333 333 return baseui
334 334
335 335
336 336 def set_rhodecode_config(config):
337 337 """
338 338 Updates pylons config with new settings from database
339 339
340 340 :param config:
341 341 """
342 342 hgsettings = RhodeCodeSetting.get_app_settings()
343 343
344 344 for k, v in hgsettings.items():
345 345 config[k] = v
346 346
347 347
348 348 def invalidate_cache(cache_key, *args):
349 349 """
350 350 Puts cache invalidation task into db for
351 351 further global cache invalidation
352 352 """
353 353
354 354 from rhodecode.model.scm import ScmModel
355 355
356 356 if cache_key.startswith('get_repo_cached_'):
357 357 name = cache_key.split('get_repo_cached_')[-1]
358 358 ScmModel().mark_for_invalidation(name)
359 359
360 360
361 361 def map_groups(path):
362 362 """
363 363 Given a full path to a repository, create all nested groups that this
364 364 repo is inside. This function creates parent-child relationships between
365 365 groups and creates default perms for all new groups.
366 366
367 367 :param paths: full path to repository
368 368 """
369 369 sa = meta.Session()
370 370 groups = path.split(Repository.url_sep())
371 371 parent = None
372 372 group = None
373 373
374 374 # last element is repo in nested groups structure
375 375 groups = groups[:-1]
376 376 rgm = ReposGroupModel(sa)
377 377 for lvl, group_name in enumerate(groups):
378 378 group_name = '/'.join(groups[:lvl] + [group_name])
379 379 group = RepoGroup.get_by_group_name(group_name)
380 380 desc = '%s group' % group_name
381 381
382 382 # skip folders that are now removed repos
383 383 if REMOVED_REPO_PAT.match(group_name):
384 384 break
385 385
386 386 if group is None:
387 387 log.debug('creating group level: %s group_name: %s' % (lvl,
388 388 group_name))
389 389 group = RepoGroup(group_name, parent)
390 390 group.group_description = desc
391 391 sa.add(group)
392 392 rgm._create_default_perms(group)
393 393 sa.flush()
394 394 parent = group
395 395 return group
396 396
397 397
398 398 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
399 399 install_git_hook=False):
400 400 """
401 401 maps all repos given in initial_repo_list, non existing repositories
402 402 are created, if remove_obsolete is True it also check for db entries
403 403 that are not in initial_repo_list and removes them.
404 404
405 405 :param initial_repo_list: list of repositories found by scanning methods
406 406 :param remove_obsolete: check for obsolete entries in database
407 407 :param install_git_hook: if this is True, also check and install githook
408 408 for a repo if missing
409 409 """
410 410 from rhodecode.model.repo import RepoModel
411 411 from rhodecode.model.scm import ScmModel
412 412 sa = meta.Session()
413 413 rm = RepoModel()
414 414 user = sa.query(User).filter(User.admin == True).first()
415 415 if user is None:
416 416 raise Exception('Missing administrative account!')
417 417 added = []
418 418
419 419 # # clear cache keys
420 420 # log.debug("Clearing cache keys now...")
421 421 # CacheInvalidation.clear_cache()
422 422 # sa.commit()
423 423
424 424 ##creation defaults
425 425 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
426 426 enable_statistics = defs.get('repo_enable_statistics')
427 427 enable_locking = defs.get('repo_enable_locking')
428 428 enable_downloads = defs.get('repo_enable_downloads')
429 429 private = defs.get('repo_private')
430 430
431 431 for name, repo in initial_repo_list.items():
432 432 group = map_groups(name)
433 433 db_repo = rm.get_by_repo_name(name)
434 434 # found repo that is on filesystem not in RhodeCode database
435 435 if not db_repo:
436 436 log.info('repository %s not found, creating now' % name)
437 437 added.append(name)
438 438 desc = (repo.description
439 439 if repo.description != 'unknown'
440 440 else '%s repository' % name)
441 441
442 442 new_repo = rm.create_repo(
443 443 repo_name=name,
444 444 repo_type=repo.alias,
445 445 description=desc,
446 446 repos_group=getattr(group, 'group_id', None),
447 447 owner=user,
448 448 just_db=True,
449 449 enable_locking=enable_locking,
450 450 enable_downloads=enable_downloads,
451 451 enable_statistics=enable_statistics,
452 452 private=private
453 453 )
454 454 # we added that repo just now, and make sure it has githook
455 455 # installed
456 456 if new_repo.repo_type == 'git':
457 457 ScmModel().install_git_hook(new_repo.scm_instance)
458 458 new_repo.update_changeset_cache()
459 459 elif install_git_hook:
460 460 if db_repo.repo_type == 'git':
461 461 ScmModel().install_git_hook(db_repo.scm_instance)
462 462 # during starting install all cache keys for all repositories in the
463 463 # system, this will register all repos and multiple instances
464 464 key, _prefix, _org_key = CacheInvalidation._get_key(name)
465 465 CacheInvalidation.invalidate(name)
466 466 log.debug("Creating a cache key for %s, instance_id %s"
467 467 % (name, _prefix or 'unknown'))
468 468
469 469 sa.commit()
470 470 removed = []
471 471 if remove_obsolete:
472 472 # remove from database those repositories that are not in the filesystem
473 473 for repo in sa.query(Repository).all():
474 474 if repo.repo_name not in initial_repo_list.keys():
475 475 log.debug("Removing non-existing repository found in db `%s`" %
476 476 repo.repo_name)
477 477 try:
478 478 sa.delete(repo)
479 479 sa.commit()
480 480 removed.append(repo.repo_name)
481 481 except:
482 482 #don't hold further removals on error
483 483 log.error(traceback.format_exc())
484 484 sa.rollback()
485 485
486 486 return added, removed
487 487
488 488
489 489 # set cache regions for beaker so celery can utilise it
490 490 def add_cache(settings):
491 491 cache_settings = {'regions': None}
492 492 for key in settings.keys():
493 493 for prefix in ['beaker.cache.', 'cache.']:
494 494 if key.startswith(prefix):
495 495 name = key.split(prefix)[1].strip()
496 496 cache_settings[name] = settings[key].strip()
497 497 if cache_settings['regions']:
498 498 for region in cache_settings['regions'].split(','):
499 499 region = region.strip()
500 500 region_settings = {}
501 501 for key, value in cache_settings.items():
502 502 if key.startswith(region):
503 503 region_settings[key.split('.')[1]] = value
504 504 region_settings['expire'] = int(region_settings.get('expire',
505 505 60))
506 506 region_settings.setdefault('lock_dir',
507 507 cache_settings.get('lock_dir'))
508 508 region_settings.setdefault('data_dir',
509 509 cache_settings.get('data_dir'))
510 510
511 511 if 'type' not in region_settings:
512 512 region_settings['type'] = cache_settings.get('type',
513 513 'memory')
514 514 beaker.cache.cache_regions[region] = region_settings
515 515
516 516
517 517 def load_rcextensions(root_path):
518 518 import rhodecode
519 519 from rhodecode.config import conf
520 520
521 521 path = os.path.join(root_path, 'rcextensions', '__init__.py')
522 522 if os.path.isfile(path):
523 523 rcext = create_module('rc', path)
524 524 EXT = rhodecode.EXTENSIONS = rcext
525 525 log.debug('Found rcextensions now loading %s...' % rcext)
526 526
527 527 # Additional mappings that are not present in the pygments lexers
528 528 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
529 529
530 530 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
531 531
532 532 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
533 533 log.debug('settings custom INDEX_EXTENSIONS')
534 534 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
535 535
536 536 #ADDITIONAL MAPPINGS
537 537 log.debug('adding extra into INDEX_EXTENSIONS')
538 538 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
539 539
540 540
541 541 #==============================================================================
542 542 # TEST FUNCTIONS AND CREATORS
543 543 #==============================================================================
544 544 def create_test_index(repo_location, config, full_index):
545 545 """
546 546 Makes default test index
547 547
548 548 :param config: test config
549 549 :param full_index:
550 550 """
551 551
552 552 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
553 553 from rhodecode.lib.pidlock import DaemonLock, LockHeld
554 554
555 555 repo_location = repo_location
556 556
557 557 index_location = os.path.join(config['app_conf']['index_dir'])
558 558 if not os.path.exists(index_location):
559 559 os.makedirs(index_location)
560 560
561 561 try:
562 562 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
563 563 WhooshIndexingDaemon(index_location=index_location,
564 564 repo_location=repo_location)\
565 565 .run(full_index=full_index)
566 566 l.release()
567 567 except LockHeld:
568 568 pass
569 569
570 570
571 571 def create_test_env(repos_test_path, config):
572 572 """
573 573 Makes a fresh database and
574 574 install test repository into tmp dir
575 575 """
576 576 from rhodecode.lib.db_manage import DbManage
577 577 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
578 578
579 579 # PART ONE create db
580 580 dbconf = config['sqlalchemy.db1.url']
581 581 log.debug('making test db %s' % dbconf)
582 582
583 583 # create test dir if it doesn't exist
584 584 if not os.path.isdir(repos_test_path):
585 585 log.debug('Creating testdir %s' % repos_test_path)
586 586 os.makedirs(repos_test_path)
587 587
588 588 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
589 589 tests=True)
590 590 dbmanage.create_tables(override=True)
591 591 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
592 592 dbmanage.create_default_user()
593 593 dbmanage.admin_prompt()
594 594 dbmanage.create_permissions()
595 595 dbmanage.populate_default_permissions()
596 596 Session().commit()
597 597 # PART TWO make test repo
598 598 log.debug('making test vcs repositories')
599 599
600 600 idx_path = config['app_conf']['index_dir']
601 601 data_path = config['app_conf']['cache_dir']
602 602
603 603 #clean index and data
604 604 if idx_path and os.path.exists(idx_path):
605 605 log.debug('remove %s' % idx_path)
606 606 shutil.rmtree(idx_path)
607 607
608 608 if data_path and os.path.exists(data_path):
609 609 log.debug('remove %s' % data_path)
610 610 shutil.rmtree(data_path)
611 611
612 612 #CREATE DEFAULT TEST REPOS
613 613 cur_dir = dn(dn(abspath(__file__)))
614 614 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
615 615 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
616 616 tar.close()
617 617
618 618 cur_dir = dn(dn(abspath(__file__)))
619 619 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
620 620 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
621 621 tar.close()
622 622
623 623 #LOAD VCS test stuff
624 624 from rhodecode.tests.vcs import setup_package
625 625 setup_package()
626 626
627 627
628 628 #==============================================================================
629 629 # PASTER COMMANDS
630 630 #==============================================================================
631 631 class BasePasterCommand(Command):
632 632 """
633 633 Abstract Base Class for paster commands.
634 634
635 635 The celery commands are somewhat aggressive about loading
636 636 celery.conf, and since our module sets the `CELERY_LOADER`
637 637 environment variable to our loader, we have to bootstrap a bit and
638 638 make sure we've had a chance to load the pylons config off of the
639 639 command line, otherwise everything fails.
640 640 """
641 641 min_args = 1
642 642 min_args_error = "Please provide a paster config file as an argument."
643 643 takes_config_file = 1
644 644 requires_config_file = True
645 645
646 646 def notify_msg(self, msg, log=False):
647 647 """Make a notification to user, additionally if logger is passed
648 648 it logs this action using given logger
649 649
650 650 :param msg: message that will be printed to user
651 651 :param log: logging instance, to use to additionally log this message
652 652
653 653 """
654 654 if log and isinstance(log, logging):
655 655 log(msg)
656 656
657 657 def run(self, args):
658 658 """
659 659 Overrides Command.run
660 660
661 661 Checks for a config file argument and loads it.
662 662 """
663 663 if len(args) < self.min_args:
664 664 raise BadCommand(
665 665 self.min_args_error % {'min_args': self.min_args,
666 666 'actual_args': len(args)})
667 667
668 668 # Decrement because we're going to lob off the first argument.
669 669 # @@ This is hacky
670 670 self.min_args -= 1
671 671 self.bootstrap_config(args[0])
672 672 self.update_parser()
673 673 return super(BasePasterCommand, self).run(args[1:])
674 674
675 675 def update_parser(self):
676 676 """
677 677 Abstract method. Allows for the class's parser to be updated
678 678 before the superclass's `run` method is called. Necessary to
679 679 allow options/arguments to be passed through to the underlying
680 680 celery command.
681 681 """
682 682 raise NotImplementedError("Abstract Method.")
683 683
684 684 def bootstrap_config(self, conf):
685 685 """
686 686 Loads the pylons configuration.
687 687 """
688 688 from pylons import config as pylonsconfig
689 689
690 690 self.path_to_ini_file = os.path.realpath(conf)
691 691 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
692 692 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
693 693
694 694
695 695 def check_git_version():
696 696 """
697 697 Checks what version of git is installed in system, and issues a warning
698 698 if it's too old for RhodeCode to properly work.
699 699 """
700 700 import subprocess
701 701 from distutils.version import StrictVersion
702 702 from rhodecode import BACKENDS
703 703
704 704 p = subprocess.Popen('git --version', shell=True,
705 705 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
706 706 stdout, stderr = p.communicate()
707 707 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
708 708 if len(ver.split('.')) > 3:
709 709 #StrictVersion needs to be only 3 element type
710 710 ver = '.'.join(ver.split('.')[:3])
711 711 try:
712 712 _ver = StrictVersion(ver)
713 713 except:
714 714 _ver = StrictVersion('0.0.0')
715 715 stderr = traceback.format_exc()
716 716
717 717 req_ver = '1.7.4'
718 718 to_old_git = False
719 719 if _ver < StrictVersion(req_ver):
720 720 to_old_git = True
721 721
722 722 if 'git' in BACKENDS:
723 723 log.debug('GIT version detected: %s' % stdout)
724 724 if stderr:
725 725 log.warning('Unable to detect git version org error was:%r' % stderr)
726 726 elif to_old_git:
727 727 log.warning('RhodeCode detected git version %s, which is too old '
728 728 'for the system to function properly. Make sure '
729 729 'its version is at least %s' % (ver, req_ver))
730 730 return _ver
731 731
732 732
733 733 @decorator.decorator
734 734 def jsonify(func, *args, **kwargs):
735 735 """Action decorator that formats output for JSON
736 736
737 737 Given a function that will return content, this decorator will turn
738 738 the result into JSON, with a content-type of 'application/json' and
739 739 output it.
740 740
741 741 """
742 742 from pylons.decorators.util import get_pylons
743 743 from rhodecode.lib.ext_json import json
744 744 pylons = get_pylons(args)
745 745 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
746 746 data = func(*args, **kwargs)
747 747 if isinstance(data, (list, tuple)):
748 748 msg = "JSON responses with Array envelopes are susceptible to " \
749 749 "cross-site data leak attacks, see " \
750 750 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
751 751 warnings.warn(msg, Warning, 2)
752 752 log.warning(msg)
753 753 log.debug("Returning JSON wrapped action output")
754 return json.dumps(data, encoding='utf-8') No newline at end of file
754 return json.dumps(data, encoding='utf-8')
@@ -1,745 +1,745 b''
1 1 """
2 2 Set of generic validators
3 3 """
4 4 import os
5 5 import re
6 6 import formencode
7 7 import logging
8 8 from collections import defaultdict
9 9 from pylons.i18n.translation import _
10 10 from webhelpers.pylonslib.secure_form import authentication_token
11 11
12 12 from formencode.validators import (
13 13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
14 14 NotEmpty, IPAddress, CIDR
15 15 )
16 16 from rhodecode.lib.compat import OrderedSet
17 17 from rhodecode.lib.utils import repo_name_slug
18 18 from rhodecode.model.db import RepoGroup, Repository, UsersGroup, User,\
19 19 ChangesetStatus
20 20 from rhodecode.lib.exceptions import LdapImportError
21 21 from rhodecode.config.routing import ADMIN_PREFIX
22 22 from rhodecode.lib.auth import HasReposGroupPermissionAny
23 23
24 24 # silence warnings and pylint
25 25 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
26 26 NotEmpty, IPAddress, CIDR
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class UniqueList(formencode.FancyValidator):
32 32 """
33 33 Unique List !
34 34 """
35 35 messages = dict(
36 36 empty=_('Value cannot be an empty list'),
37 37 missing_value=_('Value cannot be an empty list'),
38 38 )
39 39
40 40 def _to_python(self, value, state):
41 41 if isinstance(value, list):
42 42 return value
43 43 elif isinstance(value, set):
44 44 return list(value)
45 45 elif isinstance(value, tuple):
46 46 return list(value)
47 47 elif value is None:
48 48 return []
49 49 else:
50 50 return [value]
51 51
52 52 def empty_value(self, value):
53 53 return []
54 54
55 55
56 56 class StateObj(object):
57 57 """
58 58 this is needed to translate the messages using _() in validators
59 59 """
60 60 _ = staticmethod(_)
61 61
62 62
63 63 def M(self, key, state=None, **kwargs):
64 64 """
65 65 returns string from self.message based on given key,
66 66 passed kw params are used to substitute %(named)s params inside
67 67 translated strings
68 68
69 69 :param msg:
70 70 :param state:
71 71 """
72 72 if state is None:
73 73 state = StateObj()
74 74 else:
75 75 state._ = staticmethod(_)
76 76 #inject validator into state object
77 77 return self.message(key, state, **kwargs)
78 78
79 79
80 80 def ValidUsername(edit=False, old_data={}):
81 81 class _validator(formencode.validators.FancyValidator):
82 82 messages = {
83 83 'username_exists': _(u'Username "%(username)s" already exists'),
84 84 'system_invalid_username':
85 85 _(u'Username "%(username)s" is forbidden'),
86 86 'invalid_username':
87 87 _(u'Username may only contain alphanumeric characters '
88 88 'underscores, periods or dashes and must begin with '
89 89 'alphanumeric character')
90 90 }
91 91
92 92 def validate_python(self, value, state):
93 93 if value in ['default', 'new_user']:
94 94 msg = M(self, 'system_invalid_username', state, username=value)
95 95 raise formencode.Invalid(msg, value, state)
96 96 #check if user is unique
97 97 old_un = None
98 98 if edit:
99 99 old_un = User.get(old_data.get('user_id')).username
100 100
101 101 if old_un != value or not edit:
102 102 if User.get_by_username(value, case_insensitive=True):
103 103 msg = M(self, 'username_exists', state, username=value)
104 104 raise formencode.Invalid(msg, value, state)
105 105
106 106 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
107 107 msg = M(self, 'invalid_username', state)
108 108 raise formencode.Invalid(msg, value, state)
109 109 return _validator
110 110
111 111
112 112 def ValidRepoUser():
113 113 class _validator(formencode.validators.FancyValidator):
114 114 messages = {
115 115 'invalid_username': _(u'Username %(username)s is not valid')
116 116 }
117 117
118 118 def validate_python(self, value, state):
119 119 try:
120 120 User.query().filter(User.active == True)\
121 121 .filter(User.username == value).one()
122 122 except Exception:
123 123 msg = M(self, 'invalid_username', state, username=value)
124 124 raise formencode.Invalid(msg, value, state,
125 125 error_dict=dict(username=msg)
126 126 )
127 127
128 128 return _validator
129 129
130 130
131 131 def ValidUsersGroup(edit=False, old_data={}):
132 132 class _validator(formencode.validators.FancyValidator):
133 133 messages = {
134 134 'invalid_group': _(u'Invalid users group name'),
135 135 'group_exist': _(u'Users group "%(usersgroup)s" already exists'),
136 136 'invalid_usersgroup_name':
137 137 _(u'users group name may only contain alphanumeric '
138 138 'characters underscores, periods or dashes and must begin '
139 139 'with alphanumeric character')
140 140 }
141 141
142 142 def validate_python(self, value, state):
143 143 if value in ['default']:
144 144 msg = M(self, 'invalid_group', state)
145 145 raise formencode.Invalid(msg, value, state,
146 146 error_dict=dict(users_group_name=msg)
147 147 )
148 148 #check if group is unique
149 149 old_ugname = None
150 150 if edit:
151 151 old_id = old_data.get('users_group_id')
152 152 old_ugname = UsersGroup.get(old_id).users_group_name
153 153
154 154 if old_ugname != value or not edit:
155 155 is_existing_group = UsersGroup.get_by_group_name(value,
156 156 case_insensitive=True)
157 157 if is_existing_group:
158 158 msg = M(self, 'group_exist', state, usersgroup=value)
159 159 raise formencode.Invalid(msg, value, state,
160 160 error_dict=dict(users_group_name=msg)
161 161 )
162 162
163 163 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
164 164 msg = M(self, 'invalid_usersgroup_name', state)
165 165 raise formencode.Invalid(msg, value, state,
166 166 error_dict=dict(users_group_name=msg)
167 167 )
168 168
169 169 return _validator
170 170
171 171
172 172 def ValidReposGroup(edit=False, old_data={}):
173 173 class _validator(formencode.validators.FancyValidator):
174 174 messages = {
175 175 'group_parent_id': _(u'Cannot assign this group as parent'),
176 176 'group_exists': _(u'Group "%(group_name)s" already exists'),
177 177 'repo_exists':
178 178 _(u'Repository with name "%(group_name)s" already exists')
179 179 }
180 180
181 181 def validate_python(self, value, state):
182 182 # TODO WRITE VALIDATIONS
183 183 group_name = value.get('group_name')
184 184 group_parent_id = value.get('group_parent_id')
185 185
186 186 # slugify repo group just in case :)
187 187 slug = repo_name_slug(group_name)
188 188
189 189 # check for parent of self
190 190 parent_of_self = lambda: (
191 191 old_data['group_id'] == int(group_parent_id)
192 192 if group_parent_id else False
193 193 )
194 194 if edit and parent_of_self():
195 195 msg = M(self, 'group_parent_id', state)
196 196 raise formencode.Invalid(msg, value, state,
197 197 error_dict=dict(group_parent_id=msg)
198 198 )
199 199
200 200 old_gname = None
201 201 if edit:
202 202 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
203 203
204 204 if old_gname != group_name or not edit:
205 205
206 206 # check group
207 207 gr = RepoGroup.query()\
208 208 .filter(RepoGroup.group_name == slug)\
209 209 .filter(RepoGroup.group_parent_id == group_parent_id)\
210 210 .scalar()
211 211
212 212 if gr:
213 213 msg = M(self, 'group_exists', state, group_name=slug)
214 214 raise formencode.Invalid(msg, value, state,
215 215 error_dict=dict(group_name=msg)
216 216 )
217 217
218 218 # check for same repo
219 219 repo = Repository.query()\
220 220 .filter(Repository.repo_name == slug)\
221 221 .scalar()
222 222
223 223 if repo:
224 224 msg = M(self, 'repo_exists', state, group_name=slug)
225 225 raise formencode.Invalid(msg, value, state,
226 226 error_dict=dict(group_name=msg)
227 227 )
228 228
229 229 return _validator
230 230
231 231
232 232 def ValidPassword():
233 233 class _validator(formencode.validators.FancyValidator):
234 234 messages = {
235 235 'invalid_password':
236 236 _(u'Invalid characters (non-ascii) in password')
237 237 }
238 238
239 239 def validate_python(self, value, state):
240 240 try:
241 241 (value or '').decode('ascii')
242 242 except UnicodeError:
243 243 msg = M(self, 'invalid_password', state)
244 244 raise formencode.Invalid(msg, value, state,)
245 245 return _validator
246 246
247 247
248 248 def ValidPasswordsMatch():
249 249 class _validator(formencode.validators.FancyValidator):
250 250 messages = {
251 251 'password_mismatch': _(u'Passwords do not match'),
252 252 }
253 253
254 254 def validate_python(self, value, state):
255 255
256 256 pass_val = value.get('password') or value.get('new_password')
257 257 if pass_val != value['password_confirmation']:
258 258 msg = M(self, 'password_mismatch', state)
259 259 raise formencode.Invalid(msg, value, state,
260 260 error_dict=dict(password_confirmation=msg)
261 261 )
262 262 return _validator
263 263
264 264
265 265 def ValidAuth():
266 266 class _validator(formencode.validators.FancyValidator):
267 267 messages = {
268 268 'invalid_password': _(u'invalid password'),
269 269 'invalid_username': _(u'invalid user name'),
270 270 'disabled_account': _(u'Your account is disabled')
271 271 }
272 272
273 273 def validate_python(self, value, state):
274 274 from rhodecode.lib.auth import authenticate
275 275
276 276 password = value['password']
277 277 username = value['username']
278 278
279 279 if not authenticate(username, password):
280 280 user = User.get_by_username(username)
281 281 if user and user.active is False:
282 282 log.warning('user %s is disabled' % username)
283 283 msg = M(self, 'disabled_account', state)
284 284 raise formencode.Invalid(msg, value, state,
285 285 error_dict=dict(username=msg)
286 286 )
287 287 else:
288 288 log.warning('user %s failed to authenticate' % username)
289 289 msg = M(self, 'invalid_username', state)
290 290 msg2 = M(self, 'invalid_password', state)
291 291 raise formencode.Invalid(msg, value, state,
292 292 error_dict=dict(username=msg, password=msg2)
293 293 )
294 294 return _validator
295 295
296 296
297 297 def ValidAuthToken():
298 298 class _validator(formencode.validators.FancyValidator):
299 299 messages = {
300 300 'invalid_token': _(u'Token mismatch')
301 301 }
302 302
303 303 def validate_python(self, value, state):
304 304 if value != authentication_token():
305 305 msg = M(self, 'invalid_token', state)
306 306 raise formencode.Invalid(msg, value, state)
307 307 return _validator
308 308
309 309
310 310 def ValidRepoName(edit=False, old_data={}):
311 311 class _validator(formencode.validators.FancyValidator):
312 312 messages = {
313 313 'invalid_repo_name':
314 314 _(u'Repository name %(repo)s is disallowed'),
315 315 'repository_exists':
316 316 _(u'Repository named %(repo)s already exists'),
317 317 'repository_in_group_exists': _(u'Repository "%(repo)s" already '
318 318 'exists in group "%(group)s"'),
319 319 'same_group_exists': _(u'Repositories group with name "%(repo)s" '
320 320 'already exists')
321 321 }
322 322
323 323 def _to_python(self, value, state):
324 324 repo_name = repo_name_slug(value.get('repo_name', ''))
325 325 repo_group = value.get('repo_group')
326 326 if repo_group:
327 327 gr = RepoGroup.get(repo_group)
328 328 group_path = gr.full_path
329 329 group_name = gr.group_name
330 330 # value needs to be aware of group name in order to check
331 331 # db key This is an actual just the name to store in the
332 332 # database
333 333 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
334 334 else:
335 335 group_name = group_path = ''
336 336 repo_name_full = repo_name
337 337
338 338 value['repo_name'] = repo_name
339 339 value['repo_name_full'] = repo_name_full
340 340 value['group_path'] = group_path
341 341 value['group_name'] = group_name
342 342 return value
343 343
344 344 def validate_python(self, value, state):
345 345
346 346 repo_name = value.get('repo_name')
347 347 repo_name_full = value.get('repo_name_full')
348 348 group_path = value.get('group_path')
349 349 group_name = value.get('group_name')
350 350
351 351 if repo_name in [ADMIN_PREFIX, '']:
352 352 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
353 353 raise formencode.Invalid(msg, value, state,
354 354 error_dict=dict(repo_name=msg)
355 355 )
356 356
357 357 rename = old_data.get('repo_name') != repo_name_full
358 358 create = not edit
359 359 if rename or create:
360 360
361 361 if group_path != '':
362 362 if Repository.get_by_repo_name(repo_name_full):
363 363 msg = M(self, 'repository_in_group_exists', state,
364 364 repo=repo_name, group=group_name)
365 365 raise formencode.Invalid(msg, value, state,
366 366 error_dict=dict(repo_name=msg)
367 367 )
368 368 elif RepoGroup.get_by_group_name(repo_name_full):
369 369 msg = M(self, 'same_group_exists', state,
370 370 repo=repo_name)
371 371 raise formencode.Invalid(msg, value, state,
372 372 error_dict=dict(repo_name=msg)
373 373 )
374 374
375 375 elif Repository.get_by_repo_name(repo_name_full):
376 376 msg = M(self, 'repository_exists', state,
377 377 repo=repo_name)
378 378 raise formencode.Invalid(msg, value, state,
379 379 error_dict=dict(repo_name=msg)
380 380 )
381 381 return value
382 382 return _validator
383 383
384 384
385 385 def ValidForkName(*args, **kwargs):
386 386 return ValidRepoName(*args, **kwargs)
387 387
388 388
389 389 def SlugifyName():
390 390 class _validator(formencode.validators.FancyValidator):
391 391
392 392 def _to_python(self, value, state):
393 393 return repo_name_slug(value)
394 394
395 395 def validate_python(self, value, state):
396 396 pass
397 397
398 398 return _validator
399 399
400 400
401 401 def ValidCloneUri():
402 402 from rhodecode.lib.utils import make_ui
403 403
404 404 def url_handler(repo_type, url, ui=None):
405 405 if repo_type == 'hg':
406 406 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
407 407 from mercurial.httppeer import httppeer
408 408 if url.startswith('http'):
409 409 ## initially check if it's at least the proper URL
410 410 ## or does it pass basic auth
411 411 MercurialRepository._check_url(url)
412 412 httppeer(ui, url)._capabilities()
413 413 elif url.startswith('svn+http'):
414 414 from hgsubversion.svnrepo import svnremoterepo
415 415 svnremoterepo(ui, url).capabilities
416 416 elif url.startswith('git+http'):
417 417 raise NotImplementedError()
418 418
419 419 elif repo_type == 'git':
420 420 from rhodecode.lib.vcs.backends.git.repository import GitRepository
421 421 if url.startswith('http'):
422 422 ## initially check if it's at least the proper URL
423 423 ## or does it pass basic auth
424 424 GitRepository._check_url(url)
425 425 elif url.startswith('svn+http'):
426 426 raise NotImplementedError()
427 427 elif url.startswith('hg+http'):
428 428 raise NotImplementedError()
429 429
430 430 class _validator(formencode.validators.FancyValidator):
431 431 messages = {
432 432 'clone_uri': _(u'invalid clone url'),
433 433 'invalid_clone_uri': _(u'Invalid clone url, provide a '
434 434 'valid clone http(s)/svn+http(s) url')
435 435 }
436 436
437 437 def validate_python(self, value, state):
438 438 repo_type = value.get('repo_type')
439 439 url = value.get('clone_uri')
440 440
441 441 if not url:
442 442 pass
443 443 else:
444 444 try:
445 445 url_handler(repo_type, url, make_ui('db', clear_session=False))
446 446 except Exception:
447 447 log.exception('Url validation failed')
448 448 msg = M(self, 'clone_uri')
449 449 raise formencode.Invalid(msg, value, state,
450 450 error_dict=dict(clone_uri=msg)
451 451 )
452 452 return _validator
453 453
454 454
455 455 def ValidForkType(old_data={}):
456 456 class _validator(formencode.validators.FancyValidator):
457 457 messages = {
458 458 'invalid_fork_type': _(u'Fork have to be the same type as parent')
459 459 }
460 460
461 461 def validate_python(self, value, state):
462 462 if old_data['repo_type'] != value:
463 463 msg = M(self, 'invalid_fork_type', state)
464 464 raise formencode.Invalid(msg, value, state,
465 465 error_dict=dict(repo_type=msg)
466 466 )
467 467 return _validator
468 468
469 469
470 470 def CanWriteGroup():
471 471 class _validator(formencode.validators.FancyValidator):
472 472 messages = {
473 473 'permission_denied': _(u"You don't have permissions "
474 474 "to create repository in this group")
475 475 }
476 476
477 477 def validate_python(self, value, state):
478 478 gr = RepoGroup.get(value)
479 479 if not HasReposGroupPermissionAny(
480 480 'group.write', 'group.admin'
481 481 )(gr.group_name, 'get group of repo form'):
482 482 msg = M(self, 'permission_denied', state)
483 483 raise formencode.Invalid(msg, value, state,
484 484 error_dict=dict(repo_type=msg)
485 485 )
486 486 return _validator
487 487
488 488
489 489 def ValidPerms(type_='repo'):
490 490 if type_ == 'group':
491 491 EMPTY_PERM = 'group.none'
492 492 elif type_ == 'repo':
493 493 EMPTY_PERM = 'repository.none'
494 494
495 495 class _validator(formencode.validators.FancyValidator):
496 496 messages = {
497 497 'perm_new_member_name':
498 498 _(u'This username or users group name is not valid')
499 499 }
500 500
501 501 def to_python(self, value, state):
502 502 perms_update = OrderedSet()
503 503 perms_new = OrderedSet()
504 504 # build a list of permission to update and new permission to create
505 505
506 506 #CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
507 507 new_perms_group = defaultdict(dict)
508 508 for k, v in value.copy().iteritems():
509 509 if k.startswith('perm_new_member'):
510 510 del value[k]
511 511 _type, part = k.split('perm_new_member_')
512 512 args = part.split('_')
513 513 if len(args) == 1:
514 514 new_perms_group[args[0]]['perm'] = v
515 515 elif len(args) == 2:
516 516 _key, pos = args
517 517 new_perms_group[pos][_key] = v
518 518
519 519 # fill new permissions in order of how they were added
520 520 for k in sorted(map(int, new_perms_group.keys())):
521 521 perm_dict = new_perms_group[str(k)]
522 522 new_member = perm_dict.get('name')
523 523 new_perm = perm_dict.get('perm')
524 524 new_type = perm_dict.get('type')
525 525 if new_member and new_perm and new_type:
526 526 perms_new.add((new_member, new_perm, new_type))
527 527
528 528 for k, v in value.iteritems():
529 529 if k.startswith('u_perm_') or k.startswith('g_perm_'):
530 530 member = k[7:]
531 531 t = {'u': 'user',
532 532 'g': 'users_group'
533 533 }[k[0]]
534 534 if member == 'default':
535 535 if value.get('private'):
536 536 # set none for default when updating to
537 537 # private repo
538 538 v = EMPTY_PERM
539 539 perms_update.add((member, v, t))
540 540
541 541 value['perms_updates'] = list(perms_update)
542 542 value['perms_new'] = list(perms_new)
543 543
544 544 # update permissions
545 545 for k, v, t in perms_new:
546 546 try:
547 547 if t is 'user':
548 548 self.user_db = User.query()\
549 549 .filter(User.active == True)\
550 550 .filter(User.username == k).one()
551 551 if t is 'users_group':
552 552 self.user_db = UsersGroup.query()\
553 553 .filter(UsersGroup.users_group_active == True)\
554 554 .filter(UsersGroup.users_group_name == k).one()
555 555
556 556 except Exception:
557 557 log.exception('Updated permission failed')
558 558 msg = M(self, 'perm_new_member_type', state)
559 559 raise formencode.Invalid(msg, value, state,
560 560 error_dict=dict(perm_new_member_name=msg)
561 561 )
562 562 return value
563 563 return _validator
564 564
565 565
566 566 def ValidSettings():
567 567 class _validator(formencode.validators.FancyValidator):
568 568 def _to_python(self, value, state):
569 # settings form for users that are not admin
569 # settings form for users that are not admin
570 570 # can't edit certain parameters, it's extra backup if they mangle
571 571 # with forms
572 572
573 573 forbidden_params = [
574 574 'user', 'repo_type', 'repo_enable_locking',
575 575 'repo_enable_downloads', 'repo_enable_statistics'
576 576 ]
577 577
578 578 for param in forbidden_params:
579 579 if param in value:
580 580 del value[param]
581 581 return value
582 582
583 583 def validate_python(self, value, state):
584 584 pass
585 585 return _validator
586 586
587 587
588 588 def ValidPath():
589 589 class _validator(formencode.validators.FancyValidator):
590 590 messages = {
591 591 'invalid_path': _(u'This is not a valid path')
592 592 }
593 593
594 594 def validate_python(self, value, state):
595 595 if not os.path.isdir(value):
596 596 msg = M(self, 'invalid_path', state)
597 597 raise formencode.Invalid(msg, value, state,
598 598 error_dict=dict(paths_root_path=msg)
599 599 )
600 600 return _validator
601 601
602 602
603 603 def UniqSystemEmail(old_data={}):
604 604 class _validator(formencode.validators.FancyValidator):
605 605 messages = {
606 606 'email_taken': _(u'This e-mail address is already taken')
607 607 }
608 608
609 609 def _to_python(self, value, state):
610 610 return value.lower()
611 611
612 612 def validate_python(self, value, state):
613 613 if (old_data.get('email') or '').lower() != value:
614 614 user = User.get_by_email(value, case_insensitive=True)
615 615 if user:
616 616 msg = M(self, 'email_taken', state)
617 617 raise formencode.Invalid(msg, value, state,
618 618 error_dict=dict(email=msg)
619 619 )
620 620 return _validator
621 621
622 622
623 623 def ValidSystemEmail():
624 624 class _validator(formencode.validators.FancyValidator):
625 625 messages = {
626 626 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
627 627 }
628 628
629 629 def _to_python(self, value, state):
630 630 return value.lower()
631 631
632 632 def validate_python(self, value, state):
633 633 user = User.get_by_email(value, case_insensitive=True)
634 634 if user is None:
635 635 msg = M(self, 'non_existing_email', state, email=value)
636 636 raise formencode.Invalid(msg, value, state,
637 637 error_dict=dict(email=msg)
638 638 )
639 639
640 640 return _validator
641 641
642 642
643 643 def LdapLibValidator():
644 644 class _validator(formencode.validators.FancyValidator):
645 645 messages = {
646 646
647 647 }
648 648
649 649 def validate_python(self, value, state):
650 650 try:
651 651 import ldap
652 652 ldap # pyflakes silence !
653 653 except ImportError:
654 654 raise LdapImportError()
655 655
656 656 return _validator
657 657
658 658
659 659 def AttrLoginValidator():
660 660 class _validator(formencode.validators.FancyValidator):
661 661 messages = {
662 662 'invalid_cn':
663 663 _(u'The LDAP Login attribute of the CN must be specified - '
664 664 'this is the name of the attribute that is equivalent '
665 665 'to "username"')
666 666 }
667 667
668 668 def validate_python(self, value, state):
669 669 if not value or not isinstance(value, (str, unicode)):
670 670 msg = M(self, 'invalid_cn', state)
671 671 raise formencode.Invalid(msg, value, state,
672 672 error_dict=dict(ldap_attr_login=msg)
673 673 )
674 674
675 675 return _validator
676 676
677 677
678 678 def NotReviewedRevisions(repo_id):
679 679 class _validator(formencode.validators.FancyValidator):
680 680 messages = {
681 681 'rev_already_reviewed':
682 682 _(u'Revisions %(revs)s are already part of pull request '
683 683 'or have set status')
684 684 }
685 685
686 686 def validate_python(self, value, state):
687 687 # check revisions if they are not reviewed, or a part of another
688 688 # pull request
689 689 statuses = ChangesetStatus.query()\
690 690 .filter(ChangesetStatus.revision.in_(value))\
691 691 .filter(ChangesetStatus.repo_id == repo_id)\
692 692 .all()
693 693
694 694 errors = []
695 695 for cs in statuses:
696 696 if cs.pull_request_id:
697 697 errors.append(['pull_req', cs.revision[:12]])
698 698 elif cs.status:
699 699 errors.append(['status', cs.revision[:12]])
700 700
701 701 if errors:
702 702 revs = ','.join([x[1] for x in errors])
703 703 msg = M(self, 'rev_already_reviewed', state, revs=revs)
704 704 raise formencode.Invalid(msg, value, state,
705 705 error_dict=dict(revisions=revs)
706 706 )
707 707
708 708 return _validator
709 709
710 710
711 711 def ValidIp():
712 712 class _validator(CIDR):
713 713 messages = dict(
714 714 badFormat=_('Please enter a valid IP address (a.b.c.d)'),
715 715 illegalOctets=_('The octets must be within the range of 0-255'
716 716 ' (not %(octet)r)'),
717 717 illegalBits=_('The network size (bits) must be within the range'
718 718 ' of 0-32 (not %(bits)r)'))
719 719
720 720 def validate_python(self, value, state):
721 721 try:
722 722 # Split into octets and bits
723 723 if '/' in value: # a.b.c.d/e
724 724 addr, bits = value.split('/')
725 725 else: # a.b.c.d
726 726 addr, bits = value, 32
727 727 # Use IPAddress validator to validate the IP part
728 728 IPAddress.validate_python(self, addr, state)
729 729 # Bits (netmask) correct?
730 730 if not 0 <= int(bits) <= 32:
731 731 raise formencode.Invalid(
732 732 self.message('illegalBits', state, bits=bits),
733 733 value, state)
734 734 # Splitting faild: wrong syntax
735 735 except ValueError:
736 736 raise formencode.Invalid(self.message('badFormat', state),
737 737 value, state)
738 738
739 739 def to_python(self, value, state):
740 740 v = super(_validator, self).to_python(value, state)
741 741 #if IP doesn't end with a mask, add /32
742 742 if '/' not in value:
743 743 v += '/32'
744 744 return v
745 745 return _validator
@@ -1,56 +1,55 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Admin journal')} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 <form id="filter_form">
10 10 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('journal filter...')}"/>
11 11 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
12 12 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
13 13 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
14 14 </form>
15 15 ${h.end_form()}
16 16 </%def>
17 17
18 18 <%def name="page_nav()">
19 19 ${self.menu('admin')}
20 20 </%def>
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <!-- end box / title -->
28 28 <div class="table">
29 29 <div id="user_log">
30 30 ${c.log_data}
31 31 </div>
32 32 </div>
33 33 </div>
34 34
35 35 <script>
36 36 YUE.on('j_filter','click',function(){
37 37 var jfilter = YUD.get('j_filter');
38 38 if(YUD.hasClass(jfilter, 'initial')){
39 39 jfilter.value = '';
40 40 }
41 41 });
42 42 var fix_j_filter_width = function(len){
43 43 YUD.setStyle(YUD.get('j_filter'),'width',Math.max(80, len*6.50)+'px');
44 44 }
45 45 YUE.on('j_filter','keyup',function(){
46 46 fix_j_filter_width(YUD.get('j_filter').value.length);
47 47 });
48 48 YUE.on('filter_form','submit',function(e){
49 49 YUE.preventDefault(e)
50 50 var val = YUD.get('j_filter').value;
51 51 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
52 52 });
53 53 fix_j_filter_width(YUD.get('j_filter').value.length);
54 54 </script>
55 55 </%def>
56
@@ -1,64 +1,64 b''
1 1 ## -*- coding: utf-8 -*-
2 2 %if c.users_log:
3 3 <table>
4 4 <tr>
5 5 <th class="left">${_('Username')}</th>
6 6 <th class="left">${_('Action')}</th>
7 7 <th class="left">${_('Repository')}</th>
8 8 <th class="left">${_('Date')}</th>
9 9 <th class="left">${_('From IP')}</th>
10 10 </tr>
11 11
12 12 %for cnt,l in enumerate(c.users_log):
13 13 <tr class="parity${cnt%2}">
14 14 <td>
15 15 %if l.user is not None:
16 16 ${h.link_to(l.user.username,h.url('edit_user', id=l.user.user_id))}
17 17 %else:
18 18 ${l.username}
19 %endif
19 %endif
20 20 </td>
21 21 <td>${h.action_parser(l)[0]()}
22 22 <div class="journal_action_params">
23 23 ${h.literal(h.action_parser(l)[1]())}
24 24 </div>
25 25 </td>
26 26 <td>
27 27 %if l.repository is not None:
28 28 ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))}
29 29 %else:
30 30 ${l.repository_name}
31 31 %endif
32 32 </td>
33 33
34 34 <td>${h.fmt_date(l.action_date)}</td>
35 35 <td>${l.user_ip}</td>
36 36 </tr>
37 37 %endfor
38 38 </table>
39 39
40 40 <script type="text/javascript">
41 41 YUE.onDOMReady(function(){
42 42 YUE.delegate("user_log","click",function(e, matchedEl, container){
43 43 ypjax(e.target.href,"user_log",function(){
44 44 show_more_event();
45 45 tooltip_activate();
46 46 show_changeset_tooltip();
47 47 });
48 48 YUE.preventDefault(e);
49 49 },'.pager_link');
50 50
51 51 YUE.delegate("user_log","click",function(e,matchedEl,container){
52 52 var el = e.target;
53 53 YUD.setStyle(YUD.get(el.id.substring(1)),'display','');
54 54 YUD.setStyle(el.parentNode,'display','none');
55 55 },'.show_more');
56 56 });
57 57 </script>
58 58
59 59 <div class="pagination-wh pagination-left">
60 60 ${c.users_log.pager('$link_previous ~2~ $link_next')}
61 61 </div>
62 62 %else:
63 63 ${_('No actions yet')}
64 64 %endif
@@ -1,122 +1,122 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3 <html xmlns="http://www.w3.org/1999/xhtml">
4 4 <head>
5 5 <title>${self.title()}</title>
6 6 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
7 7 <meta name="robots" content="index, nofollow"/>
8 8 <link rel="icon" href="${h.url('/images/icons/database_gear.png')}" type="image/png" />
9 9
10 10 ## CSS ###
11 11 <%def name="css()">
12 12 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css', ver=c.rhodecode_version)}" media="screen"/>
13 13 <link rel="stylesheet" type="text/css" href="${h.url('/css/pygments.css', ver=c.rhodecode_version)}"/>
14 14 ## EXTRA FOR CSS
15 15 ${self.css_extra()}
16 16 </%def>
17 17 <%def name="css_extra()">
18 18 </%def>
19 19
20 20 ${self.css()}
21 21
22 22 %if c.ga_code:
23 23 <!-- Analytics -->
24 24 <script type="text/javascript">
25 25 var _gaq = _gaq || [];
26 26 _gaq.push(['_setAccount', '${c.ga_code}']);
27 27 _gaq.push(['_trackPageview']);
28 28
29 29 (function() {
30 30 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
31 31 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
32 32 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
33 33 })();
34 34 </script>
35 35 %endif
36 36
37 37 ## JAVASCRIPT ##
38 38 <%def name="js()">
39 39 <script type="text/javascript">
40 40 //JS translations map
41 41 var TRANSLATION_MAP = {
42 42 'add another comment':'${_("add another comment")}',
43 43 'Stop following this repository':"${_('Stop following this repository')}",
44 44 'Start following this repository':"${_('Start following this repository')}",
45 45 'Group':"${_('Group')}",
46 46 'members':"${_('members')}",
47 47 'loading...':"${_('loading...')}",
48 48 'search truncated': "${_('search truncated')}",
49 49 'no matching files': "${_('no matching files')}",
50 50 'Open new pull request': "${_('Open new pull request')}",
51 51 'Open new pull request for selected changesets': "${_('Open new pull request for selected changesets')}",
52 52 'Show selected changes __S -> __E': "${_('Show selected changes __S -> __E')}",
53 53 'Selection link': "${_('Selection link')}",
54 54 };
55 55 var _TM = TRANSLATION_MAP;
56 56 var TOGGLE_FOLLOW_URL = "${h.url('toggle_following')}";
57 57 </script>
58 58 <script type="text/javascript" src="${h.url('/js/yui.2.9.js', ver=c.rhodecode_version)}"></script>
59 59 <!--[if lt IE 9]>
60 60 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
61 61 <![endif]-->
62 62 <script type="text/javascript" src="${h.url('/js/yui.flot.js', ver=c.rhodecode_version)}"></script>
63 63 <script type="text/javascript" src="${h.url('/js/native.history.js', ver=c.rhodecode_version)}"></script>
64 64 <script type="text/javascript" src="${h.url('/js/rhodecode.js', ver=c.rhodecode_version)}"></script>
65 65 ## EXTRA FOR JS
66 66 ${self.js_extra()}
67 67 <script type="text/javascript">
68 68 (function(window,undefined){
69 69 // Prepare
70 70 var History = window.History; // Note: We are using a capital H instead of a lower h
71 71 if ( !History.enabled ) {
72 72 // History.js is disabled for this browser.
73 73 // This is because we can optionally choose to support HTML4 browsers or not.
74 74 return false;
75 75 }
76 76 })(window);
77
77
78 78 YUE.onDOMReady(function(){
79 79 tooltip_activate();
80 80 show_more_event();
81 81 show_changeset_tooltip();
82 82
83 83 YUE.on('quick_login_link','click',function(e){
84 84 // make sure we don't redirect
85 85 YUE.preventDefault(e);
86
86
87 87 if(YUD.hasClass('quick_login_link','enabled')){
88 88 YUD.setStyle('quick_login','display','none');
89 89 YUD.removeClass('quick_login_link','enabled');
90 90 }
91 91 else{
92 92 YUD.setStyle('quick_login','display','');
93 93 YUD.addClass('quick_login_link','enabled');
94 94 var usr = YUD.get('username');
95 95 if(usr){
96 96 usr.focus();
97 97 }
98 98 }
99 99 });
100 100 })
101 101 </script>
102 102 </%def>
103 103 <%def name="js_extra()"></%def>
104 104 ${self.js()}
105 105 <%def name="head_extra()"></%def>
106 106 ${self.head_extra()}
107 107 </head>
108 108 <body id="body">
109 109 ## IE hacks
110 110 <!--[if IE 7]>
111 111 <script>YUD.addClass(document.body,'ie7')</script>
112 112 <![endif]-->
113 113 <!--[if IE 8]>
114 114 <script>YUD.addClass(document.body,'ie8')</script>
115 115 <![endif]-->
116 116 <!--[if IE 9]>
117 117 <script>YUD.addClass(document.body,'ie9')</script>
118 118 <![endif]-->
119 119
120 120 ${next.body()}
121 121 </body>
122 122 </html>
@@ -1,202 +1,202 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changeset') % c.repo_name} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(_(u'Home'),h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('Changeset')} - <span class='hash'>r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}</span>
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <script>
28 28 var _USERS_AC_DATA = ${c.users_array|n};
29 29 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
30 30 AJAX_COMMENT_URL = "${url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id)}";
31 31 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
32 32 </script>
33 33 <div class="table">
34 34 <div class="diffblock">
35 35 <div class="parents">
36 36 %if c.changeset.parents:
37 37 %for n, p_cs in enumerate(reversed(c.changeset.parents)):
38 38 <span class="changeset_hash">&laquo; ${h.link_to('%s:%s' % (p_cs.revision,p_cs.raw_id[:6]),h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span>
39 39 <br>
40 40 %endfor
41 41 %else:
42 42 <span>${_('No parents')}</span>
43 %endif
43 %endif
44 44 </div>
45 45 <div class="children">
46 46 %if c.changeset.children:
47 47 %for n, p_cs in enumerate(reversed(c.changeset.children)):
48 48 <span class="changeset_hash">${h.link_to('%s:%s' % (p_cs.revision,p_cs.raw_id[:6]),h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)} &raquo;</span>
49 49 <br>
50 50 %endfor
51 51 %else:
52 52 <span>${_('No children')}</span>
53 %endif
54 </div>
53 %endif
54 </div>
55 55 <div class="code-header banner">
56
56
57 57 <div class="hash">
58 58 r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
59 59 </div>
60 60 <div class="date">
61 61 ${h.fmt_date(c.changeset.date)}
62 62 </div>
63 63 <div class="changeset-status-container">
64 64 %if c.statuses:
65 65 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.statuses[0])}]</div>
66 66 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[0])}" /></div>
67 67 %endif
68 68 </div>
69 69 <div class="diff-actions">
70 70 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id)}" class="tooltip" title="${h.tooltip(_('raw diff'))}"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
71 71 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.changeset.raw_id)}" class="tooltip" title="${h.tooltip(_('patch diff'))}"><img class="icon" src="${h.url('/images/icons/page_add.png')}"/></a>
72 72 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('download diff'))}"><img class="icon" src="${h.url('/images/icons/page_save.png')}"/></a>
73 73 ${c.ignorews_url(request.GET)}
74 74 ${c.context_url(request.GET)}
75 75 </div>
76 76 <div class="comments-number" style="float:right;padding-right:5px">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
77 </div>
77 </div>
78 78 </div>
79 79 <div id="changeset_content">
80 80 <div class="container">
81 81 <div class="left">
82 82 <div class="author">
83 83 <div class="gravatar">
84 84 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.changeset.author),20)}"/>
85 85 </div>
86 86 <span>${h.person(c.changeset.author)}</span><br/>
87 87 <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
88 88 </div>
89 89 <div class="message">${h.urlify_commit(c.changeset.message, c.repo_name)}</div>
90 90 </div>
91 91 <div class="right">
92 92 <div class="changes">
93 93 % if (len(c.changeset.affected_files) <= c.affected_files_cut_off) or c.fulldiff:
94 94 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
95 95 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
96 96 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
97 97 % else:
98 98 <span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
99 99 <span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
100 100 <span class="added" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
101 101 % endif
102 102 </div>
103 103
104 104 <span class="logtags">
105 105 %if len(c.changeset.parents)>1:
106 106 <span class="merge">${_('merge')}</span>
107 107 %endif
108 108 %if c.changeset.branch:
109 109 <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
110 110 ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
111 111 </span>
112 112 %endif
113 113 %for tag in c.changeset.tags:
114 114 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
115 115 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
116 116 %endfor
117 117 </span>
118 118 </div>
119 119 </div>
120 120 <span>
121 121 % if c.limited_diff:
122 122 ${ungettext('%s file changed','%s files changed',len(c.changeset.affected_files)) % (len(c.changeset.affected_files))}:
123 123 % else:
124 124 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.changeset.affected_files)) % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}:
125 125 %endif
126 126 </span>
127 127 <div class="cs_files">
128 128 %for FID, (cs1, cs2, change, path, diff, stats) in c.changes[c.changeset.raw_id].iteritems():
129 129 <div class="cs_${change}">
130 130 <div class="node">
131 131 <a href="#${FID}">${h.safe_unicode(path)}</a>
132 132 </div>
133 133 <div class="changes">${h.fancy_file_stats(stats)}</div>
134 134 </div>
135 135 %endfor
136 136 % if c.limited_diff:
137 137 <h5>${_('Changeset was too big and was cut off...')}</h5>
138 138 % endif
139 139 </div>
140 140 </div>
141 141
142 142 </div>
143 143
144 144 ## diff block
145 145 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
146 146 ${diff_block.diff_block(c.changes[c.changeset.raw_id])}
147 147
148 148 % if c.limited_diff:
149 149 <h4>${_('Changeset was too big and was cut off...')}</h4>
150 150 % endif
151 151
152 152 ## template for inline comment form
153 153 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
154 154 ${comment.comment_inline_form()}
155 155
156 156 ## render comments and inlines
157 157 ${comment.generate_comments()}
158 158
159 159 ## main comment form and it status
160 160 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.changeset.raw_id),
161 161 h.changeset_status(c.rhodecode_db_repo, c.changeset.raw_id))}
162 162
163 163 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
164 164 <script type="text/javascript">
165 165 YUE.onDOMReady(function(){
166 166 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
167 167 var show = 'none';
168 168 var target = e.currentTarget;
169 169 if(target == null){
170 170 target = this;
171 171 }
172 172 if(target.checked){
173 173 var show = ''
174 174 }
175 175 var boxid = YUD.getAttribute(target,'id_for');
176 176 var comments = YUQ('#{0} .inline-comments'.format(boxid));
177 177 for(c in comments){
178 178 YUD.setStyle(comments[c],'display',show);
179 179 }
180 180 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
181 181 for(c in btns){
182 182 YUD.setStyle(btns[c],'display',show);
183 183 }
184 184 })
185 185
186 186 YUE.on(YUQ('.line'),'click',function(e){
187 187 var tr = e.currentTarget;
188 188 if(tr == null){
189 189 tr = this;
190 190 }
191 191 injectInlineForm(tr);
192 192 });
193 193
194 194 // inject comments into they proper positions
195 195 var file_comments = YUQ('.inline-comment-placeholder');
196 196 renderInlineComments(file_comments);
197 197 })
198 198
199 199 </script>
200 200
201 201 </div>
202 202 </%def>
@@ -1,176 +1,176 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(co)}
5 5 ##
6 6 <%def name="comment_block(co)">
7 7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 8 <div class="comment-wrapp">
9 9 <div class="meta">
10 10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
11 11 <div class="user">
12 12 ${co.author.username}
13 13 </div>
14 14 <div class="date">
15 15 ${h.age(co.modified_at)} <a class="permalink" href="#comment-${co.comment_id}">&para;</a>
16 16 </div>
17 17 %if co.status_change:
18 18 <div style="float:left" class="changeset-status-container">
19 19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
20 20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change[0].status_lbl}</div>
21 21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change[0].status))}" /></div>
22 22 </div>
23 23 %endif
24 24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
25 25 <div class="buttons">
26 26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
27 27 </div>
28 28 %endif
29 29 </div>
30 30 <div class="text">
31 31 ${h.rst_w_mentions(co.text)|n}
32 32 </div>
33 33 </div>
34 34 </div>
35 35 </%def>
36 36
37 37
38 38 <%def name="comment_inline_form()">
39 39 <div id='comment-inline-form-template' style="display:none">
40 40 <div class="comment-inline-form ac">
41 41 %if c.rhodecode_user.username != 'default':
42 42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
43 43 ${h.form('#', class_='inline-form')}
44 44 <div class="clearfix">
45 45 <div class="comment-help">${_('Commenting on line {1}.')}
46 46 ${(_('Comments parsed using %s syntax with %s support.') % (
47 47 ('<a href="%s">RST</a>' % h.url('rst_help')),
48 48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
49 49 )
50 50 )|n
51 51 }
52 52 </div>
53 53 <div class="mentions-container" id="mentions_container_{1}"></div>
54 54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
55 55 </div>
56 56 <div class="comment-button">
57 57 <input type="hidden" name="f_path" value="{0}">
58 58 <input type="hidden" name="line" value="{1}">
59 59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
60 60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
61 61 </div>
62 62 ${h.end_form()}
63 63 %else:
64 64 ${h.form('')}
65 65 <div class="clearfix">
66 66 <div class="comment-help">
67 67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button">
71 71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
72 72 </div>
73 73 ${h.end_form()}
74 74 %endif
75 75 </div>
76 76 </div>
77 77 </%def>
78 78
79 79
80 80 ## generates inlines taken from c.comments var
81 81 <%def name="inlines()">
82 82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
83 83 %for path, lines in c.inline_comments:
84 84 % for line,comments in lines.iteritems():
85 85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
86 86 %for co in comments:
87 87 ${comment_block(co)}
88 88 %endfor
89 89 </div>
90 90 %endfor
91 91 %endfor
92 92
93 93 </%def>
94 94
95 95 ## generate inline comments and the main ones
96 96 <%def name="generate_comments()">
97 97 <div class="comments">
98 98 <div id="inline-comments-container">
99 99 ## generate inlines for this changeset
100 100 ${inlines()}
101 101 </div>
102 102
103 103 %for co in c.comments:
104 104 <div id="comment-tr-${co.comment_id}">
105 105 ${comment_block(co)}
106 106 </div>
107 107 %endfor
108 108 </div>
109 109 </%def>
110 110
111 111 ## MAIN COMMENT FORM
112 112 <%def name="comments(post_url, cur_status, close_btn=False, change_status=True)">
113 113
114 114 <div class="comments">
115 115 %if c.rhodecode_user.username != 'default':
116 116 <div class="comment-form ac">
117 117 ${h.form(post_url)}
118 118 <strong>${_('Leave a comment')}</strong>
119 119 <div class="clearfix">
120 120 <div class="comment-help">
121 121 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
122 122 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
123 123 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
124 124 %if change_status:
125 125 | <label for="show_changeset_status_box" class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}</label>
126 126 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
127 127 %endif
128 128 </div>
129 129 %if change_status:
130 130 <div id="status_block_container" class="status-block" style="display:none">
131 131 %for status,lbl in c.changeset_statuses:
132 132 <div class="">
133 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
133 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
134 134 <label for="${status}">${lbl}</label>
135 135 </div>
136 136 %endfor
137 137 </div>
138 138 %endif
139 139 <div class="mentions-container" id="mentions_container"></div>
140 140 ${h.textarea('text')}
141 141 </div>
142 142 <div class="comment-button">
143 143 ${h.submit('save', _('Comment'), class_="ui-btn large")}
144 144 %if close_btn and change_status:
145 145 ${h.submit('save_close', _('Comment and close'), class_='ui-btn blue large %s' % ('hidden' if cur_status in ['not_reviewed','under_review'] else ''))}
146 146 %endif
147 147 </div>
148 148 ${h.end_form()}
149 149 </div>
150 150 %endif
151 151 </div>
152 152 <script>
153 153 YUE.onDOMReady(function () {
154 154 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
155 155
156 156 // changeset status box listener
157 157 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
158 158 if(e.currentTarget.checked){
159 159 YUD.setStyle('status_block_container','display','');
160 160 }
161 161 else{
162 162 YUD.setStyle('status_block_container','display','none');
163 163 }
164 164 })
165 165 YUE.on(YUQ('.status_change_radio'), 'change',function(e){
166 166 var val = e.currentTarget.value;
167 167 if (val == 'approved' || val == 'rejected') {
168 168 YUD.removeClass('save_close', 'hidden');
169 169 }else{
170 170 YUD.addClass('save_close', 'hidden');
171 171 }
172 172 })
173 173
174 174 });
175 175 </script>
176 176 </%def>
@@ -1,19 +1,17 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="main.html"/>
3 3
4 4 ${_('User %s opened pull request for repository %s and wants you to review changes.') % (('<b>%s</b>' % pr_user_created),pr_repo_url) |n}
5 5 <div>${_('title')}: ${pr_title}</div>
6 6 <div>${_('description')}:</div>
7 7 <div>${_('View this pull request here')}: ${pr_url}</div>
8 8 <p>
9 9 ${body}
10 10 </p>
11 11
12 12 <div>${_('revisions for reviewing')}</div>
13 13 <ul>
14 14 %for r in pr_revisions:
15 15 <li>${r}</li>
16 16 %endfor
17 17 </ul>
18
19
@@ -1,334 +1,334 b''
1 1 <%page args="parent" />
2 2 <div class="box">
3 3 <!-- box / title -->
4 4 <div class="title">
5 5 <h5>
6 6 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
7 7 </h5>
8 8 %if c.rhodecode_user.username != 'default':
9 9 %if h.HasPermissionAny('hg.admin','hg.create.repository')():
10 10 <ul class="links">
11 11 <li>
12 12 %if c.group:
13 13 <span>${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
14 14 %else:
15 15 <span>${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository'))}</span>
16 16 %endif
17 17 </li>
18 18 </ul>
19 19 %endif
20 20 %endif
21 21 </div>
22 22 <!-- end box / title -->
23 23 <div class="table">
24 24 % if c.groups:
25 25 <div id='groups_list_wrap' class="yui-skin-sam">
26 26 <table id="groups_list">
27 27 <thead>
28 28 <tr>
29 29 <th class="left"><a href="#">${_('Group name')}</a></th>
30 30 <th class="left"><a href="#">${_('Description')}</a></th>
31 31 ##<th class="left"><a href="#">${_('Number of repositories')}</a></th>
32 32 </tr>
33 33 </thead>
34 34
35 35 ## REPO GROUPS
36 36 % for gr in c.groups:
37 37 <tr>
38 38 <td>
39 39 <div style="white-space: nowrap">
40 40 <img class="icon" alt="${_('Repositories group')}" src="${h.url('/images/icons/database_link.png')}"/>
41 41 ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))}
42 42 </div>
43 43 </td>
44 44 %if c.visual.stylify_metatags:
45 45 <td>${h.urlify_text(h.desc_stylize(gr.group_description))}</td>
46 46 %else:
47 47 <td>${gr.group_description}</td>
48 48 %endif
49 49 ## this is commented out since for multi nested repos can be HEAVY!
50 50 ## in number of executed queries during traversing uncomment at will
51 51 ##<td><b>${gr.repositories_recursive_count}</b></td>
52 52 </tr>
53 53 % endfor
54 54
55 55 </table>
56 56 </div>
57 57 <div style="height: 20px"></div>
58 58 % endif
59 59 <div id="welcome" style="display:none;text-align:center">
60 60 <h1><a href="${h.url('home')}">${c.rhodecode_name} ${c.rhodecode_version}</a></h1>
61 61 </div>
62 62 <%cnt=0%>
63 63 <%namespace name="dt" file="/data_table/_dt_elements.html"/>
64 64 % if c.visual.lightweight_dashboard is False:
65 65 ## old full detailed version
66 66 <div id='repos_list_wrap' class="yui-skin-sam">
67 67 <table id="repos_list">
68 68 <thead>
69 69 <tr>
70 70 <th class="left"></th>
71 71 <th class="left">${_('Name')}</th>
72 72 <th class="left">${_('Description')}</th>
73 73 <th class="left">${_('Last change')}</th>
74 74 <th class="left">${_('Tip')}</th>
75 75 <th class="left">${_('Owner')}</th>
76 76 <th class="left">${_('RSS')}</th>
77 77 <th class="left">${_('Atom')}</th>
78 78 </tr>
79 79 </thead>
80 80 <tbody>
81 81 %for cnt,repo in enumerate(c.repos_list):
82 82 <tr class="parity${(cnt+1)%2}">
83 83 ##QUICK MENU
84 84 <td class="quick_repo_menu">
85 85 ${dt.quick_menu(repo['name'])}
86 86 </td>
87 87 ##REPO NAME AND ICONS
88 88 <td class="reponame">
89 89 ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']),pageargs.get('short_repo_names'))}
90 90 </td>
91 91 ##DESCRIPTION
92 92 <td><span class="tooltip" title="${h.tooltip(repo['description'])}">
93 93 %if c.visual.stylify_metatags:
94 94 ${h.urlify_text(h.desc_stylize(h.truncate(repo['description'],60)))}</span>
95 95 %else:
96 96 ${h.truncate(repo['description'],60)}</span>
97 97 %endif
98 98 </td>
99 99 ##LAST CHANGE DATE
100 100 <td>
101 101 ${dt.last_change(repo['last_change'])}
102 102 </td>
103 103 ##LAST REVISION
104 104 <td>
105 105 ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])}
106 106 </td>
107 107 ##
108 108 <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
109 109 <td>
110 110 ${dt.rss(repo['name'])}
111 111 </td>
112 112 <td>
113 113 ${dt.atom(repo['name'])}
114 114 </td>
115 115 </tr>
116 116 %endfor
117 117 </tbody>
118 118 </table>
119 119 </div>
120 120 % else:
121 121 ## lightweight version
122 122 <div class="yui-skin-sam" id="repos_list_wrap"></div>
123 123 <div id="user-paginator" style="padding: 0px 0px 0px 0px"></div>
124 124 % endif
125 125 </div>
126 126 </div>
127 127 % if c.visual.lightweight_dashboard is False:
128 128 <script>
129 129 YUD.get('repo_count').innerHTML = ${cnt+1 if cnt else 0};
130 130 var func = function(node){
131 131 return node.parentNode.parentNode.parentNode.parentNode;
132 132 }
133 133
134 134 // groups table sorting
135 135 var myColumnDefs = [
136 136 {key:"name",label:"${_('Group name')}",sortable:true,
137 137 sortOptions: { sortFunction: groupNameSort }},
138 138 {key:"desc",label:"${_('Description')}",sortable:true},
139 139 ];
140 140
141 141 var myDataSource = new YAHOO.util.DataSource(YUD.get("groups_list"));
142 142
143 143 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
144 144 myDataSource.responseSchema = {
145 145 fields: [
146 146 {key:"name"},
147 147 {key:"desc"},
148 148 ]
149 149 };
150 150
151 151 var myDataTable = new YAHOO.widget.DataTable("groups_list_wrap", myColumnDefs, myDataSource,{
152 152 sortedBy:{key:"name",dir:"asc"},
153 153 paginator: new YAHOO.widget.Paginator({
154 154 rowsPerPage: 5,
155 155 alwaysVisible: false,
156 156 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
157 157 pageLinks: 5,
158 158 containerClass: 'pagination-wh',
159 159 currentPageClass: 'pager_curpage',
160 160 pageLinkClass: 'pager_link',
161 161 nextPageLinkLabel: '&gt;',
162 162 previousPageLinkLabel: '&lt;',
163 163 firstPageLinkLabel: '&lt;&lt;',
164 164 lastPageLinkLabel: '&gt;&gt;',
165 165 containers:['user-paginator']
166 166 }),
167 167 MSG_SORTASC:"${_('Click to sort ascending')}",
168 168 MSG_SORTDESC:"${_('Click to sort descending')}"
169 169 });
170 170
171 171 // main table sorting
172 172 var myColumnDefs = [
173 173 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
174 174 {key:"name",label:"${_('Name')}",sortable:true,
175 175 sortOptions: { sortFunction: nameSort }},
176 176 {key:"desc",label:"${_('Description')}",sortable:true},
177 177 {key:"last_change",label:"${_('Last Change')}",sortable:true,
178 178 sortOptions: { sortFunction: ageSort }},
179 179 {key:"tip",label:"${_('Tip')}",sortable:true,
180 180 sortOptions: { sortFunction: revisionSort }},
181 181 {key:"owner",label:"${_('Owner')}",sortable:true},
182 182 {key:"rss",label:"",sortable:false},
183 183 {key:"atom",label:"",sortable:false},
184 184 ];
185 185
186 186 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
187 187
188 188 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
189 189
190 190 myDataSource.responseSchema = {
191 191 fields: [
192 192 {key:"menu"},
193 193 //{key:"raw_name"},
194 194 {key:"name"},
195 195 {key:"desc"},
196 196 {key:"last_change"},
197 197 {key:"tip"},
198 198 {key:"owner"},
199 199 {key:"rss"},
200 200 {key:"atom"},
201 201 ]
202 202 };
203 203
204 204 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
205 205 {
206 206 sortedBy:{key:"name",dir:"asc"},
207 207 MSG_SORTASC:"${_('Click to sort ascending')}",
208 208 MSG_SORTDESC:"${_('Click to sort descending')}",
209 209 MSG_EMPTY:"${_('No records found.')}",
210 210 MSG_ERROR:"${_('Data error.')}",
211 211 MSG_LOADING:"${_('Loading...')}",
212 212 }
213 213 );
214 214 myDataTable.subscribe('postRenderEvent',function(oArgs) {
215 215 tooltip_activate();
216 216 quick_repo_menu();
217 217 q_filter('q_filter',YUQ('div.table tr td a.repo_name'),func);
218 218 });
219 219
220 220 </script>
221 221 % else:
222 222 <script>
223 223 //var url = "${h.url('formatted_users', format='json')}";
224 224 var data = ${c.data|n};
225 225 var myDataSource = new YAHOO.util.DataSource(data);
226 226 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
227 227
228 228 myDataSource.responseSchema = {
229 229 resultsList: "records",
230 230 fields: [
231 231 {key:"menu"},
232 232 {key:"raw_name"},
233 233 {key:"name"},
234 234 {key:"desc"},
235 235 {key:"last_change"},
236 236 {key: "tip"},
237 237 {key:"owner"},
238 238 {key:"rss"},
239 239 {key:"atom"},
240 240 ]
241 241 };
242 242 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
243 243 // This is the filter function
244 244 var data = res.results || [],
245 245 filtered = [],
246 246 i,l;
247 247
248 248 if (req) {
249 249 req = req.toLowerCase();
250 250 for (i = 0; i<data.length; i++) {
251 251 var pos = data[i].raw_name.toLowerCase().indexOf(req)
252 252 if (pos != -1) {
253 253 filtered.push(data[i]);
254 254 }
255 255 }
256 256 res.results = filtered;
257 257 }
258 258 YUD.get('repo_count').innerHTML = res.results.length;
259 259 return res;
260 260 }
261 261
262 262 // main table sorting
263 263 var myColumnDefs = [
264 264 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
265 265 {key:"name",label:"${_('Name')}",sortable:true,
266 266 sortOptions: { sortFunction: nameSort }},
267 267 {key:"desc",label:"${_('Description')}",sortable:true},
268 268 {key:"last_change",label:"${_('Last Change')}",sortable:true,
269 269 sortOptions: { sortFunction: ageSort }},
270 270 {key:"tip",label:"${_('Tip')}",sortable:true,
271 sortOptions: { sortFunction: revisionSort }},
271 sortOptions: { sortFunction: revisionSort }},
272 272 {key:"owner",label:"${_('Owner')}",sortable:true},
273 273 {key:"rss",label:"",sortable:false},
274 274 {key:"atom",label:"",sortable:false},
275 275 ];
276 276
277 277 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
278 278 sortedBy:{key:"name",dir:"asc"},
279 279 paginator: new YAHOO.widget.Paginator({
280 280 rowsPerPage: ${c.visual.lightweight_dashboard_items},
281 281 alwaysVisible: false,
282 282 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
283 283 pageLinks: 5,
284 284 containerClass: 'pagination-wh',
285 285 currentPageClass: 'pager_curpage',
286 286 pageLinkClass: 'pager_link',
287 287 nextPageLinkLabel: '&gt;',
288 288 previousPageLinkLabel: '&lt;',
289 289 firstPageLinkLabel: '&lt;&lt;',
290 290 lastPageLinkLabel: '&gt;&gt;',
291 291 containers:['user-paginator']
292 292 }),
293 293
294 294 MSG_SORTASC:"${_('Click to sort ascending')}",
295 295 MSG_SORTDESC:"${_('Click to sort descending')}",
296 296 MSG_EMPTY:"${_('No records found.')}",
297 297 MSG_ERROR:"${_('Data error.')}",
298 298 MSG_LOADING:"${_('Loading...')}",
299 299 }
300 300 );
301 301 myDataTable.subscribe('postRenderEvent',function(oArgs) {
302 302 tooltip_activate();
303 303 quick_repo_menu();
304 304 });
305 305
306 306 var filterTimeout = null;
307 307
308 308 updateFilter = function () {
309 309 // Reset timeout
310 310 filterTimeout = null;
311 311
312 312 // Reset sort
313 313 var state = myDataTable.getState();
314 314 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
315 315
316 316 // Get filtered data
317 317 myDataSource.sendRequest(YUD.get('q_filter').value,{
318 318 success : myDataTable.onDataReturnInitializeTable,
319 319 failure : myDataTable.onDataReturnInitializeTable,
320 320 scope : myDataTable,
321 321 argument: state
322 322 });
323 323
324 324 };
325 325 YUE.on('q_filter','click',function(){
326 326 YUD.get('q_filter').value = '';
327 327 });
328 328
329 329 YUE.on('q_filter','keyup',function (e) {
330 330 clearTimeout(filterTimeout);
331 331 filterTimeout = setTimeout(updateFilter,600);
332 332 });
333 333 </script>
334 334 % endif
@@ -1,241 +1,241 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3 <%def name="title()">
4 4 ${_('Journal')} - ${c.rhodecode_name}
5 5 </%def>
6 6 <%def name="breadcrumbs()">
7 7 <h5>
8 8 <form id="filter_form">
9 9 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('quick filter...')}"/>
10 10 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
11 11 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
12 12 ${_('journal')} - ${ungettext('%s entry', '%s entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
13 13 </form>
14 14 ${h.end_form()}
15 15 </h5>
16 16 </%def>
17 17 <%def name="page_nav()">
18 18 ${self.menu('home')}
19 19 </%def>
20 20 <%def name="head_extra()">
21 21 <link href="${h.url('journal_atom', api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('ATOM journal feed')}" type="application/atom+xml" />
22 22 <link href="${h.url('journal_rss', api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('RSS journal feed')}" type="application/rss+xml" />
23 23 </%def>
24 24 <%def name="main()">
25 25
26 26 <div class="box box-left">
27 27 <!-- box / title -->
28 28 <div class="title">
29 29 ${self.breadcrumbs()}
30 30 <ul class="links">
31 31 <li>
32 32 <span><a id="refresh" href="${h.url('journal')}"><img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/></a></span>
33 33 </li>
34 34 <li>
35 35 <span><a href="${h.url('journal_rss', api_key=c.rhodecode_user.api_key)}"><img class="icon" title="${_('RSS feed')}" alt="${_('RSS feed')}" src="${h.url('/images/icons/rss_16.png')}"/></a></span>
36 36 </li>
37 37 <li>
38 38 <span><a href="${h.url('journal_atom', api_key=c.rhodecode_user.api_key)}"><img class="icon" title="${_('ATOM feed')}" alt="${_('ATOM feed')}" src="${h.url('/images/icons/atom.png')}"/></a></span>
39 39 </li>
40 40 </ul>
41 41 </div>
42 42 <div id="journal">${c.journal_data}</div>
43 43 </div>
44 44 <div class="box box-right">
45 45 <!-- box / title -->
46 46 <div class="title">
47 47 <h5>
48 48 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
49 49 <a id="show_watched" class="link-white" href="#watched">${_('Watched')}</a> / <a id="show_my" class="link-white" href="#my">${_('My repos')}</a>
50 50 </h5>
51 51 %if h.HasPermissionAny('hg.admin','hg.create.repository')():
52 52 <ul class="links">
53 53 <li>
54 54 <span>${h.link_to(_('ADD'),h.url('admin_settings_create_repository'))}</span>
55 55 </li>
56 56 </ul>
57 57 %endif
58 58 </div>
59 59 <!-- end box / title -->
60 60 <div id="my" class="table" style="display:none">
61 61 ## loaded via AJAX
62 62 ${_('Loading...')}
63 63 </div>
64 64
65 65 <div id="watched" class="table">
66 66 %if c.following:
67 67 <table>
68 68 <thead>
69 69 <tr>
70 70 <th class="left">${_('Name')}</th>
71 71 </thead>
72 72 <tbody>
73 73 %for entry in c.following:
74 74 <tr>
75 75 <td>
76 76 %if entry.follows_user_id:
77 77 <img title="${_('following user')}" alt="${_('user')}" src="${h.url('/images/icons/user.png')}"/>
78 78 ${entry.follows_user.full_contact}
79 79 %endif
80 80
81 81 %if entry.follows_repo_id:
82 82 <div style="float:right;padding-right:5px">
83 83 <span id="follow_toggle_${entry.follows_repository.repo_id}" class="following" title="${_('Stop following this repository')}"
84 84 onclick="javascript:toggleFollowingRepo(this,${entry.follows_repository.repo_id},'${str(h.get_token())}')">
85 85 </span>
86 86 </div>
87 87
88 88 %if h.is_hg(entry.follows_repository):
89 89 <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
90 90 %elif h.is_git(entry.follows_repository):
91 91 <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
92 92 %endif
93 93
94 94 %if entry.follows_repository.private and c.visual.show_private_icon:
95 95 <img class="icon" title="${_('private repository')}" alt="${_('private repository')}" src="${h.url('/images/icons/lock.png')}"/>
96 96 %elif not entry.follows_repository.private and c.visual.show_public_icon:
97 97 <img class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="${h.url('/images/icons/lock_open.png')}"/>
98 98 %endif
99 99 <span class="watched_repo">
100 100 ${h.link_to(entry.follows_repository.repo_name,h.url('summary_home',repo_name=entry.follows_repository.repo_name))}
101 101 </span>
102 102 %endif
103 103 </td>
104 104 </tr>
105 105 %endfor
106 106 </tbody>
107 107 </table>
108 108 %else:
109 109 <div style="padding:5px 0px 10px 0px;">
110 110 ${_('You are not following any users or repositories')}
111 111 </div>
112 112 %endif
113 113 </div>
114 114 </div>
115 115
116 116 <script type="text/javascript">
117
117
118 118 YUE.on('j_filter','click',function(){
119 119 var jfilter = YUD.get('j_filter');
120 120 if(YUD.hasClass(jfilter, 'initial')){
121 121 jfilter.value = '';
122 122 }
123 123 });
124 124 var fix_j_filter_width = function(len){
125 125 YUD.setStyle(YUD.get('j_filter'),'width',Math.max(80, len*6.50)+'px');
126 126 }
127 127 YUE.on('j_filter','keyup',function(){
128 128 fix_j_filter_width(YUD.get('j_filter').value.length);
129 129 });
130 130 YUE.on('filter_form','submit',function(e){
131 131 YUE.preventDefault(e)
132 132 var val = YUD.get('j_filter').value;
133 133 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
134 134 });
135 fix_j_filter_width(YUD.get('j_filter').value.length);
136
135 fix_j_filter_width(YUD.get('j_filter').value.length);
136
137 137 var show_my = function(e){
138 138 YUD.setStyle('watched','display','none');
139 139 YUD.setStyle('my','display','');
140 140
141 141 var url = "${h.url('admin_settings_my_repos')}";
142 142 ypjax(url, 'my', function(){
143 143 tooltip_activate();
144 144 quick_repo_menu();
145 145 var nodes = YUQ('#my tr td a.repo_name');
146 146 var func = function(node){
147 147 return node.parentNode.parentNode.parentNode;
148 148 }
149 149 q_filter('q_filter',nodes,func);
150 150 });
151 151
152 152 }
153 153 YUE.on('show_my','click',function(e){
154 154 show_my(e);
155 155 })
156 156 var show_watched = function(e){
157 157 YUD.setStyle('my','display','none');
158 158 YUD.setStyle('watched','display','');
159 159 var nodes = YUQ('#watched .watched_repo a');
160 160 var target = 'q_filter';
161 161 var func = function(node){
162 162 return node.parentNode.parentNode;
163 163 }
164 164 q_filter(target,nodes,func);
165 165 }
166 166 YUE.on('show_watched','click',function(e){
167 167 show_watched(e);
168 168 })
169 169 //init watched
170 170 show_watched();
171 171
172 172 var tabs = {
173 173 'watched': show_watched,
174 174 'my': show_my,
175 175 }
176 176 var url = location.href.split('#');
177 177 if (url[1]) {
178 178 //We have a hash
179 179 var tabHash = url[1];
180 180 var func = tabs[tabHash]
181 181 if (func){
182 182 func();
183 183 }
184 184 }
185 185
186 186 YUE.on('refresh','click',function(e){
187 187 ypjax("${h.url.current(filter=c.search_term)}","journal",function(){
188 188 show_more_event();
189 189 tooltip_activate();
190 190 show_changeset_tooltip();
191 191 });
192 192 YUE.preventDefault(e);
193 193 });
194 194
195 195
196 196 // main table sorting
197 197 var myColumnDefs = [
198 198 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
199 199 {key:"name",label:"${_('Name')}",sortable:true,
200 200 sortOptions: { sortFunction: nameSort }},
201 201 {key:"tip",label:"${_('Tip')}",sortable:true,
202 202 sortOptions: { sortFunction: revisionSort }},
203 203 {key:"action1",label:"",sortable:false},
204 204 {key:"action2",label:"",sortable:false},
205 205 ];
206 206
207 207 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
208 208
209 209 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
210 210
211 211 myDataSource.responseSchema = {
212 212 fields: [
213 213 {key:"menu"},
214 214 {key:"name"},
215 215 {key:"tip"},
216 216 {key:"action1"},
217 217 {key:"action2"}
218 218 ]
219 219 };
220 220
221 221 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
222 222 {
223 223 sortedBy:{key:"name",dir:"asc"},
224 224 MSG_SORTASC:"${_('Click to sort ascending')}",
225 225 MSG_SORTDESC:"${_('Click to sort descending')}",
226 226 MSG_EMPTY:"${_('No records found.')}",
227 227 MSG_ERROR:"${_('Data error.')}",
228 228 MSG_LOADING:"${_('Loading...')}",
229 229 }
230 230 );
231 231 myDataTable.subscribe('postRenderEvent',function(oArgs) {
232 232 tooltip_activate();
233 233 quick_repo_menu();
234 234 var func = function(node){
235 235 return node.parentNode.parentNode.parentNode.parentNode;
236 236 }
237 237 q_filter('q_filter',YUQ('#my tr td a.repo_name'),func);
238 238 });
239 239
240 240 </script>
241 241 </%def>
@@ -1,210 +1,210 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(_(u'Home'),h.url('/'))}
9 9 &raquo;
10 10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 %if c.pull_request.is_closed():
23 23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
24 24 %endif
25 25 <h3>${_('Title')}: ${c.pull_request.title}</h3>
26 26
27 27 <div class="form">
28 28 <div id="summary" class="fields">
29 29 <div class="field">
30 30 <div class="label-summary">
31 31 <label>${_('Status')}:</label>
32 32 </div>
33 33 <div class="input">
34 34 <div class="changeset-status-container" style="float:none;clear:both">
35 35 %if c.current_changeset_status:
36 36 <div title="${_('Pull request status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
37 37 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
38 38 %endif
39 39 </div>
40 40 </div>
41 41 </div>
42 42 <div class="field">
43 43 <div class="label-summary">
44 44 <label>${_('Still not reviewed by')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 % if len(c.pull_request_pending_reviewers) > 0:
48 48 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
49 49 %else:
50 50 <div>${_('pull request was reviewed by all reviewers')}</div>
51 51 %endif
52 52 </div>
53 53 </div>
54 54 </div>
55 55 </div>
56 56 <div style="white-space:pre-wrap;padding:3px 3px 5px 20px">${h.literal(c.pull_request.description)}</div>
57 57 <div style="padding:4px 4px 10px 20px">
58 58 <div>${_('Created on')}: ${h.fmt_date(c.pull_request.created_on)}</div>
59 59 </div>
60 60
61 61 <div style="overflow: auto;">
62 62 ##DIFF
63 63 <div class="table" style="float:left;clear:none">
64 64 <div id="body" class="diffblock">
65 65 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
66 66 </div>
67 67 <div id="changeset_compare_view_content">
68 68 ##CS
69 69 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
70 70 <%include file="/compare/compare_cs.html" />
71 71
72 72 ## FILES
73 73 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
74 74
75 75 % if c.limited_diff:
76 76 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
77 77 % else:
78 78 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
79 79 %endif
80 80
81 81 </div>
82 82 <div class="cs_files">
83 83 %if not c.files:
84 84 <span class="empty_data">${_('No files')}</span>
85 85 %endif
86 86 %for fid, change, f, stat in c.files:
87 87 <div class="cs_${change}">
88 88 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
89 89 <div class="changes">${h.fancy_file_stats(stat)}</div>
90 90 </div>
91 91 %endfor
92 92 </div>
93 93 % if c.limited_diff:
94 94 <h5>${_('Changeset was too big and was cut off...')}</h5>
95 95 % endif
96 96 </div>
97 97 </div>
98 98 ## REVIEWERS
99 99 <div style="float:left; border-left:1px dashed #eee">
100 100 <h4>${_('Pull request reviewers')}</h4>
101 101 <div id="reviewers" style="padding:0px 0px 5px 10px">
102 102 ## members goes here !
103 103 <div class="group_members_wrap" style="min-height:45px">
104 104 <ul id="review_members" class="group_members">
105 105 %for member,status in c.pull_request_reviewers:
106 106 <li id="reviewer_${member.user_id}">
107 107 <div class="reviewers_member">
108 108 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
109 109 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
110 110 </div>
111 111 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
112 112 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
113 113 <input type="hidden" value="${member.user_id}" name="review_members" />
114 114 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
115 115 <span class="delete_icon action_button" onclick="removeReviewer(${member.user_id})"></span>
116 116 %endif
117 117 </div>
118 118 </li>
119 119 %endfor
120 120 </ul>
121 121 </div>
122 122 %if not c.pull_request.is_closed():
123 123 <div class='ac'>
124 124 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
125 125 <div class="reviewer_ac">
126 126 ${h.text('user', class_='yui-ac-input')}
127 127 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
128 128 <div id="reviewers_container"></div>
129 129 </div>
130 130 <div style="padding:0px 10px">
131 131 <span id="update_pull_request" class="ui-btn xsmall">${_('save')}</span>
132 132 </div>
133 133 %endif
134 134 </div>
135 135 %endif
136 136 </div>
137 137 </div>
138 138 </div>
139 139 <script>
140 140 var _USERS_AC_DATA = ${c.users_array|n};
141 141 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
142 142 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
143 143 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
144 144 AJAX_UPDATE_PULLREQUEST = "${url('pullrequest_update',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}"
145 145 </script>
146 146
147 147 ## diff block
148 148 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
149 149 %for fid, change, f, stat in c.files:
150 150 ${diff_block.diff_block_simple([c.changes[fid]])}
151 151 %endfor
152 152 % if c.limited_diff:
153 153 <h4>${_('Changeset was too big and was cut off...')}</h4>
154 154 % endif
155 155
156 156
157 157 ## template for inline comment form
158 158 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
159 159 ${comment.comment_inline_form()}
160 160
161 161 ## render comments and inlines
162 162 ${comment.generate_comments()}
163 163
164 164 % if not c.pull_request.is_closed():
165 165 ## main comment form and it status
166 166 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
167 167 pull_request_id=c.pull_request.pull_request_id),
168 168 c.current_changeset_status,
169 169 close_btn=True, change_status=c.allowed_to_change_status)}
170 170 %endif
171 171
172 172 <script type="text/javascript">
173 173 YUE.onDOMReady(function(){
174 174 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
175 175
176 176 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
177 177 var show = 'none';
178 178 var target = e.currentTarget;
179 179 if(target.checked){
180 180 var show = ''
181 181 }
182 182 var boxid = YUD.getAttribute(target,'id_for');
183 183 var comments = YUQ('#{0} .inline-comments'.format(boxid));
184 184 for(c in comments){
185 185 YUD.setStyle(comments[c],'display',show);
186 186 }
187 187 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
188 188 for(c in btns){
189 189 YUD.setStyle(btns[c],'display',show);
190 190 }
191 191 })
192 192
193 193 YUE.on(YUQ('.line'),'click',function(e){
194 194 var tr = e.currentTarget;
195 195 injectInlineForm(tr);
196 196 });
197 197
198 198 // inject comments into they proper positions
199 199 var file_comments = YUQ('.inline-comment-placeholder');
200 200 renderInlineComments(file_comments);
201
201
202 202 YUE.on(YUD.get('update_pull_request'),'click',function(e){
203 203 updateReviewers();
204 204 })
205 205 })
206 206 </script>
207 207
208 208 </div>
209 209
210 210 </%def>
@@ -1,130 +1,130 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Settings') % c.repo_name} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_(u'Home'),h.url('/'))}
10 10 &raquo;
11 11 ${h.link_to(c.repo_info.repo_name,h.url('summary_home',repo_name=c.repo_info.repo_name))}
12 12 &raquo;
13 13 ${_('Settings')}
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('settings')}
18 18 </%def>
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <!-- box / title -->
22 22 <div class="title">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25 ${h.form(url('repo_settings_update', repo_name=c.repo_info.repo_name),method='put')}
26 26 <div class="form">
27 27 <!-- fields -->
28 28 <div class="fields">
29 29 <div class="field">
30 30 <div class="label">
31 31 <label for="repo_name">${_('Name')}:</label>
32 32 </div>
33 33 <div class="input input-medium">
34 34 ${h.text('repo_name',class_="small")}
35 35 </div>
36 36 </div>
37 37 <div class="field">
38 38 <div class="label">
39 39 <label for="clone_uri">${_('Clone uri')}:</label>
40 40 </div>
41 41 <div class="input">
42 42 ${h.text('clone_uri',class_="medium")}
43 43 <span class="help-block">${_('Optional http[s] url from which repository should be cloned.')}</span>
44 44 </div>
45 45 </div>
46 46 <div class="field">
47 47 <div class="label">
48 48 <label for="repo_group">${_('Repository group')}:</label>
49 49 </div>
50 50 <div class="input">
51 51 ${h.select('repo_group','',c.repo_groups,class_="medium")}
52 52 <span class="help-block">${_('Optional select a group to put this repository into.')}</span>
53 53 </div>
54 54 </div>
55 55 <div class="field">
56 56 <div class="label">
57 57 <label for="landing_rev">${_('Landing revision')}:</label>
58 58 </div>
59 59 <div class="input">
60 60 ${h.select('repo_landing_rev','',c.landing_revs,class_="medium")}
61 61 <span class="help-block">${_('Default revision for files page, downloads, whoosh and readme')}</span>
62 62 </div>
63 63 </div>
64 64 <div class="field">
65 65 <div class="label label-textarea">
66 66 <label for="repo_description">${_('Description')}:</label>
67 67 </div>
68 68 <div class="textarea text-area editor">
69 69 ${h.textarea('repo_description')}
70 70 <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span>
71 71 </div>
72 72 </div>
73 73
74 74 <div class="field">
75 75 <div class="label label-checkbox">
76 76 <label for="repo_private">${_('Private repository')}:</label>
77 77 </div>
78 78 <div class="checkboxes">
79 79 ${h.checkbox('repo_private',value="True")}
80 80 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
81 81 </div>
82 82 </div>
83 83
84 84 <div class="field">
85 85 <div class="label">
86 86 <label for="">${_('Permissions')}:</label>
87 87 </div>
88 88 <div class="input">
89 89 <%include file="../admin/repos/repo_edit_perms.html"/>
90 90 </div>
91 91 </div>
92 92
93 93 <div class="buttons">
94 94 ${h.submit('save',_('Save'),class_="ui-btn large")}
95 95 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
96 96 </div>
97
97
98 98 </div>
99 99 ${h.end_form()}
100 100 </div>
101 101
102 102 <h3>${_('Delete repository')}</h3>
103 103 <div class="form">
104 104 <!-- fields -->
105 105 <div class="fields">
106 106
107 107 <div class="field">
108 108 <div class="label">
109 109 <label for="">${_('Remove repo')}:</label>
110 110 </div>
111 111 <div class="checkboxes">
112 112 ${h.form(url('repo_settings_delete', repo_name=c.repo_info.repo_name),method='delete')}
113 113 <div class="">
114 114 <div class="fields">
115 115 ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")}
116 116 </div>
117 117 <div class="field" style="border:none;color:#888">
118 118 <ul>
119 119 <li>${_('This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. If you need fully delete it from file system please do it manually')}</li>
120 120 </ul>
121 121 </div>
122 122 </div>
123 123 ${h.end_form()}
124 124 </div>
125 125 </div>
126 126 </div>
127 127 </div>
128 128
129 129 </div>
130 130 </%def>
@@ -1,1 +1,1 b''
1 #TODO; write tests when we activate algo for permissions. No newline at end of file
1 #TODO; write tests when we activate algo for permissions.
General Comments 0
You need to be logged in to leave comments. Login now