##// END OF EJS Templates
files: fixed rendering of readme files under non-ascii paths.
marcink -
r3747:b066f13d new-ui
parent child Browse files
Show More
@@ -1,394 +1,396 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import string
23 23 import rhodecode
24 24
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.lib.view_utils import get_format_ref_id
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 30 from rhodecode.lib import helpers as h, rc_cache
31 31 from rhodecode.lib.utils2 import safe_str, safe_int
32 32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 33 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 36 from rhodecode.lib.vcs.exceptions import (
37 37 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 38 from rhodecode.model.db import Statistics, CacheKey, User
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.repo import ReadmeFinder
41 41 from rhodecode.model.scm import ScmModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class RepoSummaryView(RepoAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 50 c.rhodecode_repo = None
51 51 if not c.repository_requirements_missing:
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53 return c
54 54
55 55 def _get_readme_data(self, db_repo, renderer_type):
56 56
57 57 log.debug('Looking for README file')
58 58
59 59 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
60 60 db_repo.repo_id, CacheKey.CACHE_TYPE_README)
61 61 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
62 62 repo_id=self.db_repo.repo_id)
63 63 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
64 64
65 65 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
66 66 def generate_repo_readme(repo_id, _repo_name, _renderer_type):
67 67 readme_data = None
68 68 readme_node = None
69 69 readme_filename = None
70 70 commit = self._get_landing_commit_or_none(db_repo)
71 71 if commit:
72 72 log.debug("Searching for a README file.")
73 73 readme_node = ReadmeFinder(_renderer_type).search(commit)
74 74 if readme_node:
75 log.debug('Found README node: %s', readme_node)
75 76 relative_urls = {
76 77 'raw': h.route_path(
77 78 'repo_file_raw', repo_name=_repo_name,
78 79 commit_id=commit.raw_id, f_path=readme_node.path),
79 80 'standard': h.route_path(
80 81 'repo_files', repo_name=_repo_name,
81 82 commit_id=commit.raw_id, f_path=readme_node.path),
82 83 }
83 84 readme_data = self._render_readme_or_none(
84 85 commit, readme_node, relative_urls)
85 readme_filename = readme_node.path
86 readme_filename = readme_node.unicode_path
87
86 88 return readme_data, readme_filename
87 89
88 90 inv_context_manager = rc_cache.InvalidationContext(
89 91 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
90 92 with inv_context_manager as invalidation_context:
91 93 args = (db_repo.repo_id, db_repo.repo_name, renderer_type,)
92 94 # re-compute and store cache if we get invalidate signal
93 95 if invalidation_context.should_invalidate():
94 96 instance = generate_repo_readme.refresh(*args)
95 97 else:
96 98 instance = generate_repo_readme(*args)
97 99
98 100 log.debug(
99 101 'Repo readme generated and computed in %.3fs',
100 102 inv_context_manager.compute_time)
101 103 return instance
102 104
103 105 def _get_landing_commit_or_none(self, db_repo):
104 106 log.debug("Getting the landing commit.")
105 107 try:
106 108 commit = db_repo.get_landing_commit()
107 109 if not isinstance(commit, EmptyCommit):
108 110 return commit
109 111 else:
110 112 log.debug("Repository is empty, no README to render.")
111 113 except CommitError:
112 114 log.exception(
113 115 "Problem getting commit when trying to render the README.")
114 116
115 117 def _render_readme_or_none(self, commit, readme_node, relative_urls):
116 118 log.debug(
117 119 'Found README file `%s` rendering...', readme_node.path)
118 120 renderer = MarkupRenderer()
119 121 try:
120 122 html_source = renderer.render(
121 123 readme_node.content, filename=readme_node.path)
122 124 if relative_urls:
123 125 return relative_links(html_source, relative_urls)
124 126 return html_source
125 127 except Exception:
126 128 log.exception(
127 129 "Exception while trying to render the README")
128 130
129 131 def _load_commits_context(self, c):
130 132 p = safe_int(self.request.GET.get('page'), 1)
131 133 size = safe_int(self.request.GET.get('size'), 10)
132 134
133 135 def url_generator(**kw):
134 136 query_params = {
135 137 'size': size
136 138 }
137 139 query_params.update(kw)
138 140 return h.route_path(
139 141 'repo_summary_commits',
140 142 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
141 143
142 144 pre_load = ['author', 'branch', 'date', 'message']
143 145 try:
144 146 collection = self.rhodecode_vcs_repo.get_commits(
145 147 pre_load=pre_load, translate_tags=False)
146 148 except EmptyRepositoryError:
147 149 collection = self.rhodecode_vcs_repo
148 150
149 151 c.repo_commits = h.RepoPage(
150 152 collection, page=p, items_per_page=size, url=url_generator)
151 153 page_ids = [x.raw_id for x in c.repo_commits]
152 154 c.comments = self.db_repo.get_comments(page_ids)
153 155 c.statuses = self.db_repo.statuses(page_ids)
154 156
155 157 def _prepare_and_set_clone_url(self, c):
156 158 username = ''
157 159 if self._rhodecode_user.username != User.DEFAULT_USER:
158 160 username = safe_str(self._rhodecode_user.username)
159 161
160 162 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
161 163 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
162 164
163 165 if '{repo}' in _def_clone_uri:
164 166 _def_clone_uri_id = _def_clone_uri.replace('{repo}', '_{repoid}')
165 167 elif '{repoid}' in _def_clone_uri:
166 168 _def_clone_uri_id = _def_clone_uri.replace('_{repoid}', '{repo}')
167 169
168 170 c.clone_repo_url = self.db_repo.clone_url(
169 171 user=username, uri_tmpl=_def_clone_uri)
170 172 c.clone_repo_url_id = self.db_repo.clone_url(
171 173 user=username, uri_tmpl=_def_clone_uri_id)
172 174 c.clone_repo_url_ssh = self.db_repo.clone_url(
173 175 uri_tmpl=_def_clone_uri_ssh, ssh=True)
174 176
175 177 @LoginRequired()
176 178 @HasRepoPermissionAnyDecorator(
177 179 'repository.read', 'repository.write', 'repository.admin')
178 180 @view_config(
179 181 route_name='repo_summary_commits', request_method='GET',
180 182 renderer='rhodecode:templates/summary/summary_commits.mako')
181 183 def summary_commits(self):
182 184 c = self.load_default_context()
183 185 self._prepare_and_set_clone_url(c)
184 186 self._load_commits_context(c)
185 187 return self._get_template_context(c)
186 188
187 189 @LoginRequired()
188 190 @HasRepoPermissionAnyDecorator(
189 191 'repository.read', 'repository.write', 'repository.admin')
190 192 @view_config(
191 193 route_name='repo_summary', request_method='GET',
192 194 renderer='rhodecode:templates/summary/summary.mako')
193 195 @view_config(
194 196 route_name='repo_summary_slash', request_method='GET',
195 197 renderer='rhodecode:templates/summary/summary.mako')
196 198 @view_config(
197 199 route_name='repo_summary_explicit', request_method='GET',
198 200 renderer='rhodecode:templates/summary/summary.mako')
199 201 def summary(self):
200 202 c = self.load_default_context()
201 203
202 204 # Prepare the clone URL
203 205 self._prepare_and_set_clone_url(c)
204 206
205 207 # update every 5 min
206 208 if self.db_repo.last_commit_cache_update_diff > 60 * 5:
207 209 self.db_repo.update_commit_cache()
208 210
209 211 # If enabled, get statistics data
210 212
211 213 c.show_stats = bool(self.db_repo.enable_statistics)
212 214
213 215 stats = Session().query(Statistics) \
214 216 .filter(Statistics.repository == self.db_repo) \
215 217 .scalar()
216 218
217 219 c.stats_percentage = 0
218 220
219 221 if stats and stats.languages:
220 222 c.no_data = False is self.db_repo.enable_statistics
221 223 lang_stats_d = json.loads(stats.languages)
222 224
223 225 # Sort first by decreasing count and second by the file extension,
224 226 # so we have a consistent output.
225 227 lang_stats_items = sorted(lang_stats_d.iteritems(),
226 228 key=lambda k: (-k[1], k[0]))[:10]
227 229 lang_stats = [(x, {"count": y,
228 230 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
229 231 for x, y in lang_stats_items]
230 232
231 233 c.trending_languages = json.dumps(lang_stats)
232 234 else:
233 235 c.no_data = True
234 236 c.trending_languages = json.dumps({})
235 237
236 238 scm_model = ScmModel()
237 239 c.enable_downloads = self.db_repo.enable_downloads
238 240 c.repository_followers = scm_model.get_followers(self.db_repo)
239 241 c.repository_forks = scm_model.get_forks(self.db_repo)
240 242
241 243 # first interaction with the VCS instance after here...
242 244 if c.repository_requirements_missing:
243 245 self.request.override_renderer = \
244 246 'rhodecode:templates/summary/missing_requirements.mako'
245 247 return self._get_template_context(c)
246 248
247 249 c.readme_data, c.readme_file = \
248 250 self._get_readme_data(self.db_repo, c.visual.default_renderer)
249 251
250 252 # loads the summary commits template context
251 253 self._load_commits_context(c)
252 254
253 255 return self._get_template_context(c)
254 256
255 257 def get_request_commit_id(self):
256 258 return self.request.matchdict['commit_id']
257 259
258 260 @LoginRequired()
259 261 @HasRepoPermissionAnyDecorator(
260 262 'repository.read', 'repository.write', 'repository.admin')
261 263 @view_config(
262 264 route_name='repo_stats', request_method='GET',
263 265 renderer='json_ext')
264 266 def repo_stats(self):
265 267 commit_id = self.get_request_commit_id()
266 268 show_stats = bool(self.db_repo.enable_statistics)
267 269 repo_id = self.db_repo.repo_id
268 270
269 271 cache_seconds = safe_int(
270 272 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
271 273 cache_on = cache_seconds > 0
272 274 log.debug(
273 275 'Computing REPO TREE for repo_id %s commit_id `%s` '
274 276 'with caching: %s[TTL: %ss]' % (
275 277 repo_id, commit_id, cache_on, cache_seconds or 0))
276 278
277 279 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
278 280 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
279 281
280 282 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
281 283 condition=cache_on)
282 284 def compute_stats(repo_id, commit_id, show_stats):
283 285 code_stats = {}
284 286 size = 0
285 287 try:
286 288 scm_instance = self.db_repo.scm_instance()
287 289 commit = scm_instance.get_commit(commit_id)
288 290
289 291 for node in commit.get_filenodes_generator():
290 292 size += node.size
291 293 if not show_stats:
292 294 continue
293 295 ext = string.lower(node.extension)
294 296 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
295 297 if ext_info:
296 298 if ext in code_stats:
297 299 code_stats[ext]['count'] += 1
298 300 else:
299 301 code_stats[ext] = {"count": 1, "desc": ext_info}
300 302 except (EmptyRepositoryError, CommitDoesNotExistError):
301 303 pass
302 304 return {'size': h.format_byte_size_binary(size),
303 305 'code_stats': code_stats}
304 306
305 307 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
306 308 return stats
307 309
308 310 @LoginRequired()
309 311 @HasRepoPermissionAnyDecorator(
310 312 'repository.read', 'repository.write', 'repository.admin')
311 313 @view_config(
312 314 route_name='repo_refs_data', request_method='GET',
313 315 renderer='json_ext')
314 316 def repo_refs_data(self):
315 317 _ = self.request.translate
316 318 self.load_default_context()
317 319
318 320 repo = self.rhodecode_vcs_repo
319 321 refs_to_create = [
320 322 (_("Branch"), repo.branches, 'branch'),
321 323 (_("Tag"), repo.tags, 'tag'),
322 324 (_("Bookmark"), repo.bookmarks, 'book'),
323 325 ]
324 326 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
325 327 data = {
326 328 'more': False,
327 329 'results': res
328 330 }
329 331 return data
330 332
331 333 @LoginRequired()
332 334 @HasRepoPermissionAnyDecorator(
333 335 'repository.read', 'repository.write', 'repository.admin')
334 336 @view_config(
335 337 route_name='repo_refs_changelog_data', request_method='GET',
336 338 renderer='json_ext')
337 339 def repo_refs_changelog_data(self):
338 340 _ = self.request.translate
339 341 self.load_default_context()
340 342
341 343 repo = self.rhodecode_vcs_repo
342 344
343 345 refs_to_create = [
344 346 (_("Branches"), repo.branches, 'branch'),
345 347 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
346 348 # TODO: enable when vcs can handle bookmarks filters
347 349 # (_("Bookmarks"), repo.bookmarks, "book"),
348 350 ]
349 351 res = self._create_reference_data(
350 352 repo, self.db_repo_name, refs_to_create)
351 353 data = {
352 354 'more': False,
353 355 'results': res
354 356 }
355 357 return data
356 358
357 359 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
358 360 format_ref_id = get_format_ref_id(repo)
359 361
360 362 result = []
361 363 for title, refs, ref_type in refs_to_create:
362 364 if refs:
363 365 result.append({
364 366 'text': title,
365 367 'children': self._create_reference_items(
366 368 repo, full_repo_name, refs, ref_type,
367 369 format_ref_id),
368 370 })
369 371 return result
370 372
371 373 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
372 374 result = []
373 375 is_svn = h.is_svn(repo)
374 376 for ref_name, raw_id in refs.iteritems():
375 377 files_url = self._create_files_url(
376 378 repo, full_repo_name, ref_name, raw_id, is_svn)
377 379 result.append({
378 380 'text': ref_name,
379 381 'id': format_ref_id(ref_name, raw_id),
380 382 'raw_id': raw_id,
381 383 'type': ref_type,
382 384 'files_url': files_url,
383 385 'idx': 0,
384 386 })
385 387 return result
386 388
387 389 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
388 390 use_commit_id = '/' in ref_name or is_svn
389 391 return h.route_path(
390 392 'repo_files',
391 393 repo_name=full_repo_name,
392 394 f_path=ref_name if is_svn else '',
393 395 commit_id=raw_id if use_commit_id else ref_name,
394 396 _query=dict(at=ref_name))
@@ -1,2075 +1,2075 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 return text[:size - len(postfix)] + postfix
157 157 return text
158 158
159 159
160 160 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 161 """
162 162 Reset button
163 163 """
164 164 _set_input_attrs(attrs, type, name, value)
165 165 _set_id_attr(attrs, id, name)
166 166 convert_boolean_attrs(attrs, ["disabled"])
167 167 return HTML.input(**attrs)
168 168
169 169 reset = _reset
170 170 safeid = _make_safe_id_component
171 171
172 172
173 173 def branding(name, length=40):
174 174 return truncate(name, length, indicator="")
175 175
176 176
177 177 def FID(raw_id, path):
178 178 """
179 179 Creates a unique ID for filenode based on it's hash of path and commit
180 180 it's safe to use in urls
181 181
182 182 :param raw_id:
183 183 :param path:
184 184 """
185 185
186 186 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 187
188 188
189 189 class _GetError(object):
190 190 """Get error from form_errors, and represent it as span wrapped error
191 191 message
192 192
193 193 :param field_name: field to fetch errors for
194 194 :param form_errors: form errors dict
195 195 """
196 196
197 197 def __call__(self, field_name, form_errors):
198 198 tmpl = """<span class="error_msg">%s</span>"""
199 199 if form_errors and field_name in form_errors:
200 200 return literal(tmpl % form_errors.get(field_name))
201 201
202 202
203 203 get_error = _GetError()
204 204
205 205
206 206 class _ToolTip(object):
207 207
208 208 def __call__(self, tooltip_title, trim_at=50):
209 209 """
210 210 Special function just to wrap our text into nice formatted
211 211 autowrapped text
212 212
213 213 :param tooltip_title:
214 214 """
215 215 tooltip_title = escape(tooltip_title)
216 216 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
217 217 return tooltip_title
218 218
219 219
220 220 tooltip = _ToolTip()
221 221
222 files_icon = icon = '<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
222 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
223 223
224 224
225 225 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
226 226 if isinstance(file_path, str):
227 227 file_path = safe_unicode(file_path)
228 228
229 229 route_qry = {'at': at_ref} if at_ref else None
230 230
231 231 # first segment is a `..` link to repo files
232 232 root_name = literal(u'<i class="icon-home"></i>')
233 233 url_segments = [
234 234 link_to(
235 235 root_name,
236 236 route_path(
237 237 'repo_files',
238 238 repo_name=repo_name,
239 239 commit_id=commit_id,
240 240 f_path='',
241 241 _query=route_qry),
242 242 )]
243 243
244 244 path_segments = file_path.split('/')
245 245 last_cnt = len(path_segments) - 1
246 246 for cnt, segment in enumerate(path_segments):
247 247 if not segment:
248 248 continue
249 249 segment_html = escape(segment)
250 250
251 251 last_item = cnt == last_cnt
252 252
253 253 if last_item and linkify_last_item is False:
254 254 # plain version
255 255 url_segments.append(segment_html)
256 256 else:
257 257 url_segments.append(
258 258 link_to(
259 259 segment_html,
260 260 route_path(
261 261 'repo_files',
262 262 repo_name=repo_name,
263 263 commit_id=commit_id,
264 264 f_path='/'.join(path_segments[:cnt + 1]),
265 265 _query=route_qry),
266 266 ))
267 267
268 268 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
269 269 if limit_items and len(limited_url_segments) < len(url_segments):
270 270 url_segments = limited_url_segments
271 271
272 272 full_path = file_path
273 273 icon = files_icon.format(escape(full_path))
274 274 if file_path == '':
275 275 return root_name
276 276 else:
277 277 return literal(' / '.join(url_segments) + icon)
278 278
279 279
280 280 def files_url_data(request):
281 281 matchdict = request.matchdict
282 282
283 283 if 'f_path' not in matchdict:
284 284 matchdict['f_path'] = ''
285 285
286 286 if 'commit_id' not in matchdict:
287 287 matchdict['commit_id'] = 'tip'
288 288
289 289 return json.dumps(matchdict)
290 290
291 291
292 292 def code_highlight(code, lexer, formatter, use_hl_filter=False):
293 293 """
294 294 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
295 295
296 296 If ``outfile`` is given and a valid file object (an object
297 297 with a ``write`` method), the result will be written to it, otherwise
298 298 it is returned as a string.
299 299 """
300 300 if use_hl_filter:
301 301 # add HL filter
302 302 from rhodecode.lib.index import search_utils
303 303 lexer.add_filter(search_utils.ElasticSearchHLFilter())
304 304 return pygments.format(pygments.lex(code, lexer), formatter)
305 305
306 306
307 307 class CodeHtmlFormatter(HtmlFormatter):
308 308 """
309 309 My code Html Formatter for source codes
310 310 """
311 311
312 312 def wrap(self, source, outfile):
313 313 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
314 314
315 315 def _wrap_code(self, source):
316 316 for cnt, it in enumerate(source):
317 317 i, t = it
318 318 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
319 319 yield i, t
320 320
321 321 def _wrap_tablelinenos(self, inner):
322 322 dummyoutfile = StringIO.StringIO()
323 323 lncount = 0
324 324 for t, line in inner:
325 325 if t:
326 326 lncount += 1
327 327 dummyoutfile.write(line)
328 328
329 329 fl = self.linenostart
330 330 mw = len(str(lncount + fl - 1))
331 331 sp = self.linenospecial
332 332 st = self.linenostep
333 333 la = self.lineanchors
334 334 aln = self.anchorlinenos
335 335 nocls = self.noclasses
336 336 if sp:
337 337 lines = []
338 338
339 339 for i in range(fl, fl + lncount):
340 340 if i % st == 0:
341 341 if i % sp == 0:
342 342 if aln:
343 343 lines.append('<a href="#%s%d" class="special">%*d</a>' %
344 344 (la, i, mw, i))
345 345 else:
346 346 lines.append('<span class="special">%*d</span>' % (mw, i))
347 347 else:
348 348 if aln:
349 349 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
350 350 else:
351 351 lines.append('%*d' % (mw, i))
352 352 else:
353 353 lines.append('')
354 354 ls = '\n'.join(lines)
355 355 else:
356 356 lines = []
357 357 for i in range(fl, fl + lncount):
358 358 if i % st == 0:
359 359 if aln:
360 360 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
361 361 else:
362 362 lines.append('%*d' % (mw, i))
363 363 else:
364 364 lines.append('')
365 365 ls = '\n'.join(lines)
366 366
367 367 # in case you wonder about the seemingly redundant <div> here: since the
368 368 # content in the other cell also is wrapped in a div, some browsers in
369 369 # some configurations seem to mess up the formatting...
370 370 if nocls:
371 371 yield 0, ('<table class="%stable">' % self.cssclass +
372 372 '<tr><td><div class="linenodiv" '
373 373 'style="background-color: #f0f0f0; padding-right: 10px">'
374 374 '<pre style="line-height: 125%">' +
375 375 ls + '</pre></div></td><td id="hlcode" class="code">')
376 376 else:
377 377 yield 0, ('<table class="%stable">' % self.cssclass +
378 378 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
379 379 ls + '</pre></div></td><td id="hlcode" class="code">')
380 380 yield 0, dummyoutfile.getvalue()
381 381 yield 0, '</td></tr></table>'
382 382
383 383
384 384 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
385 385 def __init__(self, **kw):
386 386 # only show these line numbers if set
387 387 self.only_lines = kw.pop('only_line_numbers', [])
388 388 self.query_terms = kw.pop('query_terms', [])
389 389 self.max_lines = kw.pop('max_lines', 5)
390 390 self.line_context = kw.pop('line_context', 3)
391 391 self.url = kw.pop('url', None)
392 392
393 393 super(CodeHtmlFormatter, self).__init__(**kw)
394 394
395 395 def _wrap_code(self, source):
396 396 for cnt, it in enumerate(source):
397 397 i, t = it
398 398 t = '<pre>%s</pre>' % t
399 399 yield i, t
400 400
401 401 def _wrap_tablelinenos(self, inner):
402 402 yield 0, '<table class="code-highlight %stable">' % self.cssclass
403 403
404 404 last_shown_line_number = 0
405 405 current_line_number = 1
406 406
407 407 for t, line in inner:
408 408 if not t:
409 409 yield t, line
410 410 continue
411 411
412 412 if current_line_number in self.only_lines:
413 413 if last_shown_line_number + 1 != current_line_number:
414 414 yield 0, '<tr>'
415 415 yield 0, '<td class="line">...</td>'
416 416 yield 0, '<td id="hlcode" class="code"></td>'
417 417 yield 0, '</tr>'
418 418
419 419 yield 0, '<tr>'
420 420 if self.url:
421 421 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
422 422 self.url, current_line_number, current_line_number)
423 423 else:
424 424 yield 0, '<td class="line"><a href="">%i</a></td>' % (
425 425 current_line_number)
426 426 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
427 427 yield 0, '</tr>'
428 428
429 429 last_shown_line_number = current_line_number
430 430
431 431 current_line_number += 1
432 432
433 433 yield 0, '</table>'
434 434
435 435
436 436 def hsv_to_rgb(h, s, v):
437 437 """ Convert hsv color values to rgb """
438 438
439 439 if s == 0.0:
440 440 return v, v, v
441 441 i = int(h * 6.0) # XXX assume int() truncates!
442 442 f = (h * 6.0) - i
443 443 p = v * (1.0 - s)
444 444 q = v * (1.0 - s * f)
445 445 t = v * (1.0 - s * (1.0 - f))
446 446 i = i % 6
447 447 if i == 0:
448 448 return v, t, p
449 449 if i == 1:
450 450 return q, v, p
451 451 if i == 2:
452 452 return p, v, t
453 453 if i == 3:
454 454 return p, q, v
455 455 if i == 4:
456 456 return t, p, v
457 457 if i == 5:
458 458 return v, p, q
459 459
460 460
461 461 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
462 462 """
463 463 Generator for getting n of evenly distributed colors using
464 464 hsv color and golden ratio. It always return same order of colors
465 465
466 466 :param n: number of colors to generate
467 467 :param saturation: saturation of returned colors
468 468 :param lightness: lightness of returned colors
469 469 :returns: RGB tuple
470 470 """
471 471
472 472 golden_ratio = 0.618033988749895
473 473 h = 0.22717784590367374
474 474
475 475 for _ in xrange(n):
476 476 h += golden_ratio
477 477 h %= 1
478 478 HSV_tuple = [h, saturation, lightness]
479 479 RGB_tuple = hsv_to_rgb(*HSV_tuple)
480 480 yield map(lambda x: str(int(x * 256)), RGB_tuple)
481 481
482 482
483 483 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
484 484 """
485 485 Returns a function which when called with an argument returns a unique
486 486 color for that argument, eg.
487 487
488 488 :param n: number of colors to generate
489 489 :param saturation: saturation of returned colors
490 490 :param lightness: lightness of returned colors
491 491 :returns: css RGB string
492 492
493 493 >>> color_hash = color_hasher()
494 494 >>> color_hash('hello')
495 495 'rgb(34, 12, 59)'
496 496 >>> color_hash('hello')
497 497 'rgb(34, 12, 59)'
498 498 >>> color_hash('other')
499 499 'rgb(90, 224, 159)'
500 500 """
501 501
502 502 color_dict = {}
503 503 cgenerator = unique_color_generator(
504 504 saturation=saturation, lightness=lightness)
505 505
506 506 def get_color_string(thing):
507 507 if thing in color_dict:
508 508 col = color_dict[thing]
509 509 else:
510 510 col = color_dict[thing] = cgenerator.next()
511 511 return "rgb(%s)" % (', '.join(col))
512 512
513 513 return get_color_string
514 514
515 515
516 516 def get_lexer_safe(mimetype=None, filepath=None):
517 517 """
518 518 Tries to return a relevant pygments lexer using mimetype/filepath name,
519 519 defaulting to plain text if none could be found
520 520 """
521 521 lexer = None
522 522 try:
523 523 if mimetype:
524 524 lexer = get_lexer_for_mimetype(mimetype)
525 525 if not lexer:
526 526 lexer = get_lexer_for_filename(filepath)
527 527 except pygments.util.ClassNotFound:
528 528 pass
529 529
530 530 if not lexer:
531 531 lexer = get_lexer_by_name('text')
532 532
533 533 return lexer
534 534
535 535
536 536 def get_lexer_for_filenode(filenode):
537 537 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
538 538 return lexer
539 539
540 540
541 541 def pygmentize(filenode, **kwargs):
542 542 """
543 543 pygmentize function using pygments
544 544
545 545 :param filenode:
546 546 """
547 547 lexer = get_lexer_for_filenode(filenode)
548 548 return literal(code_highlight(filenode.content, lexer,
549 549 CodeHtmlFormatter(**kwargs)))
550 550
551 551
552 552 def is_following_repo(repo_name, user_id):
553 553 from rhodecode.model.scm import ScmModel
554 554 return ScmModel().is_following_repo(repo_name, user_id)
555 555
556 556
557 557 class _Message(object):
558 558 """A message returned by ``Flash.pop_messages()``.
559 559
560 560 Converting the message to a string returns the message text. Instances
561 561 also have the following attributes:
562 562
563 563 * ``message``: the message text.
564 564 * ``category``: the category specified when the message was created.
565 565 """
566 566
567 567 def __init__(self, category, message):
568 568 self.category = category
569 569 self.message = message
570 570
571 571 def __str__(self):
572 572 return self.message
573 573
574 574 __unicode__ = __str__
575 575
576 576 def __html__(self):
577 577 return escape(safe_unicode(self.message))
578 578
579 579
580 580 class Flash(object):
581 581 # List of allowed categories. If None, allow any category.
582 582 categories = ["warning", "notice", "error", "success"]
583 583
584 584 # Default category if none is specified.
585 585 default_category = "notice"
586 586
587 587 def __init__(self, session_key="flash", categories=None,
588 588 default_category=None):
589 589 """
590 590 Instantiate a ``Flash`` object.
591 591
592 592 ``session_key`` is the key to save the messages under in the user's
593 593 session.
594 594
595 595 ``categories`` is an optional list which overrides the default list
596 596 of categories.
597 597
598 598 ``default_category`` overrides the default category used for messages
599 599 when none is specified.
600 600 """
601 601 self.session_key = session_key
602 602 if categories is not None:
603 603 self.categories = categories
604 604 if default_category is not None:
605 605 self.default_category = default_category
606 606 if self.categories and self.default_category not in self.categories:
607 607 raise ValueError(
608 608 "unrecognized default category %r" % (self.default_category,))
609 609
610 610 def pop_messages(self, session=None, request=None):
611 611 """
612 612 Return all accumulated messages and delete them from the session.
613 613
614 614 The return value is a list of ``Message`` objects.
615 615 """
616 616 messages = []
617 617
618 618 if not session:
619 619 if not request:
620 620 request = get_current_request()
621 621 session = request.session
622 622
623 623 # Pop the 'old' pylons flash messages. They are tuples of the form
624 624 # (category, message)
625 625 for cat, msg in session.pop(self.session_key, []):
626 626 messages.append(_Message(cat, msg))
627 627
628 628 # Pop the 'new' pyramid flash messages for each category as list
629 629 # of strings.
630 630 for cat in self.categories:
631 631 for msg in session.pop_flash(queue=cat):
632 632 messages.append(_Message(cat, msg))
633 633 # Map messages from the default queue to the 'notice' category.
634 634 for msg in session.pop_flash():
635 635 messages.append(_Message('notice', msg))
636 636
637 637 session.save()
638 638 return messages
639 639
640 640 def json_alerts(self, session=None, request=None):
641 641 payloads = []
642 642 messages = flash.pop_messages(session=session, request=request)
643 643 if messages:
644 644 for message in messages:
645 645 subdata = {}
646 646 if hasattr(message.message, 'rsplit'):
647 647 flash_data = message.message.rsplit('|DELIM|', 1)
648 648 org_message = flash_data[0]
649 649 if len(flash_data) > 1:
650 650 subdata = json.loads(flash_data[1])
651 651 else:
652 652 org_message = message.message
653 653 payloads.append({
654 654 'message': {
655 655 'message': u'{}'.format(org_message),
656 656 'level': message.category,
657 657 'force': True,
658 658 'subdata': subdata
659 659 }
660 660 })
661 661 return json.dumps(payloads)
662 662
663 663 def __call__(self, message, category=None, ignore_duplicate=False,
664 664 session=None, request=None):
665 665
666 666 if not session:
667 667 if not request:
668 668 request = get_current_request()
669 669 session = request.session
670 670
671 671 session.flash(
672 672 message, queue=category, allow_duplicate=not ignore_duplicate)
673 673
674 674
675 675 flash = Flash()
676 676
677 677 #==============================================================================
678 678 # SCM FILTERS available via h.
679 679 #==============================================================================
680 680 from rhodecode.lib.vcs.utils import author_name, author_email
681 681 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
682 682 from rhodecode.model.db import User, ChangesetStatus
683 683
684 684 capitalize = lambda x: x.capitalize()
685 685 email = author_email
686 686 short_id = lambda x: x[:12]
687 687 hide_credentials = lambda x: ''.join(credentials_filter(x))
688 688
689 689
690 690 import pytz
691 691 import tzlocal
692 692 local_timezone = tzlocal.get_localzone()
693 693
694 694
695 695 def age_component(datetime_iso, value=None, time_is_local=False):
696 696 title = value or format_date(datetime_iso)
697 697 tzinfo = '+00:00'
698 698
699 699 # detect if we have a timezone info, otherwise, add it
700 700 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
701 701 force_timezone = os.environ.get('RC_TIMEZONE', '')
702 702 if force_timezone:
703 703 force_timezone = pytz.timezone(force_timezone)
704 704 timezone = force_timezone or local_timezone
705 705 offset = timezone.localize(datetime_iso).strftime('%z')
706 706 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
707 707
708 708 return literal(
709 709 '<time class="timeago tooltip" '
710 710 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
711 711 datetime_iso, title, tzinfo))
712 712
713 713
714 714 def _shorten_commit_id(commit_id, commit_len=None):
715 715 if commit_len is None:
716 716 request = get_current_request()
717 717 commit_len = request.call_context.visual.show_sha_length
718 718 return commit_id[:commit_len]
719 719
720 720
721 721 def show_id(commit, show_idx=None, commit_len=None):
722 722 """
723 723 Configurable function that shows ID
724 724 by default it's r123:fffeeefffeee
725 725
726 726 :param commit: commit instance
727 727 """
728 728 if show_idx is None:
729 729 request = get_current_request()
730 730 show_idx = request.call_context.visual.show_revision_number
731 731
732 732 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
733 733 if show_idx:
734 734 return 'r%s:%s' % (commit.idx, raw_id)
735 735 else:
736 736 return '%s' % (raw_id, )
737 737
738 738
739 739 def format_date(date):
740 740 """
741 741 use a standardized formatting for dates used in RhodeCode
742 742
743 743 :param date: date/datetime object
744 744 :return: formatted date
745 745 """
746 746
747 747 if date:
748 748 _fmt = "%a, %d %b %Y %H:%M:%S"
749 749 return safe_unicode(date.strftime(_fmt))
750 750
751 751 return u""
752 752
753 753
754 754 class _RepoChecker(object):
755 755
756 756 def __init__(self, backend_alias):
757 757 self._backend_alias = backend_alias
758 758
759 759 def __call__(self, repository):
760 760 if hasattr(repository, 'alias'):
761 761 _type = repository.alias
762 762 elif hasattr(repository, 'repo_type'):
763 763 _type = repository.repo_type
764 764 else:
765 765 _type = repository
766 766 return _type == self._backend_alias
767 767
768 768
769 769 is_git = _RepoChecker('git')
770 770 is_hg = _RepoChecker('hg')
771 771 is_svn = _RepoChecker('svn')
772 772
773 773
774 774 def get_repo_type_by_name(repo_name):
775 775 repo = Repository.get_by_repo_name(repo_name)
776 776 if repo:
777 777 return repo.repo_type
778 778
779 779
780 780 def is_svn_without_proxy(repository):
781 781 if is_svn(repository):
782 782 from rhodecode.model.settings import VcsSettingsModel
783 783 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
784 784 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
785 785 return False
786 786
787 787
788 788 def discover_user(author):
789 789 """
790 790 Tries to discover RhodeCode User based on the autho string. Author string
791 791 is typically `FirstName LastName <email@address.com>`
792 792 """
793 793
794 794 # if author is already an instance use it for extraction
795 795 if isinstance(author, User):
796 796 return author
797 797
798 798 # Valid email in the attribute passed, see if they're in the system
799 799 _email = author_email(author)
800 800 if _email != '':
801 801 user = User.get_by_email(_email, case_insensitive=True, cache=True)
802 802 if user is not None:
803 803 return user
804 804
805 805 # Maybe it's a username, we try to extract it and fetch by username ?
806 806 _author = author_name(author)
807 807 user = User.get_by_username(_author, case_insensitive=True, cache=True)
808 808 if user is not None:
809 809 return user
810 810
811 811 return None
812 812
813 813
814 814 def email_or_none(author):
815 815 # extract email from the commit string
816 816 _email = author_email(author)
817 817
818 818 # If we have an email, use it, otherwise
819 819 # see if it contains a username we can get an email from
820 820 if _email != '':
821 821 return _email
822 822 else:
823 823 user = User.get_by_username(
824 824 author_name(author), case_insensitive=True, cache=True)
825 825
826 826 if user is not None:
827 827 return user.email
828 828
829 829 # No valid email, not a valid user in the system, none!
830 830 return None
831 831
832 832
833 833 def link_to_user(author, length=0, **kwargs):
834 834 user = discover_user(author)
835 835 # user can be None, but if we have it already it means we can re-use it
836 836 # in the person() function, so we save 1 intensive-query
837 837 if user:
838 838 author = user
839 839
840 840 display_person = person(author, 'username_or_name_or_email')
841 841 if length:
842 842 display_person = shorter(display_person, length)
843 843
844 844 if user:
845 845 return link_to(
846 846 escape(display_person),
847 847 route_path('user_profile', username=user.username),
848 848 **kwargs)
849 849 else:
850 850 return escape(display_person)
851 851
852 852
853 853 def link_to_group(users_group_name, **kwargs):
854 854 return link_to(
855 855 escape(users_group_name),
856 856 route_path('user_group_profile', user_group_name=users_group_name),
857 857 **kwargs)
858 858
859 859
860 860 def person(author, show_attr="username_and_name"):
861 861 user = discover_user(author)
862 862 if user:
863 863 return getattr(user, show_attr)
864 864 else:
865 865 _author = author_name(author)
866 866 _email = email(author)
867 867 return _author or _email
868 868
869 869
870 870 def author_string(email):
871 871 if email:
872 872 user = User.get_by_email(email, case_insensitive=True, cache=True)
873 873 if user:
874 874 if user.first_name or user.last_name:
875 875 return '%s %s &lt;%s&gt;' % (
876 876 user.first_name, user.last_name, email)
877 877 else:
878 878 return email
879 879 else:
880 880 return email
881 881 else:
882 882 return None
883 883
884 884
885 885 def person_by_id(id_, show_attr="username_and_name"):
886 886 # attr to return from fetched user
887 887 person_getter = lambda usr: getattr(usr, show_attr)
888 888
889 889 #maybe it's an ID ?
890 890 if str(id_).isdigit() or isinstance(id_, int):
891 891 id_ = int(id_)
892 892 user = User.get(id_)
893 893 if user is not None:
894 894 return person_getter(user)
895 895 return id_
896 896
897 897
898 898 def gravatar_with_user(request, author, show_disabled=False):
899 899 _render = request.get_partial_renderer(
900 900 'rhodecode:templates/base/base.mako')
901 901 return _render('gravatar_with_user', author, show_disabled=show_disabled)
902 902
903 903
904 904 tags_paterns = OrderedDict((
905 905 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
906 906 '<div class="metatag" tag="lang">\\2</div>')),
907 907
908 908 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
909 909 '<div class="metatag" tag="see">see: \\1 </div>')),
910 910
911 911 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
912 912 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
913 913
914 914 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
915 915 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
916 916
917 917 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
918 918 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
919 919
920 920 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
921 921 '<div class="metatag" tag="state \\1">\\1</div>')),
922 922
923 923 # label in grey
924 924 ('label', (re.compile(r'\[([a-z]+)\]'),
925 925 '<div class="metatag" tag="label">\\1</div>')),
926 926
927 927 # generic catch all in grey
928 928 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
929 929 '<div class="metatag" tag="generic">\\1</div>')),
930 930 ))
931 931
932 932
933 933 def extract_metatags(value):
934 934 """
935 935 Extract supported meta-tags from given text value
936 936 """
937 937 tags = []
938 938 if not value:
939 939 return tags, ''
940 940
941 941 for key, val in tags_paterns.items():
942 942 pat, replace_html = val
943 943 tags.extend([(key, x.group()) for x in pat.finditer(value)])
944 944 value = pat.sub('', value)
945 945
946 946 return tags, value
947 947
948 948
949 949 def style_metatag(tag_type, value):
950 950 """
951 951 converts tags from value into html equivalent
952 952 """
953 953 if not value:
954 954 return ''
955 955
956 956 html_value = value
957 957 tag_data = tags_paterns.get(tag_type)
958 958 if tag_data:
959 959 pat, replace_html = tag_data
960 960 # convert to plain `unicode` instead of a markup tag to be used in
961 961 # regex expressions. safe_unicode doesn't work here
962 962 html_value = pat.sub(replace_html, unicode(value))
963 963
964 964 return html_value
965 965
966 966
967 967 def bool2icon(value, show_at_false=True):
968 968 """
969 969 Returns boolean value of a given value, represented as html element with
970 970 classes that will represent icons
971 971
972 972 :param value: given value to convert to html node
973 973 """
974 974
975 975 if value: # does bool conversion
976 976 return HTML.tag('i', class_="icon-true")
977 977 else: # not true as bool
978 978 if show_at_false:
979 979 return HTML.tag('i', class_="icon-false")
980 980 return HTML.tag('i')
981 981
982 982 #==============================================================================
983 983 # PERMS
984 984 #==============================================================================
985 985 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
986 986 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
987 987 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
988 988 csrf_token_key
989 989
990 990
991 991 #==============================================================================
992 992 # GRAVATAR URL
993 993 #==============================================================================
994 994 class InitialsGravatar(object):
995 995 def __init__(self, email_address, first_name, last_name, size=30,
996 996 background=None, text_color='#fff'):
997 997 self.size = size
998 998 self.first_name = first_name
999 999 self.last_name = last_name
1000 1000 self.email_address = email_address
1001 1001 self.background = background or self.str2color(email_address)
1002 1002 self.text_color = text_color
1003 1003
1004 1004 def get_color_bank(self):
1005 1005 """
1006 1006 returns a predefined list of colors that gravatars can use.
1007 1007 Those are randomized distinct colors that guarantee readability and
1008 1008 uniqueness.
1009 1009
1010 1010 generated with: http://phrogz.net/css/distinct-colors.html
1011 1011 """
1012 1012 return [
1013 1013 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1014 1014 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1015 1015 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1016 1016 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1017 1017 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1018 1018 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1019 1019 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1020 1020 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1021 1021 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1022 1022 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1023 1023 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1024 1024 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1025 1025 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1026 1026 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1027 1027 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1028 1028 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1029 1029 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1030 1030 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1031 1031 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1032 1032 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1033 1033 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1034 1034 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1035 1035 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1036 1036 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1037 1037 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1038 1038 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1039 1039 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1040 1040 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1041 1041 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1042 1042 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1043 1043 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1044 1044 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1045 1045 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1046 1046 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1047 1047 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1048 1048 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1049 1049 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1050 1050 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1051 1051 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1052 1052 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1053 1053 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1054 1054 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1055 1055 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1056 1056 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1057 1057 '#4f8c46', '#368dd9', '#5c0073'
1058 1058 ]
1059 1059
1060 1060 def rgb_to_hex_color(self, rgb_tuple):
1061 1061 """
1062 1062 Converts an rgb_tuple passed to an hex color.
1063 1063
1064 1064 :param rgb_tuple: tuple with 3 ints represents rgb color space
1065 1065 """
1066 1066 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1067 1067
1068 1068 def email_to_int_list(self, email_str):
1069 1069 """
1070 1070 Get every byte of the hex digest value of email and turn it to integer.
1071 1071 It's going to be always between 0-255
1072 1072 """
1073 1073 digest = md5_safe(email_str.lower())
1074 1074 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1075 1075
1076 1076 def pick_color_bank_index(self, email_str, color_bank):
1077 1077 return self.email_to_int_list(email_str)[0] % len(color_bank)
1078 1078
1079 1079 def str2color(self, email_str):
1080 1080 """
1081 1081 Tries to map in a stable algorithm an email to color
1082 1082
1083 1083 :param email_str:
1084 1084 """
1085 1085 color_bank = self.get_color_bank()
1086 1086 # pick position (module it's length so we always find it in the
1087 1087 # bank even if it's smaller than 256 values
1088 1088 pos = self.pick_color_bank_index(email_str, color_bank)
1089 1089 return color_bank[pos]
1090 1090
1091 1091 def normalize_email(self, email_address):
1092 1092 import unicodedata
1093 1093 # default host used to fill in the fake/missing email
1094 1094 default_host = u'localhost'
1095 1095
1096 1096 if not email_address:
1097 1097 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1098 1098
1099 1099 email_address = safe_unicode(email_address)
1100 1100
1101 1101 if u'@' not in email_address:
1102 1102 email_address = u'%s@%s' % (email_address, default_host)
1103 1103
1104 1104 if email_address.endswith(u'@'):
1105 1105 email_address = u'%s%s' % (email_address, default_host)
1106 1106
1107 1107 email_address = unicodedata.normalize('NFKD', email_address)\
1108 1108 .encode('ascii', 'ignore')
1109 1109 return email_address
1110 1110
1111 1111 def get_initials(self):
1112 1112 """
1113 1113 Returns 2 letter initials calculated based on the input.
1114 1114 The algorithm picks first given email address, and takes first letter
1115 1115 of part before @, and then the first letter of server name. In case
1116 1116 the part before @ is in a format of `somestring.somestring2` it replaces
1117 1117 the server letter with first letter of somestring2
1118 1118
1119 1119 In case function was initialized with both first and lastname, this
1120 1120 overrides the extraction from email by first letter of the first and
1121 1121 last name. We add special logic to that functionality, In case Full name
1122 1122 is compound, like Guido Von Rossum, we use last part of the last name
1123 1123 (Von Rossum) picking `R`.
1124 1124
1125 1125 Function also normalizes the non-ascii characters to they ascii
1126 1126 representation, eg Δ„ => A
1127 1127 """
1128 1128 import unicodedata
1129 1129 # replace non-ascii to ascii
1130 1130 first_name = unicodedata.normalize(
1131 1131 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1132 1132 last_name = unicodedata.normalize(
1133 1133 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1134 1134
1135 1135 # do NFKD encoding, and also make sure email has proper format
1136 1136 email_address = self.normalize_email(self.email_address)
1137 1137
1138 1138 # first push the email initials
1139 1139 prefix, server = email_address.split('@', 1)
1140 1140
1141 1141 # check if prefix is maybe a 'first_name.last_name' syntax
1142 1142 _dot_split = prefix.rsplit('.', 1)
1143 1143 if len(_dot_split) == 2 and _dot_split[1]:
1144 1144 initials = [_dot_split[0][0], _dot_split[1][0]]
1145 1145 else:
1146 1146 initials = [prefix[0], server[0]]
1147 1147
1148 1148 # then try to replace either first_name or last_name
1149 1149 fn_letter = (first_name or " ")[0].strip()
1150 1150 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1151 1151
1152 1152 if fn_letter:
1153 1153 initials[0] = fn_letter
1154 1154
1155 1155 if ln_letter:
1156 1156 initials[1] = ln_letter
1157 1157
1158 1158 return ''.join(initials).upper()
1159 1159
1160 1160 def get_img_data_by_type(self, font_family, img_type):
1161 1161 default_user = """
1162 1162 <svg xmlns="http://www.w3.org/2000/svg"
1163 1163 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1164 1164 viewBox="-15 -10 439.165 429.164"
1165 1165
1166 1166 xml:space="preserve"
1167 1167 style="background:{background};" >
1168 1168
1169 1169 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1170 1170 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1171 1171 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1172 1172 168.596,153.916,216.671,
1173 1173 204.583,216.671z" fill="{text_color}"/>
1174 1174 <path d="M407.164,374.717L360.88,
1175 1175 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1176 1176 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1177 1177 15.366-44.203,23.488-69.076,23.488c-24.877,
1178 1178 0-48.762-8.122-69.078-23.488
1179 1179 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1180 1180 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1181 1181 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1182 1182 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1183 1183 19.402-10.527 C409.699,390.129,
1184 1184 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1185 1185 </svg>""".format(
1186 1186 size=self.size,
1187 1187 background='#979797', # @grey4
1188 1188 text_color=self.text_color,
1189 1189 font_family=font_family)
1190 1190
1191 1191 return {
1192 1192 "default_user": default_user
1193 1193 }[img_type]
1194 1194
1195 1195 def get_img_data(self, svg_type=None):
1196 1196 """
1197 1197 generates the svg metadata for image
1198 1198 """
1199 1199 fonts = [
1200 1200 '-apple-system',
1201 1201 'BlinkMacSystemFont',
1202 1202 'Segoe UI',
1203 1203 'Roboto',
1204 1204 'Oxygen-Sans',
1205 1205 'Ubuntu',
1206 1206 'Cantarell',
1207 1207 'Helvetica Neue',
1208 1208 'sans-serif'
1209 1209 ]
1210 1210 font_family = ','.join(fonts)
1211 1211 if svg_type:
1212 1212 return self.get_img_data_by_type(font_family, svg_type)
1213 1213
1214 1214 initials = self.get_initials()
1215 1215 img_data = """
1216 1216 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1217 1217 width="{size}" height="{size}"
1218 1218 style="width: 100%; height: 100%; background-color: {background}"
1219 1219 viewBox="0 0 {size} {size}">
1220 1220 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1221 1221 pointer-events="auto" fill="{text_color}"
1222 1222 font-family="{font_family}"
1223 1223 style="font-weight: 400; font-size: {f_size}px;">{text}
1224 1224 </text>
1225 1225 </svg>""".format(
1226 1226 size=self.size,
1227 1227 f_size=self.size/2.05, # scale the text inside the box nicely
1228 1228 background=self.background,
1229 1229 text_color=self.text_color,
1230 1230 text=initials.upper(),
1231 1231 font_family=font_family)
1232 1232
1233 1233 return img_data
1234 1234
1235 1235 def generate_svg(self, svg_type=None):
1236 1236 img_data = self.get_img_data(svg_type)
1237 1237 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1238 1238
1239 1239
1240 1240 def initials_gravatar(email_address, first_name, last_name, size=30):
1241 1241 svg_type = None
1242 1242 if email_address == User.DEFAULT_USER_EMAIL:
1243 1243 svg_type = 'default_user'
1244 1244 klass = InitialsGravatar(email_address, first_name, last_name, size)
1245 1245 return klass.generate_svg(svg_type=svg_type)
1246 1246
1247 1247
1248 1248 def gravatar_url(email_address, size=30, request=None):
1249 1249 request = get_current_request()
1250 1250 _use_gravatar = request.call_context.visual.use_gravatar
1251 1251 _gravatar_url = request.call_context.visual.gravatar_url
1252 1252
1253 1253 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1254 1254
1255 1255 email_address = email_address or User.DEFAULT_USER_EMAIL
1256 1256 if isinstance(email_address, unicode):
1257 1257 # hashlib crashes on unicode items
1258 1258 email_address = safe_str(email_address)
1259 1259
1260 1260 # empty email or default user
1261 1261 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1262 1262 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1263 1263
1264 1264 if _use_gravatar:
1265 1265 # TODO: Disuse pyramid thread locals. Think about another solution to
1266 1266 # get the host and schema here.
1267 1267 request = get_current_request()
1268 1268 tmpl = safe_str(_gravatar_url)
1269 1269 tmpl = tmpl.replace('{email}', email_address)\
1270 1270 .replace('{md5email}', md5_safe(email_address.lower())) \
1271 1271 .replace('{netloc}', request.host)\
1272 1272 .replace('{scheme}', request.scheme)\
1273 1273 .replace('{size}', safe_str(size))
1274 1274 return tmpl
1275 1275 else:
1276 1276 return initials_gravatar(email_address, '', '', size=size)
1277 1277
1278 1278
1279 1279 class Page(_Page):
1280 1280 """
1281 1281 Custom pager to match rendering style with paginator
1282 1282 """
1283 1283
1284 1284 def _get_pos(self, cur_page, max_page, items):
1285 1285 edge = (items / 2) + 1
1286 1286 if (cur_page <= edge):
1287 1287 radius = max(items / 2, items - cur_page)
1288 1288 elif (max_page - cur_page) < edge:
1289 1289 radius = (items - 1) - (max_page - cur_page)
1290 1290 else:
1291 1291 radius = items / 2
1292 1292
1293 1293 left = max(1, (cur_page - (radius)))
1294 1294 right = min(max_page, cur_page + (radius))
1295 1295 return left, cur_page, right
1296 1296
1297 1297 def _range(self, regexp_match):
1298 1298 """
1299 1299 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1300 1300
1301 1301 Arguments:
1302 1302
1303 1303 regexp_match
1304 1304 A "re" (regular expressions) match object containing the
1305 1305 radius of linked pages around the current page in
1306 1306 regexp_match.group(1) as a string
1307 1307
1308 1308 This function is supposed to be called as a callable in
1309 1309 re.sub.
1310 1310
1311 1311 """
1312 1312 radius = int(regexp_match.group(1))
1313 1313
1314 1314 # Compute the first and last page number within the radius
1315 1315 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1316 1316 # -> leftmost_page = 5
1317 1317 # -> rightmost_page = 9
1318 1318 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1319 1319 self.last_page,
1320 1320 (radius * 2) + 1)
1321 1321 nav_items = []
1322 1322
1323 1323 # Create a link to the first page (unless we are on the first page
1324 1324 # or there would be no need to insert '..' spacers)
1325 1325 if self.page != self.first_page and self.first_page < leftmost_page:
1326 1326 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1327 1327
1328 1328 # Insert dots if there are pages between the first page
1329 1329 # and the currently displayed page range
1330 1330 if leftmost_page - self.first_page > 1:
1331 1331 # Wrap in a SPAN tag if nolink_attr is set
1332 1332 text = '..'
1333 1333 if self.dotdot_attr:
1334 1334 text = HTML.span(c=text, **self.dotdot_attr)
1335 1335 nav_items.append(text)
1336 1336
1337 1337 for thispage in xrange(leftmost_page, rightmost_page + 1):
1338 1338 # Hilight the current page number and do not use a link
1339 1339 if thispage == self.page:
1340 1340 text = '%s' % (thispage,)
1341 1341 # Wrap in a SPAN tag if nolink_attr is set
1342 1342 if self.curpage_attr:
1343 1343 text = HTML.span(c=text, **self.curpage_attr)
1344 1344 nav_items.append(text)
1345 1345 # Otherwise create just a link to that page
1346 1346 else:
1347 1347 text = '%s' % (thispage,)
1348 1348 nav_items.append(self._pagerlink(thispage, text))
1349 1349
1350 1350 # Insert dots if there are pages between the displayed
1351 1351 # page numbers and the end of the page range
1352 1352 if self.last_page - rightmost_page > 1:
1353 1353 text = '..'
1354 1354 # Wrap in a SPAN tag if nolink_attr is set
1355 1355 if self.dotdot_attr:
1356 1356 text = HTML.span(c=text, **self.dotdot_attr)
1357 1357 nav_items.append(text)
1358 1358
1359 1359 # Create a link to the very last page (unless we are on the last
1360 1360 # page or there would be no need to insert '..' spacers)
1361 1361 if self.page != self.last_page and rightmost_page < self.last_page:
1362 1362 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1363 1363
1364 1364 ## prerender links
1365 1365 #_page_link = url.current()
1366 1366 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1367 1367 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1368 1368 return self.separator.join(nav_items)
1369 1369
1370 1370 def pager(self, format='~2~', page_param='page', partial_param='partial',
1371 1371 show_if_single_page=False, separator=' ', onclick=None,
1372 1372 symbol_first='<<', symbol_last='>>',
1373 1373 symbol_previous='<', symbol_next='>',
1374 1374 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1375 1375 curpage_attr={'class': 'pager_curpage'},
1376 1376 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1377 1377
1378 1378 self.curpage_attr = curpage_attr
1379 1379 self.separator = separator
1380 1380 self.pager_kwargs = kwargs
1381 1381 self.page_param = page_param
1382 1382 self.partial_param = partial_param
1383 1383 self.onclick = onclick
1384 1384 self.link_attr = link_attr
1385 1385 self.dotdot_attr = dotdot_attr
1386 1386
1387 1387 # Don't show navigator if there is no more than one page
1388 1388 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1389 1389 return ''
1390 1390
1391 1391 from string import Template
1392 1392 # Replace ~...~ in token format by range of pages
1393 1393 result = re.sub(r'~(\d+)~', self._range, format)
1394 1394
1395 1395 # Interpolate '%' variables
1396 1396 result = Template(result).safe_substitute({
1397 1397 'first_page': self.first_page,
1398 1398 'last_page': self.last_page,
1399 1399 'page': self.page,
1400 1400 'page_count': self.page_count,
1401 1401 'items_per_page': self.items_per_page,
1402 1402 'first_item': self.first_item,
1403 1403 'last_item': self.last_item,
1404 1404 'item_count': self.item_count,
1405 1405 'link_first': self.page > self.first_page and \
1406 1406 self._pagerlink(self.first_page, symbol_first) or '',
1407 1407 'link_last': self.page < self.last_page and \
1408 1408 self._pagerlink(self.last_page, symbol_last) or '',
1409 1409 'link_previous': self.previous_page and \
1410 1410 self._pagerlink(self.previous_page, symbol_previous) \
1411 1411 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1412 1412 'link_next': self.next_page and \
1413 1413 self._pagerlink(self.next_page, symbol_next) \
1414 1414 or HTML.span(symbol_next, class_="pg-next disabled")
1415 1415 })
1416 1416
1417 1417 return literal(result)
1418 1418
1419 1419
1420 1420 #==============================================================================
1421 1421 # REPO PAGER, PAGER FOR REPOSITORY
1422 1422 #==============================================================================
1423 1423 class RepoPage(Page):
1424 1424
1425 1425 def __init__(self, collection, page=1, items_per_page=20,
1426 1426 item_count=None, url=None, **kwargs):
1427 1427
1428 1428 """Create a "RepoPage" instance. special pager for paging
1429 1429 repository
1430 1430 """
1431 1431 self._url_generator = url
1432 1432
1433 1433 # Safe the kwargs class-wide so they can be used in the pager() method
1434 1434 self.kwargs = kwargs
1435 1435
1436 1436 # Save a reference to the collection
1437 1437 self.original_collection = collection
1438 1438
1439 1439 self.collection = collection
1440 1440
1441 1441 # The self.page is the number of the current page.
1442 1442 # The first page has the number 1!
1443 1443 try:
1444 1444 self.page = int(page) # make it int() if we get it as a string
1445 1445 except (ValueError, TypeError):
1446 1446 self.page = 1
1447 1447
1448 1448 self.items_per_page = items_per_page
1449 1449
1450 1450 # Unless the user tells us how many items the collections has
1451 1451 # we calculate that ourselves.
1452 1452 if item_count is not None:
1453 1453 self.item_count = item_count
1454 1454 else:
1455 1455 self.item_count = len(self.collection)
1456 1456
1457 1457 # Compute the number of the first and last available page
1458 1458 if self.item_count > 0:
1459 1459 self.first_page = 1
1460 1460 self.page_count = int(math.ceil(float(self.item_count) /
1461 1461 self.items_per_page))
1462 1462 self.last_page = self.first_page + self.page_count - 1
1463 1463
1464 1464 # Make sure that the requested page number is the range of
1465 1465 # valid pages
1466 1466 if self.page > self.last_page:
1467 1467 self.page = self.last_page
1468 1468 elif self.page < self.first_page:
1469 1469 self.page = self.first_page
1470 1470
1471 1471 # Note: the number of items on this page can be less than
1472 1472 # items_per_page if the last page is not full
1473 1473 self.first_item = max(0, (self.item_count) - (self.page *
1474 1474 items_per_page))
1475 1475 self.last_item = ((self.item_count - 1) - items_per_page *
1476 1476 (self.page - 1))
1477 1477
1478 1478 self.items = list(self.collection[self.first_item:self.last_item + 1])
1479 1479
1480 1480 # Links to previous and next page
1481 1481 if self.page > self.first_page:
1482 1482 self.previous_page = self.page - 1
1483 1483 else:
1484 1484 self.previous_page = None
1485 1485
1486 1486 if self.page < self.last_page:
1487 1487 self.next_page = self.page + 1
1488 1488 else:
1489 1489 self.next_page = None
1490 1490
1491 1491 # No items available
1492 1492 else:
1493 1493 self.first_page = None
1494 1494 self.page_count = 0
1495 1495 self.last_page = None
1496 1496 self.first_item = None
1497 1497 self.last_item = None
1498 1498 self.previous_page = None
1499 1499 self.next_page = None
1500 1500 self.items = []
1501 1501
1502 1502 # This is a subclass of the 'list' type. Initialise the list now.
1503 1503 list.__init__(self, reversed(self.items))
1504 1504
1505 1505
1506 1506 def breadcrumb_repo_link(repo):
1507 1507 """
1508 1508 Makes a breadcrumbs path link to repo
1509 1509
1510 1510 ex::
1511 1511 group >> subgroup >> repo
1512 1512
1513 1513 :param repo: a Repository instance
1514 1514 """
1515 1515
1516 1516 path = [
1517 1517 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1518 1518 title='last change:{}'.format(format_date(group.last_commit_change)))
1519 1519 for group in repo.groups_with_parents
1520 1520 ] + [
1521 1521 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1522 1522 title='last change:{}'.format(format_date(repo.last_commit_change)))
1523 1523 ]
1524 1524
1525 1525 return literal(' &raquo; '.join(path))
1526 1526
1527 1527
1528 1528 def breadcrumb_repo_group_link(repo_group):
1529 1529 """
1530 1530 Makes a breadcrumbs path link to repo
1531 1531
1532 1532 ex::
1533 1533 group >> subgroup
1534 1534
1535 1535 :param repo_group: a Repository Group instance
1536 1536 """
1537 1537
1538 1538 path = [
1539 1539 link_to(group.name,
1540 1540 route_path('repo_group_home', repo_group_name=group.group_name),
1541 1541 title='last change:{}'.format(format_date(group.last_commit_change)))
1542 1542 for group in repo_group.parents
1543 1543 ] + [
1544 1544 link_to(repo_group.name,
1545 1545 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1546 1546 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1547 1547 ]
1548 1548
1549 1549 return literal(' &raquo; '.join(path))
1550 1550
1551 1551
1552 1552 def format_byte_size_binary(file_size):
1553 1553 """
1554 1554 Formats file/folder sizes to standard.
1555 1555 """
1556 1556 if file_size is None:
1557 1557 file_size = 0
1558 1558
1559 1559 formatted_size = format_byte_size(file_size, binary=True)
1560 1560 return formatted_size
1561 1561
1562 1562
1563 1563 def urlify_text(text_, safe=True):
1564 1564 """
1565 1565 Extrac urls from text and make html links out of them
1566 1566
1567 1567 :param text_:
1568 1568 """
1569 1569
1570 1570 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1571 1571 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1572 1572
1573 1573 def url_func(match_obj):
1574 1574 url_full = match_obj.groups()[0]
1575 1575 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1576 1576 _newtext = url_pat.sub(url_func, text_)
1577 1577 if safe:
1578 1578 return literal(_newtext)
1579 1579 return _newtext
1580 1580
1581 1581
1582 1582 def urlify_commits(text_, repository):
1583 1583 """
1584 1584 Extract commit ids from text and make link from them
1585 1585
1586 1586 :param text_:
1587 1587 :param repository: repo name to build the URL with
1588 1588 """
1589 1589
1590 1590 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1591 1591
1592 1592 def url_func(match_obj):
1593 1593 commit_id = match_obj.groups()[1]
1594 1594 pref = match_obj.groups()[0]
1595 1595 suf = match_obj.groups()[2]
1596 1596
1597 1597 tmpl = (
1598 1598 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1599 1599 '%(commit_id)s</a>%(suf)s'
1600 1600 )
1601 1601 return tmpl % {
1602 1602 'pref': pref,
1603 1603 'cls': 'revision-link',
1604 1604 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1605 1605 'commit_id': commit_id,
1606 1606 'suf': suf
1607 1607 }
1608 1608
1609 1609 newtext = URL_PAT.sub(url_func, text_)
1610 1610
1611 1611 return newtext
1612 1612
1613 1613
1614 1614 def _process_url_func(match_obj, repo_name, uid, entry,
1615 1615 return_raw_data=False, link_format='html'):
1616 1616 pref = ''
1617 1617 if match_obj.group().startswith(' '):
1618 1618 pref = ' '
1619 1619
1620 1620 issue_id = ''.join(match_obj.groups())
1621 1621
1622 1622 if link_format == 'html':
1623 1623 tmpl = (
1624 1624 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1625 1625 '%(issue-prefix)s%(id-repr)s'
1626 1626 '</a>')
1627 1627 elif link_format == 'rst':
1628 1628 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1629 1629 elif link_format == 'markdown':
1630 1630 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1631 1631 else:
1632 1632 raise ValueError('Bad link_format:{}'.format(link_format))
1633 1633
1634 1634 (repo_name_cleaned,
1635 1635 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1636 1636
1637 1637 # variables replacement
1638 1638 named_vars = {
1639 1639 'id': issue_id,
1640 1640 'repo': repo_name,
1641 1641 'repo_name': repo_name_cleaned,
1642 1642 'group_name': parent_group_name
1643 1643 }
1644 1644 # named regex variables
1645 1645 named_vars.update(match_obj.groupdict())
1646 1646 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1647 1647
1648 1648 def quote_cleaner(input_str):
1649 1649 """Remove quotes as it's HTML"""
1650 1650 return input_str.replace('"', '')
1651 1651
1652 1652 data = {
1653 1653 'pref': pref,
1654 1654 'cls': quote_cleaner('issue-tracker-link'),
1655 1655 'url': quote_cleaner(_url),
1656 1656 'id-repr': issue_id,
1657 1657 'issue-prefix': entry['pref'],
1658 1658 'serv': entry['url'],
1659 1659 }
1660 1660 if return_raw_data:
1661 1661 return {
1662 1662 'id': issue_id,
1663 1663 'url': _url
1664 1664 }
1665 1665 return tmpl % data
1666 1666
1667 1667
1668 1668 def get_active_pattern_entries(repo_name):
1669 1669 repo = None
1670 1670 if repo_name:
1671 1671 # Retrieving repo_name to avoid invalid repo_name to explode on
1672 1672 # IssueTrackerSettingsModel but still passing invalid name further down
1673 1673 repo = Repository.get_by_repo_name(repo_name, cache=True)
1674 1674
1675 1675 settings_model = IssueTrackerSettingsModel(repo=repo)
1676 1676 active_entries = settings_model.get_settings(cache=True)
1677 1677 return active_entries
1678 1678
1679 1679
1680 1680 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1681 1681
1682 1682 allowed_formats = ['html', 'rst', 'markdown']
1683 1683 if link_format not in allowed_formats:
1684 1684 raise ValueError('Link format can be only one of:{} got {}'.format(
1685 1685 allowed_formats, link_format))
1686 1686
1687 1687 active_entries = active_entries or get_active_pattern_entries(repo_name)
1688 1688 issues_data = []
1689 1689 newtext = text_string
1690 1690
1691 1691 for uid, entry in active_entries.items():
1692 1692 log.debug('found issue tracker entry with uid %s', uid)
1693 1693
1694 1694 if not (entry['pat'] and entry['url']):
1695 1695 log.debug('skipping due to missing data')
1696 1696 continue
1697 1697
1698 1698 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1699 1699 uid, entry['pat'], entry['url'], entry['pref'])
1700 1700
1701 1701 try:
1702 1702 pattern = re.compile(r'%s' % entry['pat'])
1703 1703 except re.error:
1704 1704 log.exception(
1705 1705 'issue tracker pattern: `%s` failed to compile',
1706 1706 entry['pat'])
1707 1707 continue
1708 1708
1709 1709 data_func = partial(
1710 1710 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1711 1711 return_raw_data=True)
1712 1712
1713 1713 for match_obj in pattern.finditer(text_string):
1714 1714 issues_data.append(data_func(match_obj))
1715 1715
1716 1716 url_func = partial(
1717 1717 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1718 1718 link_format=link_format)
1719 1719
1720 1720 newtext = pattern.sub(url_func, newtext)
1721 1721 log.debug('processed prefix:uid `%s`', uid)
1722 1722
1723 1723 return newtext, issues_data
1724 1724
1725 1725
1726 1726 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1727 1727 """
1728 1728 Parses given text message and makes proper links.
1729 1729 issues are linked to given issue-server, and rest is a commit link
1730 1730
1731 1731 :param commit_text:
1732 1732 :param repository:
1733 1733 """
1734 1734 def escaper(string):
1735 1735 return string.replace('<', '&lt;').replace('>', '&gt;')
1736 1736
1737 1737 newtext = escaper(commit_text)
1738 1738
1739 1739 # extract http/https links and make them real urls
1740 1740 newtext = urlify_text(newtext, safe=False)
1741 1741
1742 1742 # urlify commits - extract commit ids and make link out of them, if we have
1743 1743 # the scope of repository present.
1744 1744 if repository:
1745 1745 newtext = urlify_commits(newtext, repository)
1746 1746
1747 1747 # process issue tracker patterns
1748 1748 newtext, issues = process_patterns(newtext, repository or '',
1749 1749 active_entries=active_pattern_entries)
1750 1750
1751 1751 return literal(newtext)
1752 1752
1753 1753
1754 1754 def render_binary(repo_name, file_obj):
1755 1755 """
1756 1756 Choose how to render a binary file
1757 1757 """
1758 1758
1759 1759 filename = file_obj.name
1760 1760
1761 1761 # images
1762 1762 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1763 1763 if fnmatch.fnmatch(filename, pat=ext):
1764 1764 alt = escape(filename)
1765 1765 src = route_path(
1766 1766 'repo_file_raw', repo_name=repo_name,
1767 1767 commit_id=file_obj.commit.raw_id,
1768 1768 f_path=file_obj.path)
1769 1769 return literal(
1770 1770 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1771 1771
1772 1772
1773 1773 def renderer_from_filename(filename, exclude=None):
1774 1774 """
1775 1775 choose a renderer based on filename, this works only for text based files
1776 1776 """
1777 1777
1778 1778 # ipython
1779 1779 for ext in ['*.ipynb']:
1780 1780 if fnmatch.fnmatch(filename, pat=ext):
1781 1781 return 'jupyter'
1782 1782
1783 1783 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1784 1784 if is_markup:
1785 1785 return is_markup
1786 1786 return None
1787 1787
1788 1788
1789 1789 def render(source, renderer='rst', mentions=False, relative_urls=None,
1790 1790 repo_name=None):
1791 1791
1792 1792 def maybe_convert_relative_links(html_source):
1793 1793 if relative_urls:
1794 1794 return relative_links(html_source, relative_urls)
1795 1795 return html_source
1796 1796
1797 1797 if renderer == 'plain':
1798 1798 return literal(
1799 1799 MarkupRenderer.plain(source, leading_newline=False))
1800 1800
1801 1801 elif renderer == 'rst':
1802 1802 if repo_name:
1803 1803 # process patterns on comments if we pass in repo name
1804 1804 source, issues = process_patterns(
1805 1805 source, repo_name, link_format='rst')
1806 1806
1807 1807 return literal(
1808 1808 '<div class="rst-block">%s</div>' %
1809 1809 maybe_convert_relative_links(
1810 1810 MarkupRenderer.rst(source, mentions=mentions)))
1811 1811
1812 1812 elif renderer == 'markdown':
1813 1813 if repo_name:
1814 1814 # process patterns on comments if we pass in repo name
1815 1815 source, issues = process_patterns(
1816 1816 source, repo_name, link_format='markdown')
1817 1817
1818 1818 return literal(
1819 1819 '<div class="markdown-block">%s</div>' %
1820 1820 maybe_convert_relative_links(
1821 1821 MarkupRenderer.markdown(source, flavored=True,
1822 1822 mentions=mentions)))
1823 1823
1824 1824 elif renderer == 'jupyter':
1825 1825 return literal(
1826 1826 '<div class="ipynb">%s</div>' %
1827 1827 maybe_convert_relative_links(
1828 1828 MarkupRenderer.jupyter(source)))
1829 1829
1830 1830 # None means just show the file-source
1831 1831 return None
1832 1832
1833 1833
1834 1834 def commit_status(repo, commit_id):
1835 1835 return ChangesetStatusModel().get_status(repo, commit_id)
1836 1836
1837 1837
1838 1838 def commit_status_lbl(commit_status):
1839 1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1840 1840
1841 1841
1842 1842 def commit_time(repo_name, commit_id):
1843 1843 repo = Repository.get_by_repo_name(repo_name)
1844 1844 commit = repo.get_commit(commit_id=commit_id)
1845 1845 return commit.date
1846 1846
1847 1847
1848 1848 def get_permission_name(key):
1849 1849 return dict(Permission.PERMS).get(key)
1850 1850
1851 1851
1852 1852 def journal_filter_help(request):
1853 1853 _ = request.translate
1854 1854 from rhodecode.lib.audit_logger import ACTIONS
1855 1855 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1856 1856
1857 1857 return _(
1858 1858 'Example filter terms:\n' +
1859 1859 ' repository:vcs\n' +
1860 1860 ' username:marcin\n' +
1861 1861 ' username:(NOT marcin)\n' +
1862 1862 ' action:*push*\n' +
1863 1863 ' ip:127.0.0.1\n' +
1864 1864 ' date:20120101\n' +
1865 1865 ' date:[20120101100000 TO 20120102]\n' +
1866 1866 '\n' +
1867 1867 'Actions: {actions}\n' +
1868 1868 '\n' +
1869 1869 'Generate wildcards using \'*\' character:\n' +
1870 1870 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1871 1871 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1872 1872 '\n' +
1873 1873 'Optional AND / OR operators in queries\n' +
1874 1874 ' "repository:vcs OR repository:test"\n' +
1875 1875 ' "username:test AND repository:test*"\n'
1876 1876 ).format(actions=actions)
1877 1877
1878 1878
1879 1879 def not_mapped_error(repo_name):
1880 1880 from rhodecode.translation import _
1881 1881 flash(_('%s repository is not mapped to db perhaps'
1882 1882 ' it was created or renamed from the filesystem'
1883 1883 ' please run the application again'
1884 1884 ' in order to rescan repositories') % repo_name, category='error')
1885 1885
1886 1886
1887 1887 def ip_range(ip_addr):
1888 1888 from rhodecode.model.db import UserIpMap
1889 1889 s, e = UserIpMap._get_ip_range(ip_addr)
1890 1890 return '%s - %s' % (s, e)
1891 1891
1892 1892
1893 1893 def form(url, method='post', needs_csrf_token=True, **attrs):
1894 1894 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1895 1895 if method.lower() != 'get' and needs_csrf_token:
1896 1896 raise Exception(
1897 1897 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1898 1898 'CSRF token. If the endpoint does not require such token you can ' +
1899 1899 'explicitly set the parameter needs_csrf_token to false.')
1900 1900
1901 1901 return wh_form(url, method=method, **attrs)
1902 1902
1903 1903
1904 1904 def secure_form(form_url, method="POST", multipart=False, **attrs):
1905 1905 """Start a form tag that points the action to an url. This
1906 1906 form tag will also include the hidden field containing
1907 1907 the auth token.
1908 1908
1909 1909 The url options should be given either as a string, or as a
1910 1910 ``url()`` function. The method for the form defaults to POST.
1911 1911
1912 1912 Options:
1913 1913
1914 1914 ``multipart``
1915 1915 If set to True, the enctype is set to "multipart/form-data".
1916 1916 ``method``
1917 1917 The method to use when submitting the form, usually either
1918 1918 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1919 1919 hidden input with name _method is added to simulate the verb
1920 1920 over POST.
1921 1921
1922 1922 """
1923 1923 from webhelpers.pylonslib.secure_form import insecure_form
1924 1924
1925 1925 if 'request' in attrs:
1926 1926 session = attrs['request'].session
1927 1927 del attrs['request']
1928 1928 else:
1929 1929 raise ValueError(
1930 1930 'Calling this form requires request= to be passed as argument')
1931 1931
1932 1932 form = insecure_form(form_url, method, multipart, **attrs)
1933 1933 token = literal(
1934 1934 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1935 1935 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1936 1936
1937 1937 return literal("%s\n%s" % (form, token))
1938 1938
1939 1939
1940 1940 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1941 1941 select_html = select(name, selected, options, **attrs)
1942 1942 select2 = """
1943 1943 <script>
1944 1944 $(document).ready(function() {
1945 1945 $('#%s').select2({
1946 1946 containerCssClass: 'drop-menu',
1947 1947 dropdownCssClass: 'drop-menu-dropdown',
1948 1948 dropdownAutoWidth: true%s
1949 1949 });
1950 1950 });
1951 1951 </script>
1952 1952 """
1953 1953 filter_option = """,
1954 1954 minimumResultsForSearch: -1
1955 1955 """
1956 1956 input_id = attrs.get('id') or name
1957 1957 filter_enabled = "" if enable_filter else filter_option
1958 1958 select_script = literal(select2 % (input_id, filter_enabled))
1959 1959
1960 1960 return literal(select_html+select_script)
1961 1961
1962 1962
1963 1963 def get_visual_attr(tmpl_context_var, attr_name):
1964 1964 """
1965 1965 A safe way to get a variable from visual variable of template context
1966 1966
1967 1967 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1968 1968 :param attr_name: name of the attribute we fetch from the c.visual
1969 1969 """
1970 1970 visual = getattr(tmpl_context_var, 'visual', None)
1971 1971 if not visual:
1972 1972 return
1973 1973 else:
1974 1974 return getattr(visual, attr_name, None)
1975 1975
1976 1976
1977 1977 def get_last_path_part(file_node):
1978 1978 if not file_node.path:
1979 1979 return u'/'
1980 1980
1981 1981 path = safe_unicode(file_node.path.split('/')[-1])
1982 1982 return u'../' + path
1983 1983
1984 1984
1985 1985 def route_url(*args, **kwargs):
1986 1986 """
1987 1987 Wrapper around pyramids `route_url` (fully qualified url) function.
1988 1988 """
1989 1989 req = get_current_request()
1990 1990 return req.route_url(*args, **kwargs)
1991 1991
1992 1992
1993 1993 def route_path(*args, **kwargs):
1994 1994 """
1995 1995 Wrapper around pyramids `route_path` function.
1996 1996 """
1997 1997 req = get_current_request()
1998 1998 return req.route_path(*args, **kwargs)
1999 1999
2000 2000
2001 2001 def route_path_or_none(*args, **kwargs):
2002 2002 try:
2003 2003 return route_path(*args, **kwargs)
2004 2004 except KeyError:
2005 2005 return None
2006 2006
2007 2007
2008 2008 def current_route_path(request, **kw):
2009 2009 new_args = request.GET.mixed()
2010 2010 new_args.update(kw)
2011 2011 return request.current_route_path(_query=new_args)
2012 2012
2013 2013
2014 2014 def api_call_example(method, args):
2015 2015 """
2016 2016 Generates an API call example via CURL
2017 2017 """
2018 2018 args_json = json.dumps(OrderedDict([
2019 2019 ('id', 1),
2020 2020 ('auth_token', 'SECRET'),
2021 2021 ('method', method),
2022 2022 ('args', args)
2023 2023 ]))
2024 2024 return literal(
2025 2025 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2026 2026 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2027 2027 "and needs to be of `api calls` role."
2028 2028 .format(
2029 2029 api_url=route_url('apiv2'),
2030 2030 token_url=route_url('my_account_auth_tokens'),
2031 2031 data=args_json))
2032 2032
2033 2033
2034 2034 def notification_description(notification, request):
2035 2035 """
2036 2036 Generate notification human readable description based on notification type
2037 2037 """
2038 2038 from rhodecode.model.notification import NotificationModel
2039 2039 return NotificationModel().make_description(
2040 2040 notification, translate=request.translate)
2041 2041
2042 2042
2043 2043 def go_import_header(request, db_repo=None):
2044 2044 """
2045 2045 Creates a header for go-import functionality in Go Lang
2046 2046 """
2047 2047
2048 2048 if not db_repo:
2049 2049 return
2050 2050 if 'go-get' not in request.GET:
2051 2051 return
2052 2052
2053 2053 clone_url = db_repo.clone_url()
2054 2054 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2055 2055 # we have a repo and go-get flag,
2056 2056 return literal('<meta name="go-import" content="{} {} {}">'.format(
2057 2057 prefix, db_repo.repo_type, clone_url))
2058 2058
2059 2059
2060 2060 def reviewer_as_json(*args, **kwargs):
2061 2061 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2062 2062 return _reviewer_as_json(*args, **kwargs)
2063 2063
2064 2064
2065 2065 def get_repo_view_type(request):
2066 2066 route_name = request.matched_route.name
2067 2067 route_to_view_type = {
2068 2068 'repo_changelog': 'commits',
2069 2069 'repo_commits': 'commits',
2070 2070 'repo_files': 'files',
2071 2071 'repo_summary': 'summary',
2072 2072 'repo_commit': 'commit'
2073 2073 }
2074 2074
2075 2075 return route_to_view_type.get(route_name)
@@ -1,114 +1,116 b''
1 1 <%inherit file="/summary/summary_base.mako"/>
2 2
3 3 <%namespace name="components" file="/summary/components.mako"/>
4 4
5 5
6 6 <%def name="menu_bar_subnav()">
7 7 ${self.repo_menu(active='summary')}
8 8 </%def>
9 9
10 10 <%def name="main()">
11 11
12 12 <div id="repo-summary" class="summary">
13 13 ${components.summary_detail(breadcrumbs_links=self.breadcrumbs_links(), show_downloads=True)}
14 14 </div><!--end repo-summary-->
15 15
16 16
17 17 <div class="box">
18 18 %if not c.repo_commits:
19 19 <div class="empty-repo">
20 20 <div class="title">
21 21 <h3>${_('Quick start')}</h3>
22 22 </div>
23 23 <div class="clear-fix"></div>
24 24 </div>
25 25 %endif
26 26 <div class="table">
27 27 <div id="shortlog_data">
28 28 <%include file='summary_commits.mako'/>
29 29 </div>
30 30 </div>
31 31 </div>
32 32
33 33 %if c.readme_data:
34 34 <div id="readme" class="anchor">
35 35 <div class="box">
36 36 <div class="title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_rev[0], c.rhodecode_db_repo.landing_rev[1]))}">
37 37 <h3 class="breadcrumbs">
38 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_rev[1],f_path=c.readme_file)}">${c.readme_file}</a>
38 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_rev[1],f_path=c.readme_file)}">
39 ${c.readme_file}
40 </a>
39 41 </h3>
40 42 </div>
41 43 <div class="readme codeblock">
42 44 <div class="readme_box">
43 45 ${c.readme_data|n}
44 46 </div>
45 47 </div>
46 48 </div>
47 49 </div>
48 50 %endif
49 51
50 52 <script type="text/javascript">
51 53 $(document).ready(function(){
52 54
53 55 var showCloneField = function(clone_url_format){
54 56 $.each(['http', 'http_id', 'ssh'], function (idx, val) {
55 57 if(val === clone_url_format){
56 58 $('#clone_option_' + val).show();
57 59 $('#clone_option').val(val)
58 60 } else {
59 61 $('#clone_option_' + val).hide();
60 62 }
61 63 });
62 64 };
63 65 // default taken from session
64 66 showCloneField(templateContext.session_attrs.clone_url_format);
65 67
66 68 $('#clone_option').on('change', function(e) {
67 69 var selected = $(this).val();
68 70
69 71 storeUserSessionAttr('rc_user_session_attr.clone_url_format', selected);
70 72 showCloneField(selected)
71 73 });
72 74
73 75 var initialCommitData = {
74 76 id: null,
75 77 text: 'tip',
76 78 type: 'tag',
77 79 raw_id: null,
78 80 files_url: null
79 81 };
80 82
81 83 select2RefSwitcher('#download_options', initialCommitData);
82 84
83 85 // on change of download options
84 86 $('#download_options').on('change', function(e) {
85 87 // format of Object {text: "v0.0.3", type: "tag", id: "rev"}
86 88 var ext = '.zip';
87 89 var selected_cs = e.added;
88 90 var fname = e.added.raw_id + ext;
89 91 var href = pyroutes.url('repo_archivefile', {'repo_name': templateContext.repo_name, 'fname':fname});
90 92 // set new label
91 93 $('#archive_link').html('<i class="icon-archive"></i> {0}{1}'.format(escapeHtml(e.added.text), ext));
92 94
93 95 // set new url to button,
94 96 $('#archive_link').attr('href', href)
95 97 });
96 98
97 99
98 100 // calculate size of repository
99 101 calculateSize = function () {
100 102
101 103 var callback = function (data) {
102 104 % if c.show_stats:
103 105 showRepoStats('lang_stats', data);
104 106 % endif
105 107 };
106 108
107 109 showRepoSize('repo_size_container', templateContext.repo_name, templateContext.repo_landing_commit, callback);
108 110
109 111 }
110 112
111 113 })
112 114 </script>
113 115
114 116 </%def>
General Comments 0
You need to be logged in to leave comments. Login now