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