##// END OF EJS Templates
search: add syntax highlighting, line numbers and line context...
dan -
r48:5de64805 default
parent child Browse files
Show More
@@ -0,0 +1,490 b''
1 /*!***************************************************
2 * mark.js v6.1.0
3 * https://github.com/julmot/mark.js
4 * Copyright (c) 2014–2016, Julian Motz
5 * Released under the MIT license https://git.io/vwTVl
6 *****************************************************/
7
8 "use strict";
9
10 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
11
12 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
13
14 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
15
16 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
17
18 (function (factory, window, document) {
19 if (typeof define === "function" && define.amd) {
20 define(["jquery"], function (jQuery) {
21 return factory(window, document, jQuery);
22 });
23 } else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object") {
24 factory(window, document, require("jquery"));
25 } else {
26 factory(window, document, jQuery);
27 }
28 })(function (window, document, $) {
29 var Mark = function () {
30 function Mark(ctx) {
31 _classCallCheck(this, Mark);
32
33 this.ctx = ctx;
34 }
35
36 _createClass(Mark, [{
37 key: "log",
38 value: function log(msg) {
39 var level = arguments.length <= 1 || arguments[1] === undefined ? "debug" : arguments[1];
40
41 var log = this.opt.log;
42 if (!this.opt.debug) {
43 return;
44 }
45 if ((typeof log === "undefined" ? "undefined" : _typeof(log)) === "object" && typeof log[level] === "function") {
46 log[level]("mark.js: " + msg);
47 }
48 }
49 }, {
50 key: "escapeStr",
51 value: function escapeStr(str) {
52 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
53 }
54 }, {
55 key: "createRegExp",
56 value: function createRegExp(str) {
57 str = this.escapeStr(str);
58 if (Object.keys(this.opt.synonyms).length) {
59 str = this.createSynonymsRegExp(str);
60 }
61 if (this.opt.diacritics) {
62 str = this.createDiacriticsRegExp(str);
63 }
64 str = this.createAccuracyRegExp(str);
65 return str;
66 }
67 }, {
68 key: "createSynonymsRegExp",
69 value: function createSynonymsRegExp(str) {
70 var syn = this.opt.synonyms;
71 for (var index in syn) {
72 if (syn.hasOwnProperty(index)) {
73 var value = syn[index],
74 k1 = this.escapeStr(index),
75 k2 = this.escapeStr(value);
76 str = str.replace(new RegExp("(" + k1 + "|" + k2 + ")", "gmi"), "(" + k1 + "|" + k2 + ")");
77 }
78 }
79 return str;
80 }
81 }, {
82 key: "createDiacriticsRegExp",
83 value: function createDiacriticsRegExp(str) {
84 var dct = ["aÀÁÂÃÄÅàáâãäåĀāąĄ", "cÇçćĆčČ", "dđĐďĎ", "eÈÉÊËèéêëěĚĒēęĘ", "iÌÍÎÏìíîïĪī", "lłŁ", "nÑñňŇńŃ", "oÒÓÔÕÕÖØòóôõöøŌō", "rřŘ", "sŠšśŚ", "tťŤ", "uÙÚÛÜùúûüůŮŪū", "yŸÿýÝ", "zŽžżŻźŹ"];
85 var handled = [];
86 str.split("").forEach(function (ch) {
87 dct.every(function (dct) {
88 if (dct.indexOf(ch) !== -1) {
89 if (handled.indexOf(dct) > -1) {
90 return false;
91 }
92
93 str = str.replace(new RegExp("[" + dct + "]", "gmi"), "[" + dct + "]");
94 handled.push(dct);
95 }
96 return true;
97 });
98 });
99 return str;
100 }
101 }, {
102 key: "createAccuracyRegExp",
103 value: function createAccuracyRegExp(str) {
104 switch (this.opt.accuracy) {
105 case "partially":
106 return "()(" + str + ")";
107 case "complementary":
108 return "()(\\S*" + str + "\\S*)";
109 case "exactly":
110 return "(^|\\s)(" + str + ")(?=\\s|$)";
111 }
112 }
113 }, {
114 key: "getSeparatedKeywords",
115 value: function getSeparatedKeywords(sv) {
116 var _this = this;
117
118 var stack = [];
119 sv.forEach(function (kw) {
120 if (!_this.opt.separateWordSearch) {
121 if (kw.trim()) {
122 stack.push(kw);
123 }
124 } else {
125 kw.split(" ").forEach(function (kwSplitted) {
126 if (kwSplitted.trim()) {
127 stack.push(kwSplitted);
128 }
129 });
130 }
131 });
132 return {
133 "keywords": stack,
134 "length": stack.length
135 };
136 }
137 }, {
138 key: "getElements",
139 value: function getElements() {
140 var ctx = void 0,
141 stack = [];
142 if (typeof this.ctx === "undefined") {
143 ctx = [];
144 } else if (this.ctx instanceof HTMLElement) {
145 ctx = [this.ctx];
146 } else if (Array.isArray(this.ctx)) {
147 ctx = this.ctx;
148 } else {
149 ctx = Array.prototype.slice.call(this.ctx);
150 }
151 ctx.forEach(function (ctx) {
152 stack.push(ctx);
153 var childs = ctx.querySelectorAll("*");
154 if (childs.length) {
155 stack = stack.concat(Array.prototype.slice.call(childs));
156 }
157 });
158 if (!ctx.length) {
159 this.log("Empty context", "warn");
160 }
161 return {
162 "elements": stack,
163 "length": stack.length
164 };
165 }
166 }, {
167 key: "matches",
168 value: function matches(el, selector) {
169 return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
170 }
171 }, {
172 key: "matchesFilter",
173 value: function matchesFilter(el, exclM) {
174 var _this2 = this;
175
176 var remain = true;
177 var fltr = this.opt.filter.concat(["script", "style", "title"]);
178 if (!this.opt.iframes) {
179 fltr = fltr.concat(["iframe"]);
180 }
181 if (exclM) {
182 fltr = fltr.concat(["*[data-markjs='true']"]);
183 }
184 fltr.every(function (filter) {
185 if (_this2.matches(el, filter)) {
186 return remain = false;
187 }
188 return true;
189 });
190 return !remain;
191 }
192 }, {
193 key: "onIframeReady",
194 value: function onIframeReady(ifr, successFn, errorFn) {
195 try {
196 (function () {
197 var ifrWin = ifr.contentWindow,
198 bl = "about:blank",
199 compl = "complete";
200 var callCallback = function callCallback() {
201 try {
202 if (ifrWin.document === null) {
203 throw new Error("iframe inaccessible");
204 }
205 successFn(ifrWin.document);
206 } catch (e) {
207 errorFn();
208 }
209 };
210 var isBlank = function isBlank() {
211 var src = ifr.getAttribute("src").trim(),
212 href = ifrWin.location.href;
213 return href === bl && src !== bl && src;
214 };
215 var observeOnload = function observeOnload() {
216 var listener = function listener() {
217 try {
218 if (!isBlank()) {
219 ifr.removeEventListener("load", listener);
220 callCallback();
221 }
222 } catch (e) {
223 errorFn();
224 }
225 };
226 ifr.addEventListener("load", listener);
227 };
228 if (ifrWin.document.readyState === compl) {
229 if (isBlank()) {
230 observeOnload();
231 } else {
232 callCallback();
233 }
234 } else {
235 observeOnload();
236 }
237 })();
238 } catch (e) {
239 errorFn();
240 }
241 }
242 }, {
243 key: "forEachElementInIframe",
244 value: function forEachElementInIframe(ifr, cb) {
245 var _this3 = this;
246
247 var end = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2];
248
249 var open = 0;
250 var checkEnd = function checkEnd() {
251 if (--open < 1) {
252 end();
253 }
254 };
255 this.onIframeReady(ifr, function (con) {
256 var stack = Array.prototype.slice.call(con.querySelectorAll("*"));
257 if ((open = stack.length) === 0) {
258 checkEnd();
259 }
260 stack.forEach(function (el) {
261 if (el.tagName.toLowerCase() === "iframe") {
262 (function () {
263 var j = 0;
264 _this3.forEachElementInIframe(el, function (iel, len) {
265 cb(iel, len);
266 if (len - 1 === j) {
267 checkEnd();
268 }
269 j++;
270 }, checkEnd);
271 })();
272 } else {
273 cb(el, stack.length);
274 checkEnd();
275 }
276 });
277 }, function () {
278 var src = ifr.getAttribute("src");
279 _this3.log("iframe '" + src + "' could not be accessed", "warn");
280 checkEnd();
281 });
282 }
283 }, {
284 key: "forEachElement",
285 value: function forEachElement(cb) {
286 var _this4 = this;
287
288 var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
289 var exclM = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
290
291 var _getElements = this.getElements();
292
293 var stack = _getElements.elements;
294 var open = _getElements.length;
295
296 var checkEnd = function checkEnd() {
297 if (--open === 0) {
298 end();
299 }
300 };
301 checkEnd(++open);
302 stack.forEach(function (el) {
303 if (!_this4.matchesFilter(el, exclM)) {
304 if (el.tagName.toLowerCase() === "iframe") {
305 _this4.forEachElementInIframe(el, function (iel) {
306 if (!_this4.matchesFilter(iel, exclM)) {
307 cb(iel);
308 }
309 }, checkEnd);
310 return;
311 } else {
312 cb(el);
313 }
314 }
315 checkEnd();
316 });
317 }
318 }, {
319 key: "forEachNode",
320 value: function forEachNode(cb) {
321 var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
322
323 this.forEachElement(function (n) {
324 for (n = n.firstChild; n; n = n.nextSibling) {
325 if (n.nodeType === 3 && n.textContent.trim()) {
326 cb(n);
327 }
328 }
329 }, end);
330 }
331 }, {
332 key: "wrapMatches",
333 value: function wrapMatches(node, regex, custom, cb) {
334 var hEl = !this.opt.element ? "mark" : this.opt.element,
335 index = custom ? 0 : 2;
336 var match = void 0;
337 while ((match = regex.exec(node.textContent)) !== null) {
338 var pos = match.index;
339 if (!custom) {
340 pos += match[index - 1].length;
341 }
342 var startNode = node.splitText(pos);
343
344 node = startNode.splitText(match[index].length);
345 if (startNode.parentNode !== null) {
346 var repl = document.createElement(hEl);
347 repl.setAttribute("data-markjs", "true");
348 if (this.opt.className) {
349 repl.setAttribute("class", this.opt.className);
350 }
351 repl.textContent = match[index];
352 startNode.parentNode.replaceChild(repl, startNode);
353 cb(repl);
354 }
355 regex.lastIndex = 0;
356 }
357 }
358 }, {
359 key: "unwrapMatches",
360 value: function unwrapMatches(node) {
361 var parent = node.parentNode;
362 var docFrag = document.createDocumentFragment();
363 while (node.firstChild) {
364 docFrag.appendChild(node.removeChild(node.firstChild));
365 }
366 parent.replaceChild(docFrag, node);
367 parent.normalize();
368 }
369 }, {
370 key: "markRegExp",
371 value: function markRegExp(regexp, opt) {
372 var _this5 = this;
373
374 this.opt = opt;
375 this.log("Searching with expression \"" + regexp + "\"");
376 var found = false;
377 var eachCb = function eachCb(element) {
378 found = true;
379 _this5.opt.each(element);
380 };
381 this.forEachNode(function (node) {
382 _this5.wrapMatches(node, regexp, true, eachCb);
383 }, function () {
384 if (!found) {
385 _this5.opt.noMatch(regexp);
386 }
387 _this5.opt.complete();
388 _this5.opt.done();
389 });
390 }
391 }, {
392 key: "mark",
393 value: function mark(sv, opt) {
394 var _this6 = this;
395
396 this.opt = opt;
397 sv = typeof sv === "string" ? [sv] : sv;
398
399 var _getSeparatedKeywords = this.getSeparatedKeywords(sv);
400
401 var kwArr = _getSeparatedKeywords.keywords;
402 var kwArrLen = _getSeparatedKeywords.length;
403
404 if (kwArrLen === 0) {
405 this.opt.complete();
406 this.opt.done();
407 }
408 kwArr.forEach(function (kw) {
409 var regex = new RegExp(_this6.createRegExp(kw), "gmi"),
410 found = false;
411 var eachCb = function eachCb(element) {
412 found = true;
413 _this6.opt.each(element);
414 };
415 _this6.log("Searching with expression \"" + regex + "\"");
416 _this6.forEachNode(function (node) {
417 _this6.wrapMatches(node, regex, false, eachCb);
418 }, function () {
419 if (!found) {
420 _this6.opt.noMatch(kw);
421 }
422 if (kwArr[kwArrLen - 1] === kw) {
423 _this6.opt.complete();
424 _this6.opt.done();
425 }
426 });
427 });
428 }
429 }, {
430 key: "unmark",
431 value: function unmark(opt) {
432 var _this7 = this;
433
434 this.opt = opt;
435 var sel = this.opt.element ? this.opt.element : "*";
436 sel += "[data-markjs]";
437 if (this.opt.className) {
438 sel += "." + this.opt.className;
439 }
440 this.log("Removal selector \"" + sel + "\"");
441 this.forEachElement(function (el) {
442 if (_this7.matches(el, sel)) {
443 _this7.unwrapMatches(el);
444 }
445 }, function () {
446 _this7.opt.complete();
447 _this7.opt.done();
448 }, false);
449 }
450 }, {
451 key: "opt",
452 set: function set(val) {
453 this._opt = _extends({}, {
454 "element": "",
455 "className": "",
456 "filter": [],
457 "iframes": false,
458 "separateWordSearch": true,
459 "diacritics": true,
460 "synonyms": {},
461 "accuracy": "partially",
462 "each": function each() {},
463 "noMatch": function noMatch() {},
464 "done": function done() {},
465 "complete": function complete() {},
466 "debug": false,
467 "log": window.console
468 }, val);
469 },
470 get: function get() {
471 return this._opt;
472 }
473 }]);
474
475 return Mark;
476 }();
477
478 $.fn.mark = function (sv, opt) {
479 new Mark(this).mark(sv, opt);
480 return this;
481 };
482 $.fn.markRegExp = function (regexp, opt) {
483 new Mark(this).markRegExp(regexp, opt);
484 return this;
485 };
486 $.fn.unmark = function (opt) {
487 new Mark(this).unmark(opt);
488 return this;
489 };
490 }, window, document);
@@ -1,137 +1,139 b''
1 1 module.exports = function(grunt) {
2 2 grunt.initConfig({
3 3
4 4 dirs: {
5 5 css: "rhodecode/public/css",
6 6 js: {
7 7 "src": "rhodecode/public/js/src",
8 8 "dest": "rhodecode/public/js"
9 9 }
10 10 },
11 11
12 12 concat: {
13 13 dist: {
14 14 src: [
15 15 // Base libraries
16 16 '<%= dirs.js.src %>/jquery-1.11.1.min.js',
17 17 '<%= dirs.js.src %>/logging.js',
18 18 '<%= dirs.js.src %>/bootstrap.js',
19 19 '<%= dirs.js.src %>/mousetrap.js',
20 20 '<%= dirs.js.src %>/moment.js',
21 '<%= dirs.js.src %>/moment.js',
21 22 '<%= dirs.js.src %>/appenlight-client-0.4.1.min.js',
22 23
23 24 // Plugins
24 25 '<%= dirs.js.src %>/plugins/jquery.pjax.js',
25 26 '<%= dirs.js.src %>/plugins/jquery.dataTables.js',
26 27 '<%= dirs.js.src %>/plugins/flavoured_checkbox.js',
27 28 '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
28 29 '<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
29 30 '<%= dirs.js.src %>/plugins/jquery.debounce.js',
31 '<%= dirs.js.src %>/plugins/jquery.mark.js',
30 32 '<%= dirs.js.src %>/plugins/jquery.timeago.js',
31 33 '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
32 34
33 35 // Select2
34 36 '<%= dirs.js.src %>/select2/select2.js',
35 37
36 38 // Code-mirror
37 39 '<%= dirs.js.src %>/codemirror/codemirror.js',
38 40 '<%= dirs.js.src %>/codemirror/codemirror_loadmode.js',
39 41 '<%= dirs.js.src %>/codemirror/codemirror_hint.js',
40 42 '<%= dirs.js.src %>/codemirror/codemirror_overlay.js',
41 43 '<%= dirs.js.src %>/codemirror/codemirror_placeholder.js',
42 44 // TODO: mikhail: this is an exception. Since the code mirror modes
43 45 // are loaded "on the fly", we need to keep them in a public folder
44 46 '<%= dirs.js.dest %>/mode/meta.js',
45 47 '<%= dirs.js.dest %>/mode/meta_ext.js',
46 48 '<%= dirs.js.dest %>/rhodecode/i18n/select2/translations.js',
47 49
48 50 // Rhodecode utilities
49 51 '<%= dirs.js.src %>/rhodecode/utils/array.js',
50 52 '<%= dirs.js.src %>/rhodecode/utils/string.js',
51 53 '<%= dirs.js.src %>/rhodecode/utils/pyroutes.js',
52 54 '<%= dirs.js.src %>/rhodecode/utils/ajax.js',
53 55 '<%= dirs.js.src %>/rhodecode/utils/autocomplete.js',
54 56 '<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js',
55 57 '<%= dirs.js.src %>/rhodecode/utils/ie.js',
56 58 '<%= dirs.js.src %>/rhodecode/utils/os.js',
57 59
58 60 // Rhodecode widgets
59 61 '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js',
60 62
61 63 // Rhodecode components
62 64 '<%= dirs.js.src %>/rhodecode/pyroutes.js',
63 65 '<%= dirs.js.src %>/rhodecode/codemirror.js',
64 66 '<%= dirs.js.src %>/rhodecode/comments.js',
65 67 '<%= dirs.js.src %>/rhodecode/constants.js',
66 68 '<%= dirs.js.src %>/rhodecode/files.js',
67 69 '<%= dirs.js.src %>/rhodecode/followers.js',
68 70 '<%= dirs.js.src %>/rhodecode/menus.js',
69 71 '<%= dirs.js.src %>/rhodecode/notifications.js',
70 72 '<%= dirs.js.src %>/rhodecode/permissions.js',
71 73 '<%= dirs.js.src %>/rhodecode/pjax.js',
72 74 '<%= dirs.js.src %>/rhodecode/pullrequests.js',
73 75 '<%= dirs.js.src %>/rhodecode/settings.js',
74 76 '<%= dirs.js.src %>/rhodecode/select2_widgets.js',
75 77 '<%= dirs.js.src %>/rhodecode/tooltips.js',
76 78 '<%= dirs.js.src %>/rhodecode/users.js',
77 79 '<%= dirs.js.src %>/rhodecode/appenlight.js',
78 80
79 81 // Rhodecode main module
80 82 '<%= dirs.js.src %>/rhodecode.js'
81 83 ],
82 84 dest: '<%= dirs.js.dest %>/scripts.js',
83 85 nonull: true
84 86 }
85 87 },
86 88
87 89 less: {
88 90 development: {
89 91 options: {
90 92 compress: false,
91 93 yuicompress: false,
92 94 optimization: 0
93 95 },
94 96 files: {
95 97 "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less"
96 98 }
97 99 },
98 100 production: {
99 101 options: {
100 102 compress: true,
101 103 yuicompress: true,
102 104 optimization: 2
103 105 },
104 106 files: {
105 107 "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less"
106 108 }
107 109 }
108 110 },
109 111
110 112 watch: {
111 113 less: {
112 114 files: ["<%= dirs.css %>/*.less"],
113 115 tasks: ["less:production"]
114 116 },
115 117 js: {
116 118 files: ["<%= dirs.js.src %>/**/*.js"],
117 119 tasks: ["concat:dist"]
118 120 }
119 121 },
120 122
121 123 jshint: {
122 124 rhodecode: {
123 125 src: '<%= dirs.js.src %>/rhodecode/**/*.js',
124 126 options: {
125 127 jshintrc: '.jshintrc'
126 128 }
127 129 }
128 130 }
129 131 });
130 132
131 133 grunt.loadNpmTasks('grunt-contrib-less');
132 134 grunt.loadNpmTasks('grunt-contrib-concat');
133 135 grunt.loadNpmTasks('grunt-contrib-watch');
134 136 grunt.loadNpmTasks('grunt-contrib-jshint');
135 137
136 138 grunt.registerTask('default', ['less:production', 'concat:dist']);
137 139 };
@@ -1,106 +1,107 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Search controller for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import urllib
27 27
28 28 from pylons import request, config, tmpl_context as c
29 29
30 30 from webhelpers.util import update_params
31 31
32 32 from rhodecode.lib.auth import LoginRequired, AuthUser
33 33 from rhodecode.lib.base import BaseRepoController, render
34 34 from rhodecode.lib.helpers import Page
35 35 from rhodecode.lib.utils2 import safe_str, safe_int
36 36 from rhodecode.lib.index import searcher_from_config
37 37 from rhodecode.model import validation_schema
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class SearchController(BaseRepoController):
43 43
44 44 @LoginRequired()
45 45 def index(self, repo_name=None):
46 46
47 47 searcher = searcher_from_config(config)
48 48 formatted_results = []
49 49 execution_time = ''
50 50
51 51 schema = validation_schema.SearchParamsSchema()
52 52
53 53 search_params = {}
54 54 errors = []
55 55 try:
56 56 search_params = schema.deserialize(
57 57 dict(search_query=request.GET.get('q'),
58 58 search_type=request.GET.get('type'),
59 59 page_limit=request.GET.get('page_limit'),
60 60 requested_page=request.GET.get('page'))
61 61 )
62 62 except validation_schema.Invalid as e:
63 63 errors = e.children
64 64
65 65 search_query = search_params.get('search_query')
66 66 search_type = search_params.get('search_type')
67 67
68 68 if search_params.get('search_query'):
69 69 page_limit = search_params['page_limit']
70 70 requested_page = search_params['requested_page']
71 71
72 72 def url_generator(**kw):
73 73 q = urllib.quote(safe_str(search_query))
74 74 return update_params(
75 75 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
76 76
77 77 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
78 78 ip_addr=self.ip_addr)
79 79
80 80 try:
81 81 search_result = searcher.search(
82 search_query, search_type, c.perm_user, repo_name)
82 search_query, search_type, c.perm_user, repo_name,
83 requested_page, page_limit)
83 84
84 85 formatted_results = Page(
85 86 search_result['results'], page=requested_page,
86 87 item_count=search_result['count'],
87 88 items_per_page=page_limit, url=url_generator)
88 89 finally:
89 90 searcher.cleanup()
90 91
91 92 if not search_result['error']:
92 93 execution_time = '%s results (%.3f seconds)' % (
93 94 search_result['count'],
94 95 search_result['runtime'])
95 96 elif not errors:
96 97 node = schema['search_query']
97 98 errors = [
98 99 validation_schema.Invalid(node, search_result['error'])]
99 100
100 101 c.errors = errors
101 102 c.formatted_results = formatted_results
102 103 c.runtime = execution_time
103 104 c.cur_query = search_query
104 105 c.search_type = search_type
105 106 # Return a rendered template
106 107 return render('/search/search.html')
@@ -1,1712 +1,1885 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 import pygments
39 40
40 41 from datetime import datetime
41 42 from functools import partial
42 43 from pygments.formatters.html import HtmlFormatter
43 44 from pygments import highlight as code_highlight
45 from pygments.lexers import (
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
44 47 from pylons import url
45 48 from pylons.i18n.translation import _, ungettext
46 49 from pyramid.threadlocal import get_current_request
47 50
48 51 from webhelpers.html import literal, HTML, escape
49 52 from webhelpers.html.tools import *
50 53 from webhelpers.html.builder import make_tag
51 54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
52 55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
53 56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
54 57 submit, text, password, textarea, title, ul, xml_declaration, radio
55 58 from webhelpers.html.tools import auto_link, button_to, highlight, \
56 59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
57 60 from webhelpers.pylonslib import Flash as _Flash
58 61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
59 62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
60 63 replace_whitespace, urlify, truncate, wrap_paragraphs
61 64 from webhelpers.date import time_ago_in_words
62 65 from webhelpers.paginate import Page as _Page
63 66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
64 67 convert_boolean_attrs, NotGiven, _make_safe_id_component
65 68 from webhelpers2.number import format_byte_size
66 69
67 70 from rhodecode.lib.annotate import annotate_highlight
68 71 from rhodecode.lib.action_parser import action_parser
69 72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
70 73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
71 74 get_commit_safe, datetime_to_time, time_to_datetime, AttributeDict, \
72 75 safe_int, md5, md5_safe
73 76 from rhodecode.lib.markup_renderer import MarkupRenderer
74 77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
75 78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
76 79 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
77 80 from rhodecode.model.changeset_status import ChangesetStatusModel
78 81 from rhodecode.model.db import Permission, User, Repository
79 82 from rhodecode.model.repo_group import RepoGroupModel
80 83 from rhodecode.model.settings import IssueTrackerSettingsModel
81 84
82 85 log = logging.getLogger(__name__)
83 86
84 87 DEFAULT_USER = User.DEFAULT_USER
85 88 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
86 89
87 90
88 91 def html_escape(text, html_escape_table=None):
89 92 """Produce entities within text."""
90 93 if not html_escape_table:
91 94 html_escape_table = {
92 95 "&": "&amp;",
93 96 '"': "&quot;",
94 97 "'": "&apos;",
95 98 ">": "&gt;",
96 99 "<": "&lt;",
97 100 }
98 101 return "".join(html_escape_table.get(c, c) for c in text)
99 102
100 103
101 104 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
102 105 """
103 106 Truncate string ``s`` at the first occurrence of ``sub``.
104 107
105 108 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
106 109 """
107 110 suffix_if_chopped = suffix_if_chopped or ''
108 111 pos = s.find(sub)
109 112 if pos == -1:
110 113 return s
111 114
112 115 if inclusive:
113 116 pos += len(sub)
114 117
115 118 chopped = s[:pos]
116 119 left = s[pos:].strip()
117 120
118 121 if left and suffix_if_chopped:
119 122 chopped += suffix_if_chopped
120 123
121 124 return chopped
122 125
123 126
124 127 def shorter(text, size=20):
125 128 postfix = '...'
126 129 if len(text) > size:
127 130 return text[:size - len(postfix)] + postfix
128 131 return text
129 132
130 133
131 134 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
132 135 """
133 136 Reset button
134 137 """
135 138 _set_input_attrs(attrs, type, name, value)
136 139 _set_id_attr(attrs, id, name)
137 140 convert_boolean_attrs(attrs, ["disabled"])
138 141 return HTML.input(**attrs)
139 142
140 143 reset = _reset
141 144 safeid = _make_safe_id_component
142 145
143 146
144 147 def branding(name, length=40):
145 148 return truncate(name, length, indicator="")
146 149
147 150
148 151 def FID(raw_id, path):
149 152 """
150 153 Creates a unique ID for filenode based on it's hash of path and commit
151 154 it's safe to use in urls
152 155
153 156 :param raw_id:
154 157 :param path:
155 158 """
156 159
157 160 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
158 161
159 162
160 163 class _GetError(object):
161 164 """Get error from form_errors, and represent it as span wrapped error
162 165 message
163 166
164 167 :param field_name: field to fetch errors for
165 168 :param form_errors: form errors dict
166 169 """
167 170
168 171 def __call__(self, field_name, form_errors):
169 172 tmpl = """<span class="error_msg">%s</span>"""
170 173 if form_errors and field_name in form_errors:
171 174 return literal(tmpl % form_errors.get(field_name))
172 175
173 176 get_error = _GetError()
174 177
175 178
176 179 class _ToolTip(object):
177 180
178 181 def __call__(self, tooltip_title, trim_at=50):
179 182 """
180 183 Special function just to wrap our text into nice formatted
181 184 autowrapped text
182 185
183 186 :param tooltip_title:
184 187 """
185 188 tooltip_title = escape(tooltip_title)
186 189 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
187 190 return tooltip_title
188 191 tooltip = _ToolTip()
189 192
190 193
191 194 def files_breadcrumbs(repo_name, commit_id, file_path):
192 195 if isinstance(file_path, str):
193 196 file_path = safe_unicode(file_path)
194 197
195 198 # TODO: johbo: Is this always a url like path, or is this operating
196 199 # system dependent?
197 200 path_segments = file_path.split('/')
198 201
199 202 repo_name_html = escape(repo_name)
200 203 if len(path_segments) == 1 and path_segments[0] == '':
201 204 url_segments = [repo_name_html]
202 205 else:
203 206 url_segments = [
204 207 link_to(
205 208 repo_name_html,
206 209 url('files_home',
207 210 repo_name=repo_name,
208 211 revision=commit_id,
209 212 f_path=''),
210 213 class_='pjax-link')]
211 214
212 215 last_cnt = len(path_segments) - 1
213 216 for cnt, segment in enumerate(path_segments):
214 217 if not segment:
215 218 continue
216 219 segment_html = escape(segment)
217 220
218 221 if cnt != last_cnt:
219 222 url_segments.append(
220 223 link_to(
221 224 segment_html,
222 225 url('files_home',
223 226 repo_name=repo_name,
224 227 revision=commit_id,
225 228 f_path='/'.join(path_segments[:cnt + 1])),
226 229 class_='pjax-link'))
227 230 else:
228 231 url_segments.append(segment_html)
229 232
230 233 return literal('/'.join(url_segments))
231 234
232 235
233 236 class CodeHtmlFormatter(HtmlFormatter):
234 237 """
235 238 My code Html Formatter for source codes
236 239 """
237 240
238 241 def wrap(self, source, outfile):
239 242 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
240 243
241 244 def _wrap_code(self, source):
242 245 for cnt, it in enumerate(source):
243 246 i, t = it
244 247 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
245 248 yield i, t
246 249
247 250 def _wrap_tablelinenos(self, inner):
248 251 dummyoutfile = StringIO.StringIO()
249 252 lncount = 0
250 253 for t, line in inner:
251 254 if t:
252 255 lncount += 1
253 256 dummyoutfile.write(line)
254 257
255 258 fl = self.linenostart
256 259 mw = len(str(lncount + fl - 1))
257 260 sp = self.linenospecial
258 261 st = self.linenostep
259 262 la = self.lineanchors
260 263 aln = self.anchorlinenos
261 264 nocls = self.noclasses
262 265 if sp:
263 266 lines = []
264 267
265 268 for i in range(fl, fl + lncount):
266 269 if i % st == 0:
267 270 if i % sp == 0:
268 271 if aln:
269 272 lines.append('<a href="#%s%d" class="special">%*d</a>' %
270 273 (la, i, mw, i))
271 274 else:
272 275 lines.append('<span class="special">%*d</span>' % (mw, i))
273 276 else:
274 277 if aln:
275 278 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
276 279 else:
277 280 lines.append('%*d' % (mw, i))
278 281 else:
279 282 lines.append('')
280 283 ls = '\n'.join(lines)
281 284 else:
282 285 lines = []
283 286 for i in range(fl, fl + lncount):
284 287 if i % st == 0:
285 288 if aln:
286 289 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
287 290 else:
288 291 lines.append('%*d' % (mw, i))
289 292 else:
290 293 lines.append('')
291 294 ls = '\n'.join(lines)
292 295
293 296 # in case you wonder about the seemingly redundant <div> here: since the
294 297 # content in the other cell also is wrapped in a div, some browsers in
295 298 # some configurations seem to mess up the formatting...
296 299 if nocls:
297 300 yield 0, ('<table class="%stable">' % self.cssclass +
298 301 '<tr><td><div class="linenodiv" '
299 302 'style="background-color: #f0f0f0; padding-right: 10px">'
300 303 '<pre style="line-height: 125%">' +
301 304 ls + '</pre></div></td><td id="hlcode" class="code">')
302 305 else:
303 306 yield 0, ('<table class="%stable">' % self.cssclass +
304 307 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
305 308 ls + '</pre></div></td><td id="hlcode" class="code">')
306 309 yield 0, dummyoutfile.getvalue()
307 310 yield 0, '</td></tr></table>'
308 311
309 312
313 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
314 def __init__(self, **kw):
315 # only show these line numbers if set
316 self.only_lines = kw.pop('only_line_numbers', [])
317 self.query_terms = kw.pop('query_terms', [])
318 self.max_lines = kw.pop('max_lines', 5)
319 self.line_context = kw.pop('line_context', 3)
320 self.url = kw.pop('url', None)
321
322 super(CodeHtmlFormatter, self).__init__(**kw)
323
324 def _wrap_code(self, source):
325 for cnt, it in enumerate(source):
326 i, t = it
327 t = '<pre>%s</pre>' % t
328 yield i, t
329
330 def _wrap_tablelinenos(self, inner):
331 yield 0, '<table class="code-highlight %stable">' % self.cssclass
332
333 last_shown_line_number = 0
334 current_line_number = 1
335
336 for t, line in inner:
337 if not t:
338 yield t, line
339 continue
340
341 if current_line_number in self.only_lines:
342 if last_shown_line_number + 1 != current_line_number:
343 yield 0, '<tr>'
344 yield 0, '<td class="line">...</td>'
345 yield 0, '<td id="hlcode" class="code"></td>'
346 yield 0, '</tr>'
347
348 yield 0, '<tr>'
349 if self.url:
350 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
351 self.url, current_line_number, current_line_number)
352 else:
353 yield 0, '<td class="line"><a href="">%i</a></td>' % (
354 current_line_number)
355 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
356 yield 0, '</tr>'
357
358 last_shown_line_number = current_line_number
359
360 current_line_number += 1
361
362
363 yield 0, '</table>'
364
365
366 def extract_phrases(text_query):
367 """
368 Extracts phrases from search term string making sure phrases
369 contained in double quotes are kept together - and discarding empty values
370 or fully whitespace values eg.
371
372 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
373
374 """
375
376 in_phrase = False
377 buf = ''
378 phrases = []
379 for char in text_query:
380 if in_phrase:
381 if char == '"': # end phrase
382 phrases.append(buf)
383 buf = ''
384 in_phrase = False
385 continue
386 else:
387 buf += char
388 continue
389 else:
390 if char == '"': # start phrase
391 in_phrase = True
392 phrases.append(buf)
393 buf = ''
394 continue
395 elif char == ' ':
396 phrases.append(buf)
397 buf = ''
398 continue
399 else:
400 buf += char
401
402 phrases.append(buf)
403 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
404 return phrases
405
406
407 def get_matching_offsets(text, phrases):
408 """
409 Returns a list of string offsets in `text` that the list of `terms` match
410
411 >>> get_matching_offsets('some text here', ['some', 'here'])
412 [(0, 4), (10, 14)]
413
414 """
415 offsets = []
416 for phrase in phrases:
417 for match in re.finditer(phrase, text):
418 offsets.append((match.start(), match.end()))
419
420 return offsets
421
422
423 def normalize_text_for_matching(x):
424 """
425 Replaces all non alnum characters to spaces and lower cases the string,
426 useful for comparing two text strings without punctuation
427 """
428 return re.sub(r'[^\w]', ' ', x.lower())
429
430
431 def get_matching_line_offsets(lines, terms):
432 """ Return a set of `lines` indices (starting from 1) matching a
433 text search query, along with `context` lines above/below matching lines
434
435 :param lines: list of strings representing lines
436 :param terms: search term string to match in lines eg. 'some text'
437 :param context: number of lines above/below a matching line to add to result
438 :param max_lines: cut off for lines of interest
439 eg.
440
441 >>> get_matching_line_offsets('''
442 words words words
443 words words words
444 some text some
445 words words words
446 words words words
447 text here what
448 ''', 'text', context=1)
449 {3: [(5, 9)], 6: [(0, 4)]]
450 """
451 matching_lines = {}
452 phrases = [normalize_text_for_matching(phrase)
453 for phrase in extract_phrases(terms)]
454
455 for line_index, line in enumerate(lines, start=1):
456 match_offsets = get_matching_offsets(
457 normalize_text_for_matching(line), phrases)
458 if match_offsets:
459 matching_lines[line_index] = match_offsets
460
461 return matching_lines
462
463 def get_lexer_safe(mimetype=None, filepath=None):
464 """
465 Tries to return a relevant pygments lexer using mimetype/filepath name,
466 defaulting to plain text if none could be found
467 """
468 lexer = None
469 try:
470 if mimetype:
471 lexer = get_lexer_for_mimetype(mimetype)
472 if not lexer:
473 lexer = get_lexer_for_filename(path)
474 except pygments.util.ClassNotFound:
475 pass
476
477 if not lexer:
478 lexer = get_lexer_by_name('text')
479
480 return lexer
481
482
310 483 def pygmentize(filenode, **kwargs):
311 484 """
312 485 pygmentize function using pygments
313 486
314 487 :param filenode:
315 488 """
316 489 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
317 490 return literal(code_highlight(filenode.content, lexer,
318 491 CodeHtmlFormatter(**kwargs)))
319 492
320 493
321 494 def pygmentize_annotation(repo_name, filenode, **kwargs):
322 495 """
323 496 pygmentize function for annotation
324 497
325 498 :param filenode:
326 499 """
327 500
328 501 color_dict = {}
329 502
330 503 def gen_color(n=10000):
331 504 """generator for getting n of evenly distributed colors using
332 505 hsv color and golden ratio. It always return same order of colors
333 506
334 507 :returns: RGB tuple
335 508 """
336 509
337 510 def hsv_to_rgb(h, s, v):
338 511 if s == 0.0:
339 512 return v, v, v
340 513 i = int(h * 6.0) # XXX assume int() truncates!
341 514 f = (h * 6.0) - i
342 515 p = v * (1.0 - s)
343 516 q = v * (1.0 - s * f)
344 517 t = v * (1.0 - s * (1.0 - f))
345 518 i = i % 6
346 519 if i == 0:
347 520 return v, t, p
348 521 if i == 1:
349 522 return q, v, p
350 523 if i == 2:
351 524 return p, v, t
352 525 if i == 3:
353 526 return p, q, v
354 527 if i == 4:
355 528 return t, p, v
356 529 if i == 5:
357 530 return v, p, q
358 531
359 532 golden_ratio = 0.618033988749895
360 533 h = 0.22717784590367374
361 534
362 535 for _ in xrange(n):
363 536 h += golden_ratio
364 537 h %= 1
365 538 HSV_tuple = [h, 0.95, 0.95]
366 539 RGB_tuple = hsv_to_rgb(*HSV_tuple)
367 540 yield map(lambda x: str(int(x * 256)), RGB_tuple)
368 541
369 542 cgenerator = gen_color()
370 543
371 544 def get_color_string(commit_id):
372 545 if commit_id in color_dict:
373 546 col = color_dict[commit_id]
374 547 else:
375 548 col = color_dict[commit_id] = cgenerator.next()
376 549 return "color: rgb(%s)! important;" % (', '.join(col))
377 550
378 551 def url_func(repo_name):
379 552
380 553 def _url_func(commit):
381 554 author = commit.author
382 555 date = commit.date
383 556 message = tooltip(commit.message)
384 557
385 558 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
386 559 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
387 560 "</b> %s<br/></div>")
388 561
389 562 tooltip_html = tooltip_html % (author, date, message)
390 563 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
391 564 uri = link_to(
392 565 lnk_format,
393 566 url('changeset_home', repo_name=repo_name,
394 567 revision=commit.raw_id),
395 568 style=get_color_string(commit.raw_id),
396 569 class_='tooltip',
397 570 title=tooltip_html
398 571 )
399 572
400 573 uri += '\n'
401 574 return uri
402 575 return _url_func
403 576
404 577 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
405 578
406 579
407 580 def is_following_repo(repo_name, user_id):
408 581 from rhodecode.model.scm import ScmModel
409 582 return ScmModel().is_following_repo(repo_name, user_id)
410 583
411 584
412 585 class _Message(object):
413 586 """A message returned by ``Flash.pop_messages()``.
414 587
415 588 Converting the message to a string returns the message text. Instances
416 589 also have the following attributes:
417 590
418 591 * ``message``: the message text.
419 592 * ``category``: the category specified when the message was created.
420 593 """
421 594
422 595 def __init__(self, category, message):
423 596 self.category = category
424 597 self.message = message
425 598
426 599 def __str__(self):
427 600 return self.message
428 601
429 602 __unicode__ = __str__
430 603
431 604 def __html__(self):
432 605 return escape(safe_unicode(self.message))
433 606
434 607
435 608 class Flash(_Flash):
436 609
437 610 def pop_messages(self):
438 611 """Return all accumulated messages and delete them from the session.
439 612
440 613 The return value is a list of ``Message`` objects.
441 614 """
442 615 from pylons import session
443 616
444 617 messages = []
445 618
446 619 # Pop the 'old' pylons flash messages. They are tuples of the form
447 620 # (category, message)
448 621 for cat, msg in session.pop(self.session_key, []):
449 622 messages.append(_Message(cat, msg))
450 623
451 624 # Pop the 'new' pyramid flash messages for each category as list
452 625 # of strings.
453 626 for cat in self.categories:
454 627 for msg in session.pop_flash(queue=cat):
455 628 messages.append(_Message(cat, msg))
456 629 # Map messages from the default queue to the 'notice' category.
457 630 for msg in session.pop_flash():
458 631 messages.append(_Message('notice', msg))
459 632
460 633 session.save()
461 634 return messages
462 635
463 636 flash = Flash()
464 637
465 638 #==============================================================================
466 639 # SCM FILTERS available via h.
467 640 #==============================================================================
468 641 from rhodecode.lib.vcs.utils import author_name, author_email
469 642 from rhodecode.lib.utils2 import credentials_filter, age as _age
470 643 from rhodecode.model.db import User, ChangesetStatus
471 644
472 645 age = _age
473 646 capitalize = lambda x: x.capitalize()
474 647 email = author_email
475 648 short_id = lambda x: x[:12]
476 649 hide_credentials = lambda x: ''.join(credentials_filter(x))
477 650
478 651
479 652 def age_component(datetime_iso, value=None):
480 653 title = value or format_date(datetime_iso)
481 654
482 655 # detect if we have a timezone info, if not assume UTC
483 656 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
484 657 tzinfo = '+00:00'
485 658
486 659 return literal(
487 660 '<time class="timeago tooltip" '
488 661 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
489 662 datetime_iso, title, tzinfo))
490 663
491 664
492 665 def _shorten_commit_id(commit_id):
493 666 from rhodecode import CONFIG
494 667 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
495 668 return commit_id[:def_len]
496 669
497 670
498 671 def get_repo_id_from_name(repo_name):
499 672 repo = get_by_repo_name(repo_name)
500 673 return repo.repo_id
501 674
502 675
503 676 def show_id(commit):
504 677 """
505 678 Configurable function that shows ID
506 679 by default it's r123:fffeeefffeee
507 680
508 681 :param commit: commit instance
509 682 """
510 683 from rhodecode import CONFIG
511 684 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
512 685
513 686 raw_id = _shorten_commit_id(commit.raw_id)
514 687 if show_idx:
515 688 return 'r%s:%s' % (commit.idx, raw_id)
516 689 else:
517 690 return '%s' % (raw_id, )
518 691
519 692
520 693 def format_date(date):
521 694 """
522 695 use a standardized formatting for dates used in RhodeCode
523 696
524 697 :param date: date/datetime object
525 698 :return: formatted date
526 699 """
527 700
528 701 if date:
529 702 _fmt = "%a, %d %b %Y %H:%M:%S"
530 703 return safe_unicode(date.strftime(_fmt))
531 704
532 705 return u""
533 706
534 707
535 708 class _RepoChecker(object):
536 709
537 710 def __init__(self, backend_alias):
538 711 self._backend_alias = backend_alias
539 712
540 713 def __call__(self, repository):
541 714 if hasattr(repository, 'alias'):
542 715 _type = repository.alias
543 716 elif hasattr(repository, 'repo_type'):
544 717 _type = repository.repo_type
545 718 else:
546 719 _type = repository
547 720 return _type == self._backend_alias
548 721
549 722 is_git = _RepoChecker('git')
550 723 is_hg = _RepoChecker('hg')
551 724 is_svn = _RepoChecker('svn')
552 725
553 726
554 727 def get_repo_type_by_name(repo_name):
555 728 repo = Repository.get_by_repo_name(repo_name)
556 729 return repo.repo_type
557 730
558 731
559 732 def is_svn_without_proxy(repository):
560 733 from rhodecode import CONFIG
561 734 if is_svn(repository):
562 735 if not CONFIG.get('rhodecode_proxy_subversion_http_requests', False):
563 736 return True
564 737 return False
565 738
566 739
567 740 def email_or_none(author):
568 741 # extract email from the commit string
569 742 _email = author_email(author)
570 743 if _email != '':
571 744 # check it against RhodeCode database, and use the MAIN email for this
572 745 # user
573 746 user = User.get_by_email(_email, case_insensitive=True, cache=True)
574 747 if user is not None:
575 748 return user.email
576 749 return _email
577 750
578 751 # See if it contains a username we can get an email from
579 752 user = User.get_by_username(author_name(author), case_insensitive=True,
580 753 cache=True)
581 754 if user is not None:
582 755 return user.email
583 756
584 757 # No valid email, not a valid user in the system, none!
585 758 return None
586 759
587 760
588 761 def discover_user(author):
589 762 # if author is already an instance use it for extraction
590 763 if isinstance(author, User):
591 764 return author
592 765
593 766 # Valid email in the attribute passed, see if they're in the system
594 767 _email = email(author)
595 768 if _email != '':
596 769 user = User.get_by_email(_email, case_insensitive=True, cache=True)
597 770 if user is not None:
598 771 return user
599 772
600 773 # Maybe it's a username?
601 774 _author = author_name(author)
602 775 user = User.get_by_username(_author, case_insensitive=True,
603 776 cache=True)
604 777 if user is not None:
605 778 return user
606 779
607 780 return None
608 781
609 782
610 783 def link_to_user(author, length=0, **kwargs):
611 784 user = discover_user(author)
612 785 display_person = person(author, 'username_or_name_or_email')
613 786 if length:
614 787 display_person = shorter(display_person, length)
615 788
616 789 if user:
617 790 return link_to(
618 791 escape(display_person),
619 792 url('user_profile', username=user.username),
620 793 **kwargs)
621 794 else:
622 795 return escape(display_person)
623 796
624 797
625 798 def person(author, show_attr="username_and_name"):
626 799 # attr to return from fetched user
627 800 person_getter = lambda usr: getattr(usr, show_attr)
628 801 user = discover_user(author)
629 802 if user:
630 803 return person_getter(user)
631 804 else:
632 805 _author = author_name(author)
633 806 _email = email(author)
634 807 return _author or _email
635 808
636 809
637 810 def person_by_id(id_, show_attr="username_and_name"):
638 811 # attr to return from fetched user
639 812 person_getter = lambda usr: getattr(usr, show_attr)
640 813
641 814 #maybe it's an ID ?
642 815 if str(id_).isdigit() or isinstance(id_, int):
643 816 id_ = int(id_)
644 817 user = User.get(id_)
645 818 if user is not None:
646 819 return person_getter(user)
647 820 return id_
648 821
649 822
650 823 def gravatar_with_user(author):
651 824 from rhodecode.lib.utils import PartialRenderer
652 825 _render = PartialRenderer('base/base.html')
653 826 return _render('gravatar_with_user', author)
654 827
655 828
656 829 def desc_stylize(value):
657 830 """
658 831 converts tags from value into html equivalent
659 832
660 833 :param value:
661 834 """
662 835 if not value:
663 836 return ''
664 837
665 838 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
666 839 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
667 840 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
668 841 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
669 842 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
670 843 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
671 844 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
672 845 '<div class="metatag" tag="lang">\\2</div>', value)
673 846 value = re.sub(r'\[([a-z]+)\]',
674 847 '<div class="metatag" tag="\\1">\\1</div>', value)
675 848
676 849 return value
677 850
678 851
679 852 def escaped_stylize(value):
680 853 """
681 854 converts tags from value into html equivalent, but escaping its value first
682 855 """
683 856 if not value:
684 857 return ''
685 858
686 859 # Using default webhelper escape method, but has to force it as a
687 860 # plain unicode instead of a markup tag to be used in regex expressions
688 861 value = unicode(escape(safe_unicode(value)))
689 862
690 863 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
691 864 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
692 865 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
693 866 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
694 867 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
695 868 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
696 869 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
697 870 '<div class="metatag" tag="lang">\\2</div>', value)
698 871 value = re.sub(r'\[([a-z]+)\]',
699 872 '<div class="metatag" tag="\\1">\\1</div>', value)
700 873
701 874 return value
702 875
703 876
704 877 def bool2icon(value):
705 878 """
706 879 Returns boolean value of a given value, represented as html element with
707 880 classes that will represent icons
708 881
709 882 :param value: given value to convert to html node
710 883 """
711 884
712 885 if value: # does bool conversion
713 886 return HTML.tag('i', class_="icon-true")
714 887 else: # not true as bool
715 888 return HTML.tag('i', class_="icon-false")
716 889
717 890
718 891 #==============================================================================
719 892 # PERMS
720 893 #==============================================================================
721 894 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
722 895 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
723 896 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
724 897
725 898
726 899 #==============================================================================
727 900 # GRAVATAR URL
728 901 #==============================================================================
729 902 class InitialsGravatar(object):
730 903 def __init__(self, email_address, first_name, last_name, size=30,
731 904 background=None, text_color='#fff'):
732 905 self.size = size
733 906 self.first_name = first_name
734 907 self.last_name = last_name
735 908 self.email_address = email_address
736 909 self.background = background or self.str2color(email_address)
737 910 self.text_color = text_color
738 911
739 912 def get_color_bank(self):
740 913 """
741 914 returns a predefined list of colors that gravatars can use.
742 915 Those are randomized distinct colors that guarantee readability and
743 916 uniqueness.
744 917
745 918 generated with: http://phrogz.net/css/distinct-colors.html
746 919 """
747 920 return [
748 921 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
749 922 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
750 923 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
751 924 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
752 925 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
753 926 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
754 927 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
755 928 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
756 929 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
757 930 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
758 931 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
759 932 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
760 933 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
761 934 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
762 935 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
763 936 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
764 937 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
765 938 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
766 939 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
767 940 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
768 941 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
769 942 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
770 943 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
771 944 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
772 945 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
773 946 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
774 947 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
775 948 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
776 949 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
777 950 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
778 951 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
779 952 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
780 953 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
781 954 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
782 955 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
783 956 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
784 957 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
785 958 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
786 959 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
787 960 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
788 961 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
789 962 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
790 963 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
791 964 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
792 965 '#4f8c46', '#368dd9', '#5c0073'
793 966 ]
794 967
795 968 def rgb_to_hex_color(self, rgb_tuple):
796 969 """
797 970 Converts an rgb_tuple passed to an hex color.
798 971
799 972 :param rgb_tuple: tuple with 3 ints represents rgb color space
800 973 """
801 974 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
802 975
803 976 def email_to_int_list(self, email_str):
804 977 """
805 978 Get every byte of the hex digest value of email and turn it to integer.
806 979 It's going to be always between 0-255
807 980 """
808 981 digest = md5_safe(email_str.lower())
809 982 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
810 983
811 984 def pick_color_bank_index(self, email_str, color_bank):
812 985 return self.email_to_int_list(email_str)[0] % len(color_bank)
813 986
814 987 def str2color(self, email_str):
815 988 """
816 989 Tries to map in a stable algorithm an email to color
817 990
818 991 :param email_str:
819 992 """
820 993 color_bank = self.get_color_bank()
821 994 # pick position (module it's length so we always find it in the
822 995 # bank even if it's smaller than 256 values
823 996 pos = self.pick_color_bank_index(email_str, color_bank)
824 997 return color_bank[pos]
825 998
826 999 def normalize_email(self, email_address):
827 1000 import unicodedata
828 1001 # default host used to fill in the fake/missing email
829 1002 default_host = u'localhost'
830 1003
831 1004 if not email_address:
832 1005 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
833 1006
834 1007 email_address = safe_unicode(email_address)
835 1008
836 1009 if u'@' not in email_address:
837 1010 email_address = u'%s@%s' % (email_address, default_host)
838 1011
839 1012 if email_address.endswith(u'@'):
840 1013 email_address = u'%s%s' % (email_address, default_host)
841 1014
842 1015 email_address = unicodedata.normalize('NFKD', email_address)\
843 1016 .encode('ascii', 'ignore')
844 1017 return email_address
845 1018
846 1019 def get_initials(self):
847 1020 """
848 1021 Returns 2 letter initials calculated based on the input.
849 1022 The algorithm picks first given email address, and takes first letter
850 1023 of part before @, and then the first letter of server name. In case
851 1024 the part before @ is in a format of `somestring.somestring2` it replaces
852 1025 the server letter with first letter of somestring2
853 1026
854 1027 In case function was initialized with both first and lastname, this
855 1028 overrides the extraction from email by first letter of the first and
856 1029 last name. We add special logic to that functionality, In case Full name
857 1030 is compound, like Guido Von Rossum, we use last part of the last name
858 1031 (Von Rossum) picking `R`.
859 1032
860 1033 Function also normalizes the non-ascii characters to they ascii
861 1034 representation, eg Ą => A
862 1035 """
863 1036 import unicodedata
864 1037 # replace non-ascii to ascii
865 1038 first_name = unicodedata.normalize(
866 1039 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
867 1040 last_name = unicodedata.normalize(
868 1041 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
869 1042
870 1043 # do NFKD encoding, and also make sure email has proper format
871 1044 email_address = self.normalize_email(self.email_address)
872 1045
873 1046 # first push the email initials
874 1047 prefix, server = email_address.split('@', 1)
875 1048
876 1049 # check if prefix is maybe a 'firstname.lastname' syntax
877 1050 _dot_split = prefix.rsplit('.', 1)
878 1051 if len(_dot_split) == 2:
879 1052 initials = [_dot_split[0][0], _dot_split[1][0]]
880 1053 else:
881 1054 initials = [prefix[0], server[0]]
882 1055
883 1056 # then try to replace either firtname or lastname
884 1057 fn_letter = (first_name or " ")[0].strip()
885 1058 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
886 1059
887 1060 if fn_letter:
888 1061 initials[0] = fn_letter
889 1062
890 1063 if ln_letter:
891 1064 initials[1] = ln_letter
892 1065
893 1066 return ''.join(initials).upper()
894 1067
895 1068 def get_img_data_by_type(self, font_family, img_type):
896 1069 default_user = """
897 1070 <svg xmlns="http://www.w3.org/2000/svg"
898 1071 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
899 1072 viewBox="-15 -10 439.165 429.164"
900 1073
901 1074 xml:space="preserve"
902 1075 style="background:{background};" >
903 1076
904 1077 <path d="M204.583,216.671c50.664,0,91.74-48.075,
905 1078 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
906 1079 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
907 1080 168.596,153.916,216.671,
908 1081 204.583,216.671z" fill="{text_color}"/>
909 1082 <path d="M407.164,374.717L360.88,
910 1083 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
911 1084 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
912 1085 15.366-44.203,23.488-69.076,23.488c-24.877,
913 1086 0-48.762-8.122-69.078-23.488
914 1087 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
915 1088 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
916 1089 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
917 1090 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
918 1091 19.402-10.527 C409.699,390.129,
919 1092 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
920 1093 </svg>""".format(
921 1094 size=self.size,
922 1095 background='#979797', # @grey4
923 1096 text_color=self.text_color,
924 1097 font_family=font_family)
925 1098
926 1099 return {
927 1100 "default_user": default_user
928 1101 }[img_type]
929 1102
930 1103 def get_img_data(self, svg_type=None):
931 1104 """
932 1105 generates the svg metadata for image
933 1106 """
934 1107
935 1108 font_family = ','.join([
936 1109 'proximanovaregular',
937 1110 'Proxima Nova Regular',
938 1111 'Proxima Nova',
939 1112 'Arial',
940 1113 'Lucida Grande',
941 1114 'sans-serif'
942 1115 ])
943 1116 if svg_type:
944 1117 return self.get_img_data_by_type(font_family, svg_type)
945 1118
946 1119 initials = self.get_initials()
947 1120 img_data = """
948 1121 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
949 1122 width="{size}" height="{size}"
950 1123 style="width: 100%; height: 100%; background-color: {background}"
951 1124 viewBox="0 0 {size} {size}">
952 1125 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
953 1126 pointer-events="auto" fill="{text_color}"
954 1127 font-family="{font_family}"
955 1128 style="font-weight: 400; font-size: {f_size}px;">{text}
956 1129 </text>
957 1130 </svg>""".format(
958 1131 size=self.size,
959 1132 f_size=self.size/1.85, # scale the text inside the box nicely
960 1133 background=self.background,
961 1134 text_color=self.text_color,
962 1135 text=initials.upper(),
963 1136 font_family=font_family)
964 1137
965 1138 return img_data
966 1139
967 1140 def generate_svg(self, svg_type=None):
968 1141 img_data = self.get_img_data(svg_type)
969 1142 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
970 1143
971 1144
972 1145 def initials_gravatar(email_address, first_name, last_name, size=30):
973 1146 svg_type = None
974 1147 if email_address == User.DEFAULT_USER_EMAIL:
975 1148 svg_type = 'default_user'
976 1149 klass = InitialsGravatar(email_address, first_name, last_name, size)
977 1150 return klass.generate_svg(svg_type=svg_type)
978 1151
979 1152
980 1153 def gravatar_url(email_address, size=30):
981 1154 # doh, we need to re-import those to mock it later
982 1155 from pylons import tmpl_context as c
983 1156
984 1157 _use_gravatar = c.visual.use_gravatar
985 1158 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
986 1159
987 1160 email_address = email_address or User.DEFAULT_USER_EMAIL
988 1161 if isinstance(email_address, unicode):
989 1162 # hashlib crashes on unicode items
990 1163 email_address = safe_str(email_address)
991 1164
992 1165 # empty email or default user
993 1166 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
994 1167 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
995 1168
996 1169 if _use_gravatar:
997 1170 # TODO: Disuse pyramid thread locals. Think about another solution to
998 1171 # get the host and schema here.
999 1172 request = get_current_request()
1000 1173 tmpl = safe_str(_gravatar_url)
1001 1174 tmpl = tmpl.replace('{email}', email_address)\
1002 1175 .replace('{md5email}', md5_safe(email_address.lower())) \
1003 1176 .replace('{netloc}', request.host)\
1004 1177 .replace('{scheme}', request.scheme)\
1005 1178 .replace('{size}', safe_str(size))
1006 1179 return tmpl
1007 1180 else:
1008 1181 return initials_gravatar(email_address, '', '', size=size)
1009 1182
1010 1183
1011 1184 class Page(_Page):
1012 1185 """
1013 1186 Custom pager to match rendering style with paginator
1014 1187 """
1015 1188
1016 1189 def _get_pos(self, cur_page, max_page, items):
1017 1190 edge = (items / 2) + 1
1018 1191 if (cur_page <= edge):
1019 1192 radius = max(items / 2, items - cur_page)
1020 1193 elif (max_page - cur_page) < edge:
1021 1194 radius = (items - 1) - (max_page - cur_page)
1022 1195 else:
1023 1196 radius = items / 2
1024 1197
1025 1198 left = max(1, (cur_page - (radius)))
1026 1199 right = min(max_page, cur_page + (radius))
1027 1200 return left, cur_page, right
1028 1201
1029 1202 def _range(self, regexp_match):
1030 1203 """
1031 1204 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1032 1205
1033 1206 Arguments:
1034 1207
1035 1208 regexp_match
1036 1209 A "re" (regular expressions) match object containing the
1037 1210 radius of linked pages around the current page in
1038 1211 regexp_match.group(1) as a string
1039 1212
1040 1213 This function is supposed to be called as a callable in
1041 1214 re.sub.
1042 1215
1043 1216 """
1044 1217 radius = int(regexp_match.group(1))
1045 1218
1046 1219 # Compute the first and last page number within the radius
1047 1220 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1048 1221 # -> leftmost_page = 5
1049 1222 # -> rightmost_page = 9
1050 1223 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1051 1224 self.last_page,
1052 1225 (radius * 2) + 1)
1053 1226 nav_items = []
1054 1227
1055 1228 # Create a link to the first page (unless we are on the first page
1056 1229 # or there would be no need to insert '..' spacers)
1057 1230 if self.page != self.first_page and self.first_page < leftmost_page:
1058 1231 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1059 1232
1060 1233 # Insert dots if there are pages between the first page
1061 1234 # and the currently displayed page range
1062 1235 if leftmost_page - self.first_page > 1:
1063 1236 # Wrap in a SPAN tag if nolink_attr is set
1064 1237 text = '..'
1065 1238 if self.dotdot_attr:
1066 1239 text = HTML.span(c=text, **self.dotdot_attr)
1067 1240 nav_items.append(text)
1068 1241
1069 1242 for thispage in xrange(leftmost_page, rightmost_page + 1):
1070 1243 # Hilight the current page number and do not use a link
1071 1244 if thispage == self.page:
1072 1245 text = '%s' % (thispage,)
1073 1246 # Wrap in a SPAN tag if nolink_attr is set
1074 1247 if self.curpage_attr:
1075 1248 text = HTML.span(c=text, **self.curpage_attr)
1076 1249 nav_items.append(text)
1077 1250 # Otherwise create just a link to that page
1078 1251 else:
1079 1252 text = '%s' % (thispage,)
1080 1253 nav_items.append(self._pagerlink(thispage, text))
1081 1254
1082 1255 # Insert dots if there are pages between the displayed
1083 1256 # page numbers and the end of the page range
1084 1257 if self.last_page - rightmost_page > 1:
1085 1258 text = '..'
1086 1259 # Wrap in a SPAN tag if nolink_attr is set
1087 1260 if self.dotdot_attr:
1088 1261 text = HTML.span(c=text, **self.dotdot_attr)
1089 1262 nav_items.append(text)
1090 1263
1091 1264 # Create a link to the very last page (unless we are on the last
1092 1265 # page or there would be no need to insert '..' spacers)
1093 1266 if self.page != self.last_page and rightmost_page < self.last_page:
1094 1267 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1095 1268
1096 1269 ## prerender links
1097 1270 #_page_link = url.current()
1098 1271 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1099 1272 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1100 1273 return self.separator.join(nav_items)
1101 1274
1102 1275 def pager(self, format='~2~', page_param='page', partial_param='partial',
1103 1276 show_if_single_page=False, separator=' ', onclick=None,
1104 1277 symbol_first='<<', symbol_last='>>',
1105 1278 symbol_previous='<', symbol_next='>',
1106 1279 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1107 1280 curpage_attr={'class': 'pager_curpage'},
1108 1281 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1109 1282
1110 1283 self.curpage_attr = curpage_attr
1111 1284 self.separator = separator
1112 1285 self.pager_kwargs = kwargs
1113 1286 self.page_param = page_param
1114 1287 self.partial_param = partial_param
1115 1288 self.onclick = onclick
1116 1289 self.link_attr = link_attr
1117 1290 self.dotdot_attr = dotdot_attr
1118 1291
1119 1292 # Don't show navigator if there is no more than one page
1120 1293 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1121 1294 return ''
1122 1295
1123 1296 from string import Template
1124 1297 # Replace ~...~ in token format by range of pages
1125 1298 result = re.sub(r'~(\d+)~', self._range, format)
1126 1299
1127 1300 # Interpolate '%' variables
1128 1301 result = Template(result).safe_substitute({
1129 1302 'first_page': self.first_page,
1130 1303 'last_page': self.last_page,
1131 1304 'page': self.page,
1132 1305 'page_count': self.page_count,
1133 1306 'items_per_page': self.items_per_page,
1134 1307 'first_item': self.first_item,
1135 1308 'last_item': self.last_item,
1136 1309 'item_count': self.item_count,
1137 1310 'link_first': self.page > self.first_page and \
1138 1311 self._pagerlink(self.first_page, symbol_first) or '',
1139 1312 'link_last': self.page < self.last_page and \
1140 1313 self._pagerlink(self.last_page, symbol_last) or '',
1141 1314 'link_previous': self.previous_page and \
1142 1315 self._pagerlink(self.previous_page, symbol_previous) \
1143 1316 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1144 1317 'link_next': self.next_page and \
1145 1318 self._pagerlink(self.next_page, symbol_next) \
1146 1319 or HTML.span(symbol_next, class_="pg-next disabled")
1147 1320 })
1148 1321
1149 1322 return literal(result)
1150 1323
1151 1324
1152 1325 #==============================================================================
1153 1326 # REPO PAGER, PAGER FOR REPOSITORY
1154 1327 #==============================================================================
1155 1328 class RepoPage(Page):
1156 1329
1157 1330 def __init__(self, collection, page=1, items_per_page=20,
1158 1331 item_count=None, url=None, **kwargs):
1159 1332
1160 1333 """Create a "RepoPage" instance. special pager for paging
1161 1334 repository
1162 1335 """
1163 1336 self._url_generator = url
1164 1337
1165 1338 # Safe the kwargs class-wide so they can be used in the pager() method
1166 1339 self.kwargs = kwargs
1167 1340
1168 1341 # Save a reference to the collection
1169 1342 self.original_collection = collection
1170 1343
1171 1344 self.collection = collection
1172 1345
1173 1346 # The self.page is the number of the current page.
1174 1347 # The first page has the number 1!
1175 1348 try:
1176 1349 self.page = int(page) # make it int() if we get it as a string
1177 1350 except (ValueError, TypeError):
1178 1351 self.page = 1
1179 1352
1180 1353 self.items_per_page = items_per_page
1181 1354
1182 1355 # Unless the user tells us how many items the collections has
1183 1356 # we calculate that ourselves.
1184 1357 if item_count is not None:
1185 1358 self.item_count = item_count
1186 1359 else:
1187 1360 self.item_count = len(self.collection)
1188 1361
1189 1362 # Compute the number of the first and last available page
1190 1363 if self.item_count > 0:
1191 1364 self.first_page = 1
1192 1365 self.page_count = int(math.ceil(float(self.item_count) /
1193 1366 self.items_per_page))
1194 1367 self.last_page = self.first_page + self.page_count - 1
1195 1368
1196 1369 # Make sure that the requested page number is the range of
1197 1370 # valid pages
1198 1371 if self.page > self.last_page:
1199 1372 self.page = self.last_page
1200 1373 elif self.page < self.first_page:
1201 1374 self.page = self.first_page
1202 1375
1203 1376 # Note: the number of items on this page can be less than
1204 1377 # items_per_page if the last page is not full
1205 1378 self.first_item = max(0, (self.item_count) - (self.page *
1206 1379 items_per_page))
1207 1380 self.last_item = ((self.item_count - 1) - items_per_page *
1208 1381 (self.page - 1))
1209 1382
1210 1383 self.items = list(self.collection[self.first_item:self.last_item + 1])
1211 1384
1212 1385 # Links to previous and next page
1213 1386 if self.page > self.first_page:
1214 1387 self.previous_page = self.page - 1
1215 1388 else:
1216 1389 self.previous_page = None
1217 1390
1218 1391 if self.page < self.last_page:
1219 1392 self.next_page = self.page + 1
1220 1393 else:
1221 1394 self.next_page = None
1222 1395
1223 1396 # No items available
1224 1397 else:
1225 1398 self.first_page = None
1226 1399 self.page_count = 0
1227 1400 self.last_page = None
1228 1401 self.first_item = None
1229 1402 self.last_item = None
1230 1403 self.previous_page = None
1231 1404 self.next_page = None
1232 1405 self.items = []
1233 1406
1234 1407 # This is a subclass of the 'list' type. Initialise the list now.
1235 1408 list.__init__(self, reversed(self.items))
1236 1409
1237 1410
1238 1411 def changed_tooltip(nodes):
1239 1412 """
1240 1413 Generates a html string for changed nodes in commit page.
1241 1414 It limits the output to 30 entries
1242 1415
1243 1416 :param nodes: LazyNodesGenerator
1244 1417 """
1245 1418 if nodes:
1246 1419 pref = ': <br/> '
1247 1420 suf = ''
1248 1421 if len(nodes) > 30:
1249 1422 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1250 1423 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1251 1424 for x in nodes[:30]]) + suf)
1252 1425 else:
1253 1426 return ': ' + _('No Files')
1254 1427
1255 1428
1256 1429 def breadcrumb_repo_link(repo):
1257 1430 """
1258 1431 Makes a breadcrumbs path link to repo
1259 1432
1260 1433 ex::
1261 1434 group >> subgroup >> repo
1262 1435
1263 1436 :param repo: a Repository instance
1264 1437 """
1265 1438
1266 1439 path = [
1267 1440 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1268 1441 for group in repo.groups_with_parents
1269 1442 ] + [
1270 1443 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1271 1444 ]
1272 1445
1273 1446 return literal(' &raquo; '.join(path))
1274 1447
1275 1448
1276 1449 def format_byte_size_binary(file_size):
1277 1450 """
1278 1451 Formats file/folder sizes to standard.
1279 1452 """
1280 1453 formatted_size = format_byte_size(file_size, binary=True)
1281 1454 return formatted_size
1282 1455
1283 1456
1284 1457 def fancy_file_stats(stats):
1285 1458 """
1286 1459 Displays a fancy two colored bar for number of added/deleted
1287 1460 lines of code on file
1288 1461
1289 1462 :param stats: two element list of added/deleted lines of code
1290 1463 """
1291 1464 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1292 1465 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1293 1466
1294 1467 def cgen(l_type, a_v, d_v):
1295 1468 mapping = {'tr': 'top-right-rounded-corner-mid',
1296 1469 'tl': 'top-left-rounded-corner-mid',
1297 1470 'br': 'bottom-right-rounded-corner-mid',
1298 1471 'bl': 'bottom-left-rounded-corner-mid'}
1299 1472 map_getter = lambda x: mapping[x]
1300 1473
1301 1474 if l_type == 'a' and d_v:
1302 1475 #case when added and deleted are present
1303 1476 return ' '.join(map(map_getter, ['tl', 'bl']))
1304 1477
1305 1478 if l_type == 'a' and not d_v:
1306 1479 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1307 1480
1308 1481 if l_type == 'd' and a_v:
1309 1482 return ' '.join(map(map_getter, ['tr', 'br']))
1310 1483
1311 1484 if l_type == 'd' and not a_v:
1312 1485 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1313 1486
1314 1487 a, d = stats['added'], stats['deleted']
1315 1488 width = 100
1316 1489
1317 1490 if stats['binary']: # binary operations like chmod/rename etc
1318 1491 lbl = []
1319 1492 bin_op = 0 # undefined
1320 1493
1321 1494 # prefix with bin for binary files
1322 1495 if BIN_FILENODE in stats['ops']:
1323 1496 lbl += ['bin']
1324 1497
1325 1498 if NEW_FILENODE in stats['ops']:
1326 1499 lbl += [_('new file')]
1327 1500 bin_op = NEW_FILENODE
1328 1501 elif MOD_FILENODE in stats['ops']:
1329 1502 lbl += [_('mod')]
1330 1503 bin_op = MOD_FILENODE
1331 1504 elif DEL_FILENODE in stats['ops']:
1332 1505 lbl += [_('del')]
1333 1506 bin_op = DEL_FILENODE
1334 1507 elif RENAMED_FILENODE in stats['ops']:
1335 1508 lbl += [_('rename')]
1336 1509 bin_op = RENAMED_FILENODE
1337 1510
1338 1511 # chmod can go with other operations, so we add a + to lbl if needed
1339 1512 if CHMOD_FILENODE in stats['ops']:
1340 1513 lbl += [_('chmod')]
1341 1514 if bin_op == 0:
1342 1515 bin_op = CHMOD_FILENODE
1343 1516
1344 1517 lbl = '+'.join(lbl)
1345 1518 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1346 1519 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1347 1520 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1348 1521 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1349 1522
1350 1523 t = stats['added'] + stats['deleted']
1351 1524 unit = float(width) / (t or 1)
1352 1525
1353 1526 # needs > 9% of width to be visible or 0 to be hidden
1354 1527 a_p = max(9, unit * a) if a > 0 else 0
1355 1528 d_p = max(9, unit * d) if d > 0 else 0
1356 1529 p_sum = a_p + d_p
1357 1530
1358 1531 if p_sum > width:
1359 1532 #adjust the percentage to be == 100% since we adjusted to 9
1360 1533 if a_p > d_p:
1361 1534 a_p = a_p - (p_sum - width)
1362 1535 else:
1363 1536 d_p = d_p - (p_sum - width)
1364 1537
1365 1538 a_v = a if a > 0 else ''
1366 1539 d_v = d if d > 0 else ''
1367 1540
1368 1541 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1369 1542 cgen('a', a_v, d_v), a_p, a_v
1370 1543 )
1371 1544 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1372 1545 cgen('d', a_v, d_v), d_p, d_v
1373 1546 )
1374 1547 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1375 1548
1376 1549
1377 1550 def urlify_text(text_, safe=True):
1378 1551 """
1379 1552 Extrac urls from text and make html links out of them
1380 1553
1381 1554 :param text_:
1382 1555 """
1383 1556
1384 1557 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1385 1558 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1386 1559
1387 1560 def url_func(match_obj):
1388 1561 url_full = match_obj.groups()[0]
1389 1562 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1390 1563 _newtext = url_pat.sub(url_func, text_)
1391 1564 if safe:
1392 1565 return literal(_newtext)
1393 1566 return _newtext
1394 1567
1395 1568
1396 1569 def urlify_commits(text_, repository):
1397 1570 """
1398 1571 Extract commit ids from text and make link from them
1399 1572
1400 1573 :param text_:
1401 1574 :param repository: repo name to build the URL with
1402 1575 """
1403 1576 from pylons import url # doh, we need to re-import url to mock it later
1404 1577 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1405 1578
1406 1579 def url_func(match_obj):
1407 1580 commit_id = match_obj.groups()[1]
1408 1581 pref = match_obj.groups()[0]
1409 1582 suf = match_obj.groups()[2]
1410 1583
1411 1584 tmpl = (
1412 1585 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1413 1586 '%(commit_id)s</a>%(suf)s'
1414 1587 )
1415 1588 return tmpl % {
1416 1589 'pref': pref,
1417 1590 'cls': 'revision-link',
1418 1591 'url': url('changeset_home', repo_name=repository,
1419 1592 revision=commit_id),
1420 1593 'commit_id': commit_id,
1421 1594 'suf': suf
1422 1595 }
1423 1596
1424 1597 newtext = URL_PAT.sub(url_func, text_)
1425 1598
1426 1599 return newtext
1427 1600
1428 1601
1429 1602 def _process_url_func(match_obj, repo_name, uid, entry):
1430 1603 pref = ''
1431 1604 if match_obj.group().startswith(' '):
1432 1605 pref = ' '
1433 1606
1434 1607 issue_id = ''.join(match_obj.groups())
1435 1608 tmpl = (
1436 1609 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1437 1610 '%(issue-prefix)s%(id-repr)s'
1438 1611 '</a>')
1439 1612
1440 1613 (repo_name_cleaned,
1441 1614 parent_group_name) = RepoGroupModel().\
1442 1615 _get_group_name_and_parent(repo_name)
1443 1616
1444 1617 # variables replacement
1445 1618 named_vars = {
1446 1619 'id': issue_id,
1447 1620 'repo': repo_name,
1448 1621 'repo_name': repo_name_cleaned,
1449 1622 'group_name': parent_group_name
1450 1623 }
1451 1624 # named regex variables
1452 1625 named_vars.update(match_obj.groupdict())
1453 1626 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1454 1627
1455 1628 return tmpl % {
1456 1629 'pref': pref,
1457 1630 'cls': 'issue-tracker-link',
1458 1631 'url': _url,
1459 1632 'id-repr': issue_id,
1460 1633 'issue-prefix': entry['pref'],
1461 1634 'serv': entry['url'],
1462 1635 }
1463 1636
1464 1637
1465 1638 def process_patterns(text_string, repo_name, config):
1466 1639 repo = None
1467 1640 if repo_name:
1468 1641 # Retrieving repo_name to avoid invalid repo_name to explode on
1469 1642 # IssueTrackerSettingsModel but still passing invalid name further down
1470 1643 repo = Repository.get_by_repo_name(repo_name)
1471 1644
1472 1645 settings_model = IssueTrackerSettingsModel(repo=repo)
1473 1646 active_entries = settings_model.get_settings()
1474 1647
1475 1648 newtext = text_string
1476 1649 for uid, entry in active_entries.items():
1477 1650 url_func = partial(
1478 1651 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1479 1652
1480 1653 log.debug('found issue tracker entry with uid %s' % (uid,))
1481 1654
1482 1655 if not (entry['pat'] and entry['url']):
1483 1656 log.debug('skipping due to missing data')
1484 1657 continue
1485 1658
1486 1659 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1487 1660 % (uid, entry['pat'], entry['url'], entry['pref']))
1488 1661
1489 1662 try:
1490 1663 pattern = re.compile(r'%s' % entry['pat'])
1491 1664 except re.error:
1492 1665 log.exception(
1493 1666 'issue tracker pattern: `%s` failed to compile',
1494 1667 entry['pat'])
1495 1668 continue
1496 1669
1497 1670 newtext = pattern.sub(url_func, newtext)
1498 1671 log.debug('processed prefix:uid `%s`' % (uid,))
1499 1672
1500 1673 return newtext
1501 1674
1502 1675
1503 1676 def urlify_commit_message(commit_text, repository=None):
1504 1677 """
1505 1678 Parses given text message and makes proper links.
1506 1679 issues are linked to given issue-server, and rest is a commit link
1507 1680
1508 1681 :param commit_text:
1509 1682 :param repository:
1510 1683 """
1511 1684 from pylons import url # doh, we need to re-import url to mock it later
1512 1685 from rhodecode import CONFIG
1513 1686
1514 1687 def escaper(string):
1515 1688 return string.replace('<', '&lt;').replace('>', '&gt;')
1516 1689
1517 1690 newtext = escaper(commit_text)
1518 1691 # urlify commits - extract commit ids and make link out of them, if we have
1519 1692 # the scope of repository present.
1520 1693 if repository:
1521 1694 newtext = urlify_commits(newtext, repository)
1522 1695
1523 1696 # extract http/https links and make them real urls
1524 1697 newtext = urlify_text(newtext, safe=False)
1525 1698
1526 1699 # process issue tracker patterns
1527 1700 newtext = process_patterns(newtext, repository or '', CONFIG)
1528 1701
1529 1702 return literal(newtext)
1530 1703
1531 1704
1532 1705 def rst(source, mentions=False):
1533 1706 return literal('<div class="rst-block">%s</div>' %
1534 1707 MarkupRenderer.rst(source, mentions=mentions))
1535 1708
1536 1709
1537 1710 def markdown(source, mentions=False):
1538 1711 return literal('<div class="markdown-block">%s</div>' %
1539 1712 MarkupRenderer.markdown(source, flavored=False,
1540 1713 mentions=mentions))
1541 1714
1542 1715 def renderer_from_filename(filename, exclude=None):
1543 1716 from rhodecode.config.conf import MARKDOWN_EXTS, RST_EXTS
1544 1717
1545 1718 def _filter(elements):
1546 1719 if isinstance(exclude, (list, tuple)):
1547 1720 return [x for x in elements if x not in exclude]
1548 1721 return elements
1549 1722
1550 1723 if filename.endswith(tuple(_filter([x[0] for x in MARKDOWN_EXTS if x[0]]))):
1551 1724 return 'markdown'
1552 1725 if filename.endswith(tuple(_filter([x[0] for x in RST_EXTS if x[0]]))):
1553 1726 return 'rst'
1554 1727
1555 1728
1556 1729 def render(source, renderer='rst', mentions=False):
1557 1730 if renderer == 'rst':
1558 1731 return rst(source, mentions=mentions)
1559 1732 if renderer == 'markdown':
1560 1733 return markdown(source, mentions=mentions)
1561 1734
1562 1735
1563 1736 def commit_status(repo, commit_id):
1564 1737 return ChangesetStatusModel().get_status(repo, commit_id)
1565 1738
1566 1739
1567 1740 def commit_status_lbl(commit_status):
1568 1741 return dict(ChangesetStatus.STATUSES).get(commit_status)
1569 1742
1570 1743
1571 1744 def commit_time(repo_name, commit_id):
1572 1745 repo = Repository.get_by_repo_name(repo_name)
1573 1746 commit = repo.get_commit(commit_id=commit_id)
1574 1747 return commit.date
1575 1748
1576 1749
1577 1750 def get_permission_name(key):
1578 1751 return dict(Permission.PERMS).get(key)
1579 1752
1580 1753
1581 1754 def journal_filter_help():
1582 1755 return _(
1583 1756 'Example filter terms:\n' +
1584 1757 ' repository:vcs\n' +
1585 1758 ' username:marcin\n' +
1586 1759 ' action:*push*\n' +
1587 1760 ' ip:127.0.0.1\n' +
1588 1761 ' date:20120101\n' +
1589 1762 ' date:[20120101100000 TO 20120102]\n' +
1590 1763 '\n' +
1591 1764 'Generate wildcards using \'*\' character:\n' +
1592 1765 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1593 1766 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1594 1767 '\n' +
1595 1768 'Optional AND / OR operators in queries\n' +
1596 1769 ' "repository:vcs OR repository:test"\n' +
1597 1770 ' "username:test AND repository:test*"\n'
1598 1771 )
1599 1772
1600 1773
1601 1774 def not_mapped_error(repo_name):
1602 1775 flash(_('%s repository is not mapped to db perhaps'
1603 1776 ' it was created or renamed from the filesystem'
1604 1777 ' please run the application again'
1605 1778 ' in order to rescan repositories') % repo_name, category='error')
1606 1779
1607 1780
1608 1781 def ip_range(ip_addr):
1609 1782 from rhodecode.model.db import UserIpMap
1610 1783 s, e = UserIpMap._get_ip_range(ip_addr)
1611 1784 return '%s - %s' % (s, e)
1612 1785
1613 1786
1614 1787 def form(url, method='post', needs_csrf_token=True, **attrs):
1615 1788 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1616 1789 if method.lower() != 'get' and needs_csrf_token:
1617 1790 raise Exception(
1618 1791 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1619 1792 'CSRF token. If the endpoint does not require such token you can ' +
1620 1793 'explicitly set the parameter needs_csrf_token to false.')
1621 1794
1622 1795 return wh_form(url, method=method, **attrs)
1623 1796
1624 1797
1625 1798 def secure_form(url, method="POST", multipart=False, **attrs):
1626 1799 """Start a form tag that points the action to an url. This
1627 1800 form tag will also include the hidden field containing
1628 1801 the auth token.
1629 1802
1630 1803 The url options should be given either as a string, or as a
1631 1804 ``url()`` function. The method for the form defaults to POST.
1632 1805
1633 1806 Options:
1634 1807
1635 1808 ``multipart``
1636 1809 If set to True, the enctype is set to "multipart/form-data".
1637 1810 ``method``
1638 1811 The method to use when submitting the form, usually either
1639 1812 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1640 1813 hidden input with name _method is added to simulate the verb
1641 1814 over POST.
1642 1815
1643 1816 """
1644 1817 from webhelpers.pylonslib.secure_form import insecure_form
1645 1818 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1646 1819 form = insecure_form(url, method, multipart, **attrs)
1647 1820 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1648 1821 return literal("%s\n%s" % (form, token))
1649 1822
1650 1823 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1651 1824 select_html = select(name, selected, options, **attrs)
1652 1825 select2 = """
1653 1826 <script>
1654 1827 $(document).ready(function() {
1655 1828 $('#%s').select2({
1656 1829 containerCssClass: 'drop-menu',
1657 1830 dropdownCssClass: 'drop-menu-dropdown',
1658 1831 dropdownAutoWidth: true%s
1659 1832 });
1660 1833 });
1661 1834 </script>
1662 1835 """
1663 1836 filter_option = """,
1664 1837 minimumResultsForSearch: -1
1665 1838 """
1666 1839 input_id = attrs.get('id') or name
1667 1840 filter_enabled = "" if enable_filter else filter_option
1668 1841 select_script = literal(select2 % (input_id, filter_enabled))
1669 1842
1670 1843 return literal(select_html+select_script)
1671 1844
1672 1845
1673 1846 def get_visual_attr(tmpl_context_var, attr_name):
1674 1847 """
1675 1848 A safe way to get a variable from visual variable of template context
1676 1849
1677 1850 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1678 1851 :param attr_name: name of the attribute we fetch from the c.visual
1679 1852 """
1680 1853 visual = getattr(tmpl_context_var, 'visual', None)
1681 1854 if not visual:
1682 1855 return
1683 1856 else:
1684 1857 return getattr(visual, attr_name, None)
1685 1858
1686 1859
1687 1860 def get_last_path_part(file_node):
1688 1861 if not file_node.path:
1689 1862 return u''
1690 1863
1691 1864 path = safe_unicode(file_node.path.split('/')[-1])
1692 1865 return u'../' + path
1693 1866
1694 1867
1695 1868 def route_path(*args, **kwds):
1696 1869 """
1697 1870 Wrapper around pyramids `route_path` function. It is used to generate
1698 1871 URLs from within pylons views or templates. This will be removed when
1699 1872 pyramid migration if finished.
1700 1873 """
1701 1874 req = get_current_request()
1702 1875 return req.route_path(*args, **kwds)
1703 1876
1704 1877
1705 1878 def resource_path(*args, **kwds):
1706 1879 """
1707 1880 Wrapper around pyramids `route_path` function. It is used to generate
1708 1881 URLs from within pylons views or templates. This will be removed when
1709 1882 pyramid migration if finished.
1710 1883 """
1711 1884 req = get_current_request()
1712 1885 return req.resource_path(*args, **kwds)
@@ -1,260 +1,261 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 Index schema for RhodeCode
23 23 """
24 24
25 25 from __future__ import absolute_import
26 26 import logging
27 27 import os
28 28
29 29 from pylons.i18n.translation import _
30 30
31 31 from whoosh import query as query_lib, sorting
32 32 from whoosh.highlight import HtmlFormatter, ContextFragmenter
33 33 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
34 34 from whoosh.qparser import QueryParser, QueryParserError
35 35
36 36 import rhodecode.lib.helpers as h
37 37 from rhodecode.lib.index import BaseSearch
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 try:
43 43 # we first try to import from rhodecode tools, fallback to copies if
44 44 # we're unable to
45 45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
46 46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
47 47 COMMIT_SCHEMA)
48 48 except ImportError:
49 49 log.warning('rhodecode_tools schema not available, doing a fallback '
50 50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
51 51 from rhodecode.lib.index.whoosh_fallback_schema import (
52 52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
53 53 COMMIT_SCHEMA)
54 54
55 55
56 56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
57 57 FRAGMENTER = ContextFragmenter(200)
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class Search(BaseSearch):
63 63
64 64 name = 'whoosh'
65 65
66 66 def __init__(self, config):
67 67 self.config = config
68 68 if not os.path.isdir(self.config['location']):
69 69 os.makedirs(self.config['location'])
70 70
71 71 opener = create_in
72 72 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
73 73 opener = open_dir
74 74 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
75 75 indexname=FILE_INDEX_NAME)
76 76
77 77 opener = create_in
78 78 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
79 79 opener = open_dir
80 80 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
81 81 indexname=COMMIT_INDEX_NAME)
82 82
83 83 self.commit_schema = COMMIT_SCHEMA
84 84 self.commit_index = changeset_index
85 85 self.file_schema = FILE_SCHEMA
86 86 self.file_index = file_index
87 87 self.searcher = None
88 88
89 89 def cleanup(self):
90 90 if self.searcher:
91 91 self.searcher.close()
92 92
93 def search(self, query, document_type, search_user, repo_name=None):
93 def search(self, query, document_type, search_user, repo_name=None,
94 requested_page=1, page_limit=10):
94 95 log.debug(u'QUERY: %s on %s', query, document_type)
95 96 result = {
96 97 'results': [],
97 98 'count': 0,
98 99 'error': None,
99 100 'runtime': 0
100 101 }
101 102 search_type, index_name, schema_defn = self._prepare_for_search(
102 103 document_type)
103 104 self._init_searcher(index_name)
104 105 try:
105 106 qp = QueryParser(search_type, schema=schema_defn)
106 107 allowed_repos_filter = self._get_repo_filter(
107 108 search_user, repo_name)
108 109 try:
109 110 query = qp.parse(unicode(query))
110 111 log.debug('query: %s (%s)' % (query, repr(query)))
111 112
112 113 sortedby = None
113 114 if search_type == 'message':
114 115 sortedby = sorting.FieldFacet('commit_idx', reverse=True)
115 116
116 117 whoosh_results = self.searcher.search(
117 118 query, filter=allowed_repos_filter, limit=None,
118 119 sortedby=sortedby,)
119 120
120 121 # fixes for 32k limit that whoosh uses for highlight
121 122 whoosh_results.fragmenter.charlimit = None
122 123 res_ln = whoosh_results.scored_length()
123 124 result['runtime'] = whoosh_results.runtime
124 125 result['count'] = res_ln
125 126 result['results'] = WhooshResultWrapper(
126 127 search_type, res_ln, whoosh_results)
127 128
128 129 except QueryParserError:
129 130 result['error'] = _('Invalid search query. Try quoting it.')
130 131 except (EmptyIndexError, IOError, OSError):
131 132 msg = _('There is no index to search in. '
132 133 'Please run whoosh indexer')
133 134 log.exception(msg)
134 135 result['error'] = msg
135 136 except Exception:
136 137 msg = _('An error occurred during this search operation')
137 138 log.exception(msg)
138 139 result['error'] = msg
139 140
140 141 return result
141 142
142 143 def statistics(self):
143 144 stats = [
144 145 {'key': _('Index Type'), 'value': 'Whoosh'},
145 146 {'key': _('File Index'), 'value': str(self.file_index)},
146 147 {'key': _('Indexed documents'),
147 148 'value': self.file_index.doc_count()},
148 149 {'key': _('Last update'),
149 150 'value': h.time_to_datetime(self.file_index.last_modified())},
150 151 {'key': _('Commit index'), 'value': str(self.commit_index)},
151 152 {'key': _('Indexed documents'),
152 153 'value': str(self.commit_index.doc_count())},
153 154 {'key': _('Last update'),
154 155 'value': h.time_to_datetime(self.commit_index.last_modified())}
155 156 ]
156 157 return stats
157 158
158 159 def _get_repo_filter(self, auth_user, repo_name):
159 160
160 161 allowed_to_search = [
161 162 repo for repo, perm in
162 163 auth_user.permissions['repositories'].items()
163 164 if perm != 'repository.none']
164 165
165 166 if repo_name:
166 167 repo_filter = [query_lib.Term('repository', repo_name)]
167 168
168 169 elif 'hg.admin' in auth_user.permissions.get('global', []):
169 170 return None
170 171
171 172 else:
172 173 repo_filter = [query_lib.Term('repository', _rn)
173 174 for _rn in allowed_to_search]
174 175 # in case we're not allowed to search anywhere, it's a trick
175 176 # to tell whoosh we're filtering, on ALL results
176 177 repo_filter = repo_filter or [query_lib.Term('repository', '')]
177 178
178 179 return query_lib.Or(repo_filter)
179 180
180 181 def _prepare_for_search(self, cur_type):
181 182 search_type = {
182 183 'content': 'content',
183 184 'commit': 'message',
184 185 'path': 'path',
185 186 'repository': 'repository'
186 187 }.get(cur_type, 'content')
187 188
188 189 index_name = {
189 190 'content': FILE_INDEX_NAME,
190 191 'commit': COMMIT_INDEX_NAME,
191 192 'path': FILE_INDEX_NAME
192 193 }.get(cur_type, FILE_INDEX_NAME)
193 194
194 195 schema_defn = {
195 196 'content': self.file_schema,
196 197 'commit': self.commit_schema,
197 198 'path': self.file_schema
198 199 }.get(cur_type, self.file_schema)
199 200
200 201 log.debug('IDX: %s' % index_name)
201 202 log.debug('SCHEMA: %s' % schema_defn)
202 203 return search_type, index_name, schema_defn
203 204
204 205 def _init_searcher(self, index_name):
205 206 idx = open_dir(self.config['location'], indexname=index_name)
206 207 self.searcher = idx.searcher()
207 208 return self.searcher
208 209
209 210
210 211 class WhooshResultWrapper(object):
211 212 def __init__(self, search_type, total_hits, results):
212 213 self.search_type = search_type
213 214 self.results = results
214 215 self.total_hits = total_hits
215 216
216 217 def __str__(self):
217 218 return '<%s at %s>' % (self.__class__.__name__, len(self))
218 219
219 220 def __repr__(self):
220 221 return self.__str__()
221 222
222 223 def __len__(self):
223 224 return self.total_hits
224 225
225 226 def __iter__(self):
226 227 """
227 228 Allows Iteration over results,and lazy generate content
228 229
229 230 *Requires* implementation of ``__getitem__`` method.
230 231 """
231 232 for hit in self.results:
232 233 yield self.get_full_content(hit)
233 234
234 235 def __getitem__(self, key):
235 236 """
236 237 Slicing of resultWrapper
237 238 """
238 239 i, j = key.start, key.stop
239 240 for hit in self.results[i:j]:
240 241 yield self.get_full_content(hit)
241 242
242 243 def get_full_content(self, hit):
243 244 # TODO: marcink: this feels like an overkill, there's a lot of data
244 245 # inside hit object, and we don't need all
245 246 res = dict(hit)
246 247
247 248 f_path = '' # noqa
248 249 if self.search_type in ['content', 'path']:
249 250 f_path = res['path'].split(res['repository'])[-1]
250 251 f_path = f_path.lstrip(os.sep)
251 252
252 253 if self.search_type == 'content':
253 254 res.update({'content_short_hl': hit.highlights('content'),
254 255 'f_path': f_path})
255 256 elif self.search_type == 'path':
256 257 res.update({'f_path': f_path})
257 258 elif self.search_type == 'message':
258 259 res.update({'message_hl': hit.highlights('message')})
259 260
260 261 return res
@@ -1,605 +1,625 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21 .compare_view_files {
22 22
23 23 .diff-container {
24 24
25 25 .diffblock {
26 26 margin-bottom: 0;
27 27 }
28 28 }
29 29 }
30 30
31 31 div.diffblock .sidebyside {
32 32 background: #ffffff;
33 33 }
34 34
35 35 div.diffblock {
36 36 overflow-x: auto;
37 37 overflow-y: hidden;
38 38 clear: both;
39 39 padding: 0px;
40 40 background: @grey6;
41 41 border: @border-thickness solid @grey5;
42 42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 43 border-radius: @border-radius @border-radius 0px 0px;
44 44
45 45
46 46 .comments-number {
47 47 float: right;
48 48 }
49 49
50 50 // BEGIN CODE-HEADER STYLES
51 51
52 52 .code-header {
53 53 background: @grey6;
54 54 padding: 10px 0 10px 0;
55 55 height: auto;
56 56 width: 100%;
57 57
58 58 .hash {
59 59 float: left;
60 60 padding: 2px 0 0 2px;
61 61 }
62 62
63 63 .date {
64 64 float: left;
65 65 text-transform: uppercase;
66 66 padding: 4px 0px 0px 2px;
67 67 }
68 68
69 69 div {
70 70 margin-left: 4px;
71 71 }
72 72
73 73 div.compare_header {
74 74 min-height: 40px;
75 75 margin: 0;
76 76 padding: 0 @padding;
77 77
78 78 .drop-menu {
79 79 float:left;
80 80 display: block;
81 81 margin:0 0 @padding 0;
82 82 }
83 83
84 84 .compare-label {
85 85 float: left;
86 86 clear: both;
87 87 display: inline-block;
88 88 min-width: 5em;
89 89 margin: 0;
90 90 padding: @button-padding @button-padding @button-padding 0;
91 91 font-family: @text-semibold;
92 92 }
93 93
94 94 .compare-buttons {
95 95 float: left;
96 96 margin: 0;
97 97 padding: 0 0 @padding;
98 98
99 99 .btn {
100 100 margin: 0 @padding 0 0;
101 101 }
102 102 }
103 103 }
104 104
105 105 }
106 106
107 107 .parents {
108 108 float: left;
109 109 width: 100px;
110 110 font-weight: 400;
111 111 vertical-align: middle;
112 112 padding: 0px 2px 0px 2px;
113 113 background-color: @grey6;
114 114
115 115 #parent_link {
116 116 margin: 00px 2px;
117 117
118 118 &.double {
119 119 margin: 0px 2px;
120 120 }
121 121
122 122 &.disabled{
123 123 margin-right: @padding;
124 124 }
125 125 }
126 126 }
127 127
128 128 .children {
129 129 float: right;
130 130 width: 100px;
131 131 font-weight: 400;
132 132 vertical-align: middle;
133 133 text-align: right;
134 134 padding: 0px 2px 0px 2px;
135 135 background-color: @grey6;
136 136
137 137 #child_link {
138 138 margin: 0px 2px;
139 139
140 140 &.double {
141 141 margin: 0px 2px;
142 142 }
143 143
144 144 &.disabled{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148 }
149 149
150 150 .changeset_header {
151 151 height: 16px;
152 152
153 153 & > div{
154 154 margin-right: @padding;
155 155 }
156 156 }
157 157
158 158 .changeset_file {
159 159 text-align: left;
160 160 float: left;
161 161 padding: 0;
162 162
163 163 a{
164 164 display: inline-block;
165 165 margin-right: 0.5em;
166 166 }
167 167
168 168 #selected_mode{
169 169 margin-left: 0;
170 170 }
171 171 }
172 172
173 173 .diff-menu-wrapper {
174 174 float: left;
175 175 }
176 176
177 177 .diff-menu {
178 178 position: absolute;
179 179 background: none repeat scroll 0 0 #FFFFFF;
180 180 border-color: #003367 @grey3 @grey3;
181 181 border-right: 1px solid @grey3;
182 182 border-style: solid solid solid;
183 183 border-width: @border-thickness;
184 184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 185 margin-top: 5px;
186 186 margin-left: 1px;
187 187 }
188 188
189 189 .diff-actions, .editor-actions {
190 190 float: left;
191 191
192 192 input{
193 193 margin: 0 0.5em 0 0;
194 194 }
195 195 }
196 196
197 197 // END CODE-HEADER STYLES
198 198
199 199 // BEGIN CODE-BODY STYLES
200 200
201 201 .code-body {
202 202 background: white;
203 203 padding: 0;
204 204 background-color: #ffffff;
205 205 position: relative;
206 206 max-width: none;
207 207 box-sizing: border-box;
208 208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 209 // to have the intended size and to scroll. Should be simplified.
210 210 width: 100%;
211 211 overflow-x: auto;
212 212 }
213 213
214 214 pre.raw {
215 215 background: white;
216 216 color: @grey1;
217 217 }
218 218 // END CODE-BODY STYLES
219 219
220 220 }
221 221
222 222
223 223 table.code-difftable {
224 224 border-collapse: collapse;
225 225 width: 99%;
226 226 border-radius: 0px !important;
227 227
228 228 td {
229 229 padding: 0 !important;
230 230 background: none !important;
231 231 border: 0 !important;
232 232 }
233 233
234 234 .context {
235 235 background: none repeat scroll 0 0 #DDE7EF;
236 236 }
237 237
238 238 .add {
239 239 background: none repeat scroll 0 0 #DDFFDD;
240 240
241 241 ins {
242 242 background: none repeat scroll 0 0 #AAFFAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 .del {
248 248 background: none repeat scroll 0 0 #FFDDDD;
249 249
250 250 del {
251 251 background: none repeat scroll 0 0 #FFAAAA;
252 252 text-decoration: none;
253 253 }
254 254 }
255 255
256 256 /** LINE NUMBERS **/
257 257 .lineno {
258 258 padding-left: 2px;
259 259 padding-right: 2px;
260 260 text-align: right;
261 261 width: 32px;
262 262 -moz-user-select: none;
263 263 -webkit-user-select: none;
264 264 border-right: @border-thickness solid @grey5 !important;
265 265 border-left: 0px solid #CCC !important;
266 266 border-top: 0px solid #CCC !important;
267 267 border-bottom: none !important;
268 268
269 269 a {
270 270 &:extend(pre);
271 271 text-align: right;
272 272 padding-right: 2px;
273 273 cursor: pointer;
274 274 display: block;
275 275 width: 32px;
276 276 }
277 277 }
278 278
279 279 .context {
280 280 cursor: auto;
281 281 &:extend(pre);
282 282 }
283 283
284 284 .lineno-inline {
285 285 background: none repeat scroll 0 0 #FFF !important;
286 286 padding-left: 2px;
287 287 padding-right: 2px;
288 288 text-align: right;
289 289 width: 30px;
290 290 -moz-user-select: none;
291 291 -webkit-user-select: none;
292 292 }
293 293
294 294 /** CODE **/
295 295 .code {
296 296 display: block;
297 297 width: 100%;
298 298
299 299 td {
300 300 margin: 0;
301 301 padding: 0;
302 302 }
303 303
304 304 pre {
305 305 margin: 0;
306 306 padding: 0;
307 307 margin-left: .5em;
308 308 }
309 309 }
310 310 }
311 311
312 312
313 313 // Comments
314 314
315 315 div.comment:target {
316 316 border-left: 6px solid @comment-highlight-color;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
320 320
321 321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 322 //current values that might change. But to make it clear I put as a calculation
323 323 @comment-max-width: 1065px;
324 324 @pr-extra-margin: 34px;
325 325 @pr-border-spacing: 4px;
326 326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327 327
328 328 // Pull Request
329 329 .cs_files .code-difftable {
330 330 border: @border-thickness solid @grey5; //borders only on PRs
331 331
332 332 .comment-inline-form,
333 333 div.comment {
334 334 width: @pr-comment-width;
335 335 }
336 336 }
337 337
338 338 // Changeset
339 339 .code-difftable {
340 340 .comment-inline-form,
341 341 div.comment {
342 342 width: @comment-max-width;
343 343 }
344 344 }
345 345
346 346 //Style page
347 347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 348 #style-page .code-difftable{
349 349 .comment-inline-form,
350 350 div.comment {
351 351 width: @comment-max-width - @style-extra-margin;
352 352 }
353 353 }
354 354
355 355 #context-bar > h2 {
356 356 font-size: 20px;
357 357 }
358 358
359 359 #context-bar > h2> a {
360 360 font-size: 20px;
361 361 }
362 362 // end of defaults
363 363
364 364 .file_diff_buttons {
365 365 padding: 0 0 @padding;
366 366
367 367 .drop-menu {
368 368 float: left;
369 369 margin: 0 @padding 0 0;
370 370 }
371 371 .btn {
372 372 margin: 0 @padding 0 0;
373 373 }
374 374 }
375 375
376 376 .code-body.textarea.editor {
377 377 max-width: none;
378 378 padding: 15px;
379 379 }
380 380
381 381 td.injected_diff{
382 382 max-width: 1178px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385
386 386 div.diff-container,
387 387 div.diffblock{
388 388 max-width: 100%;
389 389 }
390 390
391 391 div.code-body {
392 392 max-width: 1124px;
393 393 overflow-x: auto;
394 394 padding: 0;
395 395 }
396 396 div.diffblock {
397 397 border: none;
398 398 }
399 399
400 400 &.inline-form {
401 401 width: 99%
402 402 }
403 403 }
404 404
405 405
406 406 table.code-difftable {
407 407 width: 100%;
408 408 }
409 409
410 410 /** PYGMENTS COLORING **/
411 411 div.codeblock {
412 412
413 413 // TODO: johbo: Added interim to get rid of the margin around
414 414 // Select2 widgets. This needs further cleanup.
415 415 margin-top: @padding;
416 416
417 417 overflow: auto;
418 418 padding: 0px;
419 419 border: @border-thickness solid @grey5;
420 420 background: @grey6;
421 421 .border-radius(@border-radius);
422 422
423 423 #remove_gist {
424 424 float: right;
425 425 }
426 426
427 427 .author {
428 428 clear: both;
429 429 vertical-align: middle;
430 430 font-family: @text-bold;
431 431 }
432 432
433 433 .btn-mini {
434 434 float: left;
435 435 margin: 0 5px 0 0;
436 436 }
437 437
438 438 .code-header {
439 439 padding: @padding;
440 440 border-bottom: @border-thickness solid @grey5;
441 441
442 442 .rc-user {
443 443 min-width: 0;
444 444 margin-right: .5em;
445 445 }
446 446
447 447 .stats {
448 448 clear: both;
449 449 margin: 0 0 @padding 0;
450 450 padding: 0;
451 451 .left {
452 452 float: left;
453 453 clear: left;
454 454 max-width: 75%;
455 455 margin: 0 0 @padding 0;
456 456
457 457 &.item {
458 458 margin-right: @padding;
459 459 &.last { border-right: none; }
460 460 }
461 461 }
462 462 .buttons { float: right; }
463 463 .author {
464 464 height: 25px; margin-left: 15px; font-weight: bold;
465 465 }
466 466 }
467 467
468 468 .commit {
469 469 margin: 5px 0 0 26px;
470 470 font-weight: normal;
471 471 white-space: pre-wrap;
472 472 }
473 473 }
474 474
475 475 .message {
476 476 position: relative;
477 477 margin: @padding;
478 478
479 479 .codeblock-label {
480 480 margin: 0 0 1em 0;
481 481 }
482 482 }
483 483
484 484 .code-body {
485 485 padding: @padding;
486 486 background-color: #ffffff;
487 487 min-width: 100%;
488 488 box-sizing: border-box;
489 489 // TODO: johbo: Parent has overflow: auto, this forces the child here
490 490 // to have the intended size and to scroll. Should be simplified.
491 491 width: 100%;
492 492 overflow-x: auto;
493 493 }
494 494 }
495 495
496 496 .code-highlighttable,
497 497 div.codeblock .code-body table {
498 498 width: 0 !important;
499 499 border: 0px !important;
500 500 margin: 0;
501 501 letter-spacing: normal;
502 502
503 503
504 504 td {
505 505 border: 0px !important;
506 506 vertical-align: top;
507 507 }
508 508 }
509 509
510 510 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
511 511 div.search-code-body {
512 512 background-color: #ffffff; padding: 5px 0 5px 10px;
513 513 pre {
514 514 .match { background-color: #faffa6;}
515 515 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
516 516 }
517 .code-highlighttable {
518 border-collapse: collapse;
519
520 tr:hover {
521 background: #fafafa;
522 }
523 td.code {
524 padding-left: 10px;
525 }
526 td.line {
527 border-right: 1px solid #ccc !important;
528 padding-right: 10px;
529 text-align: right;
530 font-family: "Lucida Console",Monaco,monospace;
531 span {
532 white-space: pre-wrap;
533 color: #666666;
534 }
535 }
536 }
517 537 }
518 538
519 539 div.annotatediv { margin-left: 2px; margin-right: 4px; }
520 540 .code-highlight {
521 541 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
522 542 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
523 543 pre div:target {background-color: @comment-highlight-color !important;}
524 544 }
525 545
526 546 .linenos a { text-decoration: none; }
527 547
528 548 .CodeMirror-selected { background: @rchighlightblue; }
529 549 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
530 550 .CodeMirror ::selection { background: @rchighlightblue; }
531 551 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
532 552
533 553 .code { display: block; border:0px !important; }
534 554 .code-highlight,
535 555 .codehilite {
536 556 .hll { background-color: #ffffcc }
537 557 .c { color: #408080; font-style: italic } /* Comment */
538 558 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
539 559 .k { color: #008000; font-weight: bold } /* Keyword */
540 560 .o { color: #666666 } /* Operator */
541 561 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
542 562 .cp { color: #BC7A00 } /* Comment.Preproc */
543 563 .c1 { color: #408080; font-style: italic } /* Comment.Single */
544 564 .cs { color: #408080; font-style: italic } /* Comment.Special */
545 565 .gd { color: #A00000 } /* Generic.Deleted */
546 566 .ge { font-style: italic } /* Generic.Emph */
547 567 .gr { color: #FF0000 } /* Generic.Error */
548 568 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
549 569 .gi { color: #00A000 } /* Generic.Inserted */
550 570 .go { color: #808080 } /* Generic.Output */
551 571 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
552 572 .gs { font-weight: bold } /* Generic.Strong */
553 573 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
554 574 .gt { color: #0040D0 } /* Generic.Traceback */
555 575 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
556 576 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
557 577 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
558 578 .kp { color: #008000 } /* Keyword.Pseudo */
559 579 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
560 580 .kt { color: #B00040 } /* Keyword.Type */
561 581 .m { color: #666666 } /* Literal.Number */
562 582 .s { color: #BA2121 } /* Literal.String */
563 583 .na { color: #7D9029 } /* Name.Attribute */
564 584 .nb { color: #008000 } /* Name.Builtin */
565 585 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
566 586 .no { color: #880000 } /* Name.Constant */
567 587 .nd { color: #AA22FF } /* Name.Decorator */
568 588 .ni { color: #999999; font-weight: bold } /* Name.Entity */
569 589 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
570 590 .nf { color: #0000FF } /* Name.Function */
571 591 .nl { color: #A0A000 } /* Name.Label */
572 592 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
573 593 .nt { color: #008000; font-weight: bold } /* Name.Tag */
574 594 .nv { color: #19177C } /* Name.Variable */
575 595 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
576 596 .w { color: #bbbbbb } /* Text.Whitespace */
577 597 .mf { color: #666666 } /* Literal.Number.Float */
578 598 .mh { color: #666666 } /* Literal.Number.Hex */
579 599 .mi { color: #666666 } /* Literal.Number.Integer */
580 600 .mo { color: #666666 } /* Literal.Number.Oct */
581 601 .sb { color: #BA2121 } /* Literal.String.Backtick */
582 602 .sc { color: #BA2121 } /* Literal.String.Char */
583 603 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
584 604 .s2 { color: #BA2121 } /* Literal.String.Double */
585 605 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
586 606 .sh { color: #BA2121 } /* Literal.String.Heredoc */
587 607 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
588 608 .sx { color: #008000 } /* Literal.String.Other */
589 609 .sr { color: #BB6688 } /* Literal.String.Regex */
590 610 .s1 { color: #BA2121 } /* Literal.String.Single */
591 611 .ss { color: #19177C } /* Literal.String.Symbol */
592 612 .bp { color: #008000 } /* Name.Builtin.Pseudo */
593 613 .vc { color: #19177C } /* Name.Variable.Class */
594 614 .vg { color: #19177C } /* Name.Variable.Global */
595 615 .vi { color: #19177C } /* Name.Variable.Instance */
596 616 .il { color: #666666 } /* Literal.Number.Integer.Long */
597 617 }
598 618
599 619 /* customized pre blocks for markdown/rst */
600 620 pre.literal-block, .codehilite pre{
601 621 padding: @padding;
602 622 border: 1px solid @grey6;
603 623 .border-radius(@border-radius);
604 624 background-color: @grey7;
605 625 }
@@ -1,76 +1,76 b''
1 1 <%namespace name="base" file="/base/base.html"/>
2 2
3 3 <table class="rctable search-results">
4 4 <tr>
5 5 <th>${_('Repository')}</th>
6 6 <th>${_('Commit')}</th>
7 7 <th></th>
8 8 <th>${_('Commit message')}</th>
9 9 <th>${_('Age')}</th>
10 10 <th>${_('Author')}</th>
11 11 </tr>
12 12 %for entry in c.formatted_results:
13 13 ## search results are additionally filtered, and this check is just a safe gate
14 14 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
15 15 <tr class="body">
16 16 <td class="td-componentname">
17 17 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
18 18 <i class="icon-hg"></i>
19 19 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
20 20 <i class="icon-git"></i>
21 21 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
22 22 <i class="icon-svn"></i>
23 23 %endif
24 24 ${h.link_to(entry['repository'], h.url('summary_home',repo_name=entry['repository']))}
25 25 </td>
26 26 <td class="td-commit">
27 27 ${h.link_to(h._shorten_commit_id(entry['commit_id']),
28 28 h.url('changeset_home',repo_name=entry['repository'],revision=entry['commit_id']))}
29 29 </td>
30 30 <td class="td-message expand_commit search open" data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="t-${h.md5_safe(entry['repository'])+entry['commit_id']}" title="${_('Expand commit message')}">
31 31 <div class="show_more_col">
32 32 <i class="show_more"></i>&nbsp;
33 33 </div>
34 34 </td>
35 35 <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open">
36 %if entry['message_hl']:
36 %if entry.get('message_hl'):
37 37 ${h.literal(entry['message_hl'])}
38 38 %else:
39 39 ${h.urlify_commit_message(entry['message'], entry['repository'])}
40 40 %endif
41 41 </td>
42 42 <td class="td-time">
43 43 ${h.age_component(h.time_to_datetime(entry['date']))}
44 44 </td>
45 45
46 46 <td class="td-user author">
47 47 ${base.gravatar_with_user(entry['author'])}
48 48 </td>
49 49 </tr>
50 50 % endif
51 51 %endfor
52 52 </table>
53 53
54 54 %if c.cur_query and c.formatted_results:
55 55 <div class="pagination-wh pagination-left">
56 56 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
57 57 </div>
58 58 %endif
59 59
60 60 <script>
61 61 $('.expand_commit').on('click',function(e){
62 62 var target_expand = $(this);
63 63 var cid = target_expand.data('commit-id');
64 64
65 65 if (target_expand.hasClass('open')){
66 66 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'})
67 67 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'})
68 68 target_expand.removeClass('open');
69 69 }
70 70 else {
71 71 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'})
72 72 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'})
73 73 target_expand.addClass('open');
74 74 }
75 75 });
76 76 </script>
@@ -1,51 +1,101 b''
1 <%def name="highlight_text_file(terms, text, url, line_context=3,
2 max_lines=10,
3 mimetype=None, filepath=None)">
4 <%
5 lines = text.split('\n')
6 lines_of_interest = set()
7 matching_lines = h.get_matching_line_offsets(lines, terms)
8 shown_matching_lines = 0
9
10 for line_number in matching_lines:
11 if len(lines_of_interest) < max_lines:
12 lines_of_interest |= set(range(
13 max(line_number - line_context, 0),
14 min(line_number + line_context, len(lines))))
15 shown_matching_lines += 1
16
17 %>
18 ${h.code_highlight(
19 text,
20 h.get_lexer_safe(
21 mimetype=mimetype,
22 filepath=filepath,
23 ),
24 h.SearchContentCodeHtmlFormatter(
25 linenos=True,
26 cssclass="code-highlight",
27 url=url,
28 query_terms=terms,
29 only_line_numbers=lines_of_interest
30 ))|n}
31 %if len(matching_lines) > shown_matching_lines:
32 <a href="${url}">
33 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
34 </p>
35 %endif
36 </%def>
37
1 38 <div class="search-results">
2 39 %for entry in c.formatted_results:
3 40 ## search results are additionally filtered, and this check is just a safe gate
4 41 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
5 42 <div id="codeblock" class="codeblock">
6 43 <div class="codeblock-header">
7 44 <h2>
8 45 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
9 46 <i class="icon-hg"></i>
10 47 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
11 48 <i class="icon-git"></i>
12 49 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
13 50 <i class="icon-svn"></i>
14 51 %endif
15 52 ${h.link_to(entry['repository'], h.url('summary_home',repo_name=entry['repository']))}
16 53 </h2>
17 54 <div class="stats">
18 55 ${h.link_to(h.literal(entry['f_path']), h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']))}
19 56 %if entry.get('lines'):
20 57 | ${entry.get('lines', 0.)} ${ungettext('line', 'lines', entry.get('lines', 0.))}
21 58 %endif
22 59 %if entry.get('size'):
23 60 | ${h.format_byte_size_binary(entry['size'])}
24 61 %endif
25 62 %if entry.get('mimetype'):
26 63 | ${entry.get('mimetype', "unknown mimetype")}
27 64 %endif
28 65 </div>
29 66 <div class="buttons">
30 67 <a id="file_history_overview_full" href="${h.url('changelog_file_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
31 68 ${_('Show Full History')}
32 69 </a> |
33 70 ${h.link_to(_('Annotation'), h.url('files_annotate_home', repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
34 71 | ${h.link_to(_('Raw'), h.url('files_raw_home', repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
35 72 | <a href="${h.url('files_rawfile_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
36 73 ${_('Download')}
37 74 </a>
38 75 </div>
39 76 </div>
40 77 <div class="code-body search-code-body">
41 <pre>${h.literal(entry['content_short_hl'])}</pre>
78 ${highlight_text_file(c.cur_query, entry['content'],
79 url=h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']),
80 mimetype=entry.get('mimetype'), filepath=entry.get('path'))}
42 81 </div>
43 82 </div>
44 83 % endif
45 84 %endfor
46 85 </div>
47 86 %if c.cur_query and c.formatted_results:
48 87 <div class="pagination-wh pagination-left" >
49 88 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
50 89 </div>
51 90 %endif
91
92 %if c.cur_query:
93 <script type="text/javascript">
94 $(function(){
95 $(".code").mark(
96 '${' '.join(h.normalize_text_for_matching(c.cur_query).split())}',
97 {"className": 'match',
98 });
99 })
100 </script>
101 %endif No newline at end of file
@@ -1,157 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 copy
22 22 import mock
23 23 import pytest
24 24
25 25 from pylons.util import ContextObj
26 26
27 27 from rhodecode.lib import helpers
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.model.settings import IssueTrackerSettingsModel
30 30
31 31
32 32 @pytest.mark.parametrize('url, expected_url', [
33 33 ('http://rc.rc/test', '<a href="http://rc.rc/test">http://rc.rc/test</a>'),
34 34 ('http://rc.rc/@foo', '<a href="http://rc.rc/@foo">http://rc.rc/@foo</a>'),
35 35 ('http://rc.rc/!foo', '<a href="http://rc.rc/!foo">http://rc.rc/!foo</a>'),
36 36 ('http://rc.rc/&foo', '<a href="http://rc.rc/&foo">http://rc.rc/&foo</a>'),
37 37 ('http://rc.rc/#foo', '<a href="http://rc.rc/#foo">http://rc.rc/#foo</a>'),
38 38 ])
39 39 def test_urlify_text(url, expected_url):
40 40 assert helpers.urlify_text(url) == expected_url
41 41
42 42
43 43 @pytest.mark.parametrize('repo_name, commit_id, path, expected_result', [
44 44 ('rX<X', 'cX<X', 'pX<X/aX<X/bX<X',
45 45 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/">rX&lt;X</a>/'
46 46 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/pX%3CX">pX&lt;X</a>/'
47 47 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/pX%3CX/aX%3CX">aX&lt;X'
48 48 '</a>/bX&lt;X'),
49 49 # Path with only one segment
50 50 ('rX<X', 'cX<X', 'pX<X',
51 51 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/">rX&lt;X</a>/pX&lt;X'),
52 52 # Empty path
53 53 ('rX<X', 'cX<X', '', 'rX&lt;X'),
54 54 ('rX"X', 'cX"X', 'pX"X/aX"X/bX"X',
55 55 '<a class="pjax-link" href="/rX%22X/files/cX%22X/">rX&#34;X</a>/'
56 56 '<a class="pjax-link" href="/rX%22X/files/cX%22X/pX%22X">pX&#34;X</a>/'
57 57 '<a class="pjax-link" href="/rX%22X/files/cX%22X/pX%22X/aX%22X">aX&#34;X'
58 58 '</a>/bX&#34;X'),
59 59 ], ids=['simple', 'one_segment', 'empty_path', 'simple_quote'])
60 60 def test_files_breadcrumbs_xss(
61 61 repo_name, commit_id, path, pylonsapp, expected_result):
62 62 result = helpers.files_breadcrumbs(repo_name, commit_id, path)
63 63 # Expect it to encode all path fragments properly. This is important
64 64 # because it returns an instance of `literal`.
65 65 assert result == expected_result
66 66
67 67
68 68 def test_format_binary():
69 69 assert helpers.format_byte_size_binary(298489462784) == '278.0 GiB'
70 70
71 71
72 72 @pytest.mark.parametrize('text_string, pattern, expected_text', [
73 73 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
74 74 'Fix <a class="issue-tracker-link" href="http://r.io/{repo}/i/42">#42</a>'
75 75 ),
76 76 ('Fix #42', '(?:#)?<issue_id>\d+)', 'Fix #42'), # Broken regex
77 77 ])
78 78 def test_process_patterns_repo(backend, text_string, pattern, expected_text):
79 79 repo = backend.create_repo()
80 80 config = {'123': {
81 81 'uid': '123',
82 82 'pat': pattern,
83 83 'url': 'http://r.io/${repo}/i/${issue_id}',
84 84 'pref': '#',
85 85 }
86 86 }
87 87 with mock.patch.object(IssueTrackerSettingsModel,
88 88 'get_settings', lambda s: config):
89 89 processed_text = helpers.process_patterns(
90 90 text_string, repo.repo_name, config)
91 91
92 92 assert processed_text == expected_text.format(repo=repo.repo_name)
93 93
94 94
95 95 @pytest.mark.parametrize('text_string, pattern, expected_text', [
96 96 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
97 97 'Fix <a class="issue-tracker-link" href="http://r.io/i/42">#42</a>'
98 98 ),
99 99 ('Fix #42', '(?:#)?<issue_id>\d+)', 'Fix #42'), # Broken regex
100 100 ])
101 101 def test_process_patterns_no_repo(text_string, pattern, expected_text):
102 102 config = {'123': {
103 103 'uid': '123',
104 104 'pat': pattern,
105 105 'url': 'http://r.io/i/${issue_id}',
106 106 'pref': '#',
107 107 }
108 108 }
109 109 with mock.patch.object(IssueTrackerSettingsModel,
110 110 'get_global_settings', lambda s, cache: config):
111 111 processed_text = helpers.process_patterns(
112 112 text_string, '', config)
113 113
114 114 assert processed_text == expected_text
115 115
116 116
117 117 def test_process_patterns_non_existent_repo_name(backend):
118 118 text_string = 'Fix #42'
119 119 pattern = '(?:#)(?P<issue_id>\d+)'
120 120 expected_text = ('Fix <a class="issue-tracker-link" '
121 121 'href="http://r.io/do-not-exist/i/42">#42</a>')
122 122 config = {'123': {
123 123 'uid': '123',
124 124 'pat': pattern,
125 125 'url': 'http://r.io/${repo}/i/${issue_id}',
126 126 'pref': '#',
127 127 }
128 128 }
129 129 with mock.patch.object(IssueTrackerSettingsModel,
130 130 'get_global_settings', lambda s, cache: config):
131 131 processed_text = helpers.process_patterns(
132 132 text_string, 'do-not-exist', config)
133 133
134 134 assert processed_text == expected_text
135 135
136 136
137 137 def test_get_visual_attr(pylonsapp):
138 138 c = ContextObj()
139 139 assert None is helpers.get_visual_attr(c, 'fakse')
140 140
141 141 # emulate the c.visual behaviour
142 142 c.visual = AttributeDict({})
143 143 assert None is helpers.get_visual_attr(c, 'some_var')
144 144
145 145 c.visual.some_var = 'foobar'
146 146 assert 'foobar' == helpers.get_visual_attr(c, 'some_var')
147 147
148 148
149 149 @pytest.mark.parametrize('test_text, inclusive, expected_text', [
150 150 ('just a string', False, 'just a string'),
151 151 ('just a string\n', False, 'just a string'),
152 152 ('just a string\n next line', False, 'just a string...'),
153 153 ('just a string\n next line', True, 'just a string\n...'),
154 154 ])
155 155 def test_chop_at(test_text, inclusive, expected_text):
156 156 assert helpers.chop_at_smart(
157 157 test_text, '\n', inclusive, '...') == expected_text
158
159
160 @pytest.mark.parametrize('test_text, expected_output', [
161 ('some text', ['some', 'text']),
162 ('some text', ['some', 'text']),
163 ('some text "with a phrase"', ['some', 'text', 'with a phrase']),
164 ('"a phrase" "another phrase"', ['a phrase', 'another phrase']),
165 ('"justphrase"', ['justphrase']),
166 ('""', []),
167 ('', []),
168 (' ', []),
169 ('" "', []),
170 ])
171 def test_extract_phrases(test_text, expected_output):
172 assert helpers.extract_phrases(test_text) == expected_output
173
174
175 @pytest.mark.parametrize('test_text, text_phrases, expected_output', [
176 ('some text here', ['some', 'here'], [(0, 4), (10, 14)]),
177 ('here here there', ['here'], [(0, 4), (5, 9), (11, 15)]),
178 ('irrelevant', ['not found'], []),
179 ('irrelevant', ['not found'], []),
180 ])
181 def test_get_matching_offsets(test_text, text_phrases, expected_output):
182 assert helpers.get_matching_offsets(
183 test_text, text_phrases) == expected_output
184
185 def test_normalize_text_for_matching():
186 assert helpers.normalize_text_for_matching(
187 'OJjfe)*#$*@)$JF*)3r2f80h') == 'ojjfe jf 3r2f80h'
188
189 def test_get_matching_line_offsets():
190 assert helpers.get_matching_line_offsets([
191 'words words words',
192 'words words words',
193 'some text some',
194 'words words words',
195 'words words words',
196 'text here what'], 'text') == {3: [(5, 9)], 6: [(0, 4)]} No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now