##// END OF EJS Templates
default-reviewers: introduce new voting rule logic that allows...
marcink -
r2484:3775edd6 default
parent child Browse files
Show More
@@ -0,0 +1,38 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_11_0_0 as db
18
19 reviewers_table = db.PullRequestReviewers.__table__
20
21 rule_data = Column(
22 'rule_data_json',
23 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
24 rule_data.create(table=reviewers_table)
25
26 # issue fixups
27 fixups(db, meta.Session)
28
29
30 def downgrade(migrate_engine):
31 meta = MetaData()
32 meta.bind = migrate_engine
33
34
35 def fixups(models, _SESSION):
36 pass
37
38
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_11_0_0 as db
18
19 user_group_review_table = db.RepoReviewRuleUserGroup.__table__
20
21 vote_rule = Column("vote_rule", Integer(), nullable=True,
22 default=-1)
23 vote_rule.create(table=user_group_review_table)
24
25 # issue fixups
26 fixups(db, meta.Session)
27
28
29 def downgrade(migrate_engine):
30 meta = MetaData()
31 meta.bind = migrate_engine
32
33
34 def fixups(models, _SESSION):
35 pass
36
37
This diff has been collapsed as it changes many lines, (1494 lines changed) Show them Hide them
@@ -0,0 +1,1494 b''
1 (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ejs = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
2 /*
3 * EJS Embedded JavaScript templates
4 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20 'use strict';
21
22 /**
23 * @file Embedded JavaScript templating engine. {@link http://ejs.co}
24 * @author Matthew Eernisse <mde@fleegix.org>
25 * @author Tiancheng "Timothy" Gu <timothygu99@gmail.com>
26 * @project EJS
27 * @license {@link http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0}
28 */
29
30 /**
31 * EJS internal functions.
32 *
33 * Technically this "module" lies in the same file as {@link module:ejs}, for
34 * the sake of organization all the private functions re grouped into this
35 * module.
36 *
37 * @module ejs-internal
38 * @private
39 */
40
41 /**
42 * Embedded JavaScript templating engine.
43 *
44 * @module ejs
45 * @public
46 */
47
48 var fs = require('fs');
49 var path = require('path');
50 var utils = require('./utils');
51
52 var scopeOptionWarned = false;
53 var _VERSION_STRING = require('../package.json').version;
54 var _DEFAULT_DELIMITER = '%';
55 var _DEFAULT_LOCALS_NAME = 'locals';
56 var _NAME = 'ejs';
57 var _REGEX_STRING = '(<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)';
58 var _OPTS = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
59 'client', '_with', 'rmWhitespace', 'strict', 'filename'];
60 // We don't allow 'cache' option to be passed in the data obj
61 // for the normal `render` call, but this is where Express puts it
62 // so we make an exception for `renderFile`
63 var _OPTS_EXPRESS = _OPTS.concat('cache');
64 var _BOM = /^\uFEFF/;
65
66 /**
67 * EJS template function cache. This can be a LRU object from lru-cache NPM
68 * module. By default, it is {@link module:utils.cache}, a simple in-process
69 * cache that grows continuously.
70 *
71 * @type {Cache}
72 */
73
74 exports.cache = utils.cache;
75
76 /**
77 * Custom file loader. Useful for template preprocessing or restricting access
78 * to a certain part of the filesystem.
79 *
80 * @type {fileLoader}
81 */
82
83 exports.fileLoader = fs.readFileSync;
84
85 /**
86 * Name of the object containing the locals.
87 *
88 * This variable is overridden by {@link Options}`.localsName` if it is not
89 * `undefined`.
90 *
91 * @type {String}
92 * @public
93 */
94
95 exports.localsName = _DEFAULT_LOCALS_NAME;
96
97 /**
98 * Get the path to the included file from the parent file path and the
99 * specified path.
100 *
101 * @param {String} name specified path
102 * @param {String} filename parent file path
103 * @param {Boolean} isDir parent file path whether is directory
104 * @return {String}
105 */
106 exports.resolveInclude = function(name, filename, isDir) {
107 var dirname = path.dirname;
108 var extname = path.extname;
109 var resolve = path.resolve;
110 var includePath = resolve(isDir ? filename : dirname(filename), name);
111 var ext = extname(name);
112 if (!ext) {
113 includePath += '.ejs';
114 }
115 return includePath;
116 };
117
118 /**
119 * Get the path to the included file by Options
120 *
121 * @param {String} path specified path
122 * @param {Options} options compilation options
123 * @return {String}
124 */
125 function getIncludePath(path, options) {
126 var includePath;
127 var filePath;
128 var views = options.views;
129
130 // Abs path
131 if (path.charAt(0) == '/') {
132 includePath = exports.resolveInclude(path.replace(/^\/*/,''), options.root || '/', true);
133 }
134 // Relative paths
135 else {
136 // Look relative to a passed filename first
137 if (options.filename) {
138 filePath = exports.resolveInclude(path, options.filename);
139 if (fs.existsSync(filePath)) {
140 includePath = filePath;
141 }
142 }
143 // Then look in any views directories
144 if (!includePath) {
145 if (Array.isArray(views) && views.some(function (v) {
146 filePath = exports.resolveInclude(path, v, true);
147 return fs.existsSync(filePath);
148 })) {
149 includePath = filePath;
150 }
151 }
152 if (!includePath) {
153 throw new Error('Could not find include include file.');
154 }
155 }
156 return includePath;
157 }
158
159 /**
160 * Get the template from a string or a file, either compiled on-the-fly or
161 * read from cache (if enabled), and cache the template if needed.
162 *
163 * If `template` is not set, the file specified in `options.filename` will be
164 * read.
165 *
166 * If `options.cache` is true, this function reads the file from
167 * `options.filename` so it must be set prior to calling this function.
168 *
169 * @memberof module:ejs-internal
170 * @param {Options} options compilation options
171 * @param {String} [template] template source
172 * @return {(TemplateFunction|ClientFunction)}
173 * Depending on the value of `options.client`, either type might be returned.
174 * @static
175 */
176
177 function handleCache(options, template) {
178 var func;
179 var filename = options.filename;
180 var hasTemplate = arguments.length > 1;
181
182 if (options.cache) {
183 if (!filename) {
184 throw new Error('cache option requires a filename');
185 }
186 func = exports.cache.get(filename);
187 if (func) {
188 return func;
189 }
190 if (!hasTemplate) {
191 template = fileLoader(filename).toString().replace(_BOM, '');
192 }
193 }
194 else if (!hasTemplate) {
195 // istanbul ignore if: should not happen at all
196 if (!filename) {
197 throw new Error('Internal EJS error: no file name or template '
198 + 'provided');
199 }
200 template = fileLoader(filename).toString().replace(_BOM, '');
201 }
202 func = exports.compile(template, options);
203 if (options.cache) {
204 exports.cache.set(filename, func);
205 }
206 return func;
207 }
208
209 /**
210 * Try calling handleCache with the given options and data and call the
211 * callback with the result. If an error occurs, call the callback with
212 * the error. Used by renderFile().
213 *
214 * @memberof module:ejs-internal
215 * @param {Options} options compilation options
216 * @param {Object} data template data
217 * @param {RenderFileCallback} cb callback
218 * @static
219 */
220
221 function tryHandleCache(options, data, cb) {
222 var result;
223 try {
224 result = handleCache(options)(data);
225 }
226 catch (err) {
227 return cb(err);
228 }
229 return cb(null, result);
230 }
231
232 /**
233 * fileLoader is independent
234 *
235 * @param {String} filePath ejs file path.
236 * @return {String} The contents of the specified file.
237 * @static
238 */
239
240 function fileLoader(filePath){
241 return exports.fileLoader(filePath);
242 }
243
244 /**
245 * Get the template function.
246 *
247 * If `options.cache` is `true`, then the template is cached.
248 *
249 * @memberof module:ejs-internal
250 * @param {String} path path for the specified file
251 * @param {Options} options compilation options
252 * @return {(TemplateFunction|ClientFunction)}
253 * Depending on the value of `options.client`, either type might be returned
254 * @static
255 */
256
257 function includeFile(path, options) {
258 var opts = utils.shallowCopy({}, options);
259 opts.filename = getIncludePath(path, opts);
260 return handleCache(opts);
261 }
262
263 /**
264 * Get the JavaScript source of an included file.
265 *
266 * @memberof module:ejs-internal
267 * @param {String} path path for the specified file
268 * @param {Options} options compilation options
269 * @return {Object}
270 * @static
271 */
272
273 function includeSource(path, options) {
274 var opts = utils.shallowCopy({}, options);
275 var includePath;
276 var template;
277 includePath = getIncludePath(path, opts);
278 template = fileLoader(includePath).toString().replace(_BOM, '');
279 opts.filename = includePath;
280 var templ = new Template(template, opts);
281 templ.generateSource();
282 return {
283 source: templ.source,
284 filename: includePath,
285 template: template
286 };
287 }
288
289 /**
290 * Re-throw the given `err` in context to the `str` of ejs, `filename`, and
291 * `lineno`.
292 *
293 * @implements RethrowCallback
294 * @memberof module:ejs-internal
295 * @param {Error} err Error object
296 * @param {String} str EJS source
297 * @param {String} filename file name of the EJS file
298 * @param {String} lineno line number of the error
299 * @static
300 */
301
302 function rethrow(err, str, flnm, lineno, esc){
303 var lines = str.split('\n');
304 var start = Math.max(lineno - 3, 0);
305 var end = Math.min(lines.length, lineno + 3);
306 var filename = esc(flnm); // eslint-disable-line
307 // Error context
308 var context = lines.slice(start, end).map(function (line, i){
309 var curr = i + start + 1;
310 return (curr == lineno ? ' >> ' : ' ')
311 + curr
312 + '| '
313 + line;
314 }).join('\n');
315
316 // Alter exception message
317 err.path = filename;
318 err.message = (filename || 'ejs') + ':'
319 + lineno + '\n'
320 + context + '\n\n'
321 + err.message;
322
323 throw err;
324 }
325
326 function stripSemi(str){
327 return str.replace(/;(\s*$)/, '$1');
328 }
329
330 /**
331 * Compile the given `str` of ejs into a template function.
332 *
333 * @param {String} template EJS template
334 *
335 * @param {Options} opts compilation options
336 *
337 * @return {(TemplateFunction|ClientFunction)}
338 * Depending on the value of `opts.client`, either type might be returned.
339 * @public
340 */
341
342 exports.compile = function compile(template, opts) {
343 var templ;
344
345 // v1 compat
346 // 'scope' is 'context'
347 // FIXME: Remove this in a future version
348 if (opts && opts.scope) {
349 if (!scopeOptionWarned){
350 console.warn('`scope` option is deprecated and will be removed in EJS 3');
351 scopeOptionWarned = true;
352 }
353 if (!opts.context) {
354 opts.context = opts.scope;
355 }
356 delete opts.scope;
357 }
358 templ = new Template(template, opts);
359 return templ.compile();
360 };
361
362 /**
363 * Render the given `template` of ejs.
364 *
365 * If you would like to include options but not data, you need to explicitly
366 * call this function with `data` being an empty object or `null`.
367 *
368 * @param {String} template EJS template
369 * @param {Object} [data={}] template data
370 * @param {Options} [opts={}] compilation and rendering options
371 * @return {String}
372 * @public
373 */
374
375 exports.render = function (template, d, o) {
376 var data = d || {};
377 var opts = o || {};
378
379 // No options object -- if there are optiony names
380 // in the data, copy them to options
381 if (arguments.length == 2) {
382 utils.shallowCopyFromList(opts, data, _OPTS);
383 }
384
385 return handleCache(opts, template)(data);
386 };
387
388 /**
389 * Render an EJS file at the given `path` and callback `cb(err, str)`.
390 *
391 * If you would like to include options but not data, you need to explicitly
392 * call this function with `data` being an empty object or `null`.
393 *
394 * @param {String} path path to the EJS file
395 * @param {Object} [data={}] template data
396 * @param {Options} [opts={}] compilation and rendering options
397 * @param {RenderFileCallback} cb callback
398 * @public
399 */
400
401 exports.renderFile = function () {
402 var filename = arguments[0];
403 var cb = arguments[arguments.length - 1];
404 var opts = {filename: filename};
405 var data;
406
407 if (arguments.length > 2) {
408 data = arguments[1];
409
410 // No options object -- if there are optiony names
411 // in the data, copy them to options
412 if (arguments.length === 3) {
413 // Express 4
414 if (data.settings) {
415 if (data.settings['view options']) {
416 utils.shallowCopyFromList(opts, data.settings['view options'], _OPTS_EXPRESS);
417 }
418 if (data.settings.views) {
419 opts.views = data.settings.views;
420 }
421 }
422 // Express 3 and lower
423 else {
424 utils.shallowCopyFromList(opts, data, _OPTS_EXPRESS);
425 }
426 }
427 else {
428 // Use shallowCopy so we don't pollute passed in opts obj with new vals
429 utils.shallowCopy(opts, arguments[2]);
430 }
431
432 opts.filename = filename;
433 }
434 else {
435 data = {};
436 }
437
438 return tryHandleCache(opts, data, cb);
439 };
440
441 /**
442 * Clear intermediate JavaScript cache. Calls {@link Cache#reset}.
443 * @public
444 */
445
446 exports.clearCache = function () {
447 exports.cache.reset();
448 };
449
450 function Template(text, opts) {
451 opts = opts || {};
452 var options = {};
453 this.templateText = text;
454 this.mode = null;
455 this.truncate = false;
456 this.currentLine = 1;
457 this.source = '';
458 this.dependencies = [];
459 options.client = opts.client || false;
460 options.escapeFunction = opts.escape || utils.escapeXML;
461 options.compileDebug = opts.compileDebug !== false;
462 options.debug = !!opts.debug;
463 options.filename = opts.filename;
464 options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;
465 options.strict = opts.strict || false;
466 options.context = opts.context;
467 options.cache = opts.cache || false;
468 options.rmWhitespace = opts.rmWhitespace;
469 options.root = opts.root;
470 options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;
471 options.views = opts.views;
472
473 if (options.strict) {
474 options._with = false;
475 }
476 else {
477 options._with = typeof opts._with != 'undefined' ? opts._with : true;
478 }
479
480 this.opts = options;
481
482 this.regex = this.createRegex();
483 }
484
485 Template.modes = {
486 EVAL: 'eval',
487 ESCAPED: 'escaped',
488 RAW: 'raw',
489 COMMENT: 'comment',
490 LITERAL: 'literal'
491 };
492
493 Template.prototype = {
494 createRegex: function () {
495 var str = _REGEX_STRING;
496 var delim = utils.escapeRegExpChars(this.opts.delimiter);
497 str = str.replace(/%/g, delim);
498 return new RegExp(str);
499 },
500
501 compile: function () {
502 var src;
503 var fn;
504 var opts = this.opts;
505 var prepended = '';
506 var appended = '';
507 var escapeFn = opts.escapeFunction;
508
509 if (!this.source) {
510 this.generateSource();
511 prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n';
512 if (opts._with !== false) {
513 prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
514 appended += ' }' + '\n';
515 }
516 appended += ' return __output.join("");' + '\n';
517 this.source = prepended + this.source + appended;
518 }
519
520 if (opts.compileDebug) {
521 src = 'var __line = 1' + '\n'
522 + ' , __lines = ' + JSON.stringify(this.templateText) + '\n'
523 + ' , __filename = ' + (opts.filename ?
524 JSON.stringify(opts.filename) : 'undefined') + ';' + '\n'
525 + 'try {' + '\n'
526 + this.source
527 + '} catch (e) {' + '\n'
528 + ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
529 + '}' + '\n';
530 }
531 else {
532 src = this.source;
533 }
534
535 if (opts.client) {
536 src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
537 if (opts.compileDebug) {
538 src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
539 }
540 }
541
542 if (opts.strict) {
543 src = '"use strict";\n' + src;
544 }
545 if (opts.debug) {
546 console.log(src);
547 }
548
549 try {
550 fn = new Function(opts.localsName + ', escapeFn, include, rethrow', src);
551 }
552 catch(e) {
553 // istanbul ignore else
554 if (e instanceof SyntaxError) {
555 if (opts.filename) {
556 e.message += ' in ' + opts.filename;
557 }
558 e.message += ' while compiling ejs\n\n';
559 e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
560 e.message += 'https://github.com/RyanZim/EJS-Lint';
561 }
562 throw e;
563 }
564
565 if (opts.client) {
566 fn.dependencies = this.dependencies;
567 return fn;
568 }
569
570 // Return a callable function which will execute the function
571 // created by the source-code, with the passed data as locals
572 // Adds a local `include` function which allows full recursive include
573 var returnedFn = function (data) {
574 var include = function (path, includeData) {
575 var d = utils.shallowCopy({}, data);
576 if (includeData) {
577 d = utils.shallowCopy(d, includeData);
578 }
579 return includeFile(path, opts)(d);
580 };
581 return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
582 };
583 returnedFn.dependencies = this.dependencies;
584 return returnedFn;
585 },
586
587 generateSource: function () {
588 var opts = this.opts;
589
590 if (opts.rmWhitespace) {
591 // Have to use two separate replace here as `^` and `$` operators don't
592 // work well with `\r`.
593 this.templateText =
594 this.templateText.replace(/\r/g, '').replace(/^\s+|\s+$/gm, '');
595 }
596
597 // Slurp spaces and tabs before <%_ and after _%>
598 this.templateText =
599 this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>');
600
601 var self = this;
602 var matches = this.parseTemplateText();
603 var d = this.opts.delimiter;
604
605 if (matches && matches.length) {
606 matches.forEach(function (line, index) {
607 var opening;
608 var closing;
609 var include;
610 var includeOpts;
611 var includeObj;
612 var includeSrc;
613 // If this is an opening tag, check for closing tags
614 // FIXME: May end up with some false positives here
615 // Better to store modes as k/v with '<' + delimiter as key
616 // Then this can simply check against the map
617 if ( line.indexOf('<' + d) === 0 // If it is a tag
618 && line.indexOf('<' + d + d) !== 0) { // and is not escaped
619 closing = matches[index + 2];
620 if (!(closing == d + '>' || closing == '-' + d + '>' || closing == '_' + d + '>')) {
621 throw new Error('Could not find matching close tag for "' + line + '".');
622 }
623 }
624 // HACK: backward-compat `include` preprocessor directives
625 if ((include = line.match(/^\s*include\s+(\S+)/))) {
626 opening = matches[index - 1];
627 // Must be in EVAL or RAW mode
628 if (opening && (opening == '<' + d || opening == '<' + d + '-' || opening == '<' + d + '_')) {
629 includeOpts = utils.shallowCopy({}, self.opts);
630 includeObj = includeSource(include[1], includeOpts);
631 if (self.opts.compileDebug) {
632 includeSrc =
633 ' ; (function(){' + '\n'
634 + ' var __line = 1' + '\n'
635 + ' , __lines = ' + JSON.stringify(includeObj.template) + '\n'
636 + ' , __filename = ' + JSON.stringify(includeObj.filename) + ';' + '\n'
637 + ' try {' + '\n'
638 + includeObj.source
639 + ' } catch (e) {' + '\n'
640 + ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
641 + ' }' + '\n'
642 + ' ; }).call(this)' + '\n';
643 }else{
644 includeSrc = ' ; (function(){' + '\n' + includeObj.source +
645 ' ; }).call(this)' + '\n';
646 }
647 self.source += includeSrc;
648 self.dependencies.push(exports.resolveInclude(include[1],
649 includeOpts.filename));
650 return;
651 }
652 }
653 self.scanLine(line);
654 });
655 }
656
657 },
658
659 parseTemplateText: function () {
660 var str = this.templateText;
661 var pat = this.regex;
662 var result = pat.exec(str);
663 var arr = [];
664 var firstPos;
665
666 while (result) {
667 firstPos = result.index;
668
669 if (firstPos !== 0) {
670 arr.push(str.substring(0, firstPos));
671 str = str.slice(firstPos);
672 }
673
674 arr.push(result[0]);
675 str = str.slice(result[0].length);
676 result = pat.exec(str);
677 }
678
679 if (str) {
680 arr.push(str);
681 }
682
683 return arr;
684 },
685
686 _addOutput: function (line) {
687 if (this.truncate) {
688 // Only replace single leading linebreak in the line after
689 // -%> tag -- this is the single, trailing linebreak
690 // after the tag that the truncation mode replaces
691 // Handle Win / Unix / old Mac linebreaks -- do the \r\n
692 // combo first in the regex-or
693 line = line.replace(/^(?:\r\n|\r|\n)/, '');
694 this.truncate = false;
695 }
696 else if (this.opts.rmWhitespace) {
697 // rmWhitespace has already removed trailing spaces, just need
698 // to remove linebreaks
699 line = line.replace(/^\n/, '');
700 }
701 if (!line) {
702 return line;
703 }
704
705 // Preserve literal slashes
706 line = line.replace(/\\/g, '\\\\');
707
708 // Convert linebreaks
709 line = line.replace(/\n/g, '\\n');
710 line = line.replace(/\r/g, '\\r');
711
712 // Escape double-quotes
713 // - this will be the delimiter during execution
714 line = line.replace(/"/g, '\\"');
715 this.source += ' ; __append("' + line + '")' + '\n';
716 },
717
718 scanLine: function (line) {
719 var self = this;
720 var d = this.opts.delimiter;
721 var newLineCount = 0;
722
723 newLineCount = (line.split('\n').length - 1);
724
725 switch (line) {
726 case '<' + d:
727 case '<' + d + '_':
728 this.mode = Template.modes.EVAL;
729 break;
730 case '<' + d + '=':
731 this.mode = Template.modes.ESCAPED;
732 break;
733 case '<' + d + '-':
734 this.mode = Template.modes.RAW;
735 break;
736 case '<' + d + '#':
737 this.mode = Template.modes.COMMENT;
738 break;
739 case '<' + d + d:
740 this.mode = Template.modes.LITERAL;
741 this.source += ' ; __append("' + line.replace('<' + d + d, '<' + d) + '")' + '\n';
742 break;
743 case d + d + '>':
744 this.mode = Template.modes.LITERAL;
745 this.source += ' ; __append("' + line.replace(d + d + '>', d + '>') + '")' + '\n';
746 break;
747 case d + '>':
748 case '-' + d + '>':
749 case '_' + d + '>':
750 if (this.mode == Template.modes.LITERAL) {
751 this._addOutput(line);
752 }
753
754 this.mode = null;
755 this.truncate = line.indexOf('-') === 0 || line.indexOf('_') === 0;
756 break;
757 default:
758 // In script mode, depends on type of tag
759 if (this.mode) {
760 // If '//' is found without a line break, add a line break.
761 switch (this.mode) {
762 case Template.modes.EVAL:
763 case Template.modes.ESCAPED:
764 case Template.modes.RAW:
765 if (line.lastIndexOf('//') > line.lastIndexOf('\n')) {
766 line += '\n';
767 }
768 }
769 switch (this.mode) {
770 // Just executing code
771 case Template.modes.EVAL:
772 this.source += ' ; ' + line + '\n';
773 break;
774 // Exec, esc, and output
775 case Template.modes.ESCAPED:
776 this.source += ' ; __append(escapeFn(' + stripSemi(line) + '))' + '\n';
777 break;
778 // Exec and output
779 case Template.modes.RAW:
780 this.source += ' ; __append(' + stripSemi(line) + ')' + '\n';
781 break;
782 case Template.modes.COMMENT:
783 // Do nothing
784 break;
785 // Literal <%% mode, append as raw output
786 case Template.modes.LITERAL:
787 this._addOutput(line);
788 break;
789 }
790 }
791 // In string mode, just add the output
792 else {
793 this._addOutput(line);
794 }
795 }
796
797 if (self.opts.compileDebug && newLineCount) {
798 this.currentLine += newLineCount;
799 this.source += ' ; __line = ' + this.currentLine + '\n';
800 }
801 }
802 };
803
804 /**
805 * Escape characters reserved in XML.
806 *
807 * This is simply an export of {@link module:utils.escapeXML}.
808 *
809 * If `markup` is `undefined` or `null`, the empty string is returned.
810 *
811 * @param {String} markup Input string
812 * @return {String} Escaped string
813 * @public
814 * @func
815 * */
816 exports.escapeXML = utils.escapeXML;
817
818 /**
819 * Express.js support.
820 *
821 * This is an alias for {@link module:ejs.renderFile}, in order to support
822 * Express.js out-of-the-box.
823 *
824 * @func
825 */
826
827 exports.__express = exports.renderFile;
828
829 // Add require support
830 /* istanbul ignore else */
831 if (require.extensions) {
832 require.extensions['.ejs'] = function (module, flnm) {
833 var filename = flnm || /* istanbul ignore next */ module.filename;
834 var options = {
835 filename: filename,
836 client: true
837 };
838 var template = fileLoader(filename).toString();
839 var fn = exports.compile(template, options);
840 module._compile('module.exports = ' + fn.toString() + ';', filename);
841 };
842 }
843
844 /**
845 * Version of EJS.
846 *
847 * @readonly
848 * @type {String}
849 * @public
850 */
851
852 exports.VERSION = _VERSION_STRING;
853
854 /**
855 * Name for detection of EJS.
856 *
857 * @readonly
858 * @type {String}
859 * @public
860 */
861
862 exports.name = _NAME;
863
864 /* istanbul ignore if */
865 if (typeof window != 'undefined') {
866 window.ejs = exports;
867 }
868
869 },{"../package.json":6,"./utils":2,"fs":3,"path":4}],2:[function(require,module,exports){
870 /*
871 * EJS Embedded JavaScript templates
872 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
873 *
874 * Licensed under the Apache License, Version 2.0 (the "License");
875 * you may not use this file except in compliance with the License.
876 * You may obtain a copy of the License at
877 *
878 * http://www.apache.org/licenses/LICENSE-2.0
879 *
880 * Unless required by applicable law or agreed to in writing, software
881 * distributed under the License is distributed on an "AS IS" BASIS,
882 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
883 * See the License for the specific language governing permissions and
884 * limitations under the License.
885 *
886 */
887
888 /**
889 * Private utility functions
890 * @module utils
891 * @private
892 */
893
894 'use strict';
895
896 var regExpChars = /[|\\{}()[\]^$+*?.]/g;
897
898 /**
899 * Escape characters reserved in regular expressions.
900 *
901 * If `string` is `undefined` or `null`, the empty string is returned.
902 *
903 * @param {String} string Input string
904 * @return {String} Escaped string
905 * @static
906 * @private
907 */
908 exports.escapeRegExpChars = function (string) {
909 // istanbul ignore if
910 if (!string) {
911 return '';
912 }
913 return String(string).replace(regExpChars, '\\$&');
914 };
915
916 var _ENCODE_HTML_RULES = {
917 '&': '&amp;',
918 '<': '&lt;',
919 '>': '&gt;',
920 '"': '&#34;',
921 "'": '&#39;'
922 };
923 var _MATCH_HTML = /[&<>\'"]/g;
924
925 function encode_char(c) {
926 return _ENCODE_HTML_RULES[c] || c;
927 }
928
929 /**
930 * Stringified version of constants used by {@link module:utils.escapeXML}.
931 *
932 * It is used in the process of generating {@link ClientFunction}s.
933 *
934 * @readonly
935 * @type {String}
936 */
937
938 var escapeFuncStr =
939 'var _ENCODE_HTML_RULES = {\n'
940 + ' "&": "&amp;"\n'
941 + ' , "<": "&lt;"\n'
942 + ' , ">": "&gt;"\n'
943 + ' , \'"\': "&#34;"\n'
944 + ' , "\'": "&#39;"\n'
945 + ' }\n'
946 + ' , _MATCH_HTML = /[&<>\'"]/g;\n'
947 + 'function encode_char(c) {\n'
948 + ' return _ENCODE_HTML_RULES[c] || c;\n'
949 + '};\n';
950
951 /**
952 * Escape characters reserved in XML.
953 *
954 * If `markup` is `undefined` or `null`, the empty string is returned.
955 *
956 * @implements {EscapeCallback}
957 * @param {String} markup Input string
958 * @return {String} Escaped string
959 * @static
960 * @private
961 */
962
963 exports.escapeXML = function (markup) {
964 return markup == undefined
965 ? ''
966 : String(markup)
967 .replace(_MATCH_HTML, encode_char);
968 };
969 exports.escapeXML.toString = function () {
970 return Function.prototype.toString.call(this) + ';\n' + escapeFuncStr;
971 };
972
973 /**
974 * Naive copy of properties from one object to another.
975 * Does not recurse into non-scalar properties
976 * Does not check to see if the property has a value before copying
977 *
978 * @param {Object} to Destination object
979 * @param {Object} from Source object
980 * @return {Object} Destination object
981 * @static
982 * @private
983 */
984 exports.shallowCopy = function (to, from) {
985 from = from || {};
986 for (var p in from) {
987 to[p] = from[p];
988 }
989 return to;
990 };
991
992 /**
993 * Naive copy of a list of key names, from one object to another.
994 * Only copies property if it is actually defined
995 * Does not recurse into non-scalar properties
996 *
997 * @param {Object} to Destination object
998 * @param {Object} from Source object
999 * @param {Array} list List of properties to copy
1000 * @return {Object} Destination object
1001 * @static
1002 * @private
1003 */
1004 exports.shallowCopyFromList = function (to, from, list) {
1005 for (var i = 0; i < list.length; i++) {
1006 var p = list[i];
1007 if (typeof from[p] != 'undefined') {
1008 to[p] = from[p];
1009 }
1010 }
1011 return to;
1012 };
1013
1014 /**
1015 * Simple in-process cache implementation. Does not implement limits of any
1016 * sort.
1017 *
1018 * @implements Cache
1019 * @static
1020 * @private
1021 */
1022 exports.cache = {
1023 _data: {},
1024 set: function (key, val) {
1025 this._data[key] = val;
1026 },
1027 get: function (key) {
1028 return this._data[key];
1029 },
1030 reset: function () {
1031 this._data = {};
1032 }
1033 };
1034
1035 },{}],3:[function(require,module,exports){
1036
1037 },{}],4:[function(require,module,exports){
1038 (function (process){
1039 // Copyright Joyent, Inc. and other Node contributors.
1040 //
1041 // Permission is hereby granted, free of charge, to any person obtaining a
1042 // copy of this software and associated documentation files (the
1043 // "Software"), to deal in the Software without restriction, including
1044 // without limitation the rights to use, copy, modify, merge, publish,
1045 // distribute, sublicense, and/or sell copies of the Software, and to permit
1046 // persons to whom the Software is furnished to do so, subject to the
1047 // following conditions:
1048 //
1049 // The above copyright notice and this permission notice shall be included
1050 // in all copies or substantial portions of the Software.
1051 //
1052 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1053 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1054 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
1055 // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
1056 // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
1057 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
1058 // USE OR OTHER DEALINGS IN THE SOFTWARE.
1059
1060 // resolves . and .. elements in a path array with directory names there
1061 // must be no slashes, empty elements, or device names (c:\) in the array
1062 // (so also no leading and trailing slashes - it does not distinguish
1063 // relative and absolute paths)
1064 function normalizeArray(parts, allowAboveRoot) {
1065 // if the path tries to go above the root, `up` ends up > 0
1066 var up = 0;
1067 for (var i = parts.length - 1; i >= 0; i--) {
1068 var last = parts[i];
1069 if (last === '.') {
1070 parts.splice(i, 1);
1071 } else if (last === '..') {
1072 parts.splice(i, 1);
1073 up++;
1074 } else if (up) {
1075 parts.splice(i, 1);
1076 up--;
1077 }
1078 }
1079
1080 // if the path is allowed to go above the root, restore leading ..s
1081 if (allowAboveRoot) {
1082 for (; up--; up) {
1083 parts.unshift('..');
1084 }
1085 }
1086
1087 return parts;
1088 }
1089
1090 // Split a filename into [root, dir, basename, ext], unix version
1091 // 'root' is just a slash, or nothing.
1092 var splitPathRe =
1093 /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
1094 var splitPath = function(filename) {
1095 return splitPathRe.exec(filename).slice(1);
1096 };
1097
1098 // path.resolve([from ...], to)
1099 // posix version
1100 exports.resolve = function() {
1101 var resolvedPath = '',
1102 resolvedAbsolute = false;
1103
1104 for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
1105 var path = (i >= 0) ? arguments[i] : process.cwd();
1106
1107 // Skip empty and invalid entries
1108 if (typeof path !== 'string') {
1109 throw new TypeError('Arguments to path.resolve must be strings');
1110 } else if (!path) {
1111 continue;
1112 }
1113
1114 resolvedPath = path + '/' + resolvedPath;
1115 resolvedAbsolute = path.charAt(0) === '/';
1116 }
1117
1118 // At this point the path should be resolved to a full absolute path, but
1119 // handle relative paths to be safe (might happen when process.cwd() fails)
1120
1121 // Normalize the path
1122 resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
1123 return !!p;
1124 }), !resolvedAbsolute).join('/');
1125
1126 return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
1127 };
1128
1129 // path.normalize(path)
1130 // posix version
1131 exports.normalize = function(path) {
1132 var isAbsolute = exports.isAbsolute(path),
1133 trailingSlash = substr(path, -1) === '/';
1134
1135 // Normalize the path
1136 path = normalizeArray(filter(path.split('/'), function(p) {
1137 return !!p;
1138 }), !isAbsolute).join('/');
1139
1140 if (!path && !isAbsolute) {
1141 path = '.';
1142 }
1143 if (path && trailingSlash) {
1144 path += '/';
1145 }
1146
1147 return (isAbsolute ? '/' : '') + path;
1148 };
1149
1150 // posix version
1151 exports.isAbsolute = function(path) {
1152 return path.charAt(0) === '/';
1153 };
1154
1155 // posix version
1156 exports.join = function() {
1157 var paths = Array.prototype.slice.call(arguments, 0);
1158 return exports.normalize(filter(paths, function(p, index) {
1159 if (typeof p !== 'string') {
1160 throw new TypeError('Arguments to path.join must be strings');
1161 }
1162 return p;
1163 }).join('/'));
1164 };
1165
1166
1167 // path.relative(from, to)
1168 // posix version
1169 exports.relative = function(from, to) {
1170 from = exports.resolve(from).substr(1);
1171 to = exports.resolve(to).substr(1);
1172
1173 function trim(arr) {
1174 var start = 0;
1175 for (; start < arr.length; start++) {
1176 if (arr[start] !== '') break;
1177 }
1178
1179 var end = arr.length - 1;
1180 for (; end >= 0; end--) {
1181 if (arr[end] !== '') break;
1182 }
1183
1184 if (start > end) return [];
1185 return arr.slice(start, end - start + 1);
1186 }
1187
1188 var fromParts = trim(from.split('/'));
1189 var toParts = trim(to.split('/'));
1190
1191 var length = Math.min(fromParts.length, toParts.length);
1192 var samePartsLength = length;
1193 for (var i = 0; i < length; i++) {
1194 if (fromParts[i] !== toParts[i]) {
1195 samePartsLength = i;
1196 break;
1197 }
1198 }
1199
1200 var outputParts = [];
1201 for (var i = samePartsLength; i < fromParts.length; i++) {
1202 outputParts.push('..');
1203 }
1204
1205 outputParts = outputParts.concat(toParts.slice(samePartsLength));
1206
1207 return outputParts.join('/');
1208 };
1209
1210 exports.sep = '/';
1211 exports.delimiter = ':';
1212
1213 exports.dirname = function(path) {
1214 var result = splitPath(path),
1215 root = result[0],
1216 dir = result[1];
1217
1218 if (!root && !dir) {
1219 // No dirname whatsoever
1220 return '.';
1221 }
1222
1223 if (dir) {
1224 // It has a dirname, strip trailing slash
1225 dir = dir.substr(0, dir.length - 1);
1226 }
1227
1228 return root + dir;
1229 };
1230
1231
1232 exports.basename = function(path, ext) {
1233 var f = splitPath(path)[2];
1234 // TODO: make this comparison case-insensitive on windows?
1235 if (ext && f.substr(-1 * ext.length) === ext) {
1236 f = f.substr(0, f.length - ext.length);
1237 }
1238 return f;
1239 };
1240
1241
1242 exports.extname = function(path) {
1243 return splitPath(path)[3];
1244 };
1245
1246 function filter (xs, f) {
1247 if (xs.filter) return xs.filter(f);
1248 var res = [];
1249 for (var i = 0; i < xs.length; i++) {
1250 if (f(xs[i], i, xs)) res.push(xs[i]);
1251 }
1252 return res;
1253 }
1254
1255 // String.prototype.substr - negative index don't work in IE8
1256 var substr = 'ab'.substr(-1) === 'b'
1257 ? function (str, start, len) { return str.substr(start, len) }
1258 : function (str, start, len) {
1259 if (start < 0) start = str.length + start;
1260 return str.substr(start, len);
1261 }
1262 ;
1263
1264 }).call(this,require('_process'))
1265 },{"_process":5}],5:[function(require,module,exports){
1266 // shim for using process in browser
1267 var process = module.exports = {};
1268
1269 // cached from whatever global is present so that test runners that stub it
1270 // don't break things. But we need to wrap it in a try catch in case it is
1271 // wrapped in strict mode code which doesn't define any globals. It's inside a
1272 // function because try/catches deoptimize in certain engines.
1273
1274 var cachedSetTimeout;
1275 var cachedClearTimeout;
1276
1277 function defaultSetTimout() {
1278 throw new Error('setTimeout has not been defined');
1279 }
1280 function defaultClearTimeout () {
1281 throw new Error('clearTimeout has not been defined');
1282 }
1283 (function () {
1284 try {
1285 if (typeof setTimeout === 'function') {
1286 cachedSetTimeout = setTimeout;
1287 } else {
1288 cachedSetTimeout = defaultSetTimout;
1289 }
1290 } catch (e) {
1291 cachedSetTimeout = defaultSetTimout;
1292 }
1293 try {
1294 if (typeof clearTimeout === 'function') {
1295 cachedClearTimeout = clearTimeout;
1296 } else {
1297 cachedClearTimeout = defaultClearTimeout;
1298 }
1299 } catch (e) {
1300 cachedClearTimeout = defaultClearTimeout;
1301 }
1302 } ())
1303 function runTimeout(fun) {
1304 if (cachedSetTimeout === setTimeout) {
1305 //normal enviroments in sane situations
1306 return setTimeout(fun, 0);
1307 }
1308 // if setTimeout wasn't available but was latter defined
1309 if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
1310 cachedSetTimeout = setTimeout;
1311 return setTimeout(fun, 0);
1312 }
1313 try {
1314 // when when somebody has screwed with setTimeout but no I.E. maddness
1315 return cachedSetTimeout(fun, 0);
1316 } catch(e){
1317 try {
1318 // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
1319 return cachedSetTimeout.call(null, fun, 0);
1320 } catch(e){
1321 // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
1322 return cachedSetTimeout.call(this, fun, 0);
1323 }
1324 }
1325
1326
1327 }
1328 function runClearTimeout(marker) {
1329 if (cachedClearTimeout === clearTimeout) {
1330 //normal enviroments in sane situations
1331 return clearTimeout(marker);
1332 }
1333 // if clearTimeout wasn't available but was latter defined
1334 if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
1335 cachedClearTimeout = clearTimeout;
1336 return clearTimeout(marker);
1337 }
1338 try {
1339 // when when somebody has screwed with setTimeout but no I.E. maddness
1340 return cachedClearTimeout(marker);
1341 } catch (e){
1342 try {
1343 // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
1344 return cachedClearTimeout.call(null, marker);
1345 } catch (e){
1346 // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
1347 // Some versions of I.E. have different rules for clearTimeout vs setTimeout
1348 return cachedClearTimeout.call(this, marker);
1349 }
1350 }
1351
1352
1353
1354 }
1355 var queue = [];
1356 var draining = false;
1357 var currentQueue;
1358 var queueIndex = -1;
1359
1360 function cleanUpNextTick() {
1361 if (!draining || !currentQueue) {
1362 return;
1363 }
1364 draining = false;
1365 if (currentQueue.length) {
1366 queue = currentQueue.concat(queue);
1367 } else {
1368 queueIndex = -1;
1369 }
1370 if (queue.length) {
1371 drainQueue();
1372 }
1373 }
1374
1375 function drainQueue() {
1376 if (draining) {
1377 return;
1378 }
1379 var timeout = runTimeout(cleanUpNextTick);
1380 draining = true;
1381
1382 var len = queue.length;
1383 while(len) {
1384 currentQueue = queue;
1385 queue = [];
1386 while (++queueIndex < len) {
1387 if (currentQueue) {
1388 currentQueue[queueIndex].run();
1389 }
1390 }
1391 queueIndex = -1;
1392 len = queue.length;
1393 }
1394 currentQueue = null;
1395 draining = false;
1396 runClearTimeout(timeout);
1397 }
1398
1399 process.nextTick = function (fun) {
1400 var args = new Array(arguments.length - 1);
1401 if (arguments.length > 1) {
1402 for (var i = 1; i < arguments.length; i++) {
1403 args[i - 1] = arguments[i];
1404 }
1405 }
1406 queue.push(new Item(fun, args));
1407 if (queue.length === 1 && !draining) {
1408 runTimeout(drainQueue);
1409 }
1410 };
1411
1412 // v8 likes predictible objects
1413 function Item(fun, array) {
1414 this.fun = fun;
1415 this.array = array;
1416 }
1417 Item.prototype.run = function () {
1418 this.fun.apply(null, this.array);
1419 };
1420 process.title = 'browser';
1421 process.browser = true;
1422 process.env = {};
1423 process.argv = [];
1424 process.version = ''; // empty string to avoid regexp issues
1425 process.versions = {};
1426
1427 function noop() {}
1428
1429 process.on = noop;
1430 process.addListener = noop;
1431 process.once = noop;
1432 process.off = noop;
1433 process.removeListener = noop;
1434 process.removeAllListeners = noop;
1435 process.emit = noop;
1436
1437 process.binding = function (name) {
1438 throw new Error('process.binding is not supported');
1439 };
1440
1441 process.cwd = function () { return '/' };
1442 process.chdir = function (dir) {
1443 throw new Error('process.chdir is not supported');
1444 };
1445 process.umask = function() { return 0; };
1446
1447 },{}],6:[function(require,module,exports){
1448 module.exports={
1449 "name": "ejs",
1450 "description": "Embedded JavaScript templates",
1451 "keywords": [
1452 "template",
1453 "engine",
1454 "ejs"
1455 ],
1456 "version": "2.5.7",
1457 "author": "Matthew Eernisse <mde@fleegix.org> (http://fleegix.org)",
1458 "contributors": [
1459 "Timothy Gu <timothygu99@gmail.com> (https://timothygu.github.io)"
1460 ],
1461 "license": "Apache-2.0",
1462 "main": "./lib/ejs.js",
1463 "repository": {
1464 "type": "git",
1465 "url": "git://github.com/mde/ejs.git"
1466 },
1467 "bugs": "https://github.com/mde/ejs/issues",
1468 "homepage": "https://github.com/mde/ejs",
1469 "dependencies": {},
1470 "devDependencies": {
1471 "browserify": "^13.0.1",
1472 "eslint": "^3.0.0",
1473 "git-directory-deploy": "^1.5.1",
1474 "istanbul": "~0.4.3",
1475 "jake": "^8.0.0",
1476 "jsdoc": "^3.4.0",
1477 "lru-cache": "^4.0.1",
1478 "mocha": "^3.0.2",
1479 "uglify-js": "^2.6.2"
1480 },
1481 "engines": {
1482 "node": ">=0.10.0"
1483 },
1484 "scripts": {
1485 "test": "jake test",
1486 "lint": "eslint \"**/*.js\" Jakefile",
1487 "coverage": "istanbul cover node_modules/mocha/bin/_mocha",
1488 "doc": "jake doc",
1489 "devdoc": "jake doc[dev]"
1490 }
1491 }
1492
1493 },{}]},{},[1])(1)
1494 }); No newline at end of file
@@ -0,0 +1,58 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
6 // #
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 var EJS_TEMPLATES = {};
20
21 var renderTemplate = function(tmplName, data) {
22 var tmplStr = getTemplate(tmplName);
23 var options = {};
24 var template = ejs.compile(tmplStr, options);
25 return template(data);
26 };
27
28
29 var registerTemplate = function (name) {
30 if (EJS_TEMPLATES[name] !== undefined) {
31 return
32 }
33
34 var template = $('#ejs_' + name);
35
36 if (template.get(0) !== undefined) {
37 EJS_TEMPLATES[name] = template.html();
38 } else {
39 console.log('Failed to register template', name)
40 }
41 };
42
43
44 var registerTemplates = function () {
45 $.each($('.ejsTemplate'), function(idx, value) {
46 var id = $(value).attr('id');
47 var tmplId = id.substring(0, 4);
48 var tmplName = id.substring(4);
49 if (tmplId === 'ejs_') {
50 registerTemplate(tmplName)
51 }
52 });
53 };
54
55
56 var getTemplate = function (name) {
57 return EJS_TEMPLATES[name]
58 };
@@ -0,0 +1,109 b''
1 <%text>
2 <div style="display: none">
3
4 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
5
6 <%
7 if (size > 16) {
8 var gravatar_class = 'gravatar gravatar-large';
9 } else {
10 var gravatar_class = 'gravatar';
11 }
12 %>
13
14 <%
15 if (show_disabled) {
16 var user_cls = 'user user-disabled';
17 } else {
18 var user_cls = 'user';
19 }
20 %>
21
22 <div class="rc-user">
23 <img class="<%= gravatar_class %>" src="<%- gravatar_url -%>" height="<%= size %>" width="<%= size %>">
24 <span class="<%= user_cls %>"> <%- user_link -%> </span>
25 </div>
26
27 </script>
28
29
30 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
31
32 <li id="reviewer_<%= member.user_id %>" class="reviewer_entry">
33 <div class="reviewers_member">
34 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
35 <div class="flag_status <%= review_status %> pull-left reviewer_member_status"></div>
36 </div>
37 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
38 <% if (mandatory) { %>
39 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
40 <i class="icon-lock"></i>
41 </div>
42 <% } %>
43
44 <%if (member.user_group && member.user_group.vote_rule) {%>
45 <div style="float:right">
46 <%if (member.user_group.vote_rule == -1) {%>
47 Min votes: ALL
48 <%} else {%>
49 Min votes: <%= member.user_group.vote_rule %>
50 <%}%>
51 </div>
52 <%}%>
53 <%-
54 renderTemplate('gravatarWithUser', {
55 'size': 16,
56 'show_disabled': false,
57 'user_link': member.user_link,
58 'gravatar_url': member.gravatar_link
59 })
60 %>
61 </div>
62
63 <input type="hidden" name="__start__" value="reviewer:mapping">
64
65 <input type="hidden" name="__start__" value="reasons:sequence">
66 <% for (var i = 0; i < reasons.length; i++) { %>
67 <% var reason = reasons[i] %>
68 <div class="reviewer_reason">- <%= reason %></div>
69 <input type="hidden" name="reason" value="<%= reason %>">
70 <% } %>
71 <input type="hidden" name="__end__" value="reasons:sequence">
72
73 <input type="hidden" name="__start__" value="rules:sequence">
74 <% for (var i = 0; i < member.rules.length; i++) { %>
75 <% var rule = member.rules[i] %>
76 <input type="hidden" name="rule_id" value="<%= rule %>">
77 <% } %>
78 <input type="hidden" name="__end__" value="rules:sequence">
79
80 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
81 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
82
83 <input type="hidden" name="__end__" value="reviewer:mapping">
84
85 <% if (mandatory) { %>
86 <div class="reviewer_member_mandatory_remove" style="visibility: hidden;">
87 <i class="icon-remove-sign"></i>
88 </div>
89 <% } else { %>
90 <% if (allowed_to_update) { %>
91 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: hidden;">
92 <i class="icon-remove-sign" ></i>
93 </div>
94 <% } %>
95 <% } %>
96 </div>
97 </li>
98
99 </script>
100
101
102 </div>
103
104 <script>
105 // registers the templates into global cache
106 registerTemplates();
107 </script>
108
109 </%text> No newline at end of file
@@ -0,0 +1,127 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 import random
23 from mock import Mock, MagicMock
24 from rhodecode.model import db
25 from rhodecode.model.changeset_status import ChangesetStatusModel
26
27
28 status_approved = db.ChangesetStatus.STATUS_APPROVED
29 status_rejected = db.ChangesetStatus.STATUS_REJECTED
30 status_under_review = db.ChangesetStatus.STATUS_UNDER_REVIEW
31
32
33 pytestmark = [
34 pytest.mark.backends("git", "hg"),
35 ]
36
37
38 class ReviewerMock(object):
39 def __init__(self, reviewer_def):
40 self.reviewer_def = reviewer_def
41
42 def rule_user_group_data(self):
43 return {'vote_rule': self.reviewer_def['vote_rule']}
44
45
46 class MemberMock(object):
47 def __init__(self, reviewer_def):
48 self.reviewer_def = reviewer_def
49 self.user_id = random.randint(1, 1024)
50
51
52 class Statuses(object):
53 def __init__(self, member_status):
54 self.member_status = member_status
55
56 def get_statuses(self):
57 if not self.member_status:
58 return []
59
60 ver = 1
61 latest = MagicMock(status=self.member_status)
62 return [
63 [ver, latest]
64 ]
65
66
67 @pytest.mark.parametrize("reviewers_def, expected_votes", [
68 # empty values
69 ({},
70 []),
71
72 # 3 members, 1 votes approved, 2 approvals required
73 ({'members': [status_approved, None, None], 'vote_rule':2},
74 [status_approved, status_under_review, status_under_review]),
75
76 # 3 members, 2 approvals required
77 ({'members': [status_approved, status_approved, None], 'vote_rule': 2},
78 [status_approved, status_approved, status_approved]),
79
80 # 3 members, 3 approvals required
81 ({'members': [status_approved, status_approved, None], 'vote_rule': 3},
82 [status_approved, status_approved, status_under_review]),
83
84 # 3 members, 1 votes approved, 2 approvals required
85 ({'members': [status_approved, status_approved, status_rejected], 'vote_rule': 2},
86 [status_approved, status_approved, status_approved]),
87
88 # 2 members, 1 votes approved, ALL approvals required
89 ({'members': [status_approved, None,], 'vote_rule': -1},
90 [status_approved, status_under_review]),
91
92 # 4 members, 2 votes approved, 2 rejected, 3 approvals required
93 ({'members': [status_approved, status_rejected, status_approved, status_rejected], 'vote_rule': 3},
94 [status_approved, status_rejected, status_approved, status_rejected]),
95
96 # 2 members, ALL approvals required
97 ({'members': [status_approved, status_approved], 'vote_rule': -1},
98 [status_approved, status_approved]),
99
100 # 3 members, 4 approvals required
101 ({'members': [status_approved, None, None], 'vote_rule': 4},
102 [status_approved, status_under_review, status_under_review]),
103
104 # 4 members, 3 approvals required
105 ({'members': [status_approved, status_approved, status_rejected, status_approved], 'vote_rule': 3},
106 [status_approved, status_approved, status_approved, status_approved]),
107
108 # 4 members, 3 approvals required
109 ({'members': [status_rejected, status_rejected, status_approved, status_approved], 'vote_rule': 3},
110 [status_rejected, status_rejected, status_approved, status_approved]),
111
112 ])
113 def test_calculate_group_vote(reviewers_def, expected_votes):
114 reviewers_data = []
115
116 for member_status in reviewers_def.get('members', []):
117 mandatory_flag = True
118 reviewers_data.append((
119 ReviewerMock(reviewers_def),
120 MemberMock(reviewers_def),
121 'Test Reason',
122 mandatory_flag,
123 Statuses(member_status).get_statuses()
124 ))
125
126 votes = ChangesetStatusModel().calculate_group_vote(123, reviewers_data)
127 assert votes == expected_votes
@@ -1,194 +1,196 b''
1 {
1 {
2 "dirs": {
2 "dirs": {
3 "css": {
3 "css": {
4 "src":"rhodecode/public/css",
4 "src":"rhodecode/public/css",
5 "dest":"rhodecode/public/css"
5 "dest":"rhodecode/public/css"
6 },
6 },
7 "js": {
7 "js": {
8 "src": "rhodecode/public/js/src",
8 "src": "rhodecode/public/js/src",
9 "src_rc": "rhodecode/public/js/rhodecode",
9 "src_rc": "rhodecode/public/js/rhodecode",
10 "dest": "rhodecode/public/js",
10 "dest": "rhodecode/public/js",
11 "bower": "bower_components",
11 "bower": "bower_components",
12 "node_modules": "node_modules"
12 "node_modules": "node_modules"
13 }
13 }
14 },
14 },
15 "copy": {
15 "copy": {
16 "main": {
16 "main": {
17 "expand": true,
17 "expand": true,
18 "cwd": "bower_components",
18 "cwd": "bower_components",
19 "src": "webcomponentsjs/webcomponents-lite.js",
19 "src": "webcomponentsjs/webcomponents-lite.js",
20 "dest": "<%= dirs.js.dest %>/vendors"
20 "dest": "<%= dirs.js.dest %>/vendors"
21 }
21 }
22 },
22 },
23 "concat": {
23 "concat": {
24 "polymercss": {
24 "polymercss": {
25 "src": [
25 "src": [
26 "<%= dirs.js.src %>/components/root-styles-prefix.html",
26 "<%= dirs.js.src %>/components/root-styles-prefix.html",
27 "<%= dirs.css.src %>/style-polymer.css",
27 "<%= dirs.css.src %>/style-polymer.css",
28 "<%= dirs.js.src %>/components/root-styles-suffix.html"
28 "<%= dirs.js.src %>/components/root-styles-suffix.html"
29 ],
29 ],
30 "dest": "<%= dirs.js.dest %>/src/components/root-styles.gen.html",
30 "dest": "<%= dirs.js.dest %>/src/components/root-styles.gen.html",
31 "nonull": true
31 "nonull": true
32 },
32 },
33 "dist": {
33 "dist": {
34 "src": [
34 "src": [
35 "<%= dirs.js.node_modules %>/jquery/dist/jquery.min.js",
35 "<%= dirs.js.node_modules %>/jquery/dist/jquery.min.js",
36 "<%= dirs.js.node_modules %>/mousetrap/mousetrap.min.js",
36 "<%= dirs.js.node_modules %>/mousetrap/mousetrap.min.js",
37 "<%= dirs.js.node_modules %>/moment/min/moment.min.js",
37 "<%= dirs.js.node_modules %>/moment/min/moment.min.js",
38 "<%= dirs.js.node_modules %>/clipboard/dist/clipboard.min.js",
38 "<%= dirs.js.node_modules %>/clipboard/dist/clipboard.min.js",
39 "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js",
39 "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js",
40 "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js",
40 "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js",
41 "<%= dirs.js.src %>/logging.js",
41 "<%= dirs.js.src %>/logging.js",
42 "<%= dirs.js.src %>/bootstrap.js",
42 "<%= dirs.js.src %>/bootstrap.js",
43 "<%= dirs.js.src %>/i18n_utils.js",
43 "<%= dirs.js.src %>/i18n_utils.js",
44 "<%= dirs.js.src %>/deform.js",
44 "<%= dirs.js.src %>/deform.js",
45 "<%= dirs.js.src %>/ejs.js",
46 "<%= dirs.js.src %>/ejs_templates/utils.js",
45 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
47 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
46 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
48 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
47 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
49 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
48 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
50 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
49 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
51 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
50 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
52 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
51 "<%= dirs.js.src %>/plugins/jquery.mark.js",
53 "<%= dirs.js.src %>/plugins/jquery.mark.js",
52 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
54 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
53 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
55 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
54 "<%= dirs.js.src %>/select2/select2.js",
56 "<%= dirs.js.src %>/select2/select2.js",
55 "<%= dirs.js.src %>/codemirror/codemirror.js",
57 "<%= dirs.js.src %>/codemirror/codemirror.js",
56 "<%= dirs.js.src %>/codemirror/codemirror_loadmode.js",
58 "<%= dirs.js.src %>/codemirror/codemirror_loadmode.js",
57 "<%= dirs.js.src %>/codemirror/codemirror_hint.js",
59 "<%= dirs.js.src %>/codemirror/codemirror_hint.js",
58 "<%= dirs.js.src %>/codemirror/codemirror_overlay.js",
60 "<%= dirs.js.src %>/codemirror/codemirror_overlay.js",
59 "<%= dirs.js.src %>/codemirror/codemirror_placeholder.js",
61 "<%= dirs.js.src %>/codemirror/codemirror_placeholder.js",
60 "<%= dirs.js.src %>/codemirror/codemirror_simplemode.js",
62 "<%= dirs.js.src %>/codemirror/codemirror_simplemode.js",
61 "<%= dirs.js.dest %>/mode/meta.js",
63 "<%= dirs.js.dest %>/mode/meta.js",
62 "<%= dirs.js.dest %>/mode/meta_ext.js",
64 "<%= dirs.js.dest %>/mode/meta_ext.js",
63 "<%= dirs.js.src_rc %>/i18n/select2/translations.js",
65 "<%= dirs.js.src_rc %>/i18n/select2/translations.js",
64 "<%= dirs.js.src %>/rhodecode/utils/array.js",
66 "<%= dirs.js.src %>/rhodecode/utils/array.js",
65 "<%= dirs.js.src %>/rhodecode/utils/string.js",
67 "<%= dirs.js.src %>/rhodecode/utils/string.js",
66 "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
68 "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
67 "<%= dirs.js.src %>/rhodecode/utils/ajax.js",
69 "<%= dirs.js.src %>/rhodecode/utils/ajax.js",
68 "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",
70 "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",
69 "<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js",
71 "<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js",
70 "<%= dirs.js.src %>/rhodecode/utils/ie.js",
72 "<%= dirs.js.src %>/rhodecode/utils/ie.js",
71 "<%= dirs.js.src %>/rhodecode/utils/os.js",
73 "<%= dirs.js.src %>/rhodecode/utils/os.js",
72 "<%= dirs.js.src %>/rhodecode/utils/topics.js",
74 "<%= dirs.js.src %>/rhodecode/utils/topics.js",
73 "<%= dirs.js.src %>/rhodecode/init.js",
75 "<%= dirs.js.src %>/rhodecode/init.js",
74 "<%= dirs.js.src %>/rhodecode/changelog.js",
76 "<%= dirs.js.src %>/rhodecode/changelog.js",
75 "<%= dirs.js.src %>/rhodecode/codemirror.js",
77 "<%= dirs.js.src %>/rhodecode/codemirror.js",
76 "<%= dirs.js.src %>/rhodecode/comments.js",
78 "<%= dirs.js.src %>/rhodecode/comments.js",
77 "<%= dirs.js.src %>/rhodecode/constants.js",
79 "<%= dirs.js.src %>/rhodecode/constants.js",
78 "<%= dirs.js.src %>/rhodecode/files.js",
80 "<%= dirs.js.src %>/rhodecode/files.js",
79 "<%= dirs.js.src %>/rhodecode/followers.js",
81 "<%= dirs.js.src %>/rhodecode/followers.js",
80 "<%= dirs.js.src %>/rhodecode/menus.js",
82 "<%= dirs.js.src %>/rhodecode/menus.js",
81 "<%= dirs.js.src %>/rhodecode/notifications.js",
83 "<%= dirs.js.src %>/rhodecode/notifications.js",
82 "<%= dirs.js.src %>/rhodecode/permissions.js",
84 "<%= dirs.js.src %>/rhodecode/permissions.js",
83 "<%= dirs.js.src %>/rhodecode/pjax.js",
85 "<%= dirs.js.src %>/rhodecode/pjax.js",
84 "<%= dirs.js.src %>/rhodecode/pullrequests.js",
86 "<%= dirs.js.src %>/rhodecode/pullrequests.js",
85 "<%= dirs.js.src %>/rhodecode/settings.js",
87 "<%= dirs.js.src %>/rhodecode/settings.js",
86 "<%= dirs.js.src %>/rhodecode/select2_widgets.js",
88 "<%= dirs.js.src %>/rhodecode/select2_widgets.js",
87 "<%= dirs.js.src %>/rhodecode/tooltips.js",
89 "<%= dirs.js.src %>/rhodecode/tooltips.js",
88 "<%= dirs.js.src %>/rhodecode/users.js",
90 "<%= dirs.js.src %>/rhodecode/users.js",
89 "<%= dirs.js.src %>/rhodecode/appenlight.js",
91 "<%= dirs.js.src %>/rhodecode/appenlight.js",
90 "<%= dirs.js.src %>/rhodecode.js"
92 "<%= dirs.js.src %>/rhodecode.js"
91 ],
93 ],
92 "dest": "<%= dirs.js.dest %>/scripts.js",
94 "dest": "<%= dirs.js.dest %>/scripts.js",
93 "nonull": true
95 "nonull": true
94 }
96 }
95 },
97 },
96 "crisper": {
98 "crisper": {
97 "dist": {
99 "dist": {
98 "options": {
100 "options": {
99 "cleanup": false,
101 "cleanup": false,
100 "onlySplit": true
102 "onlySplit": true
101 },
103 },
102 "src": "<%= dirs.js.dest %>/rhodecode-components.html",
104 "src": "<%= dirs.js.dest %>/rhodecode-components.html",
103 "dest": "<%= dirs.js.dest %>/rhodecode-components.js"
105 "dest": "<%= dirs.js.dest %>/rhodecode-components.js"
104 }
106 }
105 },
107 },
106 "less": {
108 "less": {
107 "development": {
109 "development": {
108 "options": {
110 "options": {
109 "compress": false,
111 "compress": false,
110 "yuicompress": false,
112 "yuicompress": false,
111 "optimization": 0
113 "optimization": 0
112 },
114 },
113 "files": {
115 "files": {
114 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
116 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
115 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
117 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
116 }
118 }
117 },
119 },
118 "production": {
120 "production": {
119 "options": {
121 "options": {
120 "compress": true,
122 "compress": true,
121 "yuicompress": true,
123 "yuicompress": true,
122 "optimization": 2
124 "optimization": 2
123 },
125 },
124 "files": {
126 "files": {
125 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
127 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
126 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
128 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
127 }
129 }
128 },
130 },
129 "components": {
131 "components": {
130 "files": [
132 "files": [
131 {
133 {
132 "cwd": "<%= dirs.js.src %>/components/",
134 "cwd": "<%= dirs.js.src %>/components/",
133 "dest": "<%= dirs.js.src %>/components/",
135 "dest": "<%= dirs.js.src %>/components/",
134 "src": [
136 "src": [
135 "**/*.less"
137 "**/*.less"
136 ],
138 ],
137 "expand": true,
139 "expand": true,
138 "ext": ".css"
140 "ext": ".css"
139 }
141 }
140 ]
142 ]
141 }
143 }
142 },
144 },
143 "watch": {
145 "watch": {
144 "less": {
146 "less": {
145 "files": [
147 "files": [
146 "<%= dirs.css.src %>/**/*.less",
148 "<%= dirs.css.src %>/**/*.less",
147 "<%= dirs.js.src %>/components/**/*.less"
149 "<%= dirs.js.src %>/components/**/*.less"
148 ],
150 ],
149 "tasks": [
151 "tasks": [
150 "less:development",
152 "less:development",
151 "less:components",
153 "less:components",
152 "concat:polymercss",
154 "concat:polymercss",
153 "vulcanize",
155 "vulcanize",
154 "crisper",
156 "crisper",
155 "concat:dist"
157 "concat:dist"
156 ]
158 ]
157 },
159 },
158 "js": {
160 "js": {
159 "files": [
161 "files": [
160 "!<%= dirs.js.src %>/components/root-styles.gen.html",
162 "!<%= dirs.js.src %>/components/root-styles.gen.html",
161 "<%= dirs.js.src %>/**/*.js",
163 "<%= dirs.js.src %>/**/*.js",
162 "<%= dirs.js.src %>/components/**/*.html"
164 "<%= dirs.js.src %>/components/**/*.html"
163 ],
165 ],
164 "tasks": [
166 "tasks": [
165 "less:components",
167 "less:components",
166 "concat:polymercss",
168 "concat:polymercss",
167 "vulcanize",
169 "vulcanize",
168 "crisper",
170 "crisper",
169 "concat:dist"
171 "concat:dist"
170 ]
172 ]
171 }
173 }
172 },
174 },
173 "jshint": {
175 "jshint": {
174 "rhodecode": {
176 "rhodecode": {
175 "src": "<%= dirs.js.src %>/rhodecode/**/*.js",
177 "src": "<%= dirs.js.src %>/rhodecode/**/*.js",
176 "options": {
178 "options": {
177 "jshintrc": ".jshintrc"
179 "jshintrc": ".jshintrc"
178 }
180 }
179 }
181 }
180 },
182 },
181 "vulcanize": {
183 "vulcanize": {
182 "default": {
184 "default": {
183 "options": {
185 "options": {
184 "abspath": "",
186 "abspath": "",
185 "inlineScripts": true,
187 "inlineScripts": true,
186 "inlineCss": true,
188 "inlineCss": true,
187 "stripComments": true
189 "stripComments": true
188 },
190 },
189 "files": {
191 "files": {
190 "<%= dirs.js.dest %>/rhodecode-components.html": "<%= dirs.js.src %>/components/shared-components.html"
192 "<%= dirs.js.dest %>/rhodecode-components.html": "<%= dirs.js.src %>/components/shared-components.html"
191 }
193 }
192 }
194 }
193 }
195 }
194 }
196 }
@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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
22
23 RhodeCode, a web based repository management software
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
25 """
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import platform
29 import platform
30
30
31 VERSION = tuple(open(os.path.join(
31 VERSION = tuple(open(os.path.join(
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33
33
34 BACKENDS = {
34 BACKENDS = {
35 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
36 'git': 'Git repository',
36 'git': 'Git repository',
37 'svn': 'Subversion repository',
37 'svn': 'Subversion repository',
38 }
38 }
39
39
40 CELERY_ENABLED = False
40 CELERY_ENABLED = False
41 CELERY_EAGER = False
41 CELERY_EAGER = False
42
42
43 # link to config for pyramid
43 # link to config for pyramid
44 CONFIG = {}
44 CONFIG = {}
45
45
46 # Populated with the settings dictionary from application init in
46 # Populated with the settings dictionary from application init in
47 # rhodecode.conf.environment.load_pyramid_environment
47 # rhodecode.conf.environment.load_pyramid_environment
48 PYRAMID_SETTINGS = {}
48 PYRAMID_SETTINGS = {}
49
49
50 # Linked module for extensions
50 # Linked module for extensions
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 83 # defines current db version for migrations
54 __dbversion__ = 85 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
58 __url__ = 'https://code.rhodecode.com'
58 __url__ = 'https://code.rhodecode.com'
59
59
60 is_windows = __platform__ in ['Windows']
60 is_windows = __platform__ in ['Windows']
61 is_unix = not is_windows
61 is_unix = not is_windows
62 is_test = False
62 is_test = False
63 disable_error_handler = False
63 disable_error_handler = False
@@ -1,142 +1,142 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 import pytest
22 import pytest
23 import urlobject
23 import urlobject
24
24
25 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_error, assert_ok)
26 build_data, api_call, assert_error, assert_ok)
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.utils2 import safe_unicode
28 from rhodecode.lib.utils2 import safe_unicode
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'get_pull_request',
40 self.apikey, 'get_pull_request',
41 pullrequestid=pull_request.pull_request_id)
41 pullrequestid=pull_request.pull_request_id)
42
42
43 response = api_call(self.app, params)
43 response = api_call(self.app, params)
44
44
45 assert response.status == '200 OK'
45 assert response.status == '200 OK'
46
46
47 url_obj = urlobject.URLObject(
47 url_obj = urlobject.URLObject(
48 h.route_url(
48 h.route_url(
49 'pullrequest_show',
49 'pullrequest_show',
50 repo_name=pull_request.target_repo.repo_name,
50 repo_name=pull_request.target_repo.repo_name,
51 pull_request_id=pull_request.pull_request_id))
51 pull_request_id=pull_request.pull_request_id))
52
52
53 pr_url = safe_unicode(
53 pr_url = safe_unicode(
54 url_obj.with_netloc(http_host_only_stub))
54 url_obj.with_netloc(http_host_only_stub))
55 source_url = safe_unicode(
55 source_url = safe_unicode(
56 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
56 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
57 target_url = safe_unicode(
57 target_url = safe_unicode(
58 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
58 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
59 shadow_url = safe_unicode(
59 shadow_url = safe_unicode(
60 PullRequestModel().get_shadow_clone_url(pull_request))
60 PullRequestModel().get_shadow_clone_url(pull_request))
61
61
62 expected = {
62 expected = {
63 'pull_request_id': pull_request.pull_request_id,
63 'pull_request_id': pull_request.pull_request_id,
64 'url': pr_url,
64 'url': pr_url,
65 'title': pull_request.title,
65 'title': pull_request.title,
66 'description': pull_request.description,
66 'description': pull_request.description,
67 'status': pull_request.status,
67 'status': pull_request.status,
68 'created_on': pull_request.created_on,
68 'created_on': pull_request.created_on,
69 'updated_on': pull_request.updated_on,
69 'updated_on': pull_request.updated_on,
70 'commit_ids': pull_request.revisions,
70 'commit_ids': pull_request.revisions,
71 'review_status': pull_request.calculated_review_status(),
71 'review_status': pull_request.calculated_review_status(),
72 'mergeable': {
72 'mergeable': {
73 'status': True,
73 'status': True,
74 'message': 'This pull request can be automatically merged.',
74 'message': 'This pull request can be automatically merged.',
75 },
75 },
76 'source': {
76 'source': {
77 'clone_url': source_url,
77 'clone_url': source_url,
78 'repository': pull_request.source_repo.repo_name,
78 'repository': pull_request.source_repo.repo_name,
79 'reference': {
79 'reference': {
80 'name': pull_request.source_ref_parts.name,
80 'name': pull_request.source_ref_parts.name,
81 'type': pull_request.source_ref_parts.type,
81 'type': pull_request.source_ref_parts.type,
82 'commit_id': pull_request.source_ref_parts.commit_id,
82 'commit_id': pull_request.source_ref_parts.commit_id,
83 },
83 },
84 },
84 },
85 'target': {
85 'target': {
86 'clone_url': target_url,
86 'clone_url': target_url,
87 'repository': pull_request.target_repo.repo_name,
87 'repository': pull_request.target_repo.repo_name,
88 'reference': {
88 'reference': {
89 'name': pull_request.target_ref_parts.name,
89 'name': pull_request.target_ref_parts.name,
90 'type': pull_request.target_ref_parts.type,
90 'type': pull_request.target_ref_parts.type,
91 'commit_id': pull_request.target_ref_parts.commit_id,
91 'commit_id': pull_request.target_ref_parts.commit_id,
92 },
92 },
93 },
93 },
94 'merge': {
94 'merge': {
95 'clone_url': shadow_url,
95 'clone_url': shadow_url,
96 'reference': {
96 'reference': {
97 'name': pull_request.shadow_merge_ref.name,
97 'name': pull_request.shadow_merge_ref.name,
98 'type': pull_request.shadow_merge_ref.type,
98 'type': pull_request.shadow_merge_ref.type,
99 'commit_id': pull_request.shadow_merge_ref.commit_id,
99 'commit_id': pull_request.shadow_merge_ref.commit_id,
100 },
100 },
101 },
101 },
102 'author': pull_request.author.get_api_data(include_secrets=False,
102 'author': pull_request.author.get_api_data(include_secrets=False,
103 details='basic'),
103 details='basic'),
104 'reviewers': [
104 'reviewers': [
105 {
105 {
106 'user': reviewer.get_api_data(include_secrets=False,
106 'user': reviewer.get_api_data(include_secrets=False,
107 details='basic'),
107 details='basic'),
108 'reasons': reasons,
108 'reasons': reasons,
109 'review_status': st[0][1].status if st else 'not_reviewed',
109 'review_status': st[0][1].status if st else 'not_reviewed',
110 }
110 }
111 for reviewer, reasons, mandatory, st in
111 for obj, reviewer, reasons, mandatory, st in
112 pull_request.reviewers_statuses()
112 pull_request.reviewers_statuses()
113 ]
113 ]
114 }
114 }
115 assert_ok(id_, expected, response.body)
115 assert_ok(id_, expected, response.body)
116
116
117 def test_api_get_pull_request_repo_error(self, pr_util):
117 def test_api_get_pull_request_repo_error(self, pr_util):
118 pull_request = pr_util.create_pull_request()
118 pull_request = pr_util.create_pull_request()
119 id_, params = build_data(
119 id_, params = build_data(
120 self.apikey, 'get_pull_request',
120 self.apikey, 'get_pull_request',
121 repoid=666, pullrequestid=pull_request.pull_request_id)
121 repoid=666, pullrequestid=pull_request.pull_request_id)
122 response = api_call(self.app, params)
122 response = api_call(self.app, params)
123
123
124 expected = 'repository `666` does not exist'
124 expected = 'repository `666` does not exist'
125 assert_error(id_, expected, given=response.body)
125 assert_error(id_, expected, given=response.body)
126
126
127 def test_api_get_pull_request_pull_request_error(self):
127 def test_api_get_pull_request_pull_request_error(self):
128 id_, params = build_data(
128 id_, params = build_data(
129 self.apikey, 'get_pull_request', pullrequestid=666)
129 self.apikey, 'get_pull_request', pullrequestid=666)
130 response = api_call(self.app, params)
130 response = api_call(self.app, params)
131
131
132 expected = 'pull request `666` does not exist'
132 expected = 'pull request `666` does not exist'
133 assert_error(id_, expected, given=response.body)
133 assert_error(id_, expected, given=response.body)
134
134
135 def test_api_get_pull_request_pull_request_error_just_pr_id(self):
135 def test_api_get_pull_request_pull_request_error_just_pr_id(self):
136 id_, params = build_data(
136 id_, params = build_data(
137 self.apikey, 'get_pull_request',
137 self.apikey, 'get_pull_request',
138 pullrequestid=666)
138 pullrequestid=666)
139 response = api_call(self.app, params)
139 response = api_call(self.app, params)
140
140
141 expected = 'pull request `666` does not exist'
141 expected = 'pull request `666` does not exist'
142 assert_error(id_, expected, given=response.body)
142 assert_error(id_, expected, given=response.body)
@@ -1,213 +1,213 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 pytest
21 import pytest
22
22
23 from rhodecode.lib.vcs.nodes import FileNode
23 from rhodecode.lib.vcs.nodes import FileNode
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error)
28 build_data, api_call, assert_ok, assert_error)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdatePullRequest(object):
32 class TestUpdatePullRequest(object):
33
33
34 @pytest.mark.backends("git", "hg")
34 @pytest.mark.backends("git", "hg")
35 def test_api_update_pull_request_title_or_description(
35 def test_api_update_pull_request_title_or_description(
36 self, pr_util, no_notifications):
36 self, pr_util, no_notifications):
37 pull_request = pr_util.create_pull_request()
37 pull_request = pr_util.create_pull_request()
38
38
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'update_pull_request',
40 self.apikey, 'update_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id,
42 pullrequestid=pull_request.pull_request_id,
43 title='New TITLE OF A PR',
43 title='New TITLE OF A PR',
44 description='New DESC OF A PR',
44 description='New DESC OF A PR',
45 )
45 )
46 response = api_call(self.app, params)
46 response = api_call(self.app, params)
47
47
48 expected = {
48 expected = {
49 "msg": "Updated pull request `{}`".format(
49 "msg": "Updated pull request `{}`".format(
50 pull_request.pull_request_id),
50 pull_request.pull_request_id),
51 "pull_request": response.json['result']['pull_request'],
51 "pull_request": response.json['result']['pull_request'],
52 "updated_commits": {"added": [], "common": [], "removed": []},
52 "updated_commits": {"added": [], "common": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
54 }
54 }
55
55
56 response_json = response.json['result']
56 response_json = response.json['result']
57 assert response_json == expected
57 assert response_json == expected
58 pr = response_json['pull_request']
58 pr = response_json['pull_request']
59 assert pr['title'] == 'New TITLE OF A PR'
59 assert pr['title'] == 'New TITLE OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
61
61
62 @pytest.mark.backends("git", "hg")
62 @pytest.mark.backends("git", "hg")
63 def test_api_try_update_closed_pull_request(
63 def test_api_try_update_closed_pull_request(
64 self, pr_util, no_notifications):
64 self, pr_util, no_notifications):
65 pull_request = pr_util.create_pull_request()
65 pull_request = pr_util.create_pull_request()
66 PullRequestModel().close_pull_request(
66 PullRequestModel().close_pull_request(
67 pull_request, TEST_USER_ADMIN_LOGIN)
67 pull_request, TEST_USER_ADMIN_LOGIN)
68
68
69 id_, params = build_data(
69 id_, params = build_data(
70 self.apikey, 'update_pull_request',
70 self.apikey, 'update_pull_request',
71 repoid=pull_request.target_repo.repo_name,
71 repoid=pull_request.target_repo.repo_name,
72 pullrequestid=pull_request.pull_request_id)
72 pullrequestid=pull_request.pull_request_id)
73 response = api_call(self.app, params)
73 response = api_call(self.app, params)
74
74
75 expected = 'pull request `{}` update failed, pull request ' \
75 expected = 'pull request `{}` update failed, pull request ' \
76 'is closed'.format(pull_request.pull_request_id)
76 'is closed'.format(pull_request.pull_request_id)
77
77
78 assert_error(id_, expected, response.body)
78 assert_error(id_, expected, response.body)
79
79
80 @pytest.mark.backends("git", "hg")
80 @pytest.mark.backends("git", "hg")
81 def test_api_update_update_commits(self, pr_util, no_notifications):
81 def test_api_update_update_commits(self, pr_util, no_notifications):
82 commits = [
82 commits = [
83 {'message': 'a'},
83 {'message': 'a'},
84 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
84 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
85 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
85 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
86 ]
86 ]
87 pull_request = pr_util.create_pull_request(
87 pull_request = pr_util.create_pull_request(
88 commits=commits, target_head='a', source_head='b', revisions=['b'])
88 commits=commits, target_head='a', source_head='b', revisions=['b'])
89 pr_util.update_source_repository(head='c')
89 pr_util.update_source_repository(head='c')
90 repo = pull_request.source_repo.scm_instance()
90 repo = pull_request.source_repo.scm_instance()
91 commits = [x for x in repo.get_commits()]
91 commits = [x for x in repo.get_commits()]
92 print commits
92 print commits
93
93
94 added_commit_id = commits[-1].raw_id # c commit
94 added_commit_id = commits[-1].raw_id # c commit
95 common_commit_id = commits[1].raw_id # b commit is common ancestor
95 common_commit_id = commits[1].raw_id # b commit is common ancestor
96 total_commits = [added_commit_id, common_commit_id]
96 total_commits = [added_commit_id, common_commit_id]
97
97
98 id_, params = build_data(
98 id_, params = build_data(
99 self.apikey, 'update_pull_request',
99 self.apikey, 'update_pull_request',
100 repoid=pull_request.target_repo.repo_name,
100 repoid=pull_request.target_repo.repo_name,
101 pullrequestid=pull_request.pull_request_id,
101 pullrequestid=pull_request.pull_request_id,
102 update_commits=True
102 update_commits=True
103 )
103 )
104 response = api_call(self.app, params)
104 response = api_call(self.app, params)
105
105
106 expected = {
106 expected = {
107 "msg": "Updated pull request `{}`".format(
107 "msg": "Updated pull request `{}`".format(
108 pull_request.pull_request_id),
108 pull_request.pull_request_id),
109 "pull_request": response.json['result']['pull_request'],
109 "pull_request": response.json['result']['pull_request'],
110 "updated_commits": {"added": [added_commit_id],
110 "updated_commits": {"added": [added_commit_id],
111 "common": [common_commit_id],
111 "common": [common_commit_id],
112 "total": total_commits,
112 "total": total_commits,
113 "removed": []},
113 "removed": []},
114 "updated_reviewers": {"added": [], "removed": []},
114 "updated_reviewers": {"added": [], "removed": []},
115 }
115 }
116
116
117 assert_ok(id_, expected, response.body)
117 assert_ok(id_, expected, response.body)
118
118
119 @pytest.mark.backends("git", "hg")
119 @pytest.mark.backends("git", "hg")
120 def test_api_update_change_reviewers(
120 def test_api_update_change_reviewers(
121 self, user_util, pr_util, no_notifications):
121 self, user_util, pr_util, no_notifications):
122 a = user_util.create_user()
122 a = user_util.create_user()
123 b = user_util.create_user()
123 b = user_util.create_user()
124 c = user_util.create_user()
124 c = user_util.create_user()
125 new_reviewers = [
125 new_reviewers = [
126 {'username': b.username,'reasons': ['updated via API'],
126 {'username': b.username,'reasons': ['updated via API'],
127 'mandatory':False},
127 'mandatory':False},
128 {'username': c.username, 'reasons': ['updated via API'],
128 {'username': c.username, 'reasons': ['updated via API'],
129 'mandatory':False},
129 'mandatory':False},
130 ]
130 ]
131
131
132 added = [b.username, c.username]
132 added = [b.username, c.username]
133 removed = [a.username]
133 removed = [a.username]
134
134
135 pull_request = pr_util.create_pull_request(
135 pull_request = pr_util.create_pull_request(
136 reviewers=[(a.username, ['added via API'], False)])
136 reviewers=[(a.username, ['added via API'], False, [])])
137
137
138 id_, params = build_data(
138 id_, params = build_data(
139 self.apikey, 'update_pull_request',
139 self.apikey, 'update_pull_request',
140 repoid=pull_request.target_repo.repo_name,
140 repoid=pull_request.target_repo.repo_name,
141 pullrequestid=pull_request.pull_request_id,
141 pullrequestid=pull_request.pull_request_id,
142 reviewers=new_reviewers)
142 reviewers=new_reviewers)
143 response = api_call(self.app, params)
143 response = api_call(self.app, params)
144 expected = {
144 expected = {
145 "msg": "Updated pull request `{}`".format(
145 "msg": "Updated pull request `{}`".format(
146 pull_request.pull_request_id),
146 pull_request.pull_request_id),
147 "pull_request": response.json['result']['pull_request'],
147 "pull_request": response.json['result']['pull_request'],
148 "updated_commits": {"added": [], "common": [], "removed": []},
148 "updated_commits": {"added": [], "common": [], "removed": []},
149 "updated_reviewers": {"added": added, "removed": removed},
149 "updated_reviewers": {"added": added, "removed": removed},
150 }
150 }
151
151
152 assert_ok(id_, expected, response.body)
152 assert_ok(id_, expected, response.body)
153
153
154 @pytest.mark.backends("git", "hg")
154 @pytest.mark.backends("git", "hg")
155 def test_api_update_bad_user_in_reviewers(self, pr_util):
155 def test_api_update_bad_user_in_reviewers(self, pr_util):
156 pull_request = pr_util.create_pull_request()
156 pull_request = pr_util.create_pull_request()
157
157
158 id_, params = build_data(
158 id_, params = build_data(
159 self.apikey, 'update_pull_request',
159 self.apikey, 'update_pull_request',
160 repoid=pull_request.target_repo.repo_name,
160 repoid=pull_request.target_repo.repo_name,
161 pullrequestid=pull_request.pull_request_id,
161 pullrequestid=pull_request.pull_request_id,
162 reviewers=[{'username': 'bad_name'}])
162 reviewers=[{'username': 'bad_name'}])
163 response = api_call(self.app, params)
163 response = api_call(self.app, params)
164
164
165 expected = 'user `bad_name` does not exist'
165 expected = 'user `bad_name` does not exist'
166
166
167 assert_error(id_, expected, response.body)
167 assert_error(id_, expected, response.body)
168
168
169 @pytest.mark.backends("git", "hg")
169 @pytest.mark.backends("git", "hg")
170 def test_api_update_repo_error(self, pr_util):
170 def test_api_update_repo_error(self, pr_util):
171 pull_request = pr_util.create_pull_request()
171 pull_request = pr_util.create_pull_request()
172 id_, params = build_data(
172 id_, params = build_data(
173 self.apikey, 'update_pull_request',
173 self.apikey, 'update_pull_request',
174 repoid='fake',
174 repoid='fake',
175 pullrequestid=pull_request.pull_request_id,
175 pullrequestid=pull_request.pull_request_id,
176 reviewers=[{'username': 'bad_name'}])
176 reviewers=[{'username': 'bad_name'}])
177 response = api_call(self.app, params)
177 response = api_call(self.app, params)
178
178
179 expected = 'repository `fake` does not exist'
179 expected = 'repository `fake` does not exist'
180
180
181 response_json = response.json['error']
181 response_json = response.json['error']
182 assert response_json == expected
182 assert response_json == expected
183
183
184 @pytest.mark.backends("git", "hg")
184 @pytest.mark.backends("git", "hg")
185 def test_api_update_pull_request_error(self, pr_util):
185 def test_api_update_pull_request_error(self, pr_util):
186 pull_request = pr_util.create_pull_request()
186 pull_request = pr_util.create_pull_request()
187
187
188 id_, params = build_data(
188 id_, params = build_data(
189 self.apikey, 'update_pull_request',
189 self.apikey, 'update_pull_request',
190 repoid=pull_request.target_repo.repo_name,
190 repoid=pull_request.target_repo.repo_name,
191 pullrequestid=999999,
191 pullrequestid=999999,
192 reviewers=[{'username': 'bad_name'}])
192 reviewers=[{'username': 'bad_name'}])
193 response = api_call(self.app, params)
193 response = api_call(self.app, params)
194
194
195 expected = 'pull request `999999` does not exist'
195 expected = 'pull request `999999` does not exist'
196 assert_error(id_, expected, response.body)
196 assert_error(id_, expected, response.body)
197
197
198 @pytest.mark.backends("git", "hg")
198 @pytest.mark.backends("git", "hg")
199 def test_api_update_pull_request_no_perms_to_update(
199 def test_api_update_pull_request_no_perms_to_update(
200 self, user_util, pr_util):
200 self, user_util, pr_util):
201 user = user_util.create_user()
201 user = user_util.create_user()
202 pull_request = pr_util.create_pull_request()
202 pull_request = pr_util.create_pull_request()
203
203
204 id_, params = build_data(
204 id_, params = build_data(
205 user.api_key, 'update_pull_request',
205 user.api_key, 'update_pull_request',
206 repoid=pull_request.target_repo.repo_name,
206 repoid=pull_request.target_repo.repo_name,
207 pullrequestid=pull_request.pull_request_id,)
207 pullrequestid=pull_request.pull_request_id,)
208 response = api_call(self.app, params)
208 response = api_call(self.app, params)
209
209
210 expected = ('pull request `%s` update failed, '
210 expected = ('pull request `%s` update failed, '
211 'no permission to update.') % pull_request.pull_request_id
211 'no permission to update.') % pull_request.pull_request_id
212
212
213 assert_error(id_, expected, response.body)
213 assert_error(id_, expected, response.body)
@@ -1,248 +1,247 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 logging
21 import logging
22
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.response import Response
28 from pyramid.response import Response
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
33 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
34 from rhodecode.lib import helpers as h, audit_logger
34 from rhodecode.lib import helpers as h, audit_logger
35 from rhodecode.lib.utils2 import safe_unicode
35 from rhodecode.lib.utils2 import safe_unicode
36
36
37 from rhodecode.model.forms import UserGroupForm
37 from rhodecode.model.forms import UserGroupForm
38 from rhodecode.model.permission import PermissionModel
38 from rhodecode.model.permission import PermissionModel
39 from rhodecode.model.scm import UserGroupList
39 from rhodecode.model.scm import UserGroupList
40 from rhodecode.model.db import (
40 from rhodecode.model.db import (
41 or_, count, User, UserGroup, UserGroupMember)
41 or_, count, User, UserGroup, UserGroupMember)
42 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
43 from rhodecode.model.user_group import UserGroupModel
43 from rhodecode.model.user_group import UserGroupModel
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class AdminUserGroupsView(BaseAppView, DataGridAppView):
48 class AdminUserGroupsView(BaseAppView, DataGridAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 c = self._get_local_tmpl_context()
51 c = self._get_local_tmpl_context()
52
52
53 PermissionModel().set_global_permission_choices(
53 PermissionModel().set_global_permission_choices(
54 c, gettext_translator=self.request.translate)
54 c, gettext_translator=self.request.translate)
55
55
56
57 return c
56 return c
58
57
59 # permission check in data loading of
58 # permission check in data loading of
60 # `user_groups_list_data` via UserGroupList
59 # `user_groups_list_data` via UserGroupList
61 @LoginRequired()
60 @LoginRequired()
62 @NotAnonymous()
61 @NotAnonymous()
63 @view_config(
62 @view_config(
64 route_name='user_groups', request_method='GET',
63 route_name='user_groups', request_method='GET',
65 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
64 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
66 def user_groups_list(self):
65 def user_groups_list(self):
67 c = self.load_default_context()
66 c = self.load_default_context()
68 return self._get_template_context(c)
67 return self._get_template_context(c)
69
68
70 # permission check inside
69 # permission check inside
71 @LoginRequired()
70 @LoginRequired()
72 @NotAnonymous()
71 @NotAnonymous()
73 @view_config(
72 @view_config(
74 route_name='user_groups_data', request_method='GET',
73 route_name='user_groups_data', request_method='GET',
75 renderer='json_ext', xhr=True)
74 renderer='json_ext', xhr=True)
76 def user_groups_list_data(self):
75 def user_groups_list_data(self):
77 self.load_default_context()
76 self.load_default_context()
78 column_map = {
77 column_map = {
79 'active': 'users_group_active',
78 'active': 'users_group_active',
80 'description': 'user_group_description',
79 'description': 'user_group_description',
81 'members': 'members_total',
80 'members': 'members_total',
82 'owner': 'user_username',
81 'owner': 'user_username',
83 'sync': 'group_data'
82 'sync': 'group_data'
84 }
83 }
85 draw, start, limit = self._extract_chunk(self.request)
84 draw, start, limit = self._extract_chunk(self.request)
86 search_q, order_by, order_dir = self._extract_ordering(
85 search_q, order_by, order_dir = self._extract_ordering(
87 self.request, column_map=column_map)
86 self.request, column_map=column_map)
88
87
89 _render = self.request.get_partial_renderer(
88 _render = self.request.get_partial_renderer(
90 'rhodecode:templates/data_table/_dt_elements.mako')
89 'rhodecode:templates/data_table/_dt_elements.mako')
91
90
92 def user_group_name(user_group_id, user_group_name):
91 def user_group_name(user_group_id, user_group_name):
93 return _render("user_group_name", user_group_id, user_group_name)
92 return _render("user_group_name", user_group_id, user_group_name)
94
93
95 def user_group_actions(user_group_id, user_group_name):
94 def user_group_actions(user_group_id, user_group_name):
96 return _render("user_group_actions", user_group_id, user_group_name)
95 return _render("user_group_actions", user_group_id, user_group_name)
97
96
98 def user_profile(username):
97 def user_profile(username):
99 return _render('user_profile', username)
98 return _render('user_profile', username)
100
99
101 auth_user_group_list = UserGroupList(
100 auth_user_group_list = UserGroupList(
102 UserGroup.query().all(), perm_set=['usergroup.admin'])
101 UserGroup.query().all(), perm_set=['usergroup.admin'])
103
102
104 allowed_ids = [-1]
103 allowed_ids = [-1]
105 for user_group in auth_user_group_list:
104 for user_group in auth_user_group_list:
106 allowed_ids.append(user_group.users_group_id)
105 allowed_ids.append(user_group.users_group_id)
107
106
108 user_groups_data_total_count = UserGroup.query()\
107 user_groups_data_total_count = UserGroup.query()\
109 .filter(UserGroup.users_group_id.in_(allowed_ids))\
108 .filter(UserGroup.users_group_id.in_(allowed_ids))\
110 .count()
109 .count()
111
110
112 member_count = count(UserGroupMember.user_id)
111 member_count = count(UserGroupMember.user_id)
113 base_q = Session.query(
112 base_q = Session.query(
114 UserGroup.users_group_name,
113 UserGroup.users_group_name,
115 UserGroup.user_group_description,
114 UserGroup.user_group_description,
116 UserGroup.users_group_active,
115 UserGroup.users_group_active,
117 UserGroup.users_group_id,
116 UserGroup.users_group_id,
118 UserGroup.group_data,
117 UserGroup.group_data,
119 User,
118 User,
120 member_count.label('member_count')
119 member_count.label('member_count')
121 ) \
120 ) \
122 .filter(UserGroup.users_group_id.in_(allowed_ids)) \
121 .filter(UserGroup.users_group_id.in_(allowed_ids)) \
123 .outerjoin(UserGroupMember) \
122 .outerjoin(UserGroupMember) \
124 .join(User, User.user_id == UserGroup.user_id) \
123 .join(User, User.user_id == UserGroup.user_id) \
125 .group_by(UserGroup, User)
124 .group_by(UserGroup, User)
126
125
127 if search_q:
126 if search_q:
128 like_expression = u'%{}%'.format(safe_unicode(search_q))
127 like_expression = u'%{}%'.format(safe_unicode(search_q))
129 base_q = base_q.filter(or_(
128 base_q = base_q.filter(or_(
130 UserGroup.users_group_name.ilike(like_expression),
129 UserGroup.users_group_name.ilike(like_expression),
131 ))
130 ))
132
131
133 user_groups_data_total_filtered_count = base_q.count()
132 user_groups_data_total_filtered_count = base_q.count()
134
133
135 if order_by == 'members_total':
134 if order_by == 'members_total':
136 sort_col = member_count
135 sort_col = member_count
137 elif order_by == 'user_username':
136 elif order_by == 'user_username':
138 sort_col = User.username
137 sort_col = User.username
139 else:
138 else:
140 sort_col = getattr(UserGroup, order_by, None)
139 sort_col = getattr(UserGroup, order_by, None)
141
140
142 if isinstance(sort_col, count) or sort_col:
141 if isinstance(sort_col, count) or sort_col:
143 if order_dir == 'asc':
142 if order_dir == 'asc':
144 sort_col = sort_col.asc()
143 sort_col = sort_col.asc()
145 else:
144 else:
146 sort_col = sort_col.desc()
145 sort_col = sort_col.desc()
147
146
148 base_q = base_q.order_by(sort_col)
147 base_q = base_q.order_by(sort_col)
149 base_q = base_q.offset(start).limit(limit)
148 base_q = base_q.offset(start).limit(limit)
150
149
151 # authenticated access to user groups
150 # authenticated access to user groups
152 auth_user_group_list = base_q.all()
151 auth_user_group_list = base_q.all()
153
152
154 user_groups_data = []
153 user_groups_data = []
155 for user_gr in auth_user_group_list:
154 for user_gr in auth_user_group_list:
156 user_groups_data.append({
155 user_groups_data.append({
157 "users_group_name": user_group_name(
156 "users_group_name": user_group_name(
158 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
157 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
159 "name_raw": h.escape(user_gr.users_group_name),
158 "name_raw": h.escape(user_gr.users_group_name),
160 "description": h.escape(user_gr.user_group_description),
159 "description": h.escape(user_gr.user_group_description),
161 "members": user_gr.member_count,
160 "members": user_gr.member_count,
162 # NOTE(marcink): because of advanced query we
161 # NOTE(marcink): because of advanced query we
163 # need to load it like that
162 # need to load it like that
164 "sync": UserGroup._load_group_data(
163 "sync": UserGroup._load_group_data(
165 user_gr.group_data).get('extern_type'),
164 user_gr.group_data).get('extern_type'),
166 "active": h.bool2icon(user_gr.users_group_active),
165 "active": h.bool2icon(user_gr.users_group_active),
167 "owner": user_profile(user_gr.User.username),
166 "owner": user_profile(user_gr.User.username),
168 "action": user_group_actions(
167 "action": user_group_actions(
169 user_gr.users_group_id, user_gr.users_group_name)
168 user_gr.users_group_id, user_gr.users_group_name)
170 })
169 })
171
170
172 data = ({
171 data = ({
173 'draw': draw,
172 'draw': draw,
174 'data': user_groups_data,
173 'data': user_groups_data,
175 'recordsTotal': user_groups_data_total_count,
174 'recordsTotal': user_groups_data_total_count,
176 'recordsFiltered': user_groups_data_total_filtered_count,
175 'recordsFiltered': user_groups_data_total_filtered_count,
177 })
176 })
178
177
179 return data
178 return data
180
179
181 @LoginRequired()
180 @LoginRequired()
182 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
181 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
183 @view_config(
182 @view_config(
184 route_name='user_groups_new', request_method='GET',
183 route_name='user_groups_new', request_method='GET',
185 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
184 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
186 def user_groups_new(self):
185 def user_groups_new(self):
187 c = self.load_default_context()
186 c = self.load_default_context()
188 return self._get_template_context(c)
187 return self._get_template_context(c)
189
188
190 @LoginRequired()
189 @LoginRequired()
191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
190 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 @CSRFRequired()
191 @CSRFRequired()
193 @view_config(
192 @view_config(
194 route_name='user_groups_create', request_method='POST',
193 route_name='user_groups_create', request_method='POST',
195 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
194 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
196 def user_groups_create(self):
195 def user_groups_create(self):
197 _ = self.request.translate
196 _ = self.request.translate
198 c = self.load_default_context()
197 c = self.load_default_context()
199 users_group_form = UserGroupForm(self.request.translate)()
198 users_group_form = UserGroupForm(self.request.translate)()
200
199
201 user_group_name = self.request.POST.get('users_group_name')
200 user_group_name = self.request.POST.get('users_group_name')
202 try:
201 try:
203 form_result = users_group_form.to_python(dict(self.request.POST))
202 form_result = users_group_form.to_python(dict(self.request.POST))
204 user_group = UserGroupModel().create(
203 user_group = UserGroupModel().create(
205 name=form_result['users_group_name'],
204 name=form_result['users_group_name'],
206 description=form_result['user_group_description'],
205 description=form_result['user_group_description'],
207 owner=self._rhodecode_user.user_id,
206 owner=self._rhodecode_user.user_id,
208 active=form_result['users_group_active'])
207 active=form_result['users_group_active'])
209 Session().flush()
208 Session().flush()
210 creation_data = user_group.get_api_data()
209 creation_data = user_group.get_api_data()
211 user_group_name = form_result['users_group_name']
210 user_group_name = form_result['users_group_name']
212
211
213 audit_logger.store_web(
212 audit_logger.store_web(
214 'user_group.create', action_data={'data': creation_data},
213 'user_group.create', action_data={'data': creation_data},
215 user=self._rhodecode_user)
214 user=self._rhodecode_user)
216
215
217 user_group_link = h.link_to(
216 user_group_link = h.link_to(
218 h.escape(user_group_name),
217 h.escape(user_group_name),
219 h.route_path(
218 h.route_path(
220 'edit_user_group', user_group_id=user_group.users_group_id))
219 'edit_user_group', user_group_id=user_group.users_group_id))
221 h.flash(h.literal(_('Created user group %(user_group_link)s')
220 h.flash(h.literal(_('Created user group %(user_group_link)s')
222 % {'user_group_link': user_group_link}),
221 % {'user_group_link': user_group_link}),
223 category='success')
222 category='success')
224 Session().commit()
223 Session().commit()
225 user_group_id = user_group.users_group_id
224 user_group_id = user_group.users_group_id
226 except formencode.Invalid as errors:
225 except formencode.Invalid as errors:
227
226
228 data = render(
227 data = render(
229 'rhodecode:templates/admin/user_groups/user_group_add.mako',
228 'rhodecode:templates/admin/user_groups/user_group_add.mako',
230 self._get_template_context(c), self.request)
229 self._get_template_context(c), self.request)
231 html = formencode.htmlfill.render(
230 html = formencode.htmlfill.render(
232 data,
231 data,
233 defaults=errors.value,
232 defaults=errors.value,
234 errors=errors.error_dict or {},
233 errors=errors.error_dict or {},
235 prefix_error=False,
234 prefix_error=False,
236 encoding="UTF-8",
235 encoding="UTF-8",
237 force_defaults=False
236 force_defaults=False
238 )
237 )
239 return Response(html)
238 return Response(html)
240
239
241 except Exception:
240 except Exception:
242 log.exception("Exception creating user group")
241 log.exception("Exception creating user group")
243 h.flash(_('Error occurred during creation of user group %s') \
242 h.flash(_('Error occurred during creation of user group %s') \
244 % user_group_name, category='error')
243 % user_group_name, category='error')
245 raise HTTPFound(h.route_path('user_groups_new'))
244 raise HTTPFound(h.route_path('user_groups_new'))
246
245
247 raise HTTPFound(
246 raise HTTPFound(
248 h.route_path('edit_user_group', user_group_id=user_group_id))
247 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1134 +1,1140 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 @pytest.mark.usefixtures('app', 'autologin_user')
63 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
65 class TestPullrequestsView(object):
65 class TestPullrequestsView(object):
66
66
67 def test_index(self, backend):
67 def test_index(self, backend):
68 self.app.get(route_path(
68 self.app.get(route_path(
69 'pullrequest_new',
69 'pullrequest_new',
70 repo_name=backend.repo_name))
70 repo_name=backend.repo_name))
71
71
72 def test_option_menu_create_pull_request_exists(self, backend):
72 def test_option_menu_create_pull_request_exists(self, backend):
73 repo_name = backend.repo_name
73 repo_name = backend.repo_name
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75
75
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 'pullrequest_new', repo_name=repo_name)
77 'pullrequest_new', repo_name=repo_name)
78 response.mustcontain(create_pr_link)
78 response.mustcontain(create_pr_link)
79
79
80 def test_create_pr_form_with_raw_commit_id(self, backend):
80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 repo = backend.repo
81 repo = backend.repo
82
82
83 self.app.get(
83 self.app.get(
84 route_path('pullrequest_new',
84 route_path('pullrequest_new',
85 repo_name=repo.repo_name,
85 repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 def test_show(self, pr_util, pr_merge_enabled):
90 def test_show(self, pr_util, pr_merge_enabled):
91 pull_request = pr_util.create_pull_request(
91 pull_request = pr_util.create_pull_request(
92 mergeable=pr_merge_enabled, enable_notifications=False)
92 mergeable=pr_merge_enabled, enable_notifications=False)
93
93
94 response = self.app.get(route_path(
94 response = self.app.get(route_path(
95 'pullrequest_show',
95 'pullrequest_show',
96 repo_name=pull_request.target_repo.scm_instance().name,
96 repo_name=pull_request.target_repo.scm_instance().name,
97 pull_request_id=pull_request.pull_request_id))
97 pull_request_id=pull_request.pull_request_id))
98
98
99 for commit_id in pull_request.revisions:
99 for commit_id in pull_request.revisions:
100 response.mustcontain(commit_id)
100 response.mustcontain(commit_id)
101
101
102 assert pull_request.target_ref_parts.type in response
102 assert pull_request.target_ref_parts.type in response
103 assert pull_request.target_ref_parts.name in response
103 assert pull_request.target_ref_parts.name in response
104 target_clone_url = pull_request.target_repo.clone_url()
104 target_clone_url = pull_request.target_repo.clone_url()
105 assert target_clone_url in response
105 assert target_clone_url in response
106
106
107 assert 'class="pull-request-merge"' in response
107 assert 'class="pull-request-merge"' in response
108 assert (
108 assert (
109 'Server-side pull request merging is disabled.'
109 'Server-side pull request merging is disabled.'
110 in response) != pr_merge_enabled
110 in response) != pr_merge_enabled
111
111
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
113 # Logout
113 # Logout
114 response = self.app.post(
114 response = self.app.post(
115 h.route_path('logout'),
115 h.route_path('logout'),
116 params={'csrf_token': csrf_token})
116 params={'csrf_token': csrf_token})
117 # Login as regular user
117 # Login as regular user
118 response = self.app.post(h.route_path('login'),
118 response = self.app.post(h.route_path('login'),
119 {'username': TEST_USER_REGULAR_LOGIN,
119 {'username': TEST_USER_REGULAR_LOGIN,
120 'password': 'test12'})
120 'password': 'test12'})
121
121
122 pull_request = pr_util.create_pull_request(
122 pull_request = pr_util.create_pull_request(
123 author=TEST_USER_REGULAR_LOGIN)
123 author=TEST_USER_REGULAR_LOGIN)
124
124
125 response = self.app.get(route_path(
125 response = self.app.get(route_path(
126 'pullrequest_show',
126 'pullrequest_show',
127 repo_name=pull_request.target_repo.scm_instance().name,
127 repo_name=pull_request.target_repo.scm_instance().name,
128 pull_request_id=pull_request.pull_request_id))
128 pull_request_id=pull_request.pull_request_id))
129
129
130 response.mustcontain('Server-side pull request merging is disabled.')
130 response.mustcontain('Server-side pull request merging is disabled.')
131
131
132 assert_response = response.assert_response()
132 assert_response = response.assert_response()
133 # for regular user without a merge permissions, we don't see it
133 # for regular user without a merge permissions, we don't see it
134 assert_response.no_element_exists('#close-pull-request-action')
134 assert_response.no_element_exists('#close-pull-request-action')
135
135
136 user_util.grant_user_permission_to_repo(
136 user_util.grant_user_permission_to_repo(
137 pull_request.target_repo,
137 pull_request.target_repo,
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
139 'repository.write')
139 'repository.write')
140 response = self.app.get(route_path(
140 response = self.app.get(route_path(
141 'pullrequest_show',
141 'pullrequest_show',
142 repo_name=pull_request.target_repo.scm_instance().name,
142 repo_name=pull_request.target_repo.scm_instance().name,
143 pull_request_id=pull_request.pull_request_id))
143 pull_request_id=pull_request.pull_request_id))
144
144
145 response.mustcontain('Server-side pull request merging is disabled.')
145 response.mustcontain('Server-side pull request merging is disabled.')
146
146
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 # now regular user has a merge permissions, we have CLOSE button
148 # now regular user has a merge permissions, we have CLOSE button
149 assert_response.one_element_exists('#close-pull-request-action')
149 assert_response.one_element_exists('#close-pull-request-action')
150
150
151 def test_show_invalid_commit_id(self, pr_util):
151 def test_show_invalid_commit_id(self, pr_util):
152 # Simulating invalid revisions which will cause a lookup error
152 # Simulating invalid revisions which will cause a lookup error
153 pull_request = pr_util.create_pull_request()
153 pull_request = pr_util.create_pull_request()
154 pull_request.revisions = ['invalid']
154 pull_request.revisions = ['invalid']
155 Session().add(pull_request)
155 Session().add(pull_request)
156 Session().commit()
156 Session().commit()
157
157
158 response = self.app.get(route_path(
158 response = self.app.get(route_path(
159 'pullrequest_show',
159 'pullrequest_show',
160 repo_name=pull_request.target_repo.scm_instance().name,
160 repo_name=pull_request.target_repo.scm_instance().name,
161 pull_request_id=pull_request.pull_request_id))
161 pull_request_id=pull_request.pull_request_id))
162
162
163 for commit_id in pull_request.revisions:
163 for commit_id in pull_request.revisions:
164 response.mustcontain(commit_id)
164 response.mustcontain(commit_id)
165
165
166 def test_show_invalid_source_reference(self, pr_util):
166 def test_show_invalid_source_reference(self, pr_util):
167 pull_request = pr_util.create_pull_request()
167 pull_request = pr_util.create_pull_request()
168 pull_request.source_ref = 'branch:b:invalid'
168 pull_request.source_ref = 'branch:b:invalid'
169 Session().add(pull_request)
169 Session().add(pull_request)
170 Session().commit()
170 Session().commit()
171
171
172 self.app.get(route_path(
172 self.app.get(route_path(
173 'pullrequest_show',
173 'pullrequest_show',
174 repo_name=pull_request.target_repo.scm_instance().name,
174 repo_name=pull_request.target_repo.scm_instance().name,
175 pull_request_id=pull_request.pull_request_id))
175 pull_request_id=pull_request.pull_request_id))
176
176
177 def test_edit_title_description(self, pr_util, csrf_token):
177 def test_edit_title_description(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180
180
181 response = self.app.post(
181 response = self.app.post(
182 route_path('pullrequest_update',
182 route_path('pullrequest_update',
183 repo_name=pull_request.target_repo.repo_name,
183 repo_name=pull_request.target_repo.repo_name,
184 pull_request_id=pull_request_id),
184 pull_request_id=pull_request_id),
185 params={
185 params={
186 'edit_pull_request': 'true',
186 'edit_pull_request': 'true',
187 'title': 'New title',
187 'title': 'New title',
188 'description': 'New description',
188 'description': 'New description',
189 'csrf_token': csrf_token})
189 'csrf_token': csrf_token})
190
190
191 assert_session_flash(
191 assert_session_flash(
192 response, u'Pull request title & description updated.',
192 response, u'Pull request title & description updated.',
193 category='success')
193 category='success')
194
194
195 pull_request = PullRequest.get(pull_request_id)
195 pull_request = PullRequest.get(pull_request_id)
196 assert pull_request.title == 'New title'
196 assert pull_request.title == 'New title'
197 assert pull_request.description == 'New description'
197 assert pull_request.description == 'New description'
198
198
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request_id = pull_request.pull_request_id
201 pull_request_id = pull_request.pull_request_id
202 repo_name = pull_request.target_repo.repo_name
202 repo_name = pull_request.target_repo.repo_name
203 pr_util.close()
203 pr_util.close()
204
204
205 response = self.app.post(
205 response = self.app.post(
206 route_path('pullrequest_update',
206 route_path('pullrequest_update',
207 repo_name=repo_name, pull_request_id=pull_request_id),
207 repo_name=repo_name, pull_request_id=pull_request_id),
208 params={
208 params={
209 'edit_pull_request': 'true',
209 'edit_pull_request': 'true',
210 'title': 'New title',
210 'title': 'New title',
211 'description': 'New description',
211 'description': 'New description',
212 'csrf_token': csrf_token}, status=200)
212 'csrf_token': csrf_token}, status=200)
213 assert_session_flash(
213 assert_session_flash(
214 response, u'Cannot update closed pull requests.',
214 response, u'Cannot update closed pull requests.',
215 category='error')
215 category='error')
216
216
217 def test_update_invalid_source_reference(self, pr_util, csrf_token):
217 def test_update_invalid_source_reference(self, pr_util, csrf_token):
218 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
218 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
219
219
220 pull_request = pr_util.create_pull_request()
220 pull_request = pr_util.create_pull_request()
221 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
221 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
222 Session().add(pull_request)
222 Session().add(pull_request)
223 Session().commit()
223 Session().commit()
224
224
225 pull_request_id = pull_request.pull_request_id
225 pull_request_id = pull_request.pull_request_id
226
226
227 response = self.app.post(
227 response = self.app.post(
228 route_path('pullrequest_update',
228 route_path('pullrequest_update',
229 repo_name=pull_request.target_repo.repo_name,
229 repo_name=pull_request.target_repo.repo_name,
230 pull_request_id=pull_request_id),
230 pull_request_id=pull_request_id),
231 params={'update_commits': 'true',
231 params={'update_commits': 'true',
232 'csrf_token': csrf_token})
232 'csrf_token': csrf_token})
233
233
234 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
234 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
235 UpdateFailureReason.MISSING_SOURCE_REF])
235 UpdateFailureReason.MISSING_SOURCE_REF])
236 assert_session_flash(response, expected_msg, category='error')
236 assert_session_flash(response, expected_msg, category='error')
237
237
238 def test_missing_target_reference(self, pr_util, csrf_token):
238 def test_missing_target_reference(self, pr_util, csrf_token):
239 from rhodecode.lib.vcs.backends.base import MergeFailureReason
239 from rhodecode.lib.vcs.backends.base import MergeFailureReason
240 pull_request = pr_util.create_pull_request(
240 pull_request = pr_util.create_pull_request(
241 approved=True, mergeable=True)
241 approved=True, mergeable=True)
242 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
242 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
243 Session().add(pull_request)
243 Session().add(pull_request)
244 Session().commit()
244 Session().commit()
245
245
246 pull_request_id = pull_request.pull_request_id
246 pull_request_id = pull_request.pull_request_id
247 pull_request_url = route_path(
247 pull_request_url = route_path(
248 'pullrequest_show',
248 'pullrequest_show',
249 repo_name=pull_request.target_repo.repo_name,
249 repo_name=pull_request.target_repo.repo_name,
250 pull_request_id=pull_request_id)
250 pull_request_id=pull_request_id)
251
251
252 response = self.app.get(pull_request_url)
252 response = self.app.get(pull_request_url)
253
253
254 assertr = AssertResponse(response)
254 assertr = AssertResponse(response)
255 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
255 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
256 MergeFailureReason.MISSING_TARGET_REF]
256 MergeFailureReason.MISSING_TARGET_REF]
257 assertr.element_contains(
257 assertr.element_contains(
258 'span[data-role="merge-message"]', str(expected_msg))
258 'span[data-role="merge-message"]', str(expected_msg))
259
259
260 def test_comment_and_close_pull_request_custom_message_approved(
260 def test_comment_and_close_pull_request_custom_message_approved(
261 self, pr_util, csrf_token, xhr_header):
261 self, pr_util, csrf_token, xhr_header):
262
262
263 pull_request = pr_util.create_pull_request(approved=True)
263 pull_request = pr_util.create_pull_request(approved=True)
264 pull_request_id = pull_request.pull_request_id
264 pull_request_id = pull_request.pull_request_id
265 author = pull_request.user_id
265 author = pull_request.user_id
266 repo = pull_request.target_repo.repo_id
266 repo = pull_request.target_repo.repo_id
267
267
268 self.app.post(
268 self.app.post(
269 route_path('pullrequest_comment_create',
269 route_path('pullrequest_comment_create',
270 repo_name=pull_request.target_repo.scm_instance().name,
270 repo_name=pull_request.target_repo.scm_instance().name,
271 pull_request_id=pull_request_id),
271 pull_request_id=pull_request_id),
272 params={
272 params={
273 'close_pull_request': '1',
273 'close_pull_request': '1',
274 'text': 'Closing a PR',
274 'text': 'Closing a PR',
275 'csrf_token': csrf_token},
275 'csrf_token': csrf_token},
276 extra_environ=xhr_header,)
276 extra_environ=xhr_header,)
277
277
278 journal = UserLog.query()\
278 journal = UserLog.query()\
279 .filter(UserLog.user_id == author)\
279 .filter(UserLog.user_id == author)\
280 .filter(UserLog.repository_id == repo) \
280 .filter(UserLog.repository_id == repo) \
281 .order_by('user_log_id') \
281 .order_by('user_log_id') \
282 .all()
282 .all()
283 assert journal[-1].action == 'repo.pull_request.close'
283 assert journal[-1].action == 'repo.pull_request.close'
284
284
285 pull_request = PullRequest.get(pull_request_id)
285 pull_request = PullRequest.get(pull_request_id)
286 assert pull_request.is_closed()
286 assert pull_request.is_closed()
287
287
288 status = ChangesetStatusModel().get_status(
288 status = ChangesetStatusModel().get_status(
289 pull_request.source_repo, pull_request=pull_request)
289 pull_request.source_repo, pull_request=pull_request)
290 assert status == ChangesetStatus.STATUS_APPROVED
290 assert status == ChangesetStatus.STATUS_APPROVED
291 comments = ChangesetComment().query() \
291 comments = ChangesetComment().query() \
292 .filter(ChangesetComment.pull_request == pull_request) \
292 .filter(ChangesetComment.pull_request == pull_request) \
293 .order_by(ChangesetComment.comment_id.asc())\
293 .order_by(ChangesetComment.comment_id.asc())\
294 .all()
294 .all()
295 assert comments[-1].text == 'Closing a PR'
295 assert comments[-1].text == 'Closing a PR'
296
296
297 def test_comment_force_close_pull_request_rejected(
297 def test_comment_force_close_pull_request_rejected(
298 self, pr_util, csrf_token, xhr_header):
298 self, pr_util, csrf_token, xhr_header):
299 pull_request = pr_util.create_pull_request()
299 pull_request = pr_util.create_pull_request()
300 pull_request_id = pull_request.pull_request_id
300 pull_request_id = pull_request.pull_request_id
301 PullRequestModel().update_reviewers(
301 PullRequestModel().update_reviewers(
302 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
302 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
303 pull_request.author)
303 pull_request.author)
304 author = pull_request.user_id
304 author = pull_request.user_id
305 repo = pull_request.target_repo.repo_id
305 repo = pull_request.target_repo.repo_id
306
306
307 self.app.post(
307 self.app.post(
308 route_path('pullrequest_comment_create',
308 route_path('pullrequest_comment_create',
309 repo_name=pull_request.target_repo.scm_instance().name,
309 repo_name=pull_request.target_repo.scm_instance().name,
310 pull_request_id=pull_request_id),
310 pull_request_id=pull_request_id),
311 params={
311 params={
312 'close_pull_request': '1',
312 'close_pull_request': '1',
313 'csrf_token': csrf_token},
313 'csrf_token': csrf_token},
314 extra_environ=xhr_header)
314 extra_environ=xhr_header)
315
315
316 pull_request = PullRequest.get(pull_request_id)
316 pull_request = PullRequest.get(pull_request_id)
317
317
318 journal = UserLog.query()\
318 journal = UserLog.query()\
319 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
319 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
320 .order_by('user_log_id') \
320 .order_by('user_log_id') \
321 .all()
321 .all()
322 assert journal[-1].action == 'repo.pull_request.close'
322 assert journal[-1].action == 'repo.pull_request.close'
323
323
324 # check only the latest status, not the review status
324 # check only the latest status, not the review status
325 status = ChangesetStatusModel().get_status(
325 status = ChangesetStatusModel().get_status(
326 pull_request.source_repo, pull_request=pull_request)
326 pull_request.source_repo, pull_request=pull_request)
327 assert status == ChangesetStatus.STATUS_REJECTED
327 assert status == ChangesetStatus.STATUS_REJECTED
328
328
329 def test_comment_and_close_pull_request(
329 def test_comment_and_close_pull_request(
330 self, pr_util, csrf_token, xhr_header):
330 self, pr_util, csrf_token, xhr_header):
331 pull_request = pr_util.create_pull_request()
331 pull_request = pr_util.create_pull_request()
332 pull_request_id = pull_request.pull_request_id
332 pull_request_id = pull_request.pull_request_id
333
333
334 response = self.app.post(
334 response = self.app.post(
335 route_path('pullrequest_comment_create',
335 route_path('pullrequest_comment_create',
336 repo_name=pull_request.target_repo.scm_instance().name,
336 repo_name=pull_request.target_repo.scm_instance().name,
337 pull_request_id=pull_request.pull_request_id),
337 pull_request_id=pull_request.pull_request_id),
338 params={
338 params={
339 'close_pull_request': 'true',
339 'close_pull_request': 'true',
340 'csrf_token': csrf_token},
340 'csrf_token': csrf_token},
341 extra_environ=xhr_header)
341 extra_environ=xhr_header)
342
342
343 assert response.json
343 assert response.json
344
344
345 pull_request = PullRequest.get(pull_request_id)
345 pull_request = PullRequest.get(pull_request_id)
346 assert pull_request.is_closed()
346 assert pull_request.is_closed()
347
347
348 # check only the latest status, not the review status
348 # check only the latest status, not the review status
349 status = ChangesetStatusModel().get_status(
349 status = ChangesetStatusModel().get_status(
350 pull_request.source_repo, pull_request=pull_request)
350 pull_request.source_repo, pull_request=pull_request)
351 assert status == ChangesetStatus.STATUS_REJECTED
351 assert status == ChangesetStatus.STATUS_REJECTED
352
352
353 def test_create_pull_request(self, backend, csrf_token):
353 def test_create_pull_request(self, backend, csrf_token):
354 commits = [
354 commits = [
355 {'message': 'ancestor'},
355 {'message': 'ancestor'},
356 {'message': 'change'},
356 {'message': 'change'},
357 {'message': 'change2'},
357 {'message': 'change2'},
358 ]
358 ]
359 commit_ids = backend.create_master_repo(commits)
359 commit_ids = backend.create_master_repo(commits)
360 target = backend.create_repo(heads=['ancestor'])
360 target = backend.create_repo(heads=['ancestor'])
361 source = backend.create_repo(heads=['change2'])
361 source = backend.create_repo(heads=['change2'])
362
362
363 response = self.app.post(
363 response = self.app.post(
364 route_path('pullrequest_create', repo_name=source.repo_name),
364 route_path('pullrequest_create', repo_name=source.repo_name),
365 [
365 [
366 ('source_repo', source.repo_name),
366 ('source_repo', source.repo_name),
367 ('source_ref', 'branch:default:' + commit_ids['change2']),
367 ('source_ref', 'branch:default:' + commit_ids['change2']),
368 ('target_repo', target.repo_name),
368 ('target_repo', target.repo_name),
369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
370 ('common_ancestor', commit_ids['ancestor']),
370 ('common_ancestor', commit_ids['ancestor']),
371 ('pullrequest_desc', 'Description'),
371 ('pullrequest_desc', 'Description'),
372 ('pullrequest_title', 'Title'),
372 ('pullrequest_title', 'Title'),
373 ('__start__', 'review_members:sequence'),
373 ('__start__', 'review_members:sequence'),
374 ('__start__', 'reviewer:mapping'),
374 ('__start__', 'reviewer:mapping'),
375 ('user_id', '1'),
375 ('user_id', '1'),
376 ('__start__', 'reasons:sequence'),
376 ('__start__', 'reasons:sequence'),
377 ('reason', 'Some reason'),
377 ('reason', 'Some reason'),
378 ('__end__', 'reasons:sequence'),
378 ('__end__', 'reasons:sequence'),
379 ('__start__', 'rules:sequence'),
380 ('__end__', 'rules:sequence'),
379 ('mandatory', 'False'),
381 ('mandatory', 'False'),
380 ('__end__', 'reviewer:mapping'),
382 ('__end__', 'reviewer:mapping'),
381 ('__end__', 'review_members:sequence'),
383 ('__end__', 'review_members:sequence'),
382 ('__start__', 'revisions:sequence'),
384 ('__start__', 'revisions:sequence'),
383 ('revisions', commit_ids['change']),
385 ('revisions', commit_ids['change']),
384 ('revisions', commit_ids['change2']),
386 ('revisions', commit_ids['change2']),
385 ('__end__', 'revisions:sequence'),
387 ('__end__', 'revisions:sequence'),
386 ('user', ''),
388 ('user', ''),
387 ('csrf_token', csrf_token),
389 ('csrf_token', csrf_token),
388 ],
390 ],
389 status=302)
391 status=302)
390
392
391 location = response.headers['Location']
393 location = response.headers['Location']
392 pull_request_id = location.rsplit('/', 1)[1]
394 pull_request_id = location.rsplit('/', 1)[1]
393 assert pull_request_id != 'new'
395 assert pull_request_id != 'new'
394 pull_request = PullRequest.get(int(pull_request_id))
396 pull_request = PullRequest.get(int(pull_request_id))
395
397
396 # check that we have now both revisions
398 # check that we have now both revisions
397 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
399 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
398 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
400 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
399 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
401 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
400 assert pull_request.target_ref == expected_target_ref
402 assert pull_request.target_ref == expected_target_ref
401
403
402 def test_reviewer_notifications(self, backend, csrf_token):
404 def test_reviewer_notifications(self, backend, csrf_token):
403 # We have to use the app.post for this test so it will create the
405 # We have to use the app.post for this test so it will create the
404 # notifications properly with the new PR
406 # notifications properly with the new PR
405 commits = [
407 commits = [
406 {'message': 'ancestor',
408 {'message': 'ancestor',
407 'added': [FileNode('file_A', content='content_of_ancestor')]},
409 'added': [FileNode('file_A', content='content_of_ancestor')]},
408 {'message': 'change',
410 {'message': 'change',
409 'added': [FileNode('file_a', content='content_of_change')]},
411 'added': [FileNode('file_a', content='content_of_change')]},
410 {'message': 'change-child'},
412 {'message': 'change-child'},
411 {'message': 'ancestor-child', 'parents': ['ancestor'],
413 {'message': 'ancestor-child', 'parents': ['ancestor'],
412 'added': [
414 'added': [
413 FileNode('file_B', content='content_of_ancestor_child')]},
415 FileNode('file_B', content='content_of_ancestor_child')]},
414 {'message': 'ancestor-child-2'},
416 {'message': 'ancestor-child-2'},
415 ]
417 ]
416 commit_ids = backend.create_master_repo(commits)
418 commit_ids = backend.create_master_repo(commits)
417 target = backend.create_repo(heads=['ancestor-child'])
419 target = backend.create_repo(heads=['ancestor-child'])
418 source = backend.create_repo(heads=['change'])
420 source = backend.create_repo(heads=['change'])
419
421
420 response = self.app.post(
422 response = self.app.post(
421 route_path('pullrequest_create', repo_name=source.repo_name),
423 route_path('pullrequest_create', repo_name=source.repo_name),
422 [
424 [
423 ('source_repo', source.repo_name),
425 ('source_repo', source.repo_name),
424 ('source_ref', 'branch:default:' + commit_ids['change']),
426 ('source_ref', 'branch:default:' + commit_ids['change']),
425 ('target_repo', target.repo_name),
427 ('target_repo', target.repo_name),
426 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
428 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
427 ('common_ancestor', commit_ids['ancestor']),
429 ('common_ancestor', commit_ids['ancestor']),
428 ('pullrequest_desc', 'Description'),
430 ('pullrequest_desc', 'Description'),
429 ('pullrequest_title', 'Title'),
431 ('pullrequest_title', 'Title'),
430 ('__start__', 'review_members:sequence'),
432 ('__start__', 'review_members:sequence'),
431 ('__start__', 'reviewer:mapping'),
433 ('__start__', 'reviewer:mapping'),
432 ('user_id', '2'),
434 ('user_id', '2'),
433 ('__start__', 'reasons:sequence'),
435 ('__start__', 'reasons:sequence'),
434 ('reason', 'Some reason'),
436 ('reason', 'Some reason'),
435 ('__end__', 'reasons:sequence'),
437 ('__end__', 'reasons:sequence'),
438 ('__start__', 'rules:sequence'),
439 ('__end__', 'rules:sequence'),
436 ('mandatory', 'False'),
440 ('mandatory', 'False'),
437 ('__end__', 'reviewer:mapping'),
441 ('__end__', 'reviewer:mapping'),
438 ('__end__', 'review_members:sequence'),
442 ('__end__', 'review_members:sequence'),
439 ('__start__', 'revisions:sequence'),
443 ('__start__', 'revisions:sequence'),
440 ('revisions', commit_ids['change']),
444 ('revisions', commit_ids['change']),
441 ('__end__', 'revisions:sequence'),
445 ('__end__', 'revisions:sequence'),
442 ('user', ''),
446 ('user', ''),
443 ('csrf_token', csrf_token),
447 ('csrf_token', csrf_token),
444 ],
448 ],
445 status=302)
449 status=302)
446
450
447 location = response.headers['Location']
451 location = response.headers['Location']
448
452
449 pull_request_id = location.rsplit('/', 1)[1]
453 pull_request_id = location.rsplit('/', 1)[1]
450 assert pull_request_id != 'new'
454 assert pull_request_id != 'new'
451 pull_request = PullRequest.get(int(pull_request_id))
455 pull_request = PullRequest.get(int(pull_request_id))
452
456
453 # Check that a notification was made
457 # Check that a notification was made
454 notifications = Notification.query()\
458 notifications = Notification.query()\
455 .filter(Notification.created_by == pull_request.author.user_id,
459 .filter(Notification.created_by == pull_request.author.user_id,
456 Notification.type_ == Notification.TYPE_PULL_REQUEST,
460 Notification.type_ == Notification.TYPE_PULL_REQUEST,
457 Notification.subject.contains(
461 Notification.subject.contains(
458 "wants you to review pull request #%s" % pull_request_id))
462 "wants you to review pull request #%s" % pull_request_id))
459 assert len(notifications.all()) == 1
463 assert len(notifications.all()) == 1
460
464
461 # Change reviewers and check that a notification was made
465 # Change reviewers and check that a notification was made
462 PullRequestModel().update_reviewers(
466 PullRequestModel().update_reviewers(
463 pull_request.pull_request_id, [(1, [], False)],
467 pull_request.pull_request_id, [(1, [], False, [])],
464 pull_request.author)
468 pull_request.author)
465 assert len(notifications.all()) == 2
469 assert len(notifications.all()) == 2
466
470
467 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
471 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
468 csrf_token):
472 csrf_token):
469 commits = [
473 commits = [
470 {'message': 'ancestor',
474 {'message': 'ancestor',
471 'added': [FileNode('file_A', content='content_of_ancestor')]},
475 'added': [FileNode('file_A', content='content_of_ancestor')]},
472 {'message': 'change',
476 {'message': 'change',
473 'added': [FileNode('file_a', content='content_of_change')]},
477 'added': [FileNode('file_a', content='content_of_change')]},
474 {'message': 'change-child'},
478 {'message': 'change-child'},
475 {'message': 'ancestor-child', 'parents': ['ancestor'],
479 {'message': 'ancestor-child', 'parents': ['ancestor'],
476 'added': [
480 'added': [
477 FileNode('file_B', content='content_of_ancestor_child')]},
481 FileNode('file_B', content='content_of_ancestor_child')]},
478 {'message': 'ancestor-child-2'},
482 {'message': 'ancestor-child-2'},
479 ]
483 ]
480 commit_ids = backend.create_master_repo(commits)
484 commit_ids = backend.create_master_repo(commits)
481 target = backend.create_repo(heads=['ancestor-child'])
485 target = backend.create_repo(heads=['ancestor-child'])
482 source = backend.create_repo(heads=['change'])
486 source = backend.create_repo(heads=['change'])
483
487
484 response = self.app.post(
488 response = self.app.post(
485 route_path('pullrequest_create', repo_name=source.repo_name),
489 route_path('pullrequest_create', repo_name=source.repo_name),
486 [
490 [
487 ('source_repo', source.repo_name),
491 ('source_repo', source.repo_name),
488 ('source_ref', 'branch:default:' + commit_ids['change']),
492 ('source_ref', 'branch:default:' + commit_ids['change']),
489 ('target_repo', target.repo_name),
493 ('target_repo', target.repo_name),
490 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
494 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
491 ('common_ancestor', commit_ids['ancestor']),
495 ('common_ancestor', commit_ids['ancestor']),
492 ('pullrequest_desc', 'Description'),
496 ('pullrequest_desc', 'Description'),
493 ('pullrequest_title', 'Title'),
497 ('pullrequest_title', 'Title'),
494 ('__start__', 'review_members:sequence'),
498 ('__start__', 'review_members:sequence'),
495 ('__start__', 'reviewer:mapping'),
499 ('__start__', 'reviewer:mapping'),
496 ('user_id', '1'),
500 ('user_id', '1'),
497 ('__start__', 'reasons:sequence'),
501 ('__start__', 'reasons:sequence'),
498 ('reason', 'Some reason'),
502 ('reason', 'Some reason'),
499 ('__end__', 'reasons:sequence'),
503 ('__end__', 'reasons:sequence'),
504 ('__start__', 'rules:sequence'),
505 ('__end__', 'rules:sequence'),
500 ('mandatory', 'False'),
506 ('mandatory', 'False'),
501 ('__end__', 'reviewer:mapping'),
507 ('__end__', 'reviewer:mapping'),
502 ('__end__', 'review_members:sequence'),
508 ('__end__', 'review_members:sequence'),
503 ('__start__', 'revisions:sequence'),
509 ('__start__', 'revisions:sequence'),
504 ('revisions', commit_ids['change']),
510 ('revisions', commit_ids['change']),
505 ('__end__', 'revisions:sequence'),
511 ('__end__', 'revisions:sequence'),
506 ('user', ''),
512 ('user', ''),
507 ('csrf_token', csrf_token),
513 ('csrf_token', csrf_token),
508 ],
514 ],
509 status=302)
515 status=302)
510
516
511 location = response.headers['Location']
517 location = response.headers['Location']
512
518
513 pull_request_id = location.rsplit('/', 1)[1]
519 pull_request_id = location.rsplit('/', 1)[1]
514 assert pull_request_id != 'new'
520 assert pull_request_id != 'new'
515 pull_request = PullRequest.get(int(pull_request_id))
521 pull_request = PullRequest.get(int(pull_request_id))
516
522
517 # target_ref has to point to the ancestor's commit_id in order to
523 # target_ref has to point to the ancestor's commit_id in order to
518 # show the correct diff
524 # show the correct diff
519 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
525 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
520 assert pull_request.target_ref == expected_target_ref
526 assert pull_request.target_ref == expected_target_ref
521
527
522 # Check generated diff contents
528 # Check generated diff contents
523 response = response.follow()
529 response = response.follow()
524 assert 'content_of_ancestor' not in response.body
530 assert 'content_of_ancestor' not in response.body
525 assert 'content_of_ancestor-child' not in response.body
531 assert 'content_of_ancestor-child' not in response.body
526 assert 'content_of_change' in response.body
532 assert 'content_of_change' in response.body
527
533
528 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
534 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
529 # Clear any previous calls to rcextensions
535 # Clear any previous calls to rcextensions
530 rhodecode.EXTENSIONS.calls.clear()
536 rhodecode.EXTENSIONS.calls.clear()
531
537
532 pull_request = pr_util.create_pull_request(
538 pull_request = pr_util.create_pull_request(
533 approved=True, mergeable=True)
539 approved=True, mergeable=True)
534 pull_request_id = pull_request.pull_request_id
540 pull_request_id = pull_request.pull_request_id
535 repo_name = pull_request.target_repo.scm_instance().name,
541 repo_name = pull_request.target_repo.scm_instance().name,
536
542
537 response = self.app.post(
543 response = self.app.post(
538 route_path('pullrequest_merge',
544 route_path('pullrequest_merge',
539 repo_name=str(repo_name[0]),
545 repo_name=str(repo_name[0]),
540 pull_request_id=pull_request_id),
546 pull_request_id=pull_request_id),
541 params={'csrf_token': csrf_token}).follow()
547 params={'csrf_token': csrf_token}).follow()
542
548
543 pull_request = PullRequest.get(pull_request_id)
549 pull_request = PullRequest.get(pull_request_id)
544
550
545 assert response.status_int == 200
551 assert response.status_int == 200
546 assert pull_request.is_closed()
552 assert pull_request.is_closed()
547 assert_pull_request_status(
553 assert_pull_request_status(
548 pull_request, ChangesetStatus.STATUS_APPROVED)
554 pull_request, ChangesetStatus.STATUS_APPROVED)
549
555
550 # Check the relevant log entries were added
556 # Check the relevant log entries were added
551 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
557 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
552 actions = [log.action for log in user_logs]
558 actions = [log.action for log in user_logs]
553 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
559 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
554 expected_actions = [
560 expected_actions = [
555 u'repo.pull_request.close',
561 u'repo.pull_request.close',
556 u'repo.pull_request.merge',
562 u'repo.pull_request.merge',
557 u'repo.pull_request.comment.create'
563 u'repo.pull_request.comment.create'
558 ]
564 ]
559 assert actions == expected_actions
565 assert actions == expected_actions
560
566
561 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
567 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
562 actions = [log for log in user_logs]
568 actions = [log for log in user_logs]
563 assert actions[-1].action == 'user.push'
569 assert actions[-1].action == 'user.push'
564 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
570 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
565
571
566 # Check post_push rcextension was really executed
572 # Check post_push rcextension was really executed
567 push_calls = rhodecode.EXTENSIONS.calls['post_push']
573 push_calls = rhodecode.EXTENSIONS.calls['post_push']
568 assert len(push_calls) == 1
574 assert len(push_calls) == 1
569 unused_last_call_args, last_call_kwargs = push_calls[0]
575 unused_last_call_args, last_call_kwargs = push_calls[0]
570 assert last_call_kwargs['action'] == 'push'
576 assert last_call_kwargs['action'] == 'push'
571 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
577 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
572
578
573 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
579 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
574 pull_request = pr_util.create_pull_request(mergeable=False)
580 pull_request = pr_util.create_pull_request(mergeable=False)
575 pull_request_id = pull_request.pull_request_id
581 pull_request_id = pull_request.pull_request_id
576 pull_request = PullRequest.get(pull_request_id)
582 pull_request = PullRequest.get(pull_request_id)
577
583
578 response = self.app.post(
584 response = self.app.post(
579 route_path('pullrequest_merge',
585 route_path('pullrequest_merge',
580 repo_name=pull_request.target_repo.scm_instance().name,
586 repo_name=pull_request.target_repo.scm_instance().name,
581 pull_request_id=pull_request.pull_request_id),
587 pull_request_id=pull_request.pull_request_id),
582 params={'csrf_token': csrf_token}).follow()
588 params={'csrf_token': csrf_token}).follow()
583
589
584 assert response.status_int == 200
590 assert response.status_int == 200
585 response.mustcontain(
591 response.mustcontain(
586 'Merge is not currently possible because of below failed checks.')
592 'Merge is not currently possible because of below failed checks.')
587 response.mustcontain('Server-side pull request merging is disabled.')
593 response.mustcontain('Server-side pull request merging is disabled.')
588
594
589 @pytest.mark.skip_backends('svn')
595 @pytest.mark.skip_backends('svn')
590 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
596 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
591 pull_request = pr_util.create_pull_request(mergeable=True)
597 pull_request = pr_util.create_pull_request(mergeable=True)
592 pull_request_id = pull_request.pull_request_id
598 pull_request_id = pull_request.pull_request_id
593 repo_name = pull_request.target_repo.scm_instance().name
599 repo_name = pull_request.target_repo.scm_instance().name
594
600
595 response = self.app.post(
601 response = self.app.post(
596 route_path('pullrequest_merge',
602 route_path('pullrequest_merge',
597 repo_name=repo_name,
603 repo_name=repo_name,
598 pull_request_id=pull_request_id),
604 pull_request_id=pull_request_id),
599 params={'csrf_token': csrf_token}).follow()
605 params={'csrf_token': csrf_token}).follow()
600
606
601 assert response.status_int == 200
607 assert response.status_int == 200
602
608
603 response.mustcontain(
609 response.mustcontain(
604 'Merge is not currently possible because of below failed checks.')
610 'Merge is not currently possible because of below failed checks.')
605 response.mustcontain('Pull request reviewer approval is pending.')
611 response.mustcontain('Pull request reviewer approval is pending.')
606
612
607 def test_merge_pull_request_renders_failure_reason(
613 def test_merge_pull_request_renders_failure_reason(
608 self, user_regular, csrf_token, pr_util):
614 self, user_regular, csrf_token, pr_util):
609 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
615 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
610 pull_request_id = pull_request.pull_request_id
616 pull_request_id = pull_request.pull_request_id
611 repo_name = pull_request.target_repo.scm_instance().name
617 repo_name = pull_request.target_repo.scm_instance().name
612
618
613 model_patcher = mock.patch.multiple(
619 model_patcher = mock.patch.multiple(
614 PullRequestModel,
620 PullRequestModel,
615 merge=mock.Mock(return_value=MergeResponse(
621 merge=mock.Mock(return_value=MergeResponse(
616 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
622 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
617 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
623 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
618
624
619 with model_patcher:
625 with model_patcher:
620 response = self.app.post(
626 response = self.app.post(
621 route_path('pullrequest_merge',
627 route_path('pullrequest_merge',
622 repo_name=repo_name,
628 repo_name=repo_name,
623 pull_request_id=pull_request_id),
629 pull_request_id=pull_request_id),
624 params={'csrf_token': csrf_token}, status=302)
630 params={'csrf_token': csrf_token}, status=302)
625
631
626 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
632 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
627 MergeFailureReason.PUSH_FAILED])
633 MergeFailureReason.PUSH_FAILED])
628
634
629 def test_update_source_revision(self, backend, csrf_token):
635 def test_update_source_revision(self, backend, csrf_token):
630 commits = [
636 commits = [
631 {'message': 'ancestor'},
637 {'message': 'ancestor'},
632 {'message': 'change'},
638 {'message': 'change'},
633 {'message': 'change-2'},
639 {'message': 'change-2'},
634 ]
640 ]
635 commit_ids = backend.create_master_repo(commits)
641 commit_ids = backend.create_master_repo(commits)
636 target = backend.create_repo(heads=['ancestor'])
642 target = backend.create_repo(heads=['ancestor'])
637 source = backend.create_repo(heads=['change'])
643 source = backend.create_repo(heads=['change'])
638
644
639 # create pr from a in source to A in target
645 # create pr from a in source to A in target
640 pull_request = PullRequest()
646 pull_request = PullRequest()
641 pull_request.source_repo = source
647 pull_request.source_repo = source
642 # TODO: johbo: Make sure that we write the source ref this way!
648 # TODO: johbo: Make sure that we write the source ref this way!
643 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
649 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
644 branch=backend.default_branch_name, commit_id=commit_ids['change'])
650 branch=backend.default_branch_name, commit_id=commit_ids['change'])
645 pull_request.target_repo = target
651 pull_request.target_repo = target
646
652
647 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
653 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
648 branch=backend.default_branch_name,
654 branch=backend.default_branch_name,
649 commit_id=commit_ids['ancestor'])
655 commit_id=commit_ids['ancestor'])
650 pull_request.revisions = [commit_ids['change']]
656 pull_request.revisions = [commit_ids['change']]
651 pull_request.title = u"Test"
657 pull_request.title = u"Test"
652 pull_request.description = u"Description"
658 pull_request.description = u"Description"
653 pull_request.author = UserModel().get_by_username(
659 pull_request.author = UserModel().get_by_username(
654 TEST_USER_ADMIN_LOGIN)
660 TEST_USER_ADMIN_LOGIN)
655 Session().add(pull_request)
661 Session().add(pull_request)
656 Session().commit()
662 Session().commit()
657 pull_request_id = pull_request.pull_request_id
663 pull_request_id = pull_request.pull_request_id
658
664
659 # source has ancestor - change - change-2
665 # source has ancestor - change - change-2
660 backend.pull_heads(source, heads=['change-2'])
666 backend.pull_heads(source, heads=['change-2'])
661
667
662 # update PR
668 # update PR
663 self.app.post(
669 self.app.post(
664 route_path('pullrequest_update',
670 route_path('pullrequest_update',
665 repo_name=target.repo_name,
671 repo_name=target.repo_name,
666 pull_request_id=pull_request_id),
672 pull_request_id=pull_request_id),
667 params={'update_commits': 'true',
673 params={'update_commits': 'true',
668 'csrf_token': csrf_token})
674 'csrf_token': csrf_token})
669
675
670 # check that we have now both revisions
676 # check that we have now both revisions
671 pull_request = PullRequest.get(pull_request_id)
677 pull_request = PullRequest.get(pull_request_id)
672 assert pull_request.revisions == [
678 assert pull_request.revisions == [
673 commit_ids['change-2'], commit_ids['change']]
679 commit_ids['change-2'], commit_ids['change']]
674
680
675 # TODO: johbo: this should be a test on its own
681 # TODO: johbo: this should be a test on its own
676 response = self.app.get(route_path(
682 response = self.app.get(route_path(
677 'pullrequest_new',
683 'pullrequest_new',
678 repo_name=target.repo_name))
684 repo_name=target.repo_name))
679 assert response.status_int == 200
685 assert response.status_int == 200
680 assert 'Pull request updated to' in response.body
686 assert 'Pull request updated to' in response.body
681 assert 'with 1 added, 0 removed commits.' in response.body
687 assert 'with 1 added, 0 removed commits.' in response.body
682
688
683 def test_update_target_revision(self, backend, csrf_token):
689 def test_update_target_revision(self, backend, csrf_token):
684 commits = [
690 commits = [
685 {'message': 'ancestor'},
691 {'message': 'ancestor'},
686 {'message': 'change'},
692 {'message': 'change'},
687 {'message': 'ancestor-new', 'parents': ['ancestor']},
693 {'message': 'ancestor-new', 'parents': ['ancestor']},
688 {'message': 'change-rebased'},
694 {'message': 'change-rebased'},
689 ]
695 ]
690 commit_ids = backend.create_master_repo(commits)
696 commit_ids = backend.create_master_repo(commits)
691 target = backend.create_repo(heads=['ancestor'])
697 target = backend.create_repo(heads=['ancestor'])
692 source = backend.create_repo(heads=['change'])
698 source = backend.create_repo(heads=['change'])
693
699
694 # create pr from a in source to A in target
700 # create pr from a in source to A in target
695 pull_request = PullRequest()
701 pull_request = PullRequest()
696 pull_request.source_repo = source
702 pull_request.source_repo = source
697 # TODO: johbo: Make sure that we write the source ref this way!
703 # TODO: johbo: Make sure that we write the source ref this way!
698 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
704 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
699 branch=backend.default_branch_name, commit_id=commit_ids['change'])
705 branch=backend.default_branch_name, commit_id=commit_ids['change'])
700 pull_request.target_repo = target
706 pull_request.target_repo = target
701 # TODO: johbo: Target ref should be branch based, since tip can jump
707 # TODO: johbo: Target ref should be branch based, since tip can jump
702 # from branch to branch
708 # from branch to branch
703 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
709 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
704 branch=backend.default_branch_name,
710 branch=backend.default_branch_name,
705 commit_id=commit_ids['ancestor'])
711 commit_id=commit_ids['ancestor'])
706 pull_request.revisions = [commit_ids['change']]
712 pull_request.revisions = [commit_ids['change']]
707 pull_request.title = u"Test"
713 pull_request.title = u"Test"
708 pull_request.description = u"Description"
714 pull_request.description = u"Description"
709 pull_request.author = UserModel().get_by_username(
715 pull_request.author = UserModel().get_by_username(
710 TEST_USER_ADMIN_LOGIN)
716 TEST_USER_ADMIN_LOGIN)
711 Session().add(pull_request)
717 Session().add(pull_request)
712 Session().commit()
718 Session().commit()
713 pull_request_id = pull_request.pull_request_id
719 pull_request_id = pull_request.pull_request_id
714
720
715 # target has ancestor - ancestor-new
721 # target has ancestor - ancestor-new
716 # source has ancestor - ancestor-new - change-rebased
722 # source has ancestor - ancestor-new - change-rebased
717 backend.pull_heads(target, heads=['ancestor-new'])
723 backend.pull_heads(target, heads=['ancestor-new'])
718 backend.pull_heads(source, heads=['change-rebased'])
724 backend.pull_heads(source, heads=['change-rebased'])
719
725
720 # update PR
726 # update PR
721 self.app.post(
727 self.app.post(
722 route_path('pullrequest_update',
728 route_path('pullrequest_update',
723 repo_name=target.repo_name,
729 repo_name=target.repo_name,
724 pull_request_id=pull_request_id),
730 pull_request_id=pull_request_id),
725 params={'update_commits': 'true',
731 params={'update_commits': 'true',
726 'csrf_token': csrf_token},
732 'csrf_token': csrf_token},
727 status=200)
733 status=200)
728
734
729 # check that we have now both revisions
735 # check that we have now both revisions
730 pull_request = PullRequest.get(pull_request_id)
736 pull_request = PullRequest.get(pull_request_id)
731 assert pull_request.revisions == [commit_ids['change-rebased']]
737 assert pull_request.revisions == [commit_ids['change-rebased']]
732 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
738 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
733 branch=backend.default_branch_name,
739 branch=backend.default_branch_name,
734 commit_id=commit_ids['ancestor-new'])
740 commit_id=commit_ids['ancestor-new'])
735
741
736 # TODO: johbo: This should be a test on its own
742 # TODO: johbo: This should be a test on its own
737 response = self.app.get(route_path(
743 response = self.app.get(route_path(
738 'pullrequest_new',
744 'pullrequest_new',
739 repo_name=target.repo_name))
745 repo_name=target.repo_name))
740 assert response.status_int == 200
746 assert response.status_int == 200
741 assert 'Pull request updated to' in response.body
747 assert 'Pull request updated to' in response.body
742 assert 'with 1 added, 1 removed commits.' in response.body
748 assert 'with 1 added, 1 removed commits.' in response.body
743
749
744 def test_update_of_ancestor_reference(self, backend, csrf_token):
750 def test_update_of_ancestor_reference(self, backend, csrf_token):
745 commits = [
751 commits = [
746 {'message': 'ancestor'},
752 {'message': 'ancestor'},
747 {'message': 'change'},
753 {'message': 'change'},
748 {'message': 'change-2'},
754 {'message': 'change-2'},
749 {'message': 'ancestor-new', 'parents': ['ancestor']},
755 {'message': 'ancestor-new', 'parents': ['ancestor']},
750 {'message': 'change-rebased'},
756 {'message': 'change-rebased'},
751 ]
757 ]
752 commit_ids = backend.create_master_repo(commits)
758 commit_ids = backend.create_master_repo(commits)
753 target = backend.create_repo(heads=['ancestor'])
759 target = backend.create_repo(heads=['ancestor'])
754 source = backend.create_repo(heads=['change'])
760 source = backend.create_repo(heads=['change'])
755
761
756 # create pr from a in source to A in target
762 # create pr from a in source to A in target
757 pull_request = PullRequest()
763 pull_request = PullRequest()
758 pull_request.source_repo = source
764 pull_request.source_repo = source
759 # TODO: johbo: Make sure that we write the source ref this way!
765 # TODO: johbo: Make sure that we write the source ref this way!
760 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
766 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
761 branch=backend.default_branch_name,
767 branch=backend.default_branch_name,
762 commit_id=commit_ids['change'])
768 commit_id=commit_ids['change'])
763 pull_request.target_repo = target
769 pull_request.target_repo = target
764 # TODO: johbo: Target ref should be branch based, since tip can jump
770 # TODO: johbo: Target ref should be branch based, since tip can jump
765 # from branch to branch
771 # from branch to branch
766 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
772 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
767 branch=backend.default_branch_name,
773 branch=backend.default_branch_name,
768 commit_id=commit_ids['ancestor'])
774 commit_id=commit_ids['ancestor'])
769 pull_request.revisions = [commit_ids['change']]
775 pull_request.revisions = [commit_ids['change']]
770 pull_request.title = u"Test"
776 pull_request.title = u"Test"
771 pull_request.description = u"Description"
777 pull_request.description = u"Description"
772 pull_request.author = UserModel().get_by_username(
778 pull_request.author = UserModel().get_by_username(
773 TEST_USER_ADMIN_LOGIN)
779 TEST_USER_ADMIN_LOGIN)
774 Session().add(pull_request)
780 Session().add(pull_request)
775 Session().commit()
781 Session().commit()
776 pull_request_id = pull_request.pull_request_id
782 pull_request_id = pull_request.pull_request_id
777
783
778 # target has ancestor - ancestor-new
784 # target has ancestor - ancestor-new
779 # source has ancestor - ancestor-new - change-rebased
785 # source has ancestor - ancestor-new - change-rebased
780 backend.pull_heads(target, heads=['ancestor-new'])
786 backend.pull_heads(target, heads=['ancestor-new'])
781 backend.pull_heads(source, heads=['change-rebased'])
787 backend.pull_heads(source, heads=['change-rebased'])
782
788
783 # update PR
789 # update PR
784 self.app.post(
790 self.app.post(
785 route_path('pullrequest_update',
791 route_path('pullrequest_update',
786 repo_name=target.repo_name,
792 repo_name=target.repo_name,
787 pull_request_id=pull_request_id),
793 pull_request_id=pull_request_id),
788 params={'update_commits': 'true',
794 params={'update_commits': 'true',
789 'csrf_token': csrf_token},
795 'csrf_token': csrf_token},
790 status=200)
796 status=200)
791
797
792 # Expect the target reference to be updated correctly
798 # Expect the target reference to be updated correctly
793 pull_request = PullRequest.get(pull_request_id)
799 pull_request = PullRequest.get(pull_request_id)
794 assert pull_request.revisions == [commit_ids['change-rebased']]
800 assert pull_request.revisions == [commit_ids['change-rebased']]
795 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
801 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
796 branch=backend.default_branch_name,
802 branch=backend.default_branch_name,
797 commit_id=commit_ids['ancestor-new'])
803 commit_id=commit_ids['ancestor-new'])
798 assert pull_request.target_ref == expected_target_ref
804 assert pull_request.target_ref == expected_target_ref
799
805
800 def test_remove_pull_request_branch(self, backend_git, csrf_token):
806 def test_remove_pull_request_branch(self, backend_git, csrf_token):
801 branch_name = 'development'
807 branch_name = 'development'
802 commits = [
808 commits = [
803 {'message': 'initial-commit'},
809 {'message': 'initial-commit'},
804 {'message': 'old-feature'},
810 {'message': 'old-feature'},
805 {'message': 'new-feature', 'branch': branch_name},
811 {'message': 'new-feature', 'branch': branch_name},
806 ]
812 ]
807 repo = backend_git.create_repo(commits)
813 repo = backend_git.create_repo(commits)
808 commit_ids = backend_git.commit_ids
814 commit_ids = backend_git.commit_ids
809
815
810 pull_request = PullRequest()
816 pull_request = PullRequest()
811 pull_request.source_repo = repo
817 pull_request.source_repo = repo
812 pull_request.target_repo = repo
818 pull_request.target_repo = repo
813 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
819 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
814 branch=branch_name, commit_id=commit_ids['new-feature'])
820 branch=branch_name, commit_id=commit_ids['new-feature'])
815 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
821 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
816 branch=backend_git.default_branch_name,
822 branch=backend_git.default_branch_name,
817 commit_id=commit_ids['old-feature'])
823 commit_id=commit_ids['old-feature'])
818 pull_request.revisions = [commit_ids['new-feature']]
824 pull_request.revisions = [commit_ids['new-feature']]
819 pull_request.title = u"Test"
825 pull_request.title = u"Test"
820 pull_request.description = u"Description"
826 pull_request.description = u"Description"
821 pull_request.author = UserModel().get_by_username(
827 pull_request.author = UserModel().get_by_username(
822 TEST_USER_ADMIN_LOGIN)
828 TEST_USER_ADMIN_LOGIN)
823 Session().add(pull_request)
829 Session().add(pull_request)
824 Session().commit()
830 Session().commit()
825
831
826 vcs = repo.scm_instance()
832 vcs = repo.scm_instance()
827 vcs.remove_ref('refs/heads/{}'.format(branch_name))
833 vcs.remove_ref('refs/heads/{}'.format(branch_name))
828
834
829 response = self.app.get(route_path(
835 response = self.app.get(route_path(
830 'pullrequest_show',
836 'pullrequest_show',
831 repo_name=repo.repo_name,
837 repo_name=repo.repo_name,
832 pull_request_id=pull_request.pull_request_id))
838 pull_request_id=pull_request.pull_request_id))
833
839
834 assert response.status_int == 200
840 assert response.status_int == 200
835 assert_response = AssertResponse(response)
841 assert_response = AssertResponse(response)
836 assert_response.element_contains(
842 assert_response.element_contains(
837 '#changeset_compare_view_content .alert strong',
843 '#changeset_compare_view_content .alert strong',
838 'Missing commits')
844 'Missing commits')
839 assert_response.element_contains(
845 assert_response.element_contains(
840 '#changeset_compare_view_content .alert',
846 '#changeset_compare_view_content .alert',
841 'This pull request cannot be displayed, because one or more'
847 'This pull request cannot be displayed, because one or more'
842 ' commits no longer exist in the source repository.')
848 ' commits no longer exist in the source repository.')
843
849
844 def test_strip_commits_from_pull_request(
850 def test_strip_commits_from_pull_request(
845 self, backend, pr_util, csrf_token):
851 self, backend, pr_util, csrf_token):
846 commits = [
852 commits = [
847 {'message': 'initial-commit'},
853 {'message': 'initial-commit'},
848 {'message': 'old-feature'},
854 {'message': 'old-feature'},
849 {'message': 'new-feature', 'parents': ['initial-commit']},
855 {'message': 'new-feature', 'parents': ['initial-commit']},
850 ]
856 ]
851 pull_request = pr_util.create_pull_request(
857 pull_request = pr_util.create_pull_request(
852 commits, target_head='initial-commit', source_head='new-feature',
858 commits, target_head='initial-commit', source_head='new-feature',
853 revisions=['new-feature'])
859 revisions=['new-feature'])
854
860
855 vcs = pr_util.source_repository.scm_instance()
861 vcs = pr_util.source_repository.scm_instance()
856 if backend.alias == 'git':
862 if backend.alias == 'git':
857 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
863 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
858 else:
864 else:
859 vcs.strip(pr_util.commit_ids['new-feature'])
865 vcs.strip(pr_util.commit_ids['new-feature'])
860
866
861 response = self.app.get(route_path(
867 response = self.app.get(route_path(
862 'pullrequest_show',
868 'pullrequest_show',
863 repo_name=pr_util.target_repository.repo_name,
869 repo_name=pr_util.target_repository.repo_name,
864 pull_request_id=pull_request.pull_request_id))
870 pull_request_id=pull_request.pull_request_id))
865
871
866 assert response.status_int == 200
872 assert response.status_int == 200
867 assert_response = AssertResponse(response)
873 assert_response = AssertResponse(response)
868 assert_response.element_contains(
874 assert_response.element_contains(
869 '#changeset_compare_view_content .alert strong',
875 '#changeset_compare_view_content .alert strong',
870 'Missing commits')
876 'Missing commits')
871 assert_response.element_contains(
877 assert_response.element_contains(
872 '#changeset_compare_view_content .alert',
878 '#changeset_compare_view_content .alert',
873 'This pull request cannot be displayed, because one or more'
879 'This pull request cannot be displayed, because one or more'
874 ' commits no longer exist in the source repository.')
880 ' commits no longer exist in the source repository.')
875 assert_response.element_contains(
881 assert_response.element_contains(
876 '#update_commits',
882 '#update_commits',
877 'Update commits')
883 'Update commits')
878
884
879 def test_strip_commits_and_update(
885 def test_strip_commits_and_update(
880 self, backend, pr_util, csrf_token):
886 self, backend, pr_util, csrf_token):
881 commits = [
887 commits = [
882 {'message': 'initial-commit'},
888 {'message': 'initial-commit'},
883 {'message': 'old-feature'},
889 {'message': 'old-feature'},
884 {'message': 'new-feature', 'parents': ['old-feature']},
890 {'message': 'new-feature', 'parents': ['old-feature']},
885 ]
891 ]
886 pull_request = pr_util.create_pull_request(
892 pull_request = pr_util.create_pull_request(
887 commits, target_head='old-feature', source_head='new-feature',
893 commits, target_head='old-feature', source_head='new-feature',
888 revisions=['new-feature'], mergeable=True)
894 revisions=['new-feature'], mergeable=True)
889
895
890 vcs = pr_util.source_repository.scm_instance()
896 vcs = pr_util.source_repository.scm_instance()
891 if backend.alias == 'git':
897 if backend.alias == 'git':
892 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
898 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
893 else:
899 else:
894 vcs.strip(pr_util.commit_ids['new-feature'])
900 vcs.strip(pr_util.commit_ids['new-feature'])
895
901
896 response = self.app.post(
902 response = self.app.post(
897 route_path('pullrequest_update',
903 route_path('pullrequest_update',
898 repo_name=pull_request.target_repo.repo_name,
904 repo_name=pull_request.target_repo.repo_name,
899 pull_request_id=pull_request.pull_request_id),
905 pull_request_id=pull_request.pull_request_id),
900 params={'update_commits': 'true',
906 params={'update_commits': 'true',
901 'csrf_token': csrf_token})
907 'csrf_token': csrf_token})
902
908
903 assert response.status_int == 200
909 assert response.status_int == 200
904 assert response.body == 'true'
910 assert response.body == 'true'
905
911
906 # Make sure that after update, it won't raise 500 errors
912 # Make sure that after update, it won't raise 500 errors
907 response = self.app.get(route_path(
913 response = self.app.get(route_path(
908 'pullrequest_show',
914 'pullrequest_show',
909 repo_name=pr_util.target_repository.repo_name,
915 repo_name=pr_util.target_repository.repo_name,
910 pull_request_id=pull_request.pull_request_id))
916 pull_request_id=pull_request.pull_request_id))
911
917
912 assert response.status_int == 200
918 assert response.status_int == 200
913 assert_response = AssertResponse(response)
919 assert_response = AssertResponse(response)
914 assert_response.element_contains(
920 assert_response.element_contains(
915 '#changeset_compare_view_content .alert strong',
921 '#changeset_compare_view_content .alert strong',
916 'Missing commits')
922 'Missing commits')
917
923
918 def test_branch_is_a_link(self, pr_util):
924 def test_branch_is_a_link(self, pr_util):
919 pull_request = pr_util.create_pull_request()
925 pull_request = pr_util.create_pull_request()
920 pull_request.source_ref = 'branch:origin:1234567890abcdef'
926 pull_request.source_ref = 'branch:origin:1234567890abcdef'
921 pull_request.target_ref = 'branch:target:abcdef1234567890'
927 pull_request.target_ref = 'branch:target:abcdef1234567890'
922 Session().add(pull_request)
928 Session().add(pull_request)
923 Session().commit()
929 Session().commit()
924
930
925 response = self.app.get(route_path(
931 response = self.app.get(route_path(
926 'pullrequest_show',
932 'pullrequest_show',
927 repo_name=pull_request.target_repo.scm_instance().name,
933 repo_name=pull_request.target_repo.scm_instance().name,
928 pull_request_id=pull_request.pull_request_id))
934 pull_request_id=pull_request.pull_request_id))
929 assert response.status_int == 200
935 assert response.status_int == 200
930 assert_response = AssertResponse(response)
936 assert_response = AssertResponse(response)
931
937
932 origin = assert_response.get_element('.pr-origininfo .tag')
938 origin = assert_response.get_element('.pr-origininfo .tag')
933 origin_children = origin.getchildren()
939 origin_children = origin.getchildren()
934 assert len(origin_children) == 1
940 assert len(origin_children) == 1
935 target = assert_response.get_element('.pr-targetinfo .tag')
941 target = assert_response.get_element('.pr-targetinfo .tag')
936 target_children = target.getchildren()
942 target_children = target.getchildren()
937 assert len(target_children) == 1
943 assert len(target_children) == 1
938
944
939 expected_origin_link = route_path(
945 expected_origin_link = route_path(
940 'repo_changelog',
946 'repo_changelog',
941 repo_name=pull_request.source_repo.scm_instance().name,
947 repo_name=pull_request.source_repo.scm_instance().name,
942 params=dict(branch='origin'))
948 params=dict(branch='origin'))
943 expected_target_link = route_path(
949 expected_target_link = route_path(
944 'repo_changelog',
950 'repo_changelog',
945 repo_name=pull_request.target_repo.scm_instance().name,
951 repo_name=pull_request.target_repo.scm_instance().name,
946 params=dict(branch='target'))
952 params=dict(branch='target'))
947 assert origin_children[0].attrib['href'] == expected_origin_link
953 assert origin_children[0].attrib['href'] == expected_origin_link
948 assert origin_children[0].text == 'branch: origin'
954 assert origin_children[0].text == 'branch: origin'
949 assert target_children[0].attrib['href'] == expected_target_link
955 assert target_children[0].attrib['href'] == expected_target_link
950 assert target_children[0].text == 'branch: target'
956 assert target_children[0].text == 'branch: target'
951
957
952 def test_bookmark_is_not_a_link(self, pr_util):
958 def test_bookmark_is_not_a_link(self, pr_util):
953 pull_request = pr_util.create_pull_request()
959 pull_request = pr_util.create_pull_request()
954 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
960 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
955 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
961 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
956 Session().add(pull_request)
962 Session().add(pull_request)
957 Session().commit()
963 Session().commit()
958
964
959 response = self.app.get(route_path(
965 response = self.app.get(route_path(
960 'pullrequest_show',
966 'pullrequest_show',
961 repo_name=pull_request.target_repo.scm_instance().name,
967 repo_name=pull_request.target_repo.scm_instance().name,
962 pull_request_id=pull_request.pull_request_id))
968 pull_request_id=pull_request.pull_request_id))
963 assert response.status_int == 200
969 assert response.status_int == 200
964 assert_response = AssertResponse(response)
970 assert_response = AssertResponse(response)
965
971
966 origin = assert_response.get_element('.pr-origininfo .tag')
972 origin = assert_response.get_element('.pr-origininfo .tag')
967 assert origin.text.strip() == 'bookmark: origin'
973 assert origin.text.strip() == 'bookmark: origin'
968 assert origin.getchildren() == []
974 assert origin.getchildren() == []
969
975
970 target = assert_response.get_element('.pr-targetinfo .tag')
976 target = assert_response.get_element('.pr-targetinfo .tag')
971 assert target.text.strip() == 'bookmark: target'
977 assert target.text.strip() == 'bookmark: target'
972 assert target.getchildren() == []
978 assert target.getchildren() == []
973
979
974 def test_tag_is_not_a_link(self, pr_util):
980 def test_tag_is_not_a_link(self, pr_util):
975 pull_request = pr_util.create_pull_request()
981 pull_request = pr_util.create_pull_request()
976 pull_request.source_ref = 'tag:origin:1234567890abcdef'
982 pull_request.source_ref = 'tag:origin:1234567890abcdef'
977 pull_request.target_ref = 'tag:target:abcdef1234567890'
983 pull_request.target_ref = 'tag:target:abcdef1234567890'
978 Session().add(pull_request)
984 Session().add(pull_request)
979 Session().commit()
985 Session().commit()
980
986
981 response = self.app.get(route_path(
987 response = self.app.get(route_path(
982 'pullrequest_show',
988 'pullrequest_show',
983 repo_name=pull_request.target_repo.scm_instance().name,
989 repo_name=pull_request.target_repo.scm_instance().name,
984 pull_request_id=pull_request.pull_request_id))
990 pull_request_id=pull_request.pull_request_id))
985 assert response.status_int == 200
991 assert response.status_int == 200
986 assert_response = AssertResponse(response)
992 assert_response = AssertResponse(response)
987
993
988 origin = assert_response.get_element('.pr-origininfo .tag')
994 origin = assert_response.get_element('.pr-origininfo .tag')
989 assert origin.text.strip() == 'tag: origin'
995 assert origin.text.strip() == 'tag: origin'
990 assert origin.getchildren() == []
996 assert origin.getchildren() == []
991
997
992 target = assert_response.get_element('.pr-targetinfo .tag')
998 target = assert_response.get_element('.pr-targetinfo .tag')
993 assert target.text.strip() == 'tag: target'
999 assert target.text.strip() == 'tag: target'
994 assert target.getchildren() == []
1000 assert target.getchildren() == []
995
1001
996 @pytest.mark.parametrize('mergeable', [True, False])
1002 @pytest.mark.parametrize('mergeable', [True, False])
997 def test_shadow_repository_link(
1003 def test_shadow_repository_link(
998 self, mergeable, pr_util, http_host_only_stub):
1004 self, mergeable, pr_util, http_host_only_stub):
999 """
1005 """
1000 Check that the pull request summary page displays a link to the shadow
1006 Check that the pull request summary page displays a link to the shadow
1001 repository if the pull request is mergeable. If it is not mergeable
1007 repository if the pull request is mergeable. If it is not mergeable
1002 the link should not be displayed.
1008 the link should not be displayed.
1003 """
1009 """
1004 pull_request = pr_util.create_pull_request(
1010 pull_request = pr_util.create_pull_request(
1005 mergeable=mergeable, enable_notifications=False)
1011 mergeable=mergeable, enable_notifications=False)
1006 target_repo = pull_request.target_repo.scm_instance()
1012 target_repo = pull_request.target_repo.scm_instance()
1007 pr_id = pull_request.pull_request_id
1013 pr_id = pull_request.pull_request_id
1008 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1014 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1009 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1015 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1010
1016
1011 response = self.app.get(route_path(
1017 response = self.app.get(route_path(
1012 'pullrequest_show',
1018 'pullrequest_show',
1013 repo_name=target_repo.name,
1019 repo_name=target_repo.name,
1014 pull_request_id=pr_id))
1020 pull_request_id=pr_id))
1015
1021
1016 assertr = AssertResponse(response)
1022 assertr = AssertResponse(response)
1017 if mergeable:
1023 if mergeable:
1018 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1024 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1019 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1025 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1020 else:
1026 else:
1021 assertr.no_element_exists('.pr-mergeinfo')
1027 assertr.no_element_exists('.pr-mergeinfo')
1022
1028
1023
1029
1024 @pytest.mark.usefixtures('app')
1030 @pytest.mark.usefixtures('app')
1025 @pytest.mark.backends("git", "hg")
1031 @pytest.mark.backends("git", "hg")
1026 class TestPullrequestsControllerDelete(object):
1032 class TestPullrequestsControllerDelete(object):
1027 def test_pull_request_delete_button_permissions_admin(
1033 def test_pull_request_delete_button_permissions_admin(
1028 self, autologin_user, user_admin, pr_util):
1034 self, autologin_user, user_admin, pr_util):
1029 pull_request = pr_util.create_pull_request(
1035 pull_request = pr_util.create_pull_request(
1030 author=user_admin.username, enable_notifications=False)
1036 author=user_admin.username, enable_notifications=False)
1031
1037
1032 response = self.app.get(route_path(
1038 response = self.app.get(route_path(
1033 'pullrequest_show',
1039 'pullrequest_show',
1034 repo_name=pull_request.target_repo.scm_instance().name,
1040 repo_name=pull_request.target_repo.scm_instance().name,
1035 pull_request_id=pull_request.pull_request_id))
1041 pull_request_id=pull_request.pull_request_id))
1036
1042
1037 response.mustcontain('id="delete_pullrequest"')
1043 response.mustcontain('id="delete_pullrequest"')
1038 response.mustcontain('Confirm to delete this pull request')
1044 response.mustcontain('Confirm to delete this pull request')
1039
1045
1040 def test_pull_request_delete_button_permissions_owner(
1046 def test_pull_request_delete_button_permissions_owner(
1041 self, autologin_regular_user, user_regular, pr_util):
1047 self, autologin_regular_user, user_regular, pr_util):
1042 pull_request = pr_util.create_pull_request(
1048 pull_request = pr_util.create_pull_request(
1043 author=user_regular.username, enable_notifications=False)
1049 author=user_regular.username, enable_notifications=False)
1044
1050
1045 response = self.app.get(route_path(
1051 response = self.app.get(route_path(
1046 'pullrequest_show',
1052 'pullrequest_show',
1047 repo_name=pull_request.target_repo.scm_instance().name,
1053 repo_name=pull_request.target_repo.scm_instance().name,
1048 pull_request_id=pull_request.pull_request_id))
1054 pull_request_id=pull_request.pull_request_id))
1049
1055
1050 response.mustcontain('id="delete_pullrequest"')
1056 response.mustcontain('id="delete_pullrequest"')
1051 response.mustcontain('Confirm to delete this pull request')
1057 response.mustcontain('Confirm to delete this pull request')
1052
1058
1053 def test_pull_request_delete_button_permissions_forbidden(
1059 def test_pull_request_delete_button_permissions_forbidden(
1054 self, autologin_regular_user, user_regular, user_admin, pr_util):
1060 self, autologin_regular_user, user_regular, user_admin, pr_util):
1055 pull_request = pr_util.create_pull_request(
1061 pull_request = pr_util.create_pull_request(
1056 author=user_admin.username, enable_notifications=False)
1062 author=user_admin.username, enable_notifications=False)
1057
1063
1058 response = self.app.get(route_path(
1064 response = self.app.get(route_path(
1059 'pullrequest_show',
1065 'pullrequest_show',
1060 repo_name=pull_request.target_repo.scm_instance().name,
1066 repo_name=pull_request.target_repo.scm_instance().name,
1061 pull_request_id=pull_request.pull_request_id))
1067 pull_request_id=pull_request.pull_request_id))
1062 response.mustcontain(no=['id="delete_pullrequest"'])
1068 response.mustcontain(no=['id="delete_pullrequest"'])
1063 response.mustcontain(no=['Confirm to delete this pull request'])
1069 response.mustcontain(no=['Confirm to delete this pull request'])
1064
1070
1065 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1071 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1066 self, autologin_regular_user, user_regular, user_admin, pr_util,
1072 self, autologin_regular_user, user_regular, user_admin, pr_util,
1067 user_util):
1073 user_util):
1068
1074
1069 pull_request = pr_util.create_pull_request(
1075 pull_request = pr_util.create_pull_request(
1070 author=user_admin.username, enable_notifications=False)
1076 author=user_admin.username, enable_notifications=False)
1071
1077
1072 user_util.grant_user_permission_to_repo(
1078 user_util.grant_user_permission_to_repo(
1073 pull_request.target_repo, user_regular,
1079 pull_request.target_repo, user_regular,
1074 'repository.write')
1080 'repository.write')
1075
1081
1076 response = self.app.get(route_path(
1082 response = self.app.get(route_path(
1077 'pullrequest_show',
1083 'pullrequest_show',
1078 repo_name=pull_request.target_repo.scm_instance().name,
1084 repo_name=pull_request.target_repo.scm_instance().name,
1079 pull_request_id=pull_request.pull_request_id))
1085 pull_request_id=pull_request.pull_request_id))
1080
1086
1081 response.mustcontain('id="open_edit_pullrequest"')
1087 response.mustcontain('id="open_edit_pullrequest"')
1082 response.mustcontain('id="delete_pullrequest"')
1088 response.mustcontain('id="delete_pullrequest"')
1083 response.mustcontain(no=['Confirm to delete this pull request'])
1089 response.mustcontain(no=['Confirm to delete this pull request'])
1084
1090
1085 def test_delete_comment_returns_404_if_comment_does_not_exist(
1091 def test_delete_comment_returns_404_if_comment_does_not_exist(
1086 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1092 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1087
1093
1088 pull_request = pr_util.create_pull_request(
1094 pull_request = pr_util.create_pull_request(
1089 author=user_admin.username, enable_notifications=False)
1095 author=user_admin.username, enable_notifications=False)
1090
1096
1091 self.app.post(
1097 self.app.post(
1092 route_path(
1098 route_path(
1093 'pullrequest_comment_delete',
1099 'pullrequest_comment_delete',
1094 repo_name=pull_request.target_repo.scm_instance().name,
1100 repo_name=pull_request.target_repo.scm_instance().name,
1095 pull_request_id=pull_request.pull_request_id,
1101 pull_request_id=pull_request.pull_request_id,
1096 comment_id=1024404),
1102 comment_id=1024404),
1097 extra_environ=xhr_header,
1103 extra_environ=xhr_header,
1098 params={'csrf_token': csrf_token},
1104 params={'csrf_token': csrf_token},
1099 status=404
1105 status=404
1100 )
1106 )
1101
1107
1102 def test_delete_comment(
1108 def test_delete_comment(
1103 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1109 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1104
1110
1105 pull_request = pr_util.create_pull_request(
1111 pull_request = pr_util.create_pull_request(
1106 author=user_admin.username, enable_notifications=False)
1112 author=user_admin.username, enable_notifications=False)
1107 comment = pr_util.create_comment()
1113 comment = pr_util.create_comment()
1108 comment_id = comment.comment_id
1114 comment_id = comment.comment_id
1109
1115
1110 response = self.app.post(
1116 response = self.app.post(
1111 route_path(
1117 route_path(
1112 'pullrequest_comment_delete',
1118 'pullrequest_comment_delete',
1113 repo_name=pull_request.target_repo.scm_instance().name,
1119 repo_name=pull_request.target_repo.scm_instance().name,
1114 pull_request_id=pull_request.pull_request_id,
1120 pull_request_id=pull_request.pull_request_id,
1115 comment_id=comment_id),
1121 comment_id=comment_id),
1116 extra_environ=xhr_header,
1122 extra_environ=xhr_header,
1117 params={'csrf_token': csrf_token},
1123 params={'csrf_token': csrf_token},
1118 status=200
1124 status=200
1119 )
1125 )
1120 assert response.body == 'true'
1126 assert response.body == 'true'
1121
1127
1122
1128
1123 def assert_pull_request_status(pull_request, expected_status):
1129 def assert_pull_request_status(pull_request, expected_status):
1124 status = ChangesetStatusModel().calculated_review_status(
1130 status = ChangesetStatusModel().calculated_review_status(
1125 pull_request=pull_request)
1131 pull_request=pull_request)
1126 assert status == expected_status
1132 assert status == expected_status
1127
1133
1128
1134
1129 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1135 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1130 @pytest.mark.usefixtures("autologin_user")
1136 @pytest.mark.usefixtures("autologin_user")
1131 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1137 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1132 response = app.get(
1138 response = app.get(
1133 route_path(route, repo_name=backend_svn.repo_name), status=404)
1139 route_path(route, repo_name=backend_svn.repo_name), status=404)
1134
1140
@@ -1,76 +1,79 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 from rhodecode.lib import helpers as h
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
22 from rhodecode.lib.utils2 import safe_int
23
23
24
24
25 def reviewer_as_json(user, reasons=None, mandatory=False):
25 def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None):
26 """
26 """
27 Returns json struct of a reviewer for frontend
27 Returns json struct of a reviewer for frontend
28
28
29 :param user: the reviewer
29 :param user: the reviewer
30 :param reasons: list of strings of why they are reviewers
30 :param reasons: list of strings of why they are reviewers
31 :param mandatory: bool, to set user as mandatory
31 :param mandatory: bool, to set user as mandatory
32 """
32 """
33
33
34 return {
34 return {
35 'user_id': user.user_id,
35 'user_id': user.user_id,
36 'reasons': reasons or [],
36 'reasons': reasons or [],
37 'rules': rules or [],
37 'mandatory': mandatory,
38 'mandatory': mandatory,
39 'user_group': user_group,
38 'username': user.username,
40 'username': user.username,
39 'first_name': user.first_name,
41 'first_name': user.first_name,
40 'last_name': user.last_name,
42 'last_name': user.last_name,
43 'user_link': h.link_to_user(user),
41 'gravatar_link': h.gravatar_url(user.email, 14),
44 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
45 }
43
46
44
47
45 def get_default_reviewers_data(
48 def get_default_reviewers_data(
46 current_user, source_repo, source_commit, target_repo, target_commit):
49 current_user, source_repo, source_commit, target_repo, target_commit):
47
50
48 """ Return json for default reviewers of a repository """
51 """ Return json for default reviewers of a repository """
49
52
50 reasons = ['Default reviewer', 'Repository owner']
53 reasons = ['Default reviewer', 'Repository owner']
51 default = reviewer_as_json(
54 default = reviewer_as_json(
52 user=current_user, reasons=reasons, mandatory=False)
55 user=current_user, reasons=reasons, mandatory=False)
53
56
54 return {
57 return {
55 'api_ver': 'v1', # define version for later possible schema upgrade
58 'api_ver': 'v1', # define version for later possible schema upgrade
56 'reviewers': [default],
59 'reviewers': [default],
57 'rules': {},
60 'rules': {},
58 'rules_data': {},
61 'rules_data': {},
59 }
62 }
60
63
61
64
62 def validate_default_reviewers(review_members, reviewer_rules):
65 def validate_default_reviewers(review_members, reviewer_rules):
63 """
66 """
64 Function to validate submitted reviewers against the saved rules
67 Function to validate submitted reviewers against the saved rules
65
68
66 """
69 """
67 reviewers = []
70 reviewers = []
68 reviewer_by_id = {}
71 reviewer_by_id = {}
69 for r in review_members:
72 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
73 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
74 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['rules'])
72
75
73 reviewer_by_id[reviewer_user_id] = entry
76 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
77 reviewers.append(entry)
75
78
76 return reviewers
79 return reviewers
@@ -1,2072 +1,2077 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 from collections import OrderedDict
39 from collections import OrderedDict
40
40
41 import pygments
41 import pygments
42 import itertools
42 import itertools
43 import fnmatch
43 import fnmatch
44
44
45 from datetime import datetime
45 from datetime import datetime
46 from functools import partial
46 from functools import partial
47 from pygments.formatters.html import HtmlFormatter
47 from pygments.formatters.html import HtmlFormatter
48 from pygments import highlight as code_highlight
48 from pygments import highlight as code_highlight
49 from pygments.lexers import (
49 from pygments.lexers import (
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51
51
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53
53
54 from webhelpers.html import literal, HTML, escape
54 from webhelpers.html import literal, HTML, escape
55 from webhelpers.html.tools import *
55 from webhelpers.html.tools import *
56 from webhelpers.html.builder import make_tag
56 from webhelpers.html.builder import make_tag
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 submit, text, password, textarea, title, ul, xml_declaration, radio
60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
65 replace_whitespace, urlify, truncate, wrap_paragraphs
65 replace_whitespace, urlify, truncate, wrap_paragraphs
66 from webhelpers.date import time_ago_in_words
66 from webhelpers.date import time_ago_in_words
67 from webhelpers.paginate import Page as _Page
67 from webhelpers.paginate import Page as _Page
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
70 from webhelpers2.number import format_byte_size
70 from webhelpers2.number import format_byte_size
71
71
72 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.action_parser import action_parser
73 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.ext_json import json
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
77 AttributeDict, safe_int, md5, md5_safe
77 AttributeDict, safe_int, md5, md5_safe
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
82 from rhodecode.model.changeset_status import ChangesetStatusModel
82 from rhodecode.model.changeset_status import ChangesetStatusModel
83 from rhodecode.model.db import Permission, User, Repository
83 from rhodecode.model.db import Permission, User, Repository
84 from rhodecode.model.repo_group import RepoGroupModel
84 from rhodecode.model.repo_group import RepoGroupModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
86
86
87 log = logging.getLogger(__name__)
87 log = logging.getLogger(__name__)
88
88
89
89
90 DEFAULT_USER = User.DEFAULT_USER
90 DEFAULT_USER = User.DEFAULT_USER
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
92
92
93
93
94 def asset(path, ver=None, **kwargs):
94 def asset(path, ver=None, **kwargs):
95 """
95 """
96 Helper to generate a static asset file path for rhodecode assets
96 Helper to generate a static asset file path for rhodecode assets
97
97
98 eg. h.asset('images/image.png', ver='3923')
98 eg. h.asset('images/image.png', ver='3923')
99
99
100 :param path: path of asset
100 :param path: path of asset
101 :param ver: optional version query param to append as ?ver=
101 :param ver: optional version query param to append as ?ver=
102 """
102 """
103 request = get_current_request()
103 request = get_current_request()
104 query = {}
104 query = {}
105 query.update(kwargs)
105 query.update(kwargs)
106 if ver:
106 if ver:
107 query = {'ver': ver}
107 query = {'ver': ver}
108 return request.static_path(
108 return request.static_path(
109 'rhodecode:public/{}'.format(path), _query=query)
109 'rhodecode:public/{}'.format(path), _query=query)
110
110
111
111
112 default_html_escape_table = {
112 default_html_escape_table = {
113 ord('&'): u'&amp;',
113 ord('&'): u'&amp;',
114 ord('<'): u'&lt;',
114 ord('<'): u'&lt;',
115 ord('>'): u'&gt;',
115 ord('>'): u'&gt;',
116 ord('"'): u'&quot;',
116 ord('"'): u'&quot;',
117 ord("'"): u'&#39;',
117 ord("'"): u'&#39;',
118 }
118 }
119
119
120
120
121 def html_escape(text, html_escape_table=default_html_escape_table):
121 def html_escape(text, html_escape_table=default_html_escape_table):
122 """Produce entities within text."""
122 """Produce entities within text."""
123 return text.translate(html_escape_table)
123 return text.translate(html_escape_table)
124
124
125
125
126 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
126 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
127 """
127 """
128 Truncate string ``s`` at the first occurrence of ``sub``.
128 Truncate string ``s`` at the first occurrence of ``sub``.
129
129
130 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
130 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
131 """
131 """
132 suffix_if_chopped = suffix_if_chopped or ''
132 suffix_if_chopped = suffix_if_chopped or ''
133 pos = s.find(sub)
133 pos = s.find(sub)
134 if pos == -1:
134 if pos == -1:
135 return s
135 return s
136
136
137 if inclusive:
137 if inclusive:
138 pos += len(sub)
138 pos += len(sub)
139
139
140 chopped = s[:pos]
140 chopped = s[:pos]
141 left = s[pos:].strip()
141 left = s[pos:].strip()
142
142
143 if left and suffix_if_chopped:
143 if left and suffix_if_chopped:
144 chopped += suffix_if_chopped
144 chopped += suffix_if_chopped
145
145
146 return chopped
146 return chopped
147
147
148
148
149 def shorter(text, size=20):
149 def shorter(text, size=20):
150 postfix = '...'
150 postfix = '...'
151 if len(text) > size:
151 if len(text) > size:
152 return text[:size - len(postfix)] + postfix
152 return text[:size - len(postfix)] + postfix
153 return text
153 return text
154
154
155
155
156 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
156 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
157 """
157 """
158 Reset button
158 Reset button
159 """
159 """
160 _set_input_attrs(attrs, type, name, value)
160 _set_input_attrs(attrs, type, name, value)
161 _set_id_attr(attrs, id, name)
161 _set_id_attr(attrs, id, name)
162 convert_boolean_attrs(attrs, ["disabled"])
162 convert_boolean_attrs(attrs, ["disabled"])
163 return HTML.input(**attrs)
163 return HTML.input(**attrs)
164
164
165 reset = _reset
165 reset = _reset
166 safeid = _make_safe_id_component
166 safeid = _make_safe_id_component
167
167
168
168
169 def branding(name, length=40):
169 def branding(name, length=40):
170 return truncate(name, length, indicator="")
170 return truncate(name, length, indicator="")
171
171
172
172
173 def FID(raw_id, path):
173 def FID(raw_id, path):
174 """
174 """
175 Creates a unique ID for filenode based on it's hash of path and commit
175 Creates a unique ID for filenode based on it's hash of path and commit
176 it's safe to use in urls
176 it's safe to use in urls
177
177
178 :param raw_id:
178 :param raw_id:
179 :param path:
179 :param path:
180 """
180 """
181
181
182 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
182 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
183
183
184
184
185 class _GetError(object):
185 class _GetError(object):
186 """Get error from form_errors, and represent it as span wrapped error
186 """Get error from form_errors, and represent it as span wrapped error
187 message
187 message
188
188
189 :param field_name: field to fetch errors for
189 :param field_name: field to fetch errors for
190 :param form_errors: form errors dict
190 :param form_errors: form errors dict
191 """
191 """
192
192
193 def __call__(self, field_name, form_errors):
193 def __call__(self, field_name, form_errors):
194 tmpl = """<span class="error_msg">%s</span>"""
194 tmpl = """<span class="error_msg">%s</span>"""
195 if form_errors and field_name in form_errors:
195 if form_errors and field_name in form_errors:
196 return literal(tmpl % form_errors.get(field_name))
196 return literal(tmpl % form_errors.get(field_name))
197
197
198 get_error = _GetError()
198 get_error = _GetError()
199
199
200
200
201 class _ToolTip(object):
201 class _ToolTip(object):
202
202
203 def __call__(self, tooltip_title, trim_at=50):
203 def __call__(self, tooltip_title, trim_at=50):
204 """
204 """
205 Special function just to wrap our text into nice formatted
205 Special function just to wrap our text into nice formatted
206 autowrapped text
206 autowrapped text
207
207
208 :param tooltip_title:
208 :param tooltip_title:
209 """
209 """
210 tooltip_title = escape(tooltip_title)
210 tooltip_title = escape(tooltip_title)
211 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
211 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
212 return tooltip_title
212 return tooltip_title
213 tooltip = _ToolTip()
213 tooltip = _ToolTip()
214
214
215
215
216 def files_breadcrumbs(repo_name, commit_id, file_path):
216 def files_breadcrumbs(repo_name, commit_id, file_path):
217 if isinstance(file_path, str):
217 if isinstance(file_path, str):
218 file_path = safe_unicode(file_path)
218 file_path = safe_unicode(file_path)
219
219
220 # TODO: johbo: Is this always a url like path, or is this operating
220 # TODO: johbo: Is this always a url like path, or is this operating
221 # system dependent?
221 # system dependent?
222 path_segments = file_path.split('/')
222 path_segments = file_path.split('/')
223
223
224 repo_name_html = escape(repo_name)
224 repo_name_html = escape(repo_name)
225 if len(path_segments) == 1 and path_segments[0] == '':
225 if len(path_segments) == 1 and path_segments[0] == '':
226 url_segments = [repo_name_html]
226 url_segments = [repo_name_html]
227 else:
227 else:
228 url_segments = [
228 url_segments = [
229 link_to(
229 link_to(
230 repo_name_html,
230 repo_name_html,
231 route_path(
231 route_path(
232 'repo_files',
232 'repo_files',
233 repo_name=repo_name,
233 repo_name=repo_name,
234 commit_id=commit_id,
234 commit_id=commit_id,
235 f_path=''),
235 f_path=''),
236 class_='pjax-link')]
236 class_='pjax-link')]
237
237
238 last_cnt = len(path_segments) - 1
238 last_cnt = len(path_segments) - 1
239 for cnt, segment in enumerate(path_segments):
239 for cnt, segment in enumerate(path_segments):
240 if not segment:
240 if not segment:
241 continue
241 continue
242 segment_html = escape(segment)
242 segment_html = escape(segment)
243
243
244 if cnt != last_cnt:
244 if cnt != last_cnt:
245 url_segments.append(
245 url_segments.append(
246 link_to(
246 link_to(
247 segment_html,
247 segment_html,
248 route_path(
248 route_path(
249 'repo_files',
249 'repo_files',
250 repo_name=repo_name,
250 repo_name=repo_name,
251 commit_id=commit_id,
251 commit_id=commit_id,
252 f_path='/'.join(path_segments[:cnt + 1])),
252 f_path='/'.join(path_segments[:cnt + 1])),
253 class_='pjax-link'))
253 class_='pjax-link'))
254 else:
254 else:
255 url_segments.append(segment_html)
255 url_segments.append(segment_html)
256
256
257 return literal('/'.join(url_segments))
257 return literal('/'.join(url_segments))
258
258
259
259
260 class CodeHtmlFormatter(HtmlFormatter):
260 class CodeHtmlFormatter(HtmlFormatter):
261 """
261 """
262 My code Html Formatter for source codes
262 My code Html Formatter for source codes
263 """
263 """
264
264
265 def wrap(self, source, outfile):
265 def wrap(self, source, outfile):
266 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
266 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
267
267
268 def _wrap_code(self, source):
268 def _wrap_code(self, source):
269 for cnt, it in enumerate(source):
269 for cnt, it in enumerate(source):
270 i, t = it
270 i, t = it
271 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
271 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
272 yield i, t
272 yield i, t
273
273
274 def _wrap_tablelinenos(self, inner):
274 def _wrap_tablelinenos(self, inner):
275 dummyoutfile = StringIO.StringIO()
275 dummyoutfile = StringIO.StringIO()
276 lncount = 0
276 lncount = 0
277 for t, line in inner:
277 for t, line in inner:
278 if t:
278 if t:
279 lncount += 1
279 lncount += 1
280 dummyoutfile.write(line)
280 dummyoutfile.write(line)
281
281
282 fl = self.linenostart
282 fl = self.linenostart
283 mw = len(str(lncount + fl - 1))
283 mw = len(str(lncount + fl - 1))
284 sp = self.linenospecial
284 sp = self.linenospecial
285 st = self.linenostep
285 st = self.linenostep
286 la = self.lineanchors
286 la = self.lineanchors
287 aln = self.anchorlinenos
287 aln = self.anchorlinenos
288 nocls = self.noclasses
288 nocls = self.noclasses
289 if sp:
289 if sp:
290 lines = []
290 lines = []
291
291
292 for i in range(fl, fl + lncount):
292 for i in range(fl, fl + lncount):
293 if i % st == 0:
293 if i % st == 0:
294 if i % sp == 0:
294 if i % sp == 0:
295 if aln:
295 if aln:
296 lines.append('<a href="#%s%d" class="special">%*d</a>' %
296 lines.append('<a href="#%s%d" class="special">%*d</a>' %
297 (la, i, mw, i))
297 (la, i, mw, i))
298 else:
298 else:
299 lines.append('<span class="special">%*d</span>' % (mw, i))
299 lines.append('<span class="special">%*d</span>' % (mw, i))
300 else:
300 else:
301 if aln:
301 if aln:
302 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
302 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
303 else:
303 else:
304 lines.append('%*d' % (mw, i))
304 lines.append('%*d' % (mw, i))
305 else:
305 else:
306 lines.append('')
306 lines.append('')
307 ls = '\n'.join(lines)
307 ls = '\n'.join(lines)
308 else:
308 else:
309 lines = []
309 lines = []
310 for i in range(fl, fl + lncount):
310 for i in range(fl, fl + lncount):
311 if i % st == 0:
311 if i % st == 0:
312 if aln:
312 if aln:
313 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
313 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
314 else:
314 else:
315 lines.append('%*d' % (mw, i))
315 lines.append('%*d' % (mw, i))
316 else:
316 else:
317 lines.append('')
317 lines.append('')
318 ls = '\n'.join(lines)
318 ls = '\n'.join(lines)
319
319
320 # in case you wonder about the seemingly redundant <div> here: since the
320 # in case you wonder about the seemingly redundant <div> here: since the
321 # content in the other cell also is wrapped in a div, some browsers in
321 # content in the other cell also is wrapped in a div, some browsers in
322 # some configurations seem to mess up the formatting...
322 # some configurations seem to mess up the formatting...
323 if nocls:
323 if nocls:
324 yield 0, ('<table class="%stable">' % self.cssclass +
324 yield 0, ('<table class="%stable">' % self.cssclass +
325 '<tr><td><div class="linenodiv" '
325 '<tr><td><div class="linenodiv" '
326 'style="background-color: #f0f0f0; padding-right: 10px">'
326 'style="background-color: #f0f0f0; padding-right: 10px">'
327 '<pre style="line-height: 125%">' +
327 '<pre style="line-height: 125%">' +
328 ls + '</pre></div></td><td id="hlcode" class="code">')
328 ls + '</pre></div></td><td id="hlcode" class="code">')
329 else:
329 else:
330 yield 0, ('<table class="%stable">' % self.cssclass +
330 yield 0, ('<table class="%stable">' % self.cssclass +
331 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
331 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
332 ls + '</pre></div></td><td id="hlcode" class="code">')
332 ls + '</pre></div></td><td id="hlcode" class="code">')
333 yield 0, dummyoutfile.getvalue()
333 yield 0, dummyoutfile.getvalue()
334 yield 0, '</td></tr></table>'
334 yield 0, '</td></tr></table>'
335
335
336
336
337 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
337 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
338 def __init__(self, **kw):
338 def __init__(self, **kw):
339 # only show these line numbers if set
339 # only show these line numbers if set
340 self.only_lines = kw.pop('only_line_numbers', [])
340 self.only_lines = kw.pop('only_line_numbers', [])
341 self.query_terms = kw.pop('query_terms', [])
341 self.query_terms = kw.pop('query_terms', [])
342 self.max_lines = kw.pop('max_lines', 5)
342 self.max_lines = kw.pop('max_lines', 5)
343 self.line_context = kw.pop('line_context', 3)
343 self.line_context = kw.pop('line_context', 3)
344 self.url = kw.pop('url', None)
344 self.url = kw.pop('url', None)
345
345
346 super(CodeHtmlFormatter, self).__init__(**kw)
346 super(CodeHtmlFormatter, self).__init__(**kw)
347
347
348 def _wrap_code(self, source):
348 def _wrap_code(self, source):
349 for cnt, it in enumerate(source):
349 for cnt, it in enumerate(source):
350 i, t = it
350 i, t = it
351 t = '<pre>%s</pre>' % t
351 t = '<pre>%s</pre>' % t
352 yield i, t
352 yield i, t
353
353
354 def _wrap_tablelinenos(self, inner):
354 def _wrap_tablelinenos(self, inner):
355 yield 0, '<table class="code-highlight %stable">' % self.cssclass
355 yield 0, '<table class="code-highlight %stable">' % self.cssclass
356
356
357 last_shown_line_number = 0
357 last_shown_line_number = 0
358 current_line_number = 1
358 current_line_number = 1
359
359
360 for t, line in inner:
360 for t, line in inner:
361 if not t:
361 if not t:
362 yield t, line
362 yield t, line
363 continue
363 continue
364
364
365 if current_line_number in self.only_lines:
365 if current_line_number in self.only_lines:
366 if last_shown_line_number + 1 != current_line_number:
366 if last_shown_line_number + 1 != current_line_number:
367 yield 0, '<tr>'
367 yield 0, '<tr>'
368 yield 0, '<td class="line">...</td>'
368 yield 0, '<td class="line">...</td>'
369 yield 0, '<td id="hlcode" class="code"></td>'
369 yield 0, '<td id="hlcode" class="code"></td>'
370 yield 0, '</tr>'
370 yield 0, '</tr>'
371
371
372 yield 0, '<tr>'
372 yield 0, '<tr>'
373 if self.url:
373 if self.url:
374 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
374 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
375 self.url, current_line_number, current_line_number)
375 self.url, current_line_number, current_line_number)
376 else:
376 else:
377 yield 0, '<td class="line"><a href="">%i</a></td>' % (
377 yield 0, '<td class="line"><a href="">%i</a></td>' % (
378 current_line_number)
378 current_line_number)
379 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
379 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
380 yield 0, '</tr>'
380 yield 0, '</tr>'
381
381
382 last_shown_line_number = current_line_number
382 last_shown_line_number = current_line_number
383
383
384 current_line_number += 1
384 current_line_number += 1
385
385
386
386
387 yield 0, '</table>'
387 yield 0, '</table>'
388
388
389
389
390 def extract_phrases(text_query):
390 def extract_phrases(text_query):
391 """
391 """
392 Extracts phrases from search term string making sure phrases
392 Extracts phrases from search term string making sure phrases
393 contained in double quotes are kept together - and discarding empty values
393 contained in double quotes are kept together - and discarding empty values
394 or fully whitespace values eg.
394 or fully whitespace values eg.
395
395
396 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
396 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
397
397
398 """
398 """
399
399
400 in_phrase = False
400 in_phrase = False
401 buf = ''
401 buf = ''
402 phrases = []
402 phrases = []
403 for char in text_query:
403 for char in text_query:
404 if in_phrase:
404 if in_phrase:
405 if char == '"': # end phrase
405 if char == '"': # end phrase
406 phrases.append(buf)
406 phrases.append(buf)
407 buf = ''
407 buf = ''
408 in_phrase = False
408 in_phrase = False
409 continue
409 continue
410 else:
410 else:
411 buf += char
411 buf += char
412 continue
412 continue
413 else:
413 else:
414 if char == '"': # start phrase
414 if char == '"': # start phrase
415 in_phrase = True
415 in_phrase = True
416 phrases.append(buf)
416 phrases.append(buf)
417 buf = ''
417 buf = ''
418 continue
418 continue
419 elif char == ' ':
419 elif char == ' ':
420 phrases.append(buf)
420 phrases.append(buf)
421 buf = ''
421 buf = ''
422 continue
422 continue
423 else:
423 else:
424 buf += char
424 buf += char
425
425
426 phrases.append(buf)
426 phrases.append(buf)
427 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
427 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
428 return phrases
428 return phrases
429
429
430
430
431 def get_matching_offsets(text, phrases):
431 def get_matching_offsets(text, phrases):
432 """
432 """
433 Returns a list of string offsets in `text` that the list of `terms` match
433 Returns a list of string offsets in `text` that the list of `terms` match
434
434
435 >>> get_matching_offsets('some text here', ['some', 'here'])
435 >>> get_matching_offsets('some text here', ['some', 'here'])
436 [(0, 4), (10, 14)]
436 [(0, 4), (10, 14)]
437
437
438 """
438 """
439 offsets = []
439 offsets = []
440 for phrase in phrases:
440 for phrase in phrases:
441 for match in re.finditer(phrase, text):
441 for match in re.finditer(phrase, text):
442 offsets.append((match.start(), match.end()))
442 offsets.append((match.start(), match.end()))
443
443
444 return offsets
444 return offsets
445
445
446
446
447 def normalize_text_for_matching(x):
447 def normalize_text_for_matching(x):
448 """
448 """
449 Replaces all non alnum characters to spaces and lower cases the string,
449 Replaces all non alnum characters to spaces and lower cases the string,
450 useful for comparing two text strings without punctuation
450 useful for comparing two text strings without punctuation
451 """
451 """
452 return re.sub(r'[^\w]', ' ', x.lower())
452 return re.sub(r'[^\w]', ' ', x.lower())
453
453
454
454
455 def get_matching_line_offsets(lines, terms):
455 def get_matching_line_offsets(lines, terms):
456 """ Return a set of `lines` indices (starting from 1) matching a
456 """ Return a set of `lines` indices (starting from 1) matching a
457 text search query, along with `context` lines above/below matching lines
457 text search query, along with `context` lines above/below matching lines
458
458
459 :param lines: list of strings representing lines
459 :param lines: list of strings representing lines
460 :param terms: search term string to match in lines eg. 'some text'
460 :param terms: search term string to match in lines eg. 'some text'
461 :param context: number of lines above/below a matching line to add to result
461 :param context: number of lines above/below a matching line to add to result
462 :param max_lines: cut off for lines of interest
462 :param max_lines: cut off for lines of interest
463 eg.
463 eg.
464
464
465 text = '''
465 text = '''
466 words words words
466 words words words
467 words words words
467 words words words
468 some text some
468 some text some
469 words words words
469 words words words
470 words words words
470 words words words
471 text here what
471 text here what
472 '''
472 '''
473 get_matching_line_offsets(text, 'text', context=1)
473 get_matching_line_offsets(text, 'text', context=1)
474 {3: [(5, 9)], 6: [(0, 4)]]
474 {3: [(5, 9)], 6: [(0, 4)]]
475
475
476 """
476 """
477 matching_lines = {}
477 matching_lines = {}
478 phrases = [normalize_text_for_matching(phrase)
478 phrases = [normalize_text_for_matching(phrase)
479 for phrase in extract_phrases(terms)]
479 for phrase in extract_phrases(terms)]
480
480
481 for line_index, line in enumerate(lines, start=1):
481 for line_index, line in enumerate(lines, start=1):
482 match_offsets = get_matching_offsets(
482 match_offsets = get_matching_offsets(
483 normalize_text_for_matching(line), phrases)
483 normalize_text_for_matching(line), phrases)
484 if match_offsets:
484 if match_offsets:
485 matching_lines[line_index] = match_offsets
485 matching_lines[line_index] = match_offsets
486
486
487 return matching_lines
487 return matching_lines
488
488
489
489
490 def hsv_to_rgb(h, s, v):
490 def hsv_to_rgb(h, s, v):
491 """ Convert hsv color values to rgb """
491 """ Convert hsv color values to rgb """
492
492
493 if s == 0.0:
493 if s == 0.0:
494 return v, v, v
494 return v, v, v
495 i = int(h * 6.0) # XXX assume int() truncates!
495 i = int(h * 6.0) # XXX assume int() truncates!
496 f = (h * 6.0) - i
496 f = (h * 6.0) - i
497 p = v * (1.0 - s)
497 p = v * (1.0 - s)
498 q = v * (1.0 - s * f)
498 q = v * (1.0 - s * f)
499 t = v * (1.0 - s * (1.0 - f))
499 t = v * (1.0 - s * (1.0 - f))
500 i = i % 6
500 i = i % 6
501 if i == 0:
501 if i == 0:
502 return v, t, p
502 return v, t, p
503 if i == 1:
503 if i == 1:
504 return q, v, p
504 return q, v, p
505 if i == 2:
505 if i == 2:
506 return p, v, t
506 return p, v, t
507 if i == 3:
507 if i == 3:
508 return p, q, v
508 return p, q, v
509 if i == 4:
509 if i == 4:
510 return t, p, v
510 return t, p, v
511 if i == 5:
511 if i == 5:
512 return v, p, q
512 return v, p, q
513
513
514
514
515 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
515 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
516 """
516 """
517 Generator for getting n of evenly distributed colors using
517 Generator for getting n of evenly distributed colors using
518 hsv color and golden ratio. It always return same order of colors
518 hsv color and golden ratio. It always return same order of colors
519
519
520 :param n: number of colors to generate
520 :param n: number of colors to generate
521 :param saturation: saturation of returned colors
521 :param saturation: saturation of returned colors
522 :param lightness: lightness of returned colors
522 :param lightness: lightness of returned colors
523 :returns: RGB tuple
523 :returns: RGB tuple
524 """
524 """
525
525
526 golden_ratio = 0.618033988749895
526 golden_ratio = 0.618033988749895
527 h = 0.22717784590367374
527 h = 0.22717784590367374
528
528
529 for _ in xrange(n):
529 for _ in xrange(n):
530 h += golden_ratio
530 h += golden_ratio
531 h %= 1
531 h %= 1
532 HSV_tuple = [h, saturation, lightness]
532 HSV_tuple = [h, saturation, lightness]
533 RGB_tuple = hsv_to_rgb(*HSV_tuple)
533 RGB_tuple = hsv_to_rgb(*HSV_tuple)
534 yield map(lambda x: str(int(x * 256)), RGB_tuple)
534 yield map(lambda x: str(int(x * 256)), RGB_tuple)
535
535
536
536
537 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
537 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
538 """
538 """
539 Returns a function which when called with an argument returns a unique
539 Returns a function which when called with an argument returns a unique
540 color for that argument, eg.
540 color for that argument, eg.
541
541
542 :param n: number of colors to generate
542 :param n: number of colors to generate
543 :param saturation: saturation of returned colors
543 :param saturation: saturation of returned colors
544 :param lightness: lightness of returned colors
544 :param lightness: lightness of returned colors
545 :returns: css RGB string
545 :returns: css RGB string
546
546
547 >>> color_hash = color_hasher()
547 >>> color_hash = color_hasher()
548 >>> color_hash('hello')
548 >>> color_hash('hello')
549 'rgb(34, 12, 59)'
549 'rgb(34, 12, 59)'
550 >>> color_hash('hello')
550 >>> color_hash('hello')
551 'rgb(34, 12, 59)'
551 'rgb(34, 12, 59)'
552 >>> color_hash('other')
552 >>> color_hash('other')
553 'rgb(90, 224, 159)'
553 'rgb(90, 224, 159)'
554 """
554 """
555
555
556 color_dict = {}
556 color_dict = {}
557 cgenerator = unique_color_generator(
557 cgenerator = unique_color_generator(
558 saturation=saturation, lightness=lightness)
558 saturation=saturation, lightness=lightness)
559
559
560 def get_color_string(thing):
560 def get_color_string(thing):
561 if thing in color_dict:
561 if thing in color_dict:
562 col = color_dict[thing]
562 col = color_dict[thing]
563 else:
563 else:
564 col = color_dict[thing] = cgenerator.next()
564 col = color_dict[thing] = cgenerator.next()
565 return "rgb(%s)" % (', '.join(col))
565 return "rgb(%s)" % (', '.join(col))
566
566
567 return get_color_string
567 return get_color_string
568
568
569
569
570 def get_lexer_safe(mimetype=None, filepath=None):
570 def get_lexer_safe(mimetype=None, filepath=None):
571 """
571 """
572 Tries to return a relevant pygments lexer using mimetype/filepath name,
572 Tries to return a relevant pygments lexer using mimetype/filepath name,
573 defaulting to plain text if none could be found
573 defaulting to plain text if none could be found
574 """
574 """
575 lexer = None
575 lexer = None
576 try:
576 try:
577 if mimetype:
577 if mimetype:
578 lexer = get_lexer_for_mimetype(mimetype)
578 lexer = get_lexer_for_mimetype(mimetype)
579 if not lexer:
579 if not lexer:
580 lexer = get_lexer_for_filename(filepath)
580 lexer = get_lexer_for_filename(filepath)
581 except pygments.util.ClassNotFound:
581 except pygments.util.ClassNotFound:
582 pass
582 pass
583
583
584 if not lexer:
584 if not lexer:
585 lexer = get_lexer_by_name('text')
585 lexer = get_lexer_by_name('text')
586
586
587 return lexer
587 return lexer
588
588
589
589
590 def get_lexer_for_filenode(filenode):
590 def get_lexer_for_filenode(filenode):
591 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
591 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
592 return lexer
592 return lexer
593
593
594
594
595 def pygmentize(filenode, **kwargs):
595 def pygmentize(filenode, **kwargs):
596 """
596 """
597 pygmentize function using pygments
597 pygmentize function using pygments
598
598
599 :param filenode:
599 :param filenode:
600 """
600 """
601 lexer = get_lexer_for_filenode(filenode)
601 lexer = get_lexer_for_filenode(filenode)
602 return literal(code_highlight(filenode.content, lexer,
602 return literal(code_highlight(filenode.content, lexer,
603 CodeHtmlFormatter(**kwargs)))
603 CodeHtmlFormatter(**kwargs)))
604
604
605
605
606 def is_following_repo(repo_name, user_id):
606 def is_following_repo(repo_name, user_id):
607 from rhodecode.model.scm import ScmModel
607 from rhodecode.model.scm import ScmModel
608 return ScmModel().is_following_repo(repo_name, user_id)
608 return ScmModel().is_following_repo(repo_name, user_id)
609
609
610
610
611 class _Message(object):
611 class _Message(object):
612 """A message returned by ``Flash.pop_messages()``.
612 """A message returned by ``Flash.pop_messages()``.
613
613
614 Converting the message to a string returns the message text. Instances
614 Converting the message to a string returns the message text. Instances
615 also have the following attributes:
615 also have the following attributes:
616
616
617 * ``message``: the message text.
617 * ``message``: the message text.
618 * ``category``: the category specified when the message was created.
618 * ``category``: the category specified when the message was created.
619 """
619 """
620
620
621 def __init__(self, category, message):
621 def __init__(self, category, message):
622 self.category = category
622 self.category = category
623 self.message = message
623 self.message = message
624
624
625 def __str__(self):
625 def __str__(self):
626 return self.message
626 return self.message
627
627
628 __unicode__ = __str__
628 __unicode__ = __str__
629
629
630 def __html__(self):
630 def __html__(self):
631 return escape(safe_unicode(self.message))
631 return escape(safe_unicode(self.message))
632
632
633
633
634 class Flash(object):
634 class Flash(object):
635 # List of allowed categories. If None, allow any category.
635 # List of allowed categories. If None, allow any category.
636 categories = ["warning", "notice", "error", "success"]
636 categories = ["warning", "notice", "error", "success"]
637
637
638 # Default category if none is specified.
638 # Default category if none is specified.
639 default_category = "notice"
639 default_category = "notice"
640
640
641 def __init__(self, session_key="flash", categories=None,
641 def __init__(self, session_key="flash", categories=None,
642 default_category=None):
642 default_category=None):
643 """
643 """
644 Instantiate a ``Flash`` object.
644 Instantiate a ``Flash`` object.
645
645
646 ``session_key`` is the key to save the messages under in the user's
646 ``session_key`` is the key to save the messages under in the user's
647 session.
647 session.
648
648
649 ``categories`` is an optional list which overrides the default list
649 ``categories`` is an optional list which overrides the default list
650 of categories.
650 of categories.
651
651
652 ``default_category`` overrides the default category used for messages
652 ``default_category`` overrides the default category used for messages
653 when none is specified.
653 when none is specified.
654 """
654 """
655 self.session_key = session_key
655 self.session_key = session_key
656 if categories is not None:
656 if categories is not None:
657 self.categories = categories
657 self.categories = categories
658 if default_category is not None:
658 if default_category is not None:
659 self.default_category = default_category
659 self.default_category = default_category
660 if self.categories and self.default_category not in self.categories:
660 if self.categories and self.default_category not in self.categories:
661 raise ValueError(
661 raise ValueError(
662 "unrecognized default category %r" % (self.default_category,))
662 "unrecognized default category %r" % (self.default_category,))
663
663
664 def pop_messages(self, session=None, request=None):
664 def pop_messages(self, session=None, request=None):
665 """
665 """
666 Return all accumulated messages and delete them from the session.
666 Return all accumulated messages and delete them from the session.
667
667
668 The return value is a list of ``Message`` objects.
668 The return value is a list of ``Message`` objects.
669 """
669 """
670 messages = []
670 messages = []
671
671
672 if not session:
672 if not session:
673 if not request:
673 if not request:
674 request = get_current_request()
674 request = get_current_request()
675 session = request.session
675 session = request.session
676
676
677 # Pop the 'old' pylons flash messages. They are tuples of the form
677 # Pop the 'old' pylons flash messages. They are tuples of the form
678 # (category, message)
678 # (category, message)
679 for cat, msg in session.pop(self.session_key, []):
679 for cat, msg in session.pop(self.session_key, []):
680 messages.append(_Message(cat, msg))
680 messages.append(_Message(cat, msg))
681
681
682 # Pop the 'new' pyramid flash messages for each category as list
682 # Pop the 'new' pyramid flash messages for each category as list
683 # of strings.
683 # of strings.
684 for cat in self.categories:
684 for cat in self.categories:
685 for msg in session.pop_flash(queue=cat):
685 for msg in session.pop_flash(queue=cat):
686 messages.append(_Message(cat, msg))
686 messages.append(_Message(cat, msg))
687 # Map messages from the default queue to the 'notice' category.
687 # Map messages from the default queue to the 'notice' category.
688 for msg in session.pop_flash():
688 for msg in session.pop_flash():
689 messages.append(_Message('notice', msg))
689 messages.append(_Message('notice', msg))
690
690
691 session.save()
691 session.save()
692 return messages
692 return messages
693
693
694 def json_alerts(self, session=None, request=None):
694 def json_alerts(self, session=None, request=None):
695 payloads = []
695 payloads = []
696 messages = flash.pop_messages(session=session, request=request)
696 messages = flash.pop_messages(session=session, request=request)
697 if messages:
697 if messages:
698 for message in messages:
698 for message in messages:
699 subdata = {}
699 subdata = {}
700 if hasattr(message.message, 'rsplit'):
700 if hasattr(message.message, 'rsplit'):
701 flash_data = message.message.rsplit('|DELIM|', 1)
701 flash_data = message.message.rsplit('|DELIM|', 1)
702 org_message = flash_data[0]
702 org_message = flash_data[0]
703 if len(flash_data) > 1:
703 if len(flash_data) > 1:
704 subdata = json.loads(flash_data[1])
704 subdata = json.loads(flash_data[1])
705 else:
705 else:
706 org_message = message.message
706 org_message = message.message
707 payloads.append({
707 payloads.append({
708 'message': {
708 'message': {
709 'message': u'{}'.format(org_message),
709 'message': u'{}'.format(org_message),
710 'level': message.category,
710 'level': message.category,
711 'force': True,
711 'force': True,
712 'subdata': subdata
712 'subdata': subdata
713 }
713 }
714 })
714 })
715 return json.dumps(payloads)
715 return json.dumps(payloads)
716
716
717 def __call__(self, message, category=None, ignore_duplicate=False,
717 def __call__(self, message, category=None, ignore_duplicate=False,
718 session=None, request=None):
718 session=None, request=None):
719
719
720 if not session:
720 if not session:
721 if not request:
721 if not request:
722 request = get_current_request()
722 request = get_current_request()
723 session = request.session
723 session = request.session
724
724
725 session.flash(
725 session.flash(
726 message, queue=category, allow_duplicate=not ignore_duplicate)
726 message, queue=category, allow_duplicate=not ignore_duplicate)
727
727
728
728
729 flash = Flash()
729 flash = Flash()
730
730
731 #==============================================================================
731 #==============================================================================
732 # SCM FILTERS available via h.
732 # SCM FILTERS available via h.
733 #==============================================================================
733 #==============================================================================
734 from rhodecode.lib.vcs.utils import author_name, author_email
734 from rhodecode.lib.vcs.utils import author_name, author_email
735 from rhodecode.lib.utils2 import credentials_filter, age as _age
735 from rhodecode.lib.utils2 import credentials_filter, age as _age
736 from rhodecode.model.db import User, ChangesetStatus
736 from rhodecode.model.db import User, ChangesetStatus
737
737
738 age = _age
738 age = _age
739 capitalize = lambda x: x.capitalize()
739 capitalize = lambda x: x.capitalize()
740 email = author_email
740 email = author_email
741 short_id = lambda x: x[:12]
741 short_id = lambda x: x[:12]
742 hide_credentials = lambda x: ''.join(credentials_filter(x))
742 hide_credentials = lambda x: ''.join(credentials_filter(x))
743
743
744
744
745 def age_component(datetime_iso, value=None, time_is_local=False):
745 def age_component(datetime_iso, value=None, time_is_local=False):
746 title = value or format_date(datetime_iso)
746 title = value or format_date(datetime_iso)
747 tzinfo = '+00:00'
747 tzinfo = '+00:00'
748
748
749 # detect if we have a timezone info, otherwise, add it
749 # detect if we have a timezone info, otherwise, add it
750 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
750 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
751 if time_is_local:
751 if time_is_local:
752 tzinfo = time.strftime("+%H:%M",
752 tzinfo = time.strftime("+%H:%M",
753 time.gmtime(
753 time.gmtime(
754 (datetime.now() - datetime.utcnow()).seconds + 1
754 (datetime.now() - datetime.utcnow()).seconds + 1
755 )
755 )
756 )
756 )
757
757
758 return literal(
758 return literal(
759 '<time class="timeago tooltip" '
759 '<time class="timeago tooltip" '
760 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
760 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
761 datetime_iso, title, tzinfo))
761 datetime_iso, title, tzinfo))
762
762
763
763
764 def _shorten_commit_id(commit_id):
764 def _shorten_commit_id(commit_id):
765 from rhodecode import CONFIG
765 from rhodecode import CONFIG
766 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
766 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
767 return commit_id[:def_len]
767 return commit_id[:def_len]
768
768
769
769
770 def show_id(commit):
770 def show_id(commit):
771 """
771 """
772 Configurable function that shows ID
772 Configurable function that shows ID
773 by default it's r123:fffeeefffeee
773 by default it's r123:fffeeefffeee
774
774
775 :param commit: commit instance
775 :param commit: commit instance
776 """
776 """
777 from rhodecode import CONFIG
777 from rhodecode import CONFIG
778 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
778 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
779
779
780 raw_id = _shorten_commit_id(commit.raw_id)
780 raw_id = _shorten_commit_id(commit.raw_id)
781 if show_idx:
781 if show_idx:
782 return 'r%s:%s' % (commit.idx, raw_id)
782 return 'r%s:%s' % (commit.idx, raw_id)
783 else:
783 else:
784 return '%s' % (raw_id, )
784 return '%s' % (raw_id, )
785
785
786
786
787 def format_date(date):
787 def format_date(date):
788 """
788 """
789 use a standardized formatting for dates used in RhodeCode
789 use a standardized formatting for dates used in RhodeCode
790
790
791 :param date: date/datetime object
791 :param date: date/datetime object
792 :return: formatted date
792 :return: formatted date
793 """
793 """
794
794
795 if date:
795 if date:
796 _fmt = "%a, %d %b %Y %H:%M:%S"
796 _fmt = "%a, %d %b %Y %H:%M:%S"
797 return safe_unicode(date.strftime(_fmt))
797 return safe_unicode(date.strftime(_fmt))
798
798
799 return u""
799 return u""
800
800
801
801
802 class _RepoChecker(object):
802 class _RepoChecker(object):
803
803
804 def __init__(self, backend_alias):
804 def __init__(self, backend_alias):
805 self._backend_alias = backend_alias
805 self._backend_alias = backend_alias
806
806
807 def __call__(self, repository):
807 def __call__(self, repository):
808 if hasattr(repository, 'alias'):
808 if hasattr(repository, 'alias'):
809 _type = repository.alias
809 _type = repository.alias
810 elif hasattr(repository, 'repo_type'):
810 elif hasattr(repository, 'repo_type'):
811 _type = repository.repo_type
811 _type = repository.repo_type
812 else:
812 else:
813 _type = repository
813 _type = repository
814 return _type == self._backend_alias
814 return _type == self._backend_alias
815
815
816 is_git = _RepoChecker('git')
816 is_git = _RepoChecker('git')
817 is_hg = _RepoChecker('hg')
817 is_hg = _RepoChecker('hg')
818 is_svn = _RepoChecker('svn')
818 is_svn = _RepoChecker('svn')
819
819
820
820
821 def get_repo_type_by_name(repo_name):
821 def get_repo_type_by_name(repo_name):
822 repo = Repository.get_by_repo_name(repo_name)
822 repo = Repository.get_by_repo_name(repo_name)
823 return repo.repo_type
823 return repo.repo_type
824
824
825
825
826 def is_svn_without_proxy(repository):
826 def is_svn_without_proxy(repository):
827 if is_svn(repository):
827 if is_svn(repository):
828 from rhodecode.model.settings import VcsSettingsModel
828 from rhodecode.model.settings import VcsSettingsModel
829 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
829 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
830 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
830 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
831 return False
831 return False
832
832
833
833
834 def discover_user(author):
834 def discover_user(author):
835 """
835 """
836 Tries to discover RhodeCode User based on the autho string. Author string
836 Tries to discover RhodeCode User based on the autho string. Author string
837 is typically `FirstName LastName <email@address.com>`
837 is typically `FirstName LastName <email@address.com>`
838 """
838 """
839
839
840 # if author is already an instance use it for extraction
840 # if author is already an instance use it for extraction
841 if isinstance(author, User):
841 if isinstance(author, User):
842 return author
842 return author
843
843
844 # Valid email in the attribute passed, see if they're in the system
844 # Valid email in the attribute passed, see if they're in the system
845 _email = author_email(author)
845 _email = author_email(author)
846 if _email != '':
846 if _email != '':
847 user = User.get_by_email(_email, case_insensitive=True, cache=True)
847 user = User.get_by_email(_email, case_insensitive=True, cache=True)
848 if user is not None:
848 if user is not None:
849 return user
849 return user
850
850
851 # Maybe it's a username, we try to extract it and fetch by username ?
851 # Maybe it's a username, we try to extract it and fetch by username ?
852 _author = author_name(author)
852 _author = author_name(author)
853 user = User.get_by_username(_author, case_insensitive=True, cache=True)
853 user = User.get_by_username(_author, case_insensitive=True, cache=True)
854 if user is not None:
854 if user is not None:
855 return user
855 return user
856
856
857 return None
857 return None
858
858
859
859
860 def email_or_none(author):
860 def email_or_none(author):
861 # extract email from the commit string
861 # extract email from the commit string
862 _email = author_email(author)
862 _email = author_email(author)
863
863
864 # If we have an email, use it, otherwise
864 # If we have an email, use it, otherwise
865 # see if it contains a username we can get an email from
865 # see if it contains a username we can get an email from
866 if _email != '':
866 if _email != '':
867 return _email
867 return _email
868 else:
868 else:
869 user = User.get_by_username(
869 user = User.get_by_username(
870 author_name(author), case_insensitive=True, cache=True)
870 author_name(author), case_insensitive=True, cache=True)
871
871
872 if user is not None:
872 if user is not None:
873 return user.email
873 return user.email
874
874
875 # No valid email, not a valid user in the system, none!
875 # No valid email, not a valid user in the system, none!
876 return None
876 return None
877
877
878
878
879 def link_to_user(author, length=0, **kwargs):
879 def link_to_user(author, length=0, **kwargs):
880 user = discover_user(author)
880 user = discover_user(author)
881 # user can be None, but if we have it already it means we can re-use it
881 # user can be None, but if we have it already it means we can re-use it
882 # in the person() function, so we save 1 intensive-query
882 # in the person() function, so we save 1 intensive-query
883 if user:
883 if user:
884 author = user
884 author = user
885
885
886 display_person = person(author, 'username_or_name_or_email')
886 display_person = person(author, 'username_or_name_or_email')
887 if length:
887 if length:
888 display_person = shorter(display_person, length)
888 display_person = shorter(display_person, length)
889
889
890 if user:
890 if user:
891 return link_to(
891 return link_to(
892 escape(display_person),
892 escape(display_person),
893 route_path('user_profile', username=user.username),
893 route_path('user_profile', username=user.username),
894 **kwargs)
894 **kwargs)
895 else:
895 else:
896 return escape(display_person)
896 return escape(display_person)
897
897
898
898
899 def person(author, show_attr="username_and_name"):
899 def person(author, show_attr="username_and_name"):
900 user = discover_user(author)
900 user = discover_user(author)
901 if user:
901 if user:
902 return getattr(user, show_attr)
902 return getattr(user, show_attr)
903 else:
903 else:
904 _author = author_name(author)
904 _author = author_name(author)
905 _email = email(author)
905 _email = email(author)
906 return _author or _email
906 return _author or _email
907
907
908
908
909 def author_string(email):
909 def author_string(email):
910 if email:
910 if email:
911 user = User.get_by_email(email, case_insensitive=True, cache=True)
911 user = User.get_by_email(email, case_insensitive=True, cache=True)
912 if user:
912 if user:
913 if user.first_name or user.last_name:
913 if user.first_name or user.last_name:
914 return '%s %s &lt;%s&gt;' % (
914 return '%s %s &lt;%s&gt;' % (
915 user.first_name, user.last_name, email)
915 user.first_name, user.last_name, email)
916 else:
916 else:
917 return email
917 return email
918 else:
918 else:
919 return email
919 return email
920 else:
920 else:
921 return None
921 return None
922
922
923
923
924 def person_by_id(id_, show_attr="username_and_name"):
924 def person_by_id(id_, show_attr="username_and_name"):
925 # attr to return from fetched user
925 # attr to return from fetched user
926 person_getter = lambda usr: getattr(usr, show_attr)
926 person_getter = lambda usr: getattr(usr, show_attr)
927
927
928 #maybe it's an ID ?
928 #maybe it's an ID ?
929 if str(id_).isdigit() or isinstance(id_, int):
929 if str(id_).isdigit() or isinstance(id_, int):
930 id_ = int(id_)
930 id_ = int(id_)
931 user = User.get(id_)
931 user = User.get(id_)
932 if user is not None:
932 if user is not None:
933 return person_getter(user)
933 return person_getter(user)
934 return id_
934 return id_
935
935
936
936
937 def gravatar_with_user(request, author, show_disabled=False):
937 def gravatar_with_user(request, author, show_disabled=False):
938 _render = request.get_partial_renderer(
938 _render = request.get_partial_renderer(
939 'rhodecode:templates/base/base.mako')
939 'rhodecode:templates/base/base.mako')
940 return _render('gravatar_with_user', author, show_disabled=show_disabled)
940 return _render('gravatar_with_user', author, show_disabled=show_disabled)
941
941
942
942
943 tags_paterns = OrderedDict((
943 tags_paterns = OrderedDict((
944 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
944 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
945 '<div class="metatag" tag="lang">\\2</div>')),
945 '<div class="metatag" tag="lang">\\2</div>')),
946
946
947 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
947 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
948 '<div class="metatag" tag="see">see: \\1 </div>')),
948 '<div class="metatag" tag="see">see: \\1 </div>')),
949
949
950 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
950 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
951 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
951 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
952
952
953 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
953 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
954 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
954 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
955
955
956 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
956 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
957 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
957 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
958
958
959 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
959 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
960 '<div class="metatag" tag="state \\1">\\1</div>')),
960 '<div class="metatag" tag="state \\1">\\1</div>')),
961
961
962 # label in grey
962 # label in grey
963 ('label', (re.compile(r'\[([a-z]+)\]'),
963 ('label', (re.compile(r'\[([a-z]+)\]'),
964 '<div class="metatag" tag="label">\\1</div>')),
964 '<div class="metatag" tag="label">\\1</div>')),
965
965
966 # generic catch all in grey
966 # generic catch all in grey
967 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
967 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
968 '<div class="metatag" tag="generic">\\1</div>')),
968 '<div class="metatag" tag="generic">\\1</div>')),
969 ))
969 ))
970
970
971
971
972 def extract_metatags(value):
972 def extract_metatags(value):
973 """
973 """
974 Extract supported meta-tags from given text value
974 Extract supported meta-tags from given text value
975 """
975 """
976 tags = []
976 tags = []
977 if not value:
977 if not value:
978 return tags, ''
978 return tags, ''
979
979
980 for key, val in tags_paterns.items():
980 for key, val in tags_paterns.items():
981 pat, replace_html = val
981 pat, replace_html = val
982 tags.extend([(key, x.group()) for x in pat.finditer(value)])
982 tags.extend([(key, x.group()) for x in pat.finditer(value)])
983 value = pat.sub('', value)
983 value = pat.sub('', value)
984
984
985 return tags, value
985 return tags, value
986
986
987
987
988 def style_metatag(tag_type, value):
988 def style_metatag(tag_type, value):
989 """
989 """
990 converts tags from value into html equivalent
990 converts tags from value into html equivalent
991 """
991 """
992 if not value:
992 if not value:
993 return ''
993 return ''
994
994
995 html_value = value
995 html_value = value
996 tag_data = tags_paterns.get(tag_type)
996 tag_data = tags_paterns.get(tag_type)
997 if tag_data:
997 if tag_data:
998 pat, replace_html = tag_data
998 pat, replace_html = tag_data
999 # convert to plain `unicode` instead of a markup tag to be used in
999 # convert to plain `unicode` instead of a markup tag to be used in
1000 # regex expressions. safe_unicode doesn't work here
1000 # regex expressions. safe_unicode doesn't work here
1001 html_value = pat.sub(replace_html, unicode(value))
1001 html_value = pat.sub(replace_html, unicode(value))
1002
1002
1003 return html_value
1003 return html_value
1004
1004
1005
1005
1006 def bool2icon(value):
1006 def bool2icon(value):
1007 """
1007 """
1008 Returns boolean value of a given value, represented as html element with
1008 Returns boolean value of a given value, represented as html element with
1009 classes that will represent icons
1009 classes that will represent icons
1010
1010
1011 :param value: given value to convert to html node
1011 :param value: given value to convert to html node
1012 """
1012 """
1013
1013
1014 if value: # does bool conversion
1014 if value: # does bool conversion
1015 return HTML.tag('i', class_="icon-true")
1015 return HTML.tag('i', class_="icon-true")
1016 else: # not true as bool
1016 else: # not true as bool
1017 return HTML.tag('i', class_="icon-false")
1017 return HTML.tag('i', class_="icon-false")
1018
1018
1019
1019
1020 #==============================================================================
1020 #==============================================================================
1021 # PERMS
1021 # PERMS
1022 #==============================================================================
1022 #==============================================================================
1023 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1023 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1024 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1024 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1025 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1025 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1026 csrf_token_key
1026 csrf_token_key
1027
1027
1028
1028
1029 #==============================================================================
1029 #==============================================================================
1030 # GRAVATAR URL
1030 # GRAVATAR URL
1031 #==============================================================================
1031 #==============================================================================
1032 class InitialsGravatar(object):
1032 class InitialsGravatar(object):
1033 def __init__(self, email_address, first_name, last_name, size=30,
1033 def __init__(self, email_address, first_name, last_name, size=30,
1034 background=None, text_color='#fff'):
1034 background=None, text_color='#fff'):
1035 self.size = size
1035 self.size = size
1036 self.first_name = first_name
1036 self.first_name = first_name
1037 self.last_name = last_name
1037 self.last_name = last_name
1038 self.email_address = email_address
1038 self.email_address = email_address
1039 self.background = background or self.str2color(email_address)
1039 self.background = background or self.str2color(email_address)
1040 self.text_color = text_color
1040 self.text_color = text_color
1041
1041
1042 def get_color_bank(self):
1042 def get_color_bank(self):
1043 """
1043 """
1044 returns a predefined list of colors that gravatars can use.
1044 returns a predefined list of colors that gravatars can use.
1045 Those are randomized distinct colors that guarantee readability and
1045 Those are randomized distinct colors that guarantee readability and
1046 uniqueness.
1046 uniqueness.
1047
1047
1048 generated with: http://phrogz.net/css/distinct-colors.html
1048 generated with: http://phrogz.net/css/distinct-colors.html
1049 """
1049 """
1050 return [
1050 return [
1051 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1051 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1052 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1052 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1053 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1053 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1054 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1054 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1055 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1055 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1056 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1056 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1057 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1057 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1058 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1058 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1059 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1059 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1060 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1060 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1061 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1061 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1062 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1062 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1063 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1063 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1064 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1064 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1065 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1065 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1066 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1066 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1067 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1067 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1068 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1068 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1069 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1069 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1070 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1070 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1071 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1071 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1072 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1072 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1073 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1073 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1074 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1074 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1075 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1075 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1076 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1076 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1077 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1077 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1078 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1078 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1079 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1079 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1080 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1080 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1081 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1081 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1082 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1082 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1083 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1083 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1084 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1084 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1085 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1085 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1086 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1086 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1087 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1087 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1088 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1088 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1089 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1089 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1090 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1090 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1091 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1091 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1092 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1092 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1093 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1093 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1094 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1094 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1095 '#4f8c46', '#368dd9', '#5c0073'
1095 '#4f8c46', '#368dd9', '#5c0073'
1096 ]
1096 ]
1097
1097
1098 def rgb_to_hex_color(self, rgb_tuple):
1098 def rgb_to_hex_color(self, rgb_tuple):
1099 """
1099 """
1100 Converts an rgb_tuple passed to an hex color.
1100 Converts an rgb_tuple passed to an hex color.
1101
1101
1102 :param rgb_tuple: tuple with 3 ints represents rgb color space
1102 :param rgb_tuple: tuple with 3 ints represents rgb color space
1103 """
1103 """
1104 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1104 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1105
1105
1106 def email_to_int_list(self, email_str):
1106 def email_to_int_list(self, email_str):
1107 """
1107 """
1108 Get every byte of the hex digest value of email and turn it to integer.
1108 Get every byte of the hex digest value of email and turn it to integer.
1109 It's going to be always between 0-255
1109 It's going to be always between 0-255
1110 """
1110 """
1111 digest = md5_safe(email_str.lower())
1111 digest = md5_safe(email_str.lower())
1112 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1112 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1113
1113
1114 def pick_color_bank_index(self, email_str, color_bank):
1114 def pick_color_bank_index(self, email_str, color_bank):
1115 return self.email_to_int_list(email_str)[0] % len(color_bank)
1115 return self.email_to_int_list(email_str)[0] % len(color_bank)
1116
1116
1117 def str2color(self, email_str):
1117 def str2color(self, email_str):
1118 """
1118 """
1119 Tries to map in a stable algorithm an email to color
1119 Tries to map in a stable algorithm an email to color
1120
1120
1121 :param email_str:
1121 :param email_str:
1122 """
1122 """
1123 color_bank = self.get_color_bank()
1123 color_bank = self.get_color_bank()
1124 # pick position (module it's length so we always find it in the
1124 # pick position (module it's length so we always find it in the
1125 # bank even if it's smaller than 256 values
1125 # bank even if it's smaller than 256 values
1126 pos = self.pick_color_bank_index(email_str, color_bank)
1126 pos = self.pick_color_bank_index(email_str, color_bank)
1127 return color_bank[pos]
1127 return color_bank[pos]
1128
1128
1129 def normalize_email(self, email_address):
1129 def normalize_email(self, email_address):
1130 import unicodedata
1130 import unicodedata
1131 # default host used to fill in the fake/missing email
1131 # default host used to fill in the fake/missing email
1132 default_host = u'localhost'
1132 default_host = u'localhost'
1133
1133
1134 if not email_address:
1134 if not email_address:
1135 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1135 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1136
1136
1137 email_address = safe_unicode(email_address)
1137 email_address = safe_unicode(email_address)
1138
1138
1139 if u'@' not in email_address:
1139 if u'@' not in email_address:
1140 email_address = u'%s@%s' % (email_address, default_host)
1140 email_address = u'%s@%s' % (email_address, default_host)
1141
1141
1142 if email_address.endswith(u'@'):
1142 if email_address.endswith(u'@'):
1143 email_address = u'%s%s' % (email_address, default_host)
1143 email_address = u'%s%s' % (email_address, default_host)
1144
1144
1145 email_address = unicodedata.normalize('NFKD', email_address)\
1145 email_address = unicodedata.normalize('NFKD', email_address)\
1146 .encode('ascii', 'ignore')
1146 .encode('ascii', 'ignore')
1147 return email_address
1147 return email_address
1148
1148
1149 def get_initials(self):
1149 def get_initials(self):
1150 """
1150 """
1151 Returns 2 letter initials calculated based on the input.
1151 Returns 2 letter initials calculated based on the input.
1152 The algorithm picks first given email address, and takes first letter
1152 The algorithm picks first given email address, and takes first letter
1153 of part before @, and then the first letter of server name. In case
1153 of part before @, and then the first letter of server name. In case
1154 the part before @ is in a format of `somestring.somestring2` it replaces
1154 the part before @ is in a format of `somestring.somestring2` it replaces
1155 the server letter with first letter of somestring2
1155 the server letter with first letter of somestring2
1156
1156
1157 In case function was initialized with both first and lastname, this
1157 In case function was initialized with both first and lastname, this
1158 overrides the extraction from email by first letter of the first and
1158 overrides the extraction from email by first letter of the first and
1159 last name. We add special logic to that functionality, In case Full name
1159 last name. We add special logic to that functionality, In case Full name
1160 is compound, like Guido Von Rossum, we use last part of the last name
1160 is compound, like Guido Von Rossum, we use last part of the last name
1161 (Von Rossum) picking `R`.
1161 (Von Rossum) picking `R`.
1162
1162
1163 Function also normalizes the non-ascii characters to they ascii
1163 Function also normalizes the non-ascii characters to they ascii
1164 representation, eg Ä„ => A
1164 representation, eg Ä„ => A
1165 """
1165 """
1166 import unicodedata
1166 import unicodedata
1167 # replace non-ascii to ascii
1167 # replace non-ascii to ascii
1168 first_name = unicodedata.normalize(
1168 first_name = unicodedata.normalize(
1169 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1169 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1170 last_name = unicodedata.normalize(
1170 last_name = unicodedata.normalize(
1171 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1171 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1172
1172
1173 # do NFKD encoding, and also make sure email has proper format
1173 # do NFKD encoding, and also make sure email has proper format
1174 email_address = self.normalize_email(self.email_address)
1174 email_address = self.normalize_email(self.email_address)
1175
1175
1176 # first push the email initials
1176 # first push the email initials
1177 prefix, server = email_address.split('@', 1)
1177 prefix, server = email_address.split('@', 1)
1178
1178
1179 # check if prefix is maybe a 'first_name.last_name' syntax
1179 # check if prefix is maybe a 'first_name.last_name' syntax
1180 _dot_split = prefix.rsplit('.', 1)
1180 _dot_split = prefix.rsplit('.', 1)
1181 if len(_dot_split) == 2 and _dot_split[1]:
1181 if len(_dot_split) == 2 and _dot_split[1]:
1182 initials = [_dot_split[0][0], _dot_split[1][0]]
1182 initials = [_dot_split[0][0], _dot_split[1][0]]
1183 else:
1183 else:
1184 initials = [prefix[0], server[0]]
1184 initials = [prefix[0], server[0]]
1185
1185
1186 # then try to replace either first_name or last_name
1186 # then try to replace either first_name or last_name
1187 fn_letter = (first_name or " ")[0].strip()
1187 fn_letter = (first_name or " ")[0].strip()
1188 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1188 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1189
1189
1190 if fn_letter:
1190 if fn_letter:
1191 initials[0] = fn_letter
1191 initials[0] = fn_letter
1192
1192
1193 if ln_letter:
1193 if ln_letter:
1194 initials[1] = ln_letter
1194 initials[1] = ln_letter
1195
1195
1196 return ''.join(initials).upper()
1196 return ''.join(initials).upper()
1197
1197
1198 def get_img_data_by_type(self, font_family, img_type):
1198 def get_img_data_by_type(self, font_family, img_type):
1199 default_user = """
1199 default_user = """
1200 <svg xmlns="http://www.w3.org/2000/svg"
1200 <svg xmlns="http://www.w3.org/2000/svg"
1201 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1201 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1202 viewBox="-15 -10 439.165 429.164"
1202 viewBox="-15 -10 439.165 429.164"
1203
1203
1204 xml:space="preserve"
1204 xml:space="preserve"
1205 style="background:{background};" >
1205 style="background:{background};" >
1206
1206
1207 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1207 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1208 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1208 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1209 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1209 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1210 168.596,153.916,216.671,
1210 168.596,153.916,216.671,
1211 204.583,216.671z" fill="{text_color}"/>
1211 204.583,216.671z" fill="{text_color}"/>
1212 <path d="M407.164,374.717L360.88,
1212 <path d="M407.164,374.717L360.88,
1213 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1213 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1214 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1214 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1215 15.366-44.203,23.488-69.076,23.488c-24.877,
1215 15.366-44.203,23.488-69.076,23.488c-24.877,
1216 0-48.762-8.122-69.078-23.488
1216 0-48.762-8.122-69.078-23.488
1217 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1217 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1218 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1218 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1219 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1219 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1220 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1220 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1221 19.402-10.527 C409.699,390.129,
1221 19.402-10.527 C409.699,390.129,
1222 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1222 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1223 </svg>""".format(
1223 </svg>""".format(
1224 size=self.size,
1224 size=self.size,
1225 background='#979797', # @grey4
1225 background='#979797', # @grey4
1226 text_color=self.text_color,
1226 text_color=self.text_color,
1227 font_family=font_family)
1227 font_family=font_family)
1228
1228
1229 return {
1229 return {
1230 "default_user": default_user
1230 "default_user": default_user
1231 }[img_type]
1231 }[img_type]
1232
1232
1233 def get_img_data(self, svg_type=None):
1233 def get_img_data(self, svg_type=None):
1234 """
1234 """
1235 generates the svg metadata for image
1235 generates the svg metadata for image
1236 """
1236 """
1237
1237
1238 font_family = ','.join([
1238 font_family = ','.join([
1239 'proximanovaregular',
1239 'proximanovaregular',
1240 'Proxima Nova Regular',
1240 'Proxima Nova Regular',
1241 'Proxima Nova',
1241 'Proxima Nova',
1242 'Arial',
1242 'Arial',
1243 'Lucida Grande',
1243 'Lucida Grande',
1244 'sans-serif'
1244 'sans-serif'
1245 ])
1245 ])
1246 if svg_type:
1246 if svg_type:
1247 return self.get_img_data_by_type(font_family, svg_type)
1247 return self.get_img_data_by_type(font_family, svg_type)
1248
1248
1249 initials = self.get_initials()
1249 initials = self.get_initials()
1250 img_data = """
1250 img_data = """
1251 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1251 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1252 width="{size}" height="{size}"
1252 width="{size}" height="{size}"
1253 style="width: 100%; height: 100%; background-color: {background}"
1253 style="width: 100%; height: 100%; background-color: {background}"
1254 viewBox="0 0 {size} {size}">
1254 viewBox="0 0 {size} {size}">
1255 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1255 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1256 pointer-events="auto" fill="{text_color}"
1256 pointer-events="auto" fill="{text_color}"
1257 font-family="{font_family}"
1257 font-family="{font_family}"
1258 style="font-weight: 400; font-size: {f_size}px;">{text}
1258 style="font-weight: 400; font-size: {f_size}px;">{text}
1259 </text>
1259 </text>
1260 </svg>""".format(
1260 </svg>""".format(
1261 size=self.size,
1261 size=self.size,
1262 f_size=self.size/1.85, # scale the text inside the box nicely
1262 f_size=self.size/1.85, # scale the text inside the box nicely
1263 background=self.background,
1263 background=self.background,
1264 text_color=self.text_color,
1264 text_color=self.text_color,
1265 text=initials.upper(),
1265 text=initials.upper(),
1266 font_family=font_family)
1266 font_family=font_family)
1267
1267
1268 return img_data
1268 return img_data
1269
1269
1270 def generate_svg(self, svg_type=None):
1270 def generate_svg(self, svg_type=None):
1271 img_data = self.get_img_data(svg_type)
1271 img_data = self.get_img_data(svg_type)
1272 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1272 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1273
1273
1274
1274
1275 def initials_gravatar(email_address, first_name, last_name, size=30):
1275 def initials_gravatar(email_address, first_name, last_name, size=30):
1276 svg_type = None
1276 svg_type = None
1277 if email_address == User.DEFAULT_USER_EMAIL:
1277 if email_address == User.DEFAULT_USER_EMAIL:
1278 svg_type = 'default_user'
1278 svg_type = 'default_user'
1279 klass = InitialsGravatar(email_address, first_name, last_name, size)
1279 klass = InitialsGravatar(email_address, first_name, last_name, size)
1280 return klass.generate_svg(svg_type=svg_type)
1280 return klass.generate_svg(svg_type=svg_type)
1281
1281
1282
1282
1283 def gravatar_url(email_address, size=30, request=None):
1283 def gravatar_url(email_address, size=30, request=None):
1284 request = get_current_request()
1284 request = get_current_request()
1285 _use_gravatar = request.call_context.visual.use_gravatar
1285 _use_gravatar = request.call_context.visual.use_gravatar
1286 _gravatar_url = request.call_context.visual.gravatar_url
1286 _gravatar_url = request.call_context.visual.gravatar_url
1287
1287
1288 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1288 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1289
1289
1290 email_address = email_address or User.DEFAULT_USER_EMAIL
1290 email_address = email_address or User.DEFAULT_USER_EMAIL
1291 if isinstance(email_address, unicode):
1291 if isinstance(email_address, unicode):
1292 # hashlib crashes on unicode items
1292 # hashlib crashes on unicode items
1293 email_address = safe_str(email_address)
1293 email_address = safe_str(email_address)
1294
1294
1295 # empty email or default user
1295 # empty email or default user
1296 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1296 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1297 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1297 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1298
1298
1299 if _use_gravatar:
1299 if _use_gravatar:
1300 # TODO: Disuse pyramid thread locals. Think about another solution to
1300 # TODO: Disuse pyramid thread locals. Think about another solution to
1301 # get the host and schema here.
1301 # get the host and schema here.
1302 request = get_current_request()
1302 request = get_current_request()
1303 tmpl = safe_str(_gravatar_url)
1303 tmpl = safe_str(_gravatar_url)
1304 tmpl = tmpl.replace('{email}', email_address)\
1304 tmpl = tmpl.replace('{email}', email_address)\
1305 .replace('{md5email}', md5_safe(email_address.lower())) \
1305 .replace('{md5email}', md5_safe(email_address.lower())) \
1306 .replace('{netloc}', request.host)\
1306 .replace('{netloc}', request.host)\
1307 .replace('{scheme}', request.scheme)\
1307 .replace('{scheme}', request.scheme)\
1308 .replace('{size}', safe_str(size))
1308 .replace('{size}', safe_str(size))
1309 return tmpl
1309 return tmpl
1310 else:
1310 else:
1311 return initials_gravatar(email_address, '', '', size=size)
1311 return initials_gravatar(email_address, '', '', size=size)
1312
1312
1313
1313
1314 class Page(_Page):
1314 class Page(_Page):
1315 """
1315 """
1316 Custom pager to match rendering style with paginator
1316 Custom pager to match rendering style with paginator
1317 """
1317 """
1318
1318
1319 def _get_pos(self, cur_page, max_page, items):
1319 def _get_pos(self, cur_page, max_page, items):
1320 edge = (items / 2) + 1
1320 edge = (items / 2) + 1
1321 if (cur_page <= edge):
1321 if (cur_page <= edge):
1322 radius = max(items / 2, items - cur_page)
1322 radius = max(items / 2, items - cur_page)
1323 elif (max_page - cur_page) < edge:
1323 elif (max_page - cur_page) < edge:
1324 radius = (items - 1) - (max_page - cur_page)
1324 radius = (items - 1) - (max_page - cur_page)
1325 else:
1325 else:
1326 radius = items / 2
1326 radius = items / 2
1327
1327
1328 left = max(1, (cur_page - (radius)))
1328 left = max(1, (cur_page - (radius)))
1329 right = min(max_page, cur_page + (radius))
1329 right = min(max_page, cur_page + (radius))
1330 return left, cur_page, right
1330 return left, cur_page, right
1331
1331
1332 def _range(self, regexp_match):
1332 def _range(self, regexp_match):
1333 """
1333 """
1334 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1334 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1335
1335
1336 Arguments:
1336 Arguments:
1337
1337
1338 regexp_match
1338 regexp_match
1339 A "re" (regular expressions) match object containing the
1339 A "re" (regular expressions) match object containing the
1340 radius of linked pages around the current page in
1340 radius of linked pages around the current page in
1341 regexp_match.group(1) as a string
1341 regexp_match.group(1) as a string
1342
1342
1343 This function is supposed to be called as a callable in
1343 This function is supposed to be called as a callable in
1344 re.sub.
1344 re.sub.
1345
1345
1346 """
1346 """
1347 radius = int(regexp_match.group(1))
1347 radius = int(regexp_match.group(1))
1348
1348
1349 # Compute the first and last page number within the radius
1349 # Compute the first and last page number within the radius
1350 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1350 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1351 # -> leftmost_page = 5
1351 # -> leftmost_page = 5
1352 # -> rightmost_page = 9
1352 # -> rightmost_page = 9
1353 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1353 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1354 self.last_page,
1354 self.last_page,
1355 (radius * 2) + 1)
1355 (radius * 2) + 1)
1356 nav_items = []
1356 nav_items = []
1357
1357
1358 # Create a link to the first page (unless we are on the first page
1358 # Create a link to the first page (unless we are on the first page
1359 # or there would be no need to insert '..' spacers)
1359 # or there would be no need to insert '..' spacers)
1360 if self.page != self.first_page and self.first_page < leftmost_page:
1360 if self.page != self.first_page and self.first_page < leftmost_page:
1361 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1361 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1362
1362
1363 # Insert dots if there are pages between the first page
1363 # Insert dots if there are pages between the first page
1364 # and the currently displayed page range
1364 # and the currently displayed page range
1365 if leftmost_page - self.first_page > 1:
1365 if leftmost_page - self.first_page > 1:
1366 # Wrap in a SPAN tag if nolink_attr is set
1366 # Wrap in a SPAN tag if nolink_attr is set
1367 text = '..'
1367 text = '..'
1368 if self.dotdot_attr:
1368 if self.dotdot_attr:
1369 text = HTML.span(c=text, **self.dotdot_attr)
1369 text = HTML.span(c=text, **self.dotdot_attr)
1370 nav_items.append(text)
1370 nav_items.append(text)
1371
1371
1372 for thispage in xrange(leftmost_page, rightmost_page + 1):
1372 for thispage in xrange(leftmost_page, rightmost_page + 1):
1373 # Hilight the current page number and do not use a link
1373 # Hilight the current page number and do not use a link
1374 if thispage == self.page:
1374 if thispage == self.page:
1375 text = '%s' % (thispage,)
1375 text = '%s' % (thispage,)
1376 # Wrap in a SPAN tag if nolink_attr is set
1376 # Wrap in a SPAN tag if nolink_attr is set
1377 if self.curpage_attr:
1377 if self.curpage_attr:
1378 text = HTML.span(c=text, **self.curpage_attr)
1378 text = HTML.span(c=text, **self.curpage_attr)
1379 nav_items.append(text)
1379 nav_items.append(text)
1380 # Otherwise create just a link to that page
1380 # Otherwise create just a link to that page
1381 else:
1381 else:
1382 text = '%s' % (thispage,)
1382 text = '%s' % (thispage,)
1383 nav_items.append(self._pagerlink(thispage, text))
1383 nav_items.append(self._pagerlink(thispage, text))
1384
1384
1385 # Insert dots if there are pages between the displayed
1385 # Insert dots if there are pages between the displayed
1386 # page numbers and the end of the page range
1386 # page numbers and the end of the page range
1387 if self.last_page - rightmost_page > 1:
1387 if self.last_page - rightmost_page > 1:
1388 text = '..'
1388 text = '..'
1389 # Wrap in a SPAN tag if nolink_attr is set
1389 # Wrap in a SPAN tag if nolink_attr is set
1390 if self.dotdot_attr:
1390 if self.dotdot_attr:
1391 text = HTML.span(c=text, **self.dotdot_attr)
1391 text = HTML.span(c=text, **self.dotdot_attr)
1392 nav_items.append(text)
1392 nav_items.append(text)
1393
1393
1394 # Create a link to the very last page (unless we are on the last
1394 # Create a link to the very last page (unless we are on the last
1395 # page or there would be no need to insert '..' spacers)
1395 # page or there would be no need to insert '..' spacers)
1396 if self.page != self.last_page and rightmost_page < self.last_page:
1396 if self.page != self.last_page and rightmost_page < self.last_page:
1397 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1397 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1398
1398
1399 ## prerender links
1399 ## prerender links
1400 #_page_link = url.current()
1400 #_page_link = url.current()
1401 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1401 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1402 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1402 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1403 return self.separator.join(nav_items)
1403 return self.separator.join(nav_items)
1404
1404
1405 def pager(self, format='~2~', page_param='page', partial_param='partial',
1405 def pager(self, format='~2~', page_param='page', partial_param='partial',
1406 show_if_single_page=False, separator=' ', onclick=None,
1406 show_if_single_page=False, separator=' ', onclick=None,
1407 symbol_first='<<', symbol_last='>>',
1407 symbol_first='<<', symbol_last='>>',
1408 symbol_previous='<', symbol_next='>',
1408 symbol_previous='<', symbol_next='>',
1409 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1409 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1410 curpage_attr={'class': 'pager_curpage'},
1410 curpage_attr={'class': 'pager_curpage'},
1411 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1411 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1412
1412
1413 self.curpage_attr = curpage_attr
1413 self.curpage_attr = curpage_attr
1414 self.separator = separator
1414 self.separator = separator
1415 self.pager_kwargs = kwargs
1415 self.pager_kwargs = kwargs
1416 self.page_param = page_param
1416 self.page_param = page_param
1417 self.partial_param = partial_param
1417 self.partial_param = partial_param
1418 self.onclick = onclick
1418 self.onclick = onclick
1419 self.link_attr = link_attr
1419 self.link_attr = link_attr
1420 self.dotdot_attr = dotdot_attr
1420 self.dotdot_attr = dotdot_attr
1421
1421
1422 # Don't show navigator if there is no more than one page
1422 # Don't show navigator if there is no more than one page
1423 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1423 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1424 return ''
1424 return ''
1425
1425
1426 from string import Template
1426 from string import Template
1427 # Replace ~...~ in token format by range of pages
1427 # Replace ~...~ in token format by range of pages
1428 result = re.sub(r'~(\d+)~', self._range, format)
1428 result = re.sub(r'~(\d+)~', self._range, format)
1429
1429
1430 # Interpolate '%' variables
1430 # Interpolate '%' variables
1431 result = Template(result).safe_substitute({
1431 result = Template(result).safe_substitute({
1432 'first_page': self.first_page,
1432 'first_page': self.first_page,
1433 'last_page': self.last_page,
1433 'last_page': self.last_page,
1434 'page': self.page,
1434 'page': self.page,
1435 'page_count': self.page_count,
1435 'page_count': self.page_count,
1436 'items_per_page': self.items_per_page,
1436 'items_per_page': self.items_per_page,
1437 'first_item': self.first_item,
1437 'first_item': self.first_item,
1438 'last_item': self.last_item,
1438 'last_item': self.last_item,
1439 'item_count': self.item_count,
1439 'item_count': self.item_count,
1440 'link_first': self.page > self.first_page and \
1440 'link_first': self.page > self.first_page and \
1441 self._pagerlink(self.first_page, symbol_first) or '',
1441 self._pagerlink(self.first_page, symbol_first) or '',
1442 'link_last': self.page < self.last_page and \
1442 'link_last': self.page < self.last_page and \
1443 self._pagerlink(self.last_page, symbol_last) or '',
1443 self._pagerlink(self.last_page, symbol_last) or '',
1444 'link_previous': self.previous_page and \
1444 'link_previous': self.previous_page and \
1445 self._pagerlink(self.previous_page, symbol_previous) \
1445 self._pagerlink(self.previous_page, symbol_previous) \
1446 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1446 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1447 'link_next': self.next_page and \
1447 'link_next': self.next_page and \
1448 self._pagerlink(self.next_page, symbol_next) \
1448 self._pagerlink(self.next_page, symbol_next) \
1449 or HTML.span(symbol_next, class_="pg-next disabled")
1449 or HTML.span(symbol_next, class_="pg-next disabled")
1450 })
1450 })
1451
1451
1452 return literal(result)
1452 return literal(result)
1453
1453
1454
1454
1455 #==============================================================================
1455 #==============================================================================
1456 # REPO PAGER, PAGER FOR REPOSITORY
1456 # REPO PAGER, PAGER FOR REPOSITORY
1457 #==============================================================================
1457 #==============================================================================
1458 class RepoPage(Page):
1458 class RepoPage(Page):
1459
1459
1460 def __init__(self, collection, page=1, items_per_page=20,
1460 def __init__(self, collection, page=1, items_per_page=20,
1461 item_count=None, url=None, **kwargs):
1461 item_count=None, url=None, **kwargs):
1462
1462
1463 """Create a "RepoPage" instance. special pager for paging
1463 """Create a "RepoPage" instance. special pager for paging
1464 repository
1464 repository
1465 """
1465 """
1466 self._url_generator = url
1466 self._url_generator = url
1467
1467
1468 # Safe the kwargs class-wide so they can be used in the pager() method
1468 # Safe the kwargs class-wide so they can be used in the pager() method
1469 self.kwargs = kwargs
1469 self.kwargs = kwargs
1470
1470
1471 # Save a reference to the collection
1471 # Save a reference to the collection
1472 self.original_collection = collection
1472 self.original_collection = collection
1473
1473
1474 self.collection = collection
1474 self.collection = collection
1475
1475
1476 # The self.page is the number of the current page.
1476 # The self.page is the number of the current page.
1477 # The first page has the number 1!
1477 # The first page has the number 1!
1478 try:
1478 try:
1479 self.page = int(page) # make it int() if we get it as a string
1479 self.page = int(page) # make it int() if we get it as a string
1480 except (ValueError, TypeError):
1480 except (ValueError, TypeError):
1481 self.page = 1
1481 self.page = 1
1482
1482
1483 self.items_per_page = items_per_page
1483 self.items_per_page = items_per_page
1484
1484
1485 # Unless the user tells us how many items the collections has
1485 # Unless the user tells us how many items the collections has
1486 # we calculate that ourselves.
1486 # we calculate that ourselves.
1487 if item_count is not None:
1487 if item_count is not None:
1488 self.item_count = item_count
1488 self.item_count = item_count
1489 else:
1489 else:
1490 self.item_count = len(self.collection)
1490 self.item_count = len(self.collection)
1491
1491
1492 # Compute the number of the first and last available page
1492 # Compute the number of the first and last available page
1493 if self.item_count > 0:
1493 if self.item_count > 0:
1494 self.first_page = 1
1494 self.first_page = 1
1495 self.page_count = int(math.ceil(float(self.item_count) /
1495 self.page_count = int(math.ceil(float(self.item_count) /
1496 self.items_per_page))
1496 self.items_per_page))
1497 self.last_page = self.first_page + self.page_count - 1
1497 self.last_page = self.first_page + self.page_count - 1
1498
1498
1499 # Make sure that the requested page number is the range of
1499 # Make sure that the requested page number is the range of
1500 # valid pages
1500 # valid pages
1501 if self.page > self.last_page:
1501 if self.page > self.last_page:
1502 self.page = self.last_page
1502 self.page = self.last_page
1503 elif self.page < self.first_page:
1503 elif self.page < self.first_page:
1504 self.page = self.first_page
1504 self.page = self.first_page
1505
1505
1506 # Note: the number of items on this page can be less than
1506 # Note: the number of items on this page can be less than
1507 # items_per_page if the last page is not full
1507 # items_per_page if the last page is not full
1508 self.first_item = max(0, (self.item_count) - (self.page *
1508 self.first_item = max(0, (self.item_count) - (self.page *
1509 items_per_page))
1509 items_per_page))
1510 self.last_item = ((self.item_count - 1) - items_per_page *
1510 self.last_item = ((self.item_count - 1) - items_per_page *
1511 (self.page - 1))
1511 (self.page - 1))
1512
1512
1513 self.items = list(self.collection[self.first_item:self.last_item + 1])
1513 self.items = list(self.collection[self.first_item:self.last_item + 1])
1514
1514
1515 # Links to previous and next page
1515 # Links to previous and next page
1516 if self.page > self.first_page:
1516 if self.page > self.first_page:
1517 self.previous_page = self.page - 1
1517 self.previous_page = self.page - 1
1518 else:
1518 else:
1519 self.previous_page = None
1519 self.previous_page = None
1520
1520
1521 if self.page < self.last_page:
1521 if self.page < self.last_page:
1522 self.next_page = self.page + 1
1522 self.next_page = self.page + 1
1523 else:
1523 else:
1524 self.next_page = None
1524 self.next_page = None
1525
1525
1526 # No items available
1526 # No items available
1527 else:
1527 else:
1528 self.first_page = None
1528 self.first_page = None
1529 self.page_count = 0
1529 self.page_count = 0
1530 self.last_page = None
1530 self.last_page = None
1531 self.first_item = None
1531 self.first_item = None
1532 self.last_item = None
1532 self.last_item = None
1533 self.previous_page = None
1533 self.previous_page = None
1534 self.next_page = None
1534 self.next_page = None
1535 self.items = []
1535 self.items = []
1536
1536
1537 # This is a subclass of the 'list' type. Initialise the list now.
1537 # This is a subclass of the 'list' type. Initialise the list now.
1538 list.__init__(self, reversed(self.items))
1538 list.__init__(self, reversed(self.items))
1539
1539
1540
1540
1541 def breadcrumb_repo_link(repo):
1541 def breadcrumb_repo_link(repo):
1542 """
1542 """
1543 Makes a breadcrumbs path link to repo
1543 Makes a breadcrumbs path link to repo
1544
1544
1545 ex::
1545 ex::
1546 group >> subgroup >> repo
1546 group >> subgroup >> repo
1547
1547
1548 :param repo: a Repository instance
1548 :param repo: a Repository instance
1549 """
1549 """
1550
1550
1551 path = [
1551 path = [
1552 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1552 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1553 for group in repo.groups_with_parents
1553 for group in repo.groups_with_parents
1554 ] + [
1554 ] + [
1555 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1555 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1556 ]
1556 ]
1557
1557
1558 return literal(' &raquo; '.join(path))
1558 return literal(' &raquo; '.join(path))
1559
1559
1560
1560
1561 def format_byte_size_binary(file_size):
1561 def format_byte_size_binary(file_size):
1562 """
1562 """
1563 Formats file/folder sizes to standard.
1563 Formats file/folder sizes to standard.
1564 """
1564 """
1565 if file_size is None:
1565 if file_size is None:
1566 file_size = 0
1566 file_size = 0
1567
1567
1568 formatted_size = format_byte_size(file_size, binary=True)
1568 formatted_size = format_byte_size(file_size, binary=True)
1569 return formatted_size
1569 return formatted_size
1570
1570
1571
1571
1572 def urlify_text(text_, safe=True):
1572 def urlify_text(text_, safe=True):
1573 """
1573 """
1574 Extrac urls from text and make html links out of them
1574 Extrac urls from text and make html links out of them
1575
1575
1576 :param text_:
1576 :param text_:
1577 """
1577 """
1578
1578
1579 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1579 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1580 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1580 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1581
1581
1582 def url_func(match_obj):
1582 def url_func(match_obj):
1583 url_full = match_obj.groups()[0]
1583 url_full = match_obj.groups()[0]
1584 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1584 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1585 _newtext = url_pat.sub(url_func, text_)
1585 _newtext = url_pat.sub(url_func, text_)
1586 if safe:
1586 if safe:
1587 return literal(_newtext)
1587 return literal(_newtext)
1588 return _newtext
1588 return _newtext
1589
1589
1590
1590
1591 def urlify_commits(text_, repository):
1591 def urlify_commits(text_, repository):
1592 """
1592 """
1593 Extract commit ids from text and make link from them
1593 Extract commit ids from text and make link from them
1594
1594
1595 :param text_:
1595 :param text_:
1596 :param repository: repo name to build the URL with
1596 :param repository: repo name to build the URL with
1597 """
1597 """
1598
1598
1599 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1599 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1600
1600
1601 def url_func(match_obj):
1601 def url_func(match_obj):
1602 commit_id = match_obj.groups()[1]
1602 commit_id = match_obj.groups()[1]
1603 pref = match_obj.groups()[0]
1603 pref = match_obj.groups()[0]
1604 suf = match_obj.groups()[2]
1604 suf = match_obj.groups()[2]
1605
1605
1606 tmpl = (
1606 tmpl = (
1607 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1607 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1608 '%(commit_id)s</a>%(suf)s'
1608 '%(commit_id)s</a>%(suf)s'
1609 )
1609 )
1610 return tmpl % {
1610 return tmpl % {
1611 'pref': pref,
1611 'pref': pref,
1612 'cls': 'revision-link',
1612 'cls': 'revision-link',
1613 'url': route_url('repo_commit', repo_name=repository,
1613 'url': route_url('repo_commit', repo_name=repository,
1614 commit_id=commit_id),
1614 commit_id=commit_id),
1615 'commit_id': commit_id,
1615 'commit_id': commit_id,
1616 'suf': suf
1616 'suf': suf
1617 }
1617 }
1618
1618
1619 newtext = URL_PAT.sub(url_func, text_)
1619 newtext = URL_PAT.sub(url_func, text_)
1620
1620
1621 return newtext
1621 return newtext
1622
1622
1623
1623
1624 def _process_url_func(match_obj, repo_name, uid, entry,
1624 def _process_url_func(match_obj, repo_name, uid, entry,
1625 return_raw_data=False, link_format='html'):
1625 return_raw_data=False, link_format='html'):
1626 pref = ''
1626 pref = ''
1627 if match_obj.group().startswith(' '):
1627 if match_obj.group().startswith(' '):
1628 pref = ' '
1628 pref = ' '
1629
1629
1630 issue_id = ''.join(match_obj.groups())
1630 issue_id = ''.join(match_obj.groups())
1631
1631
1632 if link_format == 'html':
1632 if link_format == 'html':
1633 tmpl = (
1633 tmpl = (
1634 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1634 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1635 '%(issue-prefix)s%(id-repr)s'
1635 '%(issue-prefix)s%(id-repr)s'
1636 '</a>')
1636 '</a>')
1637 elif link_format == 'rst':
1637 elif link_format == 'rst':
1638 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1638 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1639 elif link_format == 'markdown':
1639 elif link_format == 'markdown':
1640 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1640 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1641 else:
1641 else:
1642 raise ValueError('Bad link_format:{}'.format(link_format))
1642 raise ValueError('Bad link_format:{}'.format(link_format))
1643
1643
1644 (repo_name_cleaned,
1644 (repo_name_cleaned,
1645 parent_group_name) = RepoGroupModel().\
1645 parent_group_name) = RepoGroupModel().\
1646 _get_group_name_and_parent(repo_name)
1646 _get_group_name_and_parent(repo_name)
1647
1647
1648 # variables replacement
1648 # variables replacement
1649 named_vars = {
1649 named_vars = {
1650 'id': issue_id,
1650 'id': issue_id,
1651 'repo': repo_name,
1651 'repo': repo_name,
1652 'repo_name': repo_name_cleaned,
1652 'repo_name': repo_name_cleaned,
1653 'group_name': parent_group_name
1653 'group_name': parent_group_name
1654 }
1654 }
1655 # named regex variables
1655 # named regex variables
1656 named_vars.update(match_obj.groupdict())
1656 named_vars.update(match_obj.groupdict())
1657 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1657 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1658
1658
1659 data = {
1659 data = {
1660 'pref': pref,
1660 'pref': pref,
1661 'cls': 'issue-tracker-link',
1661 'cls': 'issue-tracker-link',
1662 'url': _url,
1662 'url': _url,
1663 'id-repr': issue_id,
1663 'id-repr': issue_id,
1664 'issue-prefix': entry['pref'],
1664 'issue-prefix': entry['pref'],
1665 'serv': entry['url'],
1665 'serv': entry['url'],
1666 }
1666 }
1667 if return_raw_data:
1667 if return_raw_data:
1668 return {
1668 return {
1669 'id': issue_id,
1669 'id': issue_id,
1670 'url': _url
1670 'url': _url
1671 }
1671 }
1672 return tmpl % data
1672 return tmpl % data
1673
1673
1674
1674
1675 def get_active_pattern_entries(repo_name):
1675 def get_active_pattern_entries(repo_name):
1676 repo = None
1676 repo = None
1677 if repo_name:
1677 if repo_name:
1678 # Retrieving repo_name to avoid invalid repo_name to explode on
1678 # Retrieving repo_name to avoid invalid repo_name to explode on
1679 # IssueTrackerSettingsModel but still passing invalid name further down
1679 # IssueTrackerSettingsModel but still passing invalid name further down
1680 repo = Repository.get_by_repo_name(repo_name, cache=True)
1680 repo = Repository.get_by_repo_name(repo_name, cache=True)
1681
1681
1682 settings_model = IssueTrackerSettingsModel(repo=repo)
1682 settings_model = IssueTrackerSettingsModel(repo=repo)
1683 active_entries = settings_model.get_settings(cache=True)
1683 active_entries = settings_model.get_settings(cache=True)
1684 return active_entries
1684 return active_entries
1685
1685
1686
1686
1687 def process_patterns(text_string, repo_name, link_format='html',
1687 def process_patterns(text_string, repo_name, link_format='html',
1688 active_entries=None):
1688 active_entries=None):
1689
1689
1690 allowed_formats = ['html', 'rst', 'markdown']
1690 allowed_formats = ['html', 'rst', 'markdown']
1691 if link_format not in allowed_formats:
1691 if link_format not in allowed_formats:
1692 raise ValueError('Link format can be only one of:{} got {}'.format(
1692 raise ValueError('Link format can be only one of:{} got {}'.format(
1693 allowed_formats, link_format))
1693 allowed_formats, link_format))
1694
1694
1695 active_entries = active_entries or get_active_pattern_entries(repo_name)
1695 active_entries = active_entries or get_active_pattern_entries(repo_name)
1696 issues_data = []
1696 issues_data = []
1697 newtext = text_string
1697 newtext = text_string
1698
1698
1699 for uid, entry in active_entries.items():
1699 for uid, entry in active_entries.items():
1700 log.debug('found issue tracker entry with uid %s' % (uid,))
1700 log.debug('found issue tracker entry with uid %s' % (uid,))
1701
1701
1702 if not (entry['pat'] and entry['url']):
1702 if not (entry['pat'] and entry['url']):
1703 log.debug('skipping due to missing data')
1703 log.debug('skipping due to missing data')
1704 continue
1704 continue
1705
1705
1706 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1706 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1707 % (uid, entry['pat'], entry['url'], entry['pref']))
1707 % (uid, entry['pat'], entry['url'], entry['pref']))
1708
1708
1709 try:
1709 try:
1710 pattern = re.compile(r'%s' % entry['pat'])
1710 pattern = re.compile(r'%s' % entry['pat'])
1711 except re.error:
1711 except re.error:
1712 log.exception(
1712 log.exception(
1713 'issue tracker pattern: `%s` failed to compile',
1713 'issue tracker pattern: `%s` failed to compile',
1714 entry['pat'])
1714 entry['pat'])
1715 continue
1715 continue
1716
1716
1717 data_func = partial(
1717 data_func = partial(
1718 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1718 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1719 return_raw_data=True)
1719 return_raw_data=True)
1720
1720
1721 for match_obj in pattern.finditer(text_string):
1721 for match_obj in pattern.finditer(text_string):
1722 issues_data.append(data_func(match_obj))
1722 issues_data.append(data_func(match_obj))
1723
1723
1724 url_func = partial(
1724 url_func = partial(
1725 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1725 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1726 link_format=link_format)
1726 link_format=link_format)
1727
1727
1728 newtext = pattern.sub(url_func, newtext)
1728 newtext = pattern.sub(url_func, newtext)
1729 log.debug('processed prefix:uid `%s`' % (uid,))
1729 log.debug('processed prefix:uid `%s`' % (uid,))
1730
1730
1731 return newtext, issues_data
1731 return newtext, issues_data
1732
1732
1733
1733
1734 def urlify_commit_message(commit_text, repository=None,
1734 def urlify_commit_message(commit_text, repository=None,
1735 active_pattern_entries=None):
1735 active_pattern_entries=None):
1736 """
1736 """
1737 Parses given text message and makes proper links.
1737 Parses given text message and makes proper links.
1738 issues are linked to given issue-server, and rest is a commit link
1738 issues are linked to given issue-server, and rest is a commit link
1739
1739
1740 :param commit_text:
1740 :param commit_text:
1741 :param repository:
1741 :param repository:
1742 """
1742 """
1743 def escaper(string):
1743 def escaper(string):
1744 return string.replace('<', '&lt;').replace('>', '&gt;')
1744 return string.replace('<', '&lt;').replace('>', '&gt;')
1745
1745
1746 newtext = escaper(commit_text)
1746 newtext = escaper(commit_text)
1747
1747
1748 # extract http/https links and make them real urls
1748 # extract http/https links and make them real urls
1749 newtext = urlify_text(newtext, safe=False)
1749 newtext = urlify_text(newtext, safe=False)
1750
1750
1751 # urlify commits - extract commit ids and make link out of them, if we have
1751 # urlify commits - extract commit ids and make link out of them, if we have
1752 # the scope of repository present.
1752 # the scope of repository present.
1753 if repository:
1753 if repository:
1754 newtext = urlify_commits(newtext, repository)
1754 newtext = urlify_commits(newtext, repository)
1755
1755
1756 # process issue tracker patterns
1756 # process issue tracker patterns
1757 newtext, issues = process_patterns(newtext, repository or '',
1757 newtext, issues = process_patterns(newtext, repository or '',
1758 active_entries=active_pattern_entries)
1758 active_entries=active_pattern_entries)
1759
1759
1760 return literal(newtext)
1760 return literal(newtext)
1761
1761
1762
1762
1763 def render_binary(repo_name, file_obj):
1763 def render_binary(repo_name, file_obj):
1764 """
1764 """
1765 Choose how to render a binary file
1765 Choose how to render a binary file
1766 """
1766 """
1767 filename = file_obj.name
1767 filename = file_obj.name
1768
1768
1769 # images
1769 # images
1770 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1770 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1771 if fnmatch.fnmatch(filename, pat=ext):
1771 if fnmatch.fnmatch(filename, pat=ext):
1772 alt = filename
1772 alt = filename
1773 src = route_path(
1773 src = route_path(
1774 'repo_file_raw', repo_name=repo_name,
1774 'repo_file_raw', repo_name=repo_name,
1775 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1775 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1776 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1776 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1777
1777
1778
1778
1779 def renderer_from_filename(filename, exclude=None):
1779 def renderer_from_filename(filename, exclude=None):
1780 """
1780 """
1781 choose a renderer based on filename, this works only for text based files
1781 choose a renderer based on filename, this works only for text based files
1782 """
1782 """
1783
1783
1784 # ipython
1784 # ipython
1785 for ext in ['*.ipynb']:
1785 for ext in ['*.ipynb']:
1786 if fnmatch.fnmatch(filename, pat=ext):
1786 if fnmatch.fnmatch(filename, pat=ext):
1787 return 'jupyter'
1787 return 'jupyter'
1788
1788
1789 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1789 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1790 if is_markup:
1790 if is_markup:
1791 return is_markup
1791 return is_markup
1792 return None
1792 return None
1793
1793
1794
1794
1795 def render(source, renderer='rst', mentions=False, relative_urls=None,
1795 def render(source, renderer='rst', mentions=False, relative_urls=None,
1796 repo_name=None):
1796 repo_name=None):
1797
1797
1798 def maybe_convert_relative_links(html_source):
1798 def maybe_convert_relative_links(html_source):
1799 if relative_urls:
1799 if relative_urls:
1800 return relative_links(html_source, relative_urls)
1800 return relative_links(html_source, relative_urls)
1801 return html_source
1801 return html_source
1802
1802
1803 if renderer == 'rst':
1803 if renderer == 'rst':
1804 if repo_name:
1804 if repo_name:
1805 # process patterns on comments if we pass in repo name
1805 # process patterns on comments if we pass in repo name
1806 source, issues = process_patterns(
1806 source, issues = process_patterns(
1807 source, repo_name, link_format='rst')
1807 source, repo_name, link_format='rst')
1808
1808
1809 return literal(
1809 return literal(
1810 '<div class="rst-block">%s</div>' %
1810 '<div class="rst-block">%s</div>' %
1811 maybe_convert_relative_links(
1811 maybe_convert_relative_links(
1812 MarkupRenderer.rst(source, mentions=mentions)))
1812 MarkupRenderer.rst(source, mentions=mentions)))
1813 elif renderer == 'markdown':
1813 elif renderer == 'markdown':
1814 if repo_name:
1814 if repo_name:
1815 # process patterns on comments if we pass in repo name
1815 # process patterns on comments if we pass in repo name
1816 source, issues = process_patterns(
1816 source, issues = process_patterns(
1817 source, repo_name, link_format='markdown')
1817 source, repo_name, link_format='markdown')
1818
1818
1819 return literal(
1819 return literal(
1820 '<div class="markdown-block">%s</div>' %
1820 '<div class="markdown-block">%s</div>' %
1821 maybe_convert_relative_links(
1821 maybe_convert_relative_links(
1822 MarkupRenderer.markdown(source, flavored=True,
1822 MarkupRenderer.markdown(source, flavored=True,
1823 mentions=mentions)))
1823 mentions=mentions)))
1824 elif renderer == 'jupyter':
1824 elif renderer == 'jupyter':
1825 return literal(
1825 return literal(
1826 '<div class="ipynb">%s</div>' %
1826 '<div class="ipynb">%s</div>' %
1827 maybe_convert_relative_links(
1827 maybe_convert_relative_links(
1828 MarkupRenderer.jupyter(source)))
1828 MarkupRenderer.jupyter(source)))
1829
1829
1830 # None means just show the file-source
1830 # None means just show the file-source
1831 return None
1831 return None
1832
1832
1833
1833
1834 def commit_status(repo, commit_id):
1834 def commit_status(repo, commit_id):
1835 return ChangesetStatusModel().get_status(repo, commit_id)
1835 return ChangesetStatusModel().get_status(repo, commit_id)
1836
1836
1837
1837
1838 def commit_status_lbl(commit_status):
1838 def commit_status_lbl(commit_status):
1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1840
1840
1841
1841
1842 def commit_time(repo_name, commit_id):
1842 def commit_time(repo_name, commit_id):
1843 repo = Repository.get_by_repo_name(repo_name)
1843 repo = Repository.get_by_repo_name(repo_name)
1844 commit = repo.get_commit(commit_id=commit_id)
1844 commit = repo.get_commit(commit_id=commit_id)
1845 return commit.date
1845 return commit.date
1846
1846
1847
1847
1848 def get_permission_name(key):
1848 def get_permission_name(key):
1849 return dict(Permission.PERMS).get(key)
1849 return dict(Permission.PERMS).get(key)
1850
1850
1851
1851
1852 def journal_filter_help(request):
1852 def journal_filter_help(request):
1853 _ = request.translate
1853 _ = request.translate
1854
1854
1855 return _(
1855 return _(
1856 'Example filter terms:\n' +
1856 'Example filter terms:\n' +
1857 ' repository:vcs\n' +
1857 ' repository:vcs\n' +
1858 ' username:marcin\n' +
1858 ' username:marcin\n' +
1859 ' username:(NOT marcin)\n' +
1859 ' username:(NOT marcin)\n' +
1860 ' action:*push*\n' +
1860 ' action:*push*\n' +
1861 ' ip:127.0.0.1\n' +
1861 ' ip:127.0.0.1\n' +
1862 ' date:20120101\n' +
1862 ' date:20120101\n' +
1863 ' date:[20120101100000 TO 20120102]\n' +
1863 ' date:[20120101100000 TO 20120102]\n' +
1864 '\n' +
1864 '\n' +
1865 'Generate wildcards using \'*\' character:\n' +
1865 'Generate wildcards using \'*\' character:\n' +
1866 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1866 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1867 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1867 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1868 '\n' +
1868 '\n' +
1869 'Optional AND / OR operators in queries\n' +
1869 'Optional AND / OR operators in queries\n' +
1870 ' "repository:vcs OR repository:test"\n' +
1870 ' "repository:vcs OR repository:test"\n' +
1871 ' "username:test AND repository:test*"\n'
1871 ' "username:test AND repository:test*"\n'
1872 )
1872 )
1873
1873
1874
1874
1875 def search_filter_help(searcher, request):
1875 def search_filter_help(searcher, request):
1876 _ = request.translate
1876 _ = request.translate
1877
1877
1878 terms = ''
1878 terms = ''
1879 return _(
1879 return _(
1880 'Example filter terms for `{searcher}` search:\n' +
1880 'Example filter terms for `{searcher}` search:\n' +
1881 '{terms}\n' +
1881 '{terms}\n' +
1882 'Generate wildcards using \'*\' character:\n' +
1882 'Generate wildcards using \'*\' character:\n' +
1883 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1883 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1884 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1884 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1885 '\n' +
1885 '\n' +
1886 'Optional AND / OR operators in queries\n' +
1886 'Optional AND / OR operators in queries\n' +
1887 ' "repo_name:vcs OR repo_name:test"\n' +
1887 ' "repo_name:vcs OR repo_name:test"\n' +
1888 ' "owner:test AND repo_name:test*"\n' +
1888 ' "owner:test AND repo_name:test*"\n' +
1889 'More: {search_doc}'
1889 'More: {search_doc}'
1890 ).format(searcher=searcher.name,
1890 ).format(searcher=searcher.name,
1891 terms=terms, search_doc=searcher.query_lang_doc)
1891 terms=terms, search_doc=searcher.query_lang_doc)
1892
1892
1893
1893
1894 def not_mapped_error(repo_name):
1894 def not_mapped_error(repo_name):
1895 from rhodecode.translation import _
1895 from rhodecode.translation import _
1896 flash(_('%s repository is not mapped to db perhaps'
1896 flash(_('%s repository is not mapped to db perhaps'
1897 ' it was created or renamed from the filesystem'
1897 ' it was created or renamed from the filesystem'
1898 ' please run the application again'
1898 ' please run the application again'
1899 ' in order to rescan repositories') % repo_name, category='error')
1899 ' in order to rescan repositories') % repo_name, category='error')
1900
1900
1901
1901
1902 def ip_range(ip_addr):
1902 def ip_range(ip_addr):
1903 from rhodecode.model.db import UserIpMap
1903 from rhodecode.model.db import UserIpMap
1904 s, e = UserIpMap._get_ip_range(ip_addr)
1904 s, e = UserIpMap._get_ip_range(ip_addr)
1905 return '%s - %s' % (s, e)
1905 return '%s - %s' % (s, e)
1906
1906
1907
1907
1908 def form(url, method='post', needs_csrf_token=True, **attrs):
1908 def form(url, method='post', needs_csrf_token=True, **attrs):
1909 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1909 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1910 if method.lower() != 'get' and needs_csrf_token:
1910 if method.lower() != 'get' and needs_csrf_token:
1911 raise Exception(
1911 raise Exception(
1912 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1912 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1913 'CSRF token. If the endpoint does not require such token you can ' +
1913 'CSRF token. If the endpoint does not require such token you can ' +
1914 'explicitly set the parameter needs_csrf_token to false.')
1914 'explicitly set the parameter needs_csrf_token to false.')
1915
1915
1916 return wh_form(url, method=method, **attrs)
1916 return wh_form(url, method=method, **attrs)
1917
1917
1918
1918
1919 def secure_form(form_url, method="POST", multipart=False, **attrs):
1919 def secure_form(form_url, method="POST", multipart=False, **attrs):
1920 """Start a form tag that points the action to an url. This
1920 """Start a form tag that points the action to an url. This
1921 form tag will also include the hidden field containing
1921 form tag will also include the hidden field containing
1922 the auth token.
1922 the auth token.
1923
1923
1924 The url options should be given either as a string, or as a
1924 The url options should be given either as a string, or as a
1925 ``url()`` function. The method for the form defaults to POST.
1925 ``url()`` function. The method for the form defaults to POST.
1926
1926
1927 Options:
1927 Options:
1928
1928
1929 ``multipart``
1929 ``multipart``
1930 If set to True, the enctype is set to "multipart/form-data".
1930 If set to True, the enctype is set to "multipart/form-data".
1931 ``method``
1931 ``method``
1932 The method to use when submitting the form, usually either
1932 The method to use when submitting the form, usually either
1933 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1933 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1934 hidden input with name _method is added to simulate the verb
1934 hidden input with name _method is added to simulate the verb
1935 over POST.
1935 over POST.
1936
1936
1937 """
1937 """
1938 from webhelpers.pylonslib.secure_form import insecure_form
1938 from webhelpers.pylonslib.secure_form import insecure_form
1939
1939
1940 if 'request' in attrs:
1940 if 'request' in attrs:
1941 session = attrs['request'].session
1941 session = attrs['request'].session
1942 del attrs['request']
1942 del attrs['request']
1943 else:
1943 else:
1944 raise ValueError(
1944 raise ValueError(
1945 'Calling this form requires request= to be passed as argument')
1945 'Calling this form requires request= to be passed as argument')
1946
1946
1947 form = insecure_form(form_url, method, multipart, **attrs)
1947 form = insecure_form(form_url, method, multipart, **attrs)
1948 token = literal(
1948 token = literal(
1949 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1949 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1950 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1950 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1951
1951
1952 return literal("%s\n%s" % (form, token))
1952 return literal("%s\n%s" % (form, token))
1953
1953
1954
1954
1955 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1955 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1956 select_html = select(name, selected, options, **attrs)
1956 select_html = select(name, selected, options, **attrs)
1957 select2 = """
1957 select2 = """
1958 <script>
1958 <script>
1959 $(document).ready(function() {
1959 $(document).ready(function() {
1960 $('#%s').select2({
1960 $('#%s').select2({
1961 containerCssClass: 'drop-menu',
1961 containerCssClass: 'drop-menu',
1962 dropdownCssClass: 'drop-menu-dropdown',
1962 dropdownCssClass: 'drop-menu-dropdown',
1963 dropdownAutoWidth: true%s
1963 dropdownAutoWidth: true%s
1964 });
1964 });
1965 });
1965 });
1966 </script>
1966 </script>
1967 """
1967 """
1968 filter_option = """,
1968 filter_option = """,
1969 minimumResultsForSearch: -1
1969 minimumResultsForSearch: -1
1970 """
1970 """
1971 input_id = attrs.get('id') or name
1971 input_id = attrs.get('id') or name
1972 filter_enabled = "" if enable_filter else filter_option
1972 filter_enabled = "" if enable_filter else filter_option
1973 select_script = literal(select2 % (input_id, filter_enabled))
1973 select_script = literal(select2 % (input_id, filter_enabled))
1974
1974
1975 return literal(select_html+select_script)
1975 return literal(select_html+select_script)
1976
1976
1977
1977
1978 def get_visual_attr(tmpl_context_var, attr_name):
1978 def get_visual_attr(tmpl_context_var, attr_name):
1979 """
1979 """
1980 A safe way to get a variable from visual variable of template context
1980 A safe way to get a variable from visual variable of template context
1981
1981
1982 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1982 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1983 :param attr_name: name of the attribute we fetch from the c.visual
1983 :param attr_name: name of the attribute we fetch from the c.visual
1984 """
1984 """
1985 visual = getattr(tmpl_context_var, 'visual', None)
1985 visual = getattr(tmpl_context_var, 'visual', None)
1986 if not visual:
1986 if not visual:
1987 return
1987 return
1988 else:
1988 else:
1989 return getattr(visual, attr_name, None)
1989 return getattr(visual, attr_name, None)
1990
1990
1991
1991
1992 def get_last_path_part(file_node):
1992 def get_last_path_part(file_node):
1993 if not file_node.path:
1993 if not file_node.path:
1994 return u''
1994 return u''
1995
1995
1996 path = safe_unicode(file_node.path.split('/')[-1])
1996 path = safe_unicode(file_node.path.split('/')[-1])
1997 return u'../' + path
1997 return u'../' + path
1998
1998
1999
1999
2000 def route_url(*args, **kwargs):
2000 def route_url(*args, **kwargs):
2001 """
2001 """
2002 Wrapper around pyramids `route_url` (fully qualified url) function.
2002 Wrapper around pyramids `route_url` (fully qualified url) function.
2003 """
2003 """
2004 req = get_current_request()
2004 req = get_current_request()
2005 return req.route_url(*args, **kwargs)
2005 return req.route_url(*args, **kwargs)
2006
2006
2007
2007
2008 def route_path(*args, **kwargs):
2008 def route_path(*args, **kwargs):
2009 """
2009 """
2010 Wrapper around pyramids `route_path` function.
2010 Wrapper around pyramids `route_path` function.
2011 """
2011 """
2012 req = get_current_request()
2012 req = get_current_request()
2013 return req.route_path(*args, **kwargs)
2013 return req.route_path(*args, **kwargs)
2014
2014
2015
2015
2016 def route_path_or_none(*args, **kwargs):
2016 def route_path_or_none(*args, **kwargs):
2017 try:
2017 try:
2018 return route_path(*args, **kwargs)
2018 return route_path(*args, **kwargs)
2019 except KeyError:
2019 except KeyError:
2020 return None
2020 return None
2021
2021
2022
2022
2023 def current_route_path(request, **kw):
2023 def current_route_path(request, **kw):
2024 new_args = request.GET.mixed()
2024 new_args = request.GET.mixed()
2025 new_args.update(kw)
2025 new_args.update(kw)
2026 return request.current_route_path(_query=new_args)
2026 return request.current_route_path(_query=new_args)
2027
2027
2028
2028
2029 def api_call_example(method, args):
2029 def api_call_example(method, args):
2030 """
2030 """
2031 Generates an API call example via CURL
2031 Generates an API call example via CURL
2032 """
2032 """
2033 args_json = json.dumps(OrderedDict([
2033 args_json = json.dumps(OrderedDict([
2034 ('id', 1),
2034 ('id', 1),
2035 ('auth_token', 'SECRET'),
2035 ('auth_token', 'SECRET'),
2036 ('method', method),
2036 ('method', method),
2037 ('args', args)
2037 ('args', args)
2038 ]))
2038 ]))
2039 return literal(
2039 return literal(
2040 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2040 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2041 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2041 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2042 "and needs to be of `api calls` role."
2042 "and needs to be of `api calls` role."
2043 .format(
2043 .format(
2044 api_url=route_url('apiv2'),
2044 api_url=route_url('apiv2'),
2045 token_url=route_url('my_account_auth_tokens'),
2045 token_url=route_url('my_account_auth_tokens'),
2046 data=args_json))
2046 data=args_json))
2047
2047
2048
2048
2049 def notification_description(notification, request):
2049 def notification_description(notification, request):
2050 """
2050 """
2051 Generate notification human readable description based on notification type
2051 Generate notification human readable description based on notification type
2052 """
2052 """
2053 from rhodecode.model.notification import NotificationModel
2053 from rhodecode.model.notification import NotificationModel
2054 return NotificationModel().make_description(
2054 return NotificationModel().make_description(
2055 notification, translate=request.translate)
2055 notification, translate=request.translate)
2056
2056
2057
2057
2058 def go_import_header(request, db_repo=None):
2058 def go_import_header(request, db_repo=None):
2059 """
2059 """
2060 Creates a header for go-import functionality in Go Lang
2060 Creates a header for go-import functionality in Go Lang
2061 """
2061 """
2062
2062
2063 if not db_repo:
2063 if not db_repo:
2064 return
2064 return
2065 if 'go-get' not in request.GET:
2065 if 'go-get' not in request.GET:
2066 return
2066 return
2067
2067
2068 clone_url = db_repo.clone_url()
2068 clone_url = db_repo.clone_url()
2069 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2069 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2070 # we have a repo and go-get flag,
2070 # we have a repo and go-get flag,
2071 return literal('<meta name="go-import" content="{} {} {}">'.format(
2071 return literal('<meta name="go-import" content="{} {} {}">'.format(
2072 prefix, db_repo.repo_type, clone_url))
2072 prefix, db_repo.repo_type, clone_url))
2073
2074
2075 def reviewer_as_json(*args, **kwargs):
2076 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2077 return _reviewer_as_json(*args, **kwargs)
@@ -1,267 +1,393 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 import itertools
22 import itertools
23 import logging
23 import logging
24 from collections import defaultdict
24 import collections
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class ChangesetStatusModel(BaseModel):
36 class ChangesetStatusModel(BaseModel):
37
37
38 cls = ChangesetStatus
38 cls = ChangesetStatus
39
39
40 def __get_changeset_status(self, changeset_status):
40 def __get_changeset_status(self, changeset_status):
41 return self._get_instance(ChangesetStatus, changeset_status)
41 return self._get_instance(ChangesetStatus, changeset_status)
42
42
43 def __get_pull_request(self, pull_request):
43 def __get_pull_request(self, pull_request):
44 return self._get_instance(PullRequest, pull_request)
44 return self._get_instance(PullRequest, pull_request)
45
45
46 def _get_status_query(self, repo, revision, pull_request,
46 def _get_status_query(self, repo, revision, pull_request,
47 with_revisions=False):
47 with_revisions=False):
48 repo = self._get_repo(repo)
48 repo = self._get_repo(repo)
49
49
50 q = ChangesetStatus.query()\
50 q = ChangesetStatus.query()\
51 .filter(ChangesetStatus.repo == repo)
51 .filter(ChangesetStatus.repo == repo)
52 if not with_revisions:
52 if not with_revisions:
53 q = q.filter(ChangesetStatus.version == 0)
53 q = q.filter(ChangesetStatus.version == 0)
54
54
55 if revision:
55 if revision:
56 q = q.filter(ChangesetStatus.revision == revision)
56 q = q.filter(ChangesetStatus.revision == revision)
57 elif pull_request:
57 elif pull_request:
58 pull_request = self.__get_pull_request(pull_request)
58 pull_request = self.__get_pull_request(pull_request)
59 # TODO: johbo: Think about the impact of this join, there must
59 # TODO: johbo: Think about the impact of this join, there must
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 # to the pull request. Might be that we want to do the same for
61 # to the pull request. Might be that we want to do the same for
62 # the pull_request_version_id.
62 # the pull_request_version_id.
63 q = q.join(ChangesetComment).filter(
63 q = q.join(ChangesetComment).filter(
64 ChangesetStatus.pull_request == pull_request,
64 ChangesetStatus.pull_request == pull_request,
65 ChangesetComment.pull_request_version_id == None)
65 ChangesetComment.pull_request_version_id == None)
66 else:
66 else:
67 raise Exception('Please specify revision or pull_request')
67 raise Exception('Please specify revision or pull_request')
68 q = q.order_by(ChangesetStatus.version.asc())
68 q = q.order_by(ChangesetStatus.version.asc())
69 return q
69 return q
70
70
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 trim_votes=True):
73 """
74 Calculate status based on given group members, and voting rule
75
76
77 group1 - 4 members, 3 required for approval
78 user1 - approved
79 user2 - reject
80 user3 - approved
81 user4 - rejected
82
83 final_state: rejected, reasons not at least 3 votes
84
85
86 group1 - 4 members, 2 required for approval
87 user1 - approved
88 user2 - reject
89 user3 - approved
90 user4 - rejected
91
92 final_state: approved, reasons got at least 2 approvals
93
94 group1 - 4 members, ALL required for approval
95 user1 - approved
96 user2 - reject
97 user3 - approved
98 user4 - rejected
99
100 final_state: rejected, reasons not all approvals
101
102
103 group1 - 4 members, ALL required for approval
104 user1 - approved
105 user2 - approved
106 user3 - approved
107 user4 - approved
108
109 final_state: approved, reason all approvals received
110
111 group1 - 4 members, 5 required for approval
112 (approval should be shorted to number of actual members)
113
114 user1 - approved
115 user2 - approved
116 user3 - approved
117 user4 - approved
118
119 final_state: approved, reason all approvals received
120
121 """
122 group_vote_data = {}
123 got_rule = False
124 members = collections.OrderedDict()
125 for review_obj, user, reasons, mandatory, statuses \
126 in group_statuses_by_reviewers:
127
128 if not got_rule:
129 group_vote_data = review_obj.rule_user_group_data()
130 got_rule = bool(group_vote_data)
131
132 members[user.user_id] = statuses
133
134 if not group_vote_data:
135 return []
136
137 required_votes = group_vote_data['vote_rule']
138 if required_votes == -1:
139 # -1 means all required, so we replace it with how many people
140 # are in the members
141 required_votes = len(members)
142
143 if trim_votes and required_votes > len(members):
144 # we require more votes than we have members in the group
145 # in this case we trim the required votes to the number of members
146 required_votes = len(members)
147
148 approvals = sum([
149 1 for statuses in members.values()
150 if statuses and
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152
153 calculated_votes = []
154 # we have all votes from users, now check if we have enough votes
155 # to fill other
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 if approvals >= required_votes:
158 fill_in = ChangesetStatus.STATUS_APPROVED
159
160 for member, statuses in members.items():
161 if statuses:
162 ver, latest = statuses[0]
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 calculated_votes.append(fill_in)
165 else:
166 calculated_votes.append(latest.status)
167 else:
168 calculated_votes.append(fill_in)
169
170 return calculated_votes
171
71 def calculate_status(self, statuses_by_reviewers):
172 def calculate_status(self, statuses_by_reviewers):
72 """
173 """
73 Given the approval statuses from reviewers, calculates final approval
174 Given the approval statuses from reviewers, calculates final approval
74 status. There can only be 3 results, all approved, all rejected. If
175 status. There can only be 3 results, all approved, all rejected. If
75 there is no consensus the PR is under review.
176 there is no consensus the PR is under review.
76
177
77 :param statuses_by_reviewers:
178 :param statuses_by_reviewers:
78 """
179 """
79 votes = defaultdict(int)
180
181 def group_rule(element):
182 review_obj = element[0]
183 rule_data = review_obj.rule_user_group_data()
184 if rule_data and rule_data['id']:
185 return rule_data['id']
186
187 voting_groups = itertools.groupby(
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191
80 reviewers_number = len(statuses_by_reviewers)
192 reviewers_number = len(statuses_by_reviewers)
81 for user, reasons, mandatory, statuses in statuses_by_reviewers:
193 votes = collections.defaultdict(int)
194 for group, group_statuses_by_reviewers in voting_by_groups:
195 if group:
196 # calculate how the "group" voted
197 for vote_status in self.calculate_group_vote(
198 group, group_statuses_by_reviewers):
199 votes[vote_status] += 1
200 else:
201
202 for review_obj, user, reasons, mandatory, statuses \
203 in group_statuses_by_reviewers:
204 # individual vote
82 if statuses:
205 if statuses:
83 ver, latest = statuses[0]
206 ver, latest = statuses[0]
84 votes[latest.status] += 1
207 votes[latest.status] += 1
85 else:
208
86 votes[ChangesetStatus.DEFAULT] += 1
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
87
211
88 # all approved
212 # TODO(marcink): with group voting, how does rejected work,
89 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
213 # do we ever get rejected state ?
214
215 if approved_votes_count == reviewers_number:
90 return ChangesetStatus.STATUS_APPROVED
216 return ChangesetStatus.STATUS_APPROVED
91
217
92 # all rejected
218 if rejected_votes_count == reviewers_number:
93 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
94 return ChangesetStatus.STATUS_REJECTED
219 return ChangesetStatus.STATUS_REJECTED
95
220
96 return ChangesetStatus.STATUS_UNDER_REVIEW
221 return ChangesetStatus.STATUS_UNDER_REVIEW
97
222
98 def get_statuses(self, repo, revision=None, pull_request=None,
223 def get_statuses(self, repo, revision=None, pull_request=None,
99 with_revisions=False):
224 with_revisions=False):
100 q = self._get_status_query(repo, revision, pull_request,
225 q = self._get_status_query(repo, revision, pull_request,
101 with_revisions)
226 with_revisions)
102 return q.all()
227 return q.all()
103
228
104 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
105 """
230 """
106 Returns latest status of changeset for given revision or for given
231 Returns latest status of changeset for given revision or for given
107 pull request. Statuses are versioned inside a table itself and
232 pull request. Statuses are versioned inside a table itself and
108 version == 0 is always the current one
233 version == 0 is always the current one
109
234
110 :param repo:
235 :param repo:
111 :param revision: 40char hash or None
236 :param revision: 40char hash or None
112 :param pull_request: pull_request reference
237 :param pull_request: pull_request reference
113 :param as_str: return status as string not object
238 :param as_str: return status as string not object
114 """
239 """
115 q = self._get_status_query(repo, revision, pull_request)
240 q = self._get_status_query(repo, revision, pull_request)
116
241
117 # need to use first here since there can be multiple statuses
242 # need to use first here since there can be multiple statuses
118 # returned from pull_request
243 # returned from pull_request
119 status = q.first()
244 status = q.first()
120 if as_str:
245 if as_str:
121 status = status.status if status else status
246 status = status.status if status else status
122 st = status or ChangesetStatus.DEFAULT
247 st = status or ChangesetStatus.DEFAULT
123 return str(st)
248 return str(st)
124 return status
249 return status
125
250
126 def _render_auto_status_message(
251 def _render_auto_status_message(
127 self, status, commit_id=None, pull_request=None):
252 self, status, commit_id=None, pull_request=None):
128 """
253 """
129 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
130 so it's always looking the same disregarding on which default
255 so it's always looking the same disregarding on which default
131 renderer system is using.
256 renderer system is using.
132
257
133 :param status: status text to change into
258 :param status: status text to change into
134 :param commit_id: the commit_id we change the status for
259 :param commit_id: the commit_id we change the status for
135 :param pull_request: the pull request we change the status for
260 :param pull_request: the pull request we change the status for
136 """
261 """
137
262
138 new_status = ChangesetStatus.get_status_lbl(status)
263 new_status = ChangesetStatus.get_status_lbl(status)
139
264
140 params = {
265 params = {
141 'new_status_label': new_status,
266 'new_status_label': new_status,
142 'pull_request': pull_request,
267 'pull_request': pull_request,
143 'commit_id': commit_id,
268 'commit_id': commit_id,
144 }
269 }
145 renderer = RstTemplateRenderer()
270 renderer = RstTemplateRenderer()
146 return renderer.render('auto_status_change.mako', **params)
271 return renderer.render('auto_status_change.mako', **params)
147
272
148 def set_status(self, repo, status, user, comment=None, revision=None,
273 def set_status(self, repo, status, user, comment=None, revision=None,
149 pull_request=None, dont_allow_on_closed_pull_request=False):
274 pull_request=None, dont_allow_on_closed_pull_request=False):
150 """
275 """
151 Creates new status for changeset or updates the old ones bumping their
276 Creates new status for changeset or updates the old ones bumping their
152 version, leaving the current status at
277 version, leaving the current status at
153
278
154 :param repo:
279 :param repo:
155 :param revision:
280 :param revision:
156 :param status:
281 :param status:
157 :param user:
282 :param user:
158 :param comment:
283 :param comment:
159 :param dont_allow_on_closed_pull_request: don't allow a status change
284 :param dont_allow_on_closed_pull_request: don't allow a status change
160 if last status was for pull request and it's closed. We shouldn't
285 if last status was for pull request and it's closed. We shouldn't
161 mess around this manually
286 mess around this manually
162 """
287 """
163 repo = self._get_repo(repo)
288 repo = self._get_repo(repo)
164
289
165 q = ChangesetStatus.query()
290 q = ChangesetStatus.query()
166
291
167 if revision:
292 if revision:
168 q = q.filter(ChangesetStatus.repo == repo)
293 q = q.filter(ChangesetStatus.repo == repo)
169 q = q.filter(ChangesetStatus.revision == revision)
294 q = q.filter(ChangesetStatus.revision == revision)
170 elif pull_request:
295 elif pull_request:
171 pull_request = self.__get_pull_request(pull_request)
296 pull_request = self.__get_pull_request(pull_request)
172 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
173 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
174 cur_statuses = q.all()
299 cur_statuses = q.all()
175
300
176 # if statuses exists and last is associated with a closed pull request
301 # if statuses exists and last is associated with a closed pull request
177 # we need to check if we can allow this status change
302 # we need to check if we can allow this status change
178 if (dont_allow_on_closed_pull_request and cur_statuses
303 if (dont_allow_on_closed_pull_request and cur_statuses
179 and getattr(cur_statuses[0].pull_request, 'status', '')
304 and getattr(cur_statuses[0].pull_request, 'status', '')
180 == PullRequest.STATUS_CLOSED):
305 == PullRequest.STATUS_CLOSED):
181 raise StatusChangeOnClosedPullRequestError(
306 raise StatusChangeOnClosedPullRequestError(
182 'Changing status on closed pull request is not allowed'
307 'Changing status on closed pull request is not allowed'
183 )
308 )
184
309
185 # update all current statuses with older version
310 # update all current statuses with older version
186 if cur_statuses:
311 if cur_statuses:
187 for st in cur_statuses:
312 for st in cur_statuses:
188 st.version += 1
313 st.version += 1
189 Session().add(st)
314 Session().add(st)
190
315
191 def _create_status(user, repo, status, comment, revision, pull_request):
316 def _create_status(user, repo, status, comment, revision, pull_request):
192 new_status = ChangesetStatus()
317 new_status = ChangesetStatus()
193 new_status.author = self._get_user(user)
318 new_status.author = self._get_user(user)
194 new_status.repo = self._get_repo(repo)
319 new_status.repo = self._get_repo(repo)
195 new_status.status = status
320 new_status.status = status
196 new_status.comment = comment
321 new_status.comment = comment
197 new_status.revision = revision
322 new_status.revision = revision
198 new_status.pull_request = pull_request
323 new_status.pull_request = pull_request
199 return new_status
324 return new_status
200
325
201 if not comment:
326 if not comment:
202 from rhodecode.model.comment import CommentsModel
327 from rhodecode.model.comment import CommentsModel
203 comment = CommentsModel().create(
328 comment = CommentsModel().create(
204 text=self._render_auto_status_message(
329 text=self._render_auto_status_message(
205 status, commit_id=revision, pull_request=pull_request),
330 status, commit_id=revision, pull_request=pull_request),
206 repo=repo,
331 repo=repo,
207 user=user,
332 user=user,
208 pull_request=pull_request,
333 pull_request=pull_request,
209 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
334 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
210 )
335 )
211
336
212 if revision:
337 if revision:
213 new_status = _create_status(
338 new_status = _create_status(
214 user=user, repo=repo, status=status, comment=comment,
339 user=user, repo=repo, status=status, comment=comment,
215 revision=revision, pull_request=pull_request)
340 revision=revision, pull_request=pull_request)
216 Session().add(new_status)
341 Session().add(new_status)
217 return new_status
342 return new_status
218 elif pull_request:
343 elif pull_request:
219 # pull request can have more than one revision associated to it
344 # pull request can have more than one revision associated to it
220 # we need to create new version for each one
345 # we need to create new version for each one
221 new_statuses = []
346 new_statuses = []
222 repo = pull_request.source_repo
347 repo = pull_request.source_repo
223 for rev in pull_request.revisions:
348 for rev in pull_request.revisions:
224 new_status = _create_status(
349 new_status = _create_status(
225 user=user, repo=repo, status=status, comment=comment,
350 user=user, repo=repo, status=status, comment=comment,
226 revision=rev, pull_request=pull_request)
351 revision=rev, pull_request=pull_request)
227 new_statuses.append(new_status)
352 new_statuses.append(new_status)
228 Session().add(new_status)
353 Session().add(new_status)
229 return new_statuses
354 return new_statuses
230
355
231 def reviewers_statuses(self, pull_request):
356 def reviewers_statuses(self, pull_request):
232 _commit_statuses = self.get_statuses(
357 _commit_statuses = self.get_statuses(
233 pull_request.source_repo,
358 pull_request.source_repo,
234 pull_request=pull_request,
359 pull_request=pull_request,
235 with_revisions=True)
360 with_revisions=True)
236
361
237 commit_statuses = defaultdict(list)
362 commit_statuses = collections.defaultdict(list)
238 for st in _commit_statuses:
363 for st in _commit_statuses:
239 commit_statuses[st.author.username] += [st]
364 commit_statuses[st.author.username] += [st]
240
365
241 pull_request_reviewers = []
366 pull_request_reviewers = []
242
367
243 def version(commit_status):
368 def version(commit_status):
244 return commit_status.version
369 return commit_status.version
245
370
246 for o in pull_request.reviewers:
371 for obj in pull_request.reviewers:
247 if not o.user:
372 if not obj.user:
248 continue
373 continue
249 statuses = commit_statuses.get(o.user.username, None)
374 statuses = commit_statuses.get(obj.user.username, None)
250 if statuses:
375 if statuses:
251 statuses = [(x, list(y)[0])
376 status_groups = itertools.groupby(
252 for x, y in (itertools.groupby(
377 sorted(statuses, key=version), version)
253 sorted(statuses, key=version),version))]
378 statuses = [(x, list(y)[0]) for x, y in status_groups]
254
379
255 pull_request_reviewers.append(
380 pull_request_reviewers.append(
256 (o.user, o.reasons, o.mandatory, statuses))
381 (obj, obj.user, obj.reasons, obj.mandatory, statuses))
382
257 return pull_request_reviewers
383 return pull_request_reviewers
258
384
259 def calculated_review_status(self, pull_request, reviewers_statuses=None):
385 def calculated_review_status(self, pull_request, reviewers_statuses=None):
260 """
386 """
261 calculate pull request status based on reviewers, it should be a list
387 calculate pull request status based on reviewers, it should be a list
262 of two element lists.
388 of two element lists.
263
389
264 :param reviewers_statuses:
390 :param reviewers_statuses:
265 """
391 """
266 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
392 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
267 return self.calculate_status(reviewers)
393 return self.calculate_status(reviewers)
@@ -1,4402 +1,4452 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37 from sqlalchemy import (
37 from sqlalchemy import (
38 or_, and_, not_, func, TypeDecorator, event,
38 or_, and_, not_, func, TypeDecorator, event,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType)
41 Text, Float, PickleType)
42 from sqlalchemy.sql.expression import true, false
42 from sqlalchemy.sql.expression import true, false
43 from sqlalchemy.sql.functions import coalesce, count # noqa
43 from sqlalchemy.sql.functions import coalesce, count # noqa
44 from sqlalchemy.orm import (
44 from sqlalchemy.orm import (
45 relationship, joinedload, class_mapper, validates, aliased)
45 relationship, joinedload, class_mapper, validates, aliased)
46 from sqlalchemy.ext.declarative import declared_attr
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # noqa
48 from sqlalchemy.exc import IntegrityError # noqa
49 from sqlalchemy.dialects.mysql import LONGTEXT
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from beaker.cache import cache_region
50 from beaker.cache import cache_region
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
52
52
53 from pyramid.threadlocal import get_current_request
53 from pyramid.threadlocal import get_current_request
54
54
55 from rhodecode.translation import _
55 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance
56 from rhodecode.lib.vcs import get_vcs_instance
57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
58 from rhodecode.lib.utils2 import (
58 from rhodecode.lib.utils2 import (
59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re, StrictAttributeDict, cleaned_uri)
61 glob2re, StrictAttributeDict, cleaned_uri)
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
63 JsonRaw
64 from rhodecode.lib.ext_json import json
63 from rhodecode.lib.ext_json import json
65 from rhodecode.lib.caching_query import FromCache
64 from rhodecode.lib.caching_query import FromCache
66 from rhodecode.lib.encrypt import AESCipher
65 from rhodecode.lib.encrypt import AESCipher
67
66
68 from rhodecode.model.meta import Base, Session
67 from rhodecode.model.meta import Base, Session
69
68
70 URL_SEP = '/'
69 URL_SEP = '/'
71 log = logging.getLogger(__name__)
70 log = logging.getLogger(__name__)
72
71
73 # =============================================================================
72 # =============================================================================
74 # BASE CLASSES
73 # BASE CLASSES
75 # =============================================================================
74 # =============================================================================
76
75
77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
78 # beaker.session.secret if first is not set.
77 # beaker.session.secret if first is not set.
79 # and initialized at environment.py
78 # and initialized at environment.py
80 ENCRYPTION_KEY = None
79 ENCRYPTION_KEY = None
81
80
82 # used to sort permissions by types, '#' used here is not allowed to be in
81 # used to sort permissions by types, '#' used here is not allowed to be in
83 # usernames, and it's very early in sorted string.printable table.
82 # usernames, and it's very early in sorted string.printable table.
84 PERMISSION_TYPE_SORT = {
83 PERMISSION_TYPE_SORT = {
85 'admin': '####',
84 'admin': '####',
86 'write': '###',
85 'write': '###',
87 'read': '##',
86 'read': '##',
88 'none': '#',
87 'none': '#',
89 }
88 }
90
89
91
90
92 def display_user_sort(obj):
91 def display_user_sort(obj):
93 """
92 """
94 Sort function used to sort permissions in .permissions() function of
93 Sort function used to sort permissions in .permissions() function of
95 Repository, RepoGroup, UserGroup. Also it put the default user in front
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
96 of all other resources
95 of all other resources
97 """
96 """
98
97
99 if obj.username == User.DEFAULT_USER:
98 if obj.username == User.DEFAULT_USER:
100 return '#####'
99 return '#####'
101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
102 return prefix + obj.username
101 return prefix + obj.username
103
102
104
103
105 def display_user_group_sort(obj):
104 def display_user_group_sort(obj):
106 """
105 """
107 Sort function used to sort permissions in .permissions() function of
106 Sort function used to sort permissions in .permissions() function of
108 Repository, RepoGroup, UserGroup. Also it put the default user in front
107 Repository, RepoGroup, UserGroup. Also it put the default user in front
109 of all other resources
108 of all other resources
110 """
109 """
111
110
112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
111 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
113 return prefix + obj.users_group_name
112 return prefix + obj.users_group_name
114
113
115
114
116 def _hash_key(k):
115 def _hash_key(k):
117 return md5_safe(k)
116 return md5_safe(k)
118
117
119
118
120 def in_filter_generator(qry, items, limit=500):
119 def in_filter_generator(qry, items, limit=500):
121 """
120 """
122 Splits IN() into multiple with OR
121 Splits IN() into multiple with OR
123 e.g.::
122 e.g.::
124 cnt = Repository.query().filter(
123 cnt = Repository.query().filter(
125 or_(
124 or_(
126 *in_filter_generator(Repository.repo_id, range(100000))
125 *in_filter_generator(Repository.repo_id, range(100000))
127 )).count()
126 )).count()
128 """
127 """
129 if not items:
128 if not items:
130 # empty list will cause empty query which might cause security issues
129 # empty list will cause empty query which might cause security issues
131 # this can lead to hidden unpleasant results
130 # this can lead to hidden unpleasant results
132 items = [-1]
131 items = [-1]
133
132
134 parts = []
133 parts = []
135 for chunk in xrange(0, len(items), limit):
134 for chunk in xrange(0, len(items), limit):
136 parts.append(
135 parts.append(
137 qry.in_(items[chunk: chunk + limit])
136 qry.in_(items[chunk: chunk + limit])
138 )
137 )
139
138
140 return parts
139 return parts
141
140
142
141
143 class EncryptedTextValue(TypeDecorator):
142 class EncryptedTextValue(TypeDecorator):
144 """
143 """
145 Special column for encrypted long text data, use like::
144 Special column for encrypted long text data, use like::
146
145
147 value = Column("encrypted_value", EncryptedValue(), nullable=False)
146 value = Column("encrypted_value", EncryptedValue(), nullable=False)
148
147
149 This column is intelligent so if value is in unencrypted form it return
148 This column is intelligent so if value is in unencrypted form it return
150 unencrypted form, but on save it always encrypts
149 unencrypted form, but on save it always encrypts
151 """
150 """
152 impl = Text
151 impl = Text
153
152
154 def process_bind_param(self, value, dialect):
153 def process_bind_param(self, value, dialect):
155 if not value:
154 if not value:
156 return value
155 return value
157 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
156 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
158 # protect against double encrypting if someone manually starts
157 # protect against double encrypting if someone manually starts
159 # doing
158 # doing
160 raise ValueError('value needs to be in unencrypted format, ie. '
159 raise ValueError('value needs to be in unencrypted format, ie. '
161 'not starting with enc$aes')
160 'not starting with enc$aes')
162 return 'enc$aes_hmac$%s' % AESCipher(
161 return 'enc$aes_hmac$%s' % AESCipher(
163 ENCRYPTION_KEY, hmac=True).encrypt(value)
162 ENCRYPTION_KEY, hmac=True).encrypt(value)
164
163
165 def process_result_value(self, value, dialect):
164 def process_result_value(self, value, dialect):
166 import rhodecode
165 import rhodecode
167
166
168 if not value:
167 if not value:
169 return value
168 return value
170
169
171 parts = value.split('$', 3)
170 parts = value.split('$', 3)
172 if not len(parts) == 3:
171 if not len(parts) == 3:
173 # probably not encrypted values
172 # probably not encrypted values
174 return value
173 return value
175 else:
174 else:
176 if parts[0] != 'enc':
175 if parts[0] != 'enc':
177 # parts ok but without our header ?
176 # parts ok but without our header ?
178 return value
177 return value
179 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
178 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
180 'rhodecode.encrypted_values.strict') or True)
179 'rhodecode.encrypted_values.strict') or True)
181 # at that stage we know it's our encryption
180 # at that stage we know it's our encryption
182 if parts[1] == 'aes':
181 if parts[1] == 'aes':
183 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
182 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
184 elif parts[1] == 'aes_hmac':
183 elif parts[1] == 'aes_hmac':
185 decrypted_data = AESCipher(
184 decrypted_data = AESCipher(
186 ENCRYPTION_KEY, hmac=True,
185 ENCRYPTION_KEY, hmac=True,
187 strict_verification=enc_strict_mode).decrypt(parts[2])
186 strict_verification=enc_strict_mode).decrypt(parts[2])
188 else:
187 else:
189 raise ValueError(
188 raise ValueError(
190 'Encryption type part is wrong, must be `aes` '
189 'Encryption type part is wrong, must be `aes` '
191 'or `aes_hmac`, got `%s` instead' % (parts[1]))
190 'or `aes_hmac`, got `%s` instead' % (parts[1]))
192 return decrypted_data
191 return decrypted_data
193
192
194
193
195 class BaseModel(object):
194 class BaseModel(object):
196 """
195 """
197 Base Model for all classes
196 Base Model for all classes
198 """
197 """
199
198
200 @classmethod
199 @classmethod
201 def _get_keys(cls):
200 def _get_keys(cls):
202 """return column names for this model """
201 """return column names for this model """
203 return class_mapper(cls).c.keys()
202 return class_mapper(cls).c.keys()
204
203
205 def get_dict(self):
204 def get_dict(self):
206 """
205 """
207 return dict with keys and values corresponding
206 return dict with keys and values corresponding
208 to this model data """
207 to this model data """
209
208
210 d = {}
209 d = {}
211 for k in self._get_keys():
210 for k in self._get_keys():
212 d[k] = getattr(self, k)
211 d[k] = getattr(self, k)
213
212
214 # also use __json__() if present to get additional fields
213 # also use __json__() if present to get additional fields
215 _json_attr = getattr(self, '__json__', None)
214 _json_attr = getattr(self, '__json__', None)
216 if _json_attr:
215 if _json_attr:
217 # update with attributes from __json__
216 # update with attributes from __json__
218 if callable(_json_attr):
217 if callable(_json_attr):
219 _json_attr = _json_attr()
218 _json_attr = _json_attr()
220 for k, val in _json_attr.iteritems():
219 for k, val in _json_attr.iteritems():
221 d[k] = val
220 d[k] = val
222 return d
221 return d
223
222
224 def get_appstruct(self):
223 def get_appstruct(self):
225 """return list with keys and values tuples corresponding
224 """return list with keys and values tuples corresponding
226 to this model data """
225 to this model data """
227
226
228 lst = []
227 lst = []
229 for k in self._get_keys():
228 for k in self._get_keys():
230 lst.append((k, getattr(self, k),))
229 lst.append((k, getattr(self, k),))
231 return lst
230 return lst
232
231
233 def populate_obj(self, populate_dict):
232 def populate_obj(self, populate_dict):
234 """populate model with data from given populate_dict"""
233 """populate model with data from given populate_dict"""
235
234
236 for k in self._get_keys():
235 for k in self._get_keys():
237 if k in populate_dict:
236 if k in populate_dict:
238 setattr(self, k, populate_dict[k])
237 setattr(self, k, populate_dict[k])
239
238
240 @classmethod
239 @classmethod
241 def query(cls):
240 def query(cls):
242 return Session().query(cls)
241 return Session().query(cls)
243
242
244 @classmethod
243 @classmethod
245 def get(cls, id_):
244 def get(cls, id_):
246 if id_:
245 if id_:
247 return cls.query().get(id_)
246 return cls.query().get(id_)
248
247
249 @classmethod
248 @classmethod
250 def get_or_404(cls, id_):
249 def get_or_404(cls, id_):
251 from pyramid.httpexceptions import HTTPNotFound
250 from pyramid.httpexceptions import HTTPNotFound
252
251
253 try:
252 try:
254 id_ = int(id_)
253 id_ = int(id_)
255 except (TypeError, ValueError):
254 except (TypeError, ValueError):
256 raise HTTPNotFound()
255 raise HTTPNotFound()
257
256
258 res = cls.query().get(id_)
257 res = cls.query().get(id_)
259 if not res:
258 if not res:
260 raise HTTPNotFound()
259 raise HTTPNotFound()
261 return res
260 return res
262
261
263 @classmethod
262 @classmethod
264 def getAll(cls):
263 def getAll(cls):
265 # deprecated and left for backward compatibility
264 # deprecated and left for backward compatibility
266 return cls.get_all()
265 return cls.get_all()
267
266
268 @classmethod
267 @classmethod
269 def get_all(cls):
268 def get_all(cls):
270 return cls.query().all()
269 return cls.query().all()
271
270
272 @classmethod
271 @classmethod
273 def delete(cls, id_):
272 def delete(cls, id_):
274 obj = cls.query().get(id_)
273 obj = cls.query().get(id_)
275 Session().delete(obj)
274 Session().delete(obj)
276
275
277 @classmethod
276 @classmethod
278 def identity_cache(cls, session, attr_name, value):
277 def identity_cache(cls, session, attr_name, value):
279 exist_in_session = []
278 exist_in_session = []
280 for (item_cls, pkey), instance in session.identity_map.items():
279 for (item_cls, pkey), instance in session.identity_map.items():
281 if cls == item_cls and getattr(instance, attr_name) == value:
280 if cls == item_cls and getattr(instance, attr_name) == value:
282 exist_in_session.append(instance)
281 exist_in_session.append(instance)
283 if exist_in_session:
282 if exist_in_session:
284 if len(exist_in_session) == 1:
283 if len(exist_in_session) == 1:
285 return exist_in_session[0]
284 return exist_in_session[0]
286 log.exception(
285 log.exception(
287 'multiple objects with attr %s and '
286 'multiple objects with attr %s and '
288 'value %s found with same name: %r',
287 'value %s found with same name: %r',
289 attr_name, value, exist_in_session)
288 attr_name, value, exist_in_session)
290
289
291 def __repr__(self):
290 def __repr__(self):
292 if hasattr(self, '__unicode__'):
291 if hasattr(self, '__unicode__'):
293 # python repr needs to return str
292 # python repr needs to return str
294 try:
293 try:
295 return safe_str(self.__unicode__())
294 return safe_str(self.__unicode__())
296 except UnicodeDecodeError:
295 except UnicodeDecodeError:
297 pass
296 pass
298 return '<DB:%s>' % (self.__class__.__name__)
297 return '<DB:%s>' % (self.__class__.__name__)
299
298
300
299
301 class RhodeCodeSetting(Base, BaseModel):
300 class RhodeCodeSetting(Base, BaseModel):
302 __tablename__ = 'rhodecode_settings'
301 __tablename__ = 'rhodecode_settings'
303 __table_args__ = (
302 __table_args__ = (
304 UniqueConstraint('app_settings_name'),
303 UniqueConstraint('app_settings_name'),
305 {'extend_existing': True, 'mysql_engine': 'InnoDB',
304 {'extend_existing': True, 'mysql_engine': 'InnoDB',
306 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
305 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
307 )
306 )
308
307
309 SETTINGS_TYPES = {
308 SETTINGS_TYPES = {
310 'str': safe_str,
309 'str': safe_str,
311 'int': safe_int,
310 'int': safe_int,
312 'unicode': safe_unicode,
311 'unicode': safe_unicode,
313 'bool': str2bool,
312 'bool': str2bool,
314 'list': functools.partial(aslist, sep=',')
313 'list': functools.partial(aslist, sep=',')
315 }
314 }
316 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
315 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
317 GLOBAL_CONF_KEY = 'app_settings'
316 GLOBAL_CONF_KEY = 'app_settings'
318
317
319 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
318 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
320 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
319 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
321 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
320 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
322 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
321 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
323
322
324 def __init__(self, key='', val='', type='unicode'):
323 def __init__(self, key='', val='', type='unicode'):
325 self.app_settings_name = key
324 self.app_settings_name = key
326 self.app_settings_type = type
325 self.app_settings_type = type
327 self.app_settings_value = val
326 self.app_settings_value = val
328
327
329 @validates('_app_settings_value')
328 @validates('_app_settings_value')
330 def validate_settings_value(self, key, val):
329 def validate_settings_value(self, key, val):
331 assert type(val) == unicode
330 assert type(val) == unicode
332 return val
331 return val
333
332
334 @hybrid_property
333 @hybrid_property
335 def app_settings_value(self):
334 def app_settings_value(self):
336 v = self._app_settings_value
335 v = self._app_settings_value
337 _type = self.app_settings_type
336 _type = self.app_settings_type
338 if _type:
337 if _type:
339 _type = self.app_settings_type.split('.')[0]
338 _type = self.app_settings_type.split('.')[0]
340 # decode the encrypted value
339 # decode the encrypted value
341 if 'encrypted' in self.app_settings_type:
340 if 'encrypted' in self.app_settings_type:
342 cipher = EncryptedTextValue()
341 cipher = EncryptedTextValue()
343 v = safe_unicode(cipher.process_result_value(v, None))
342 v = safe_unicode(cipher.process_result_value(v, None))
344
343
345 converter = self.SETTINGS_TYPES.get(_type) or \
344 converter = self.SETTINGS_TYPES.get(_type) or \
346 self.SETTINGS_TYPES['unicode']
345 self.SETTINGS_TYPES['unicode']
347 return converter(v)
346 return converter(v)
348
347
349 @app_settings_value.setter
348 @app_settings_value.setter
350 def app_settings_value(self, val):
349 def app_settings_value(self, val):
351 """
350 """
352 Setter that will always make sure we use unicode in app_settings_value
351 Setter that will always make sure we use unicode in app_settings_value
353
352
354 :param val:
353 :param val:
355 """
354 """
356 val = safe_unicode(val)
355 val = safe_unicode(val)
357 # encode the encrypted value
356 # encode the encrypted value
358 if 'encrypted' in self.app_settings_type:
357 if 'encrypted' in self.app_settings_type:
359 cipher = EncryptedTextValue()
358 cipher = EncryptedTextValue()
360 val = safe_unicode(cipher.process_bind_param(val, None))
359 val = safe_unicode(cipher.process_bind_param(val, None))
361 self._app_settings_value = val
360 self._app_settings_value = val
362
361
363 @hybrid_property
362 @hybrid_property
364 def app_settings_type(self):
363 def app_settings_type(self):
365 return self._app_settings_type
364 return self._app_settings_type
366
365
367 @app_settings_type.setter
366 @app_settings_type.setter
368 def app_settings_type(self, val):
367 def app_settings_type(self, val):
369 if val.split('.')[0] not in self.SETTINGS_TYPES:
368 if val.split('.')[0] not in self.SETTINGS_TYPES:
370 raise Exception('type must be one of %s got %s'
369 raise Exception('type must be one of %s got %s'
371 % (self.SETTINGS_TYPES.keys(), val))
370 % (self.SETTINGS_TYPES.keys(), val))
372 self._app_settings_type = val
371 self._app_settings_type = val
373
372
374 def __unicode__(self):
373 def __unicode__(self):
375 return u"<%s('%s:%s[%s]')>" % (
374 return u"<%s('%s:%s[%s]')>" % (
376 self.__class__.__name__,
375 self.__class__.__name__,
377 self.app_settings_name, self.app_settings_value,
376 self.app_settings_name, self.app_settings_value,
378 self.app_settings_type
377 self.app_settings_type
379 )
378 )
380
379
381
380
382 class RhodeCodeUi(Base, BaseModel):
381 class RhodeCodeUi(Base, BaseModel):
383 __tablename__ = 'rhodecode_ui'
382 __tablename__ = 'rhodecode_ui'
384 __table_args__ = (
383 __table_args__ = (
385 UniqueConstraint('ui_key'),
384 UniqueConstraint('ui_key'),
386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 )
387 )
389
388
390 HOOK_REPO_SIZE = 'changegroup.repo_size'
389 HOOK_REPO_SIZE = 'changegroup.repo_size'
391 # HG
390 # HG
392 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
391 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
393 HOOK_PULL = 'outgoing.pull_logger'
392 HOOK_PULL = 'outgoing.pull_logger'
394 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
393 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
395 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
394 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
396 HOOK_PUSH = 'changegroup.push_logger'
395 HOOK_PUSH = 'changegroup.push_logger'
397 HOOK_PUSH_KEY = 'pushkey.key_push'
396 HOOK_PUSH_KEY = 'pushkey.key_push'
398
397
399 # TODO: johbo: Unify way how hooks are configured for git and hg,
398 # TODO: johbo: Unify way how hooks are configured for git and hg,
400 # git part is currently hardcoded.
399 # git part is currently hardcoded.
401
400
402 # SVN PATTERNS
401 # SVN PATTERNS
403 SVN_BRANCH_ID = 'vcs_svn_branch'
402 SVN_BRANCH_ID = 'vcs_svn_branch'
404 SVN_TAG_ID = 'vcs_svn_tag'
403 SVN_TAG_ID = 'vcs_svn_tag'
405
404
406 ui_id = Column(
405 ui_id = Column(
407 "ui_id", Integer(), nullable=False, unique=True, default=None,
406 "ui_id", Integer(), nullable=False, unique=True, default=None,
408 primary_key=True)
407 primary_key=True)
409 ui_section = Column(
408 ui_section = Column(
410 "ui_section", String(255), nullable=True, unique=None, default=None)
409 "ui_section", String(255), nullable=True, unique=None, default=None)
411 ui_key = Column(
410 ui_key = Column(
412 "ui_key", String(255), nullable=True, unique=None, default=None)
411 "ui_key", String(255), nullable=True, unique=None, default=None)
413 ui_value = Column(
412 ui_value = Column(
414 "ui_value", String(255), nullable=True, unique=None, default=None)
413 "ui_value", String(255), nullable=True, unique=None, default=None)
415 ui_active = Column(
414 ui_active = Column(
416 "ui_active", Boolean(), nullable=True, unique=None, default=True)
415 "ui_active", Boolean(), nullable=True, unique=None, default=True)
417
416
418 def __repr__(self):
417 def __repr__(self):
419 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
418 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
420 self.ui_key, self.ui_value)
419 self.ui_key, self.ui_value)
421
420
422
421
423 class RepoRhodeCodeSetting(Base, BaseModel):
422 class RepoRhodeCodeSetting(Base, BaseModel):
424 __tablename__ = 'repo_rhodecode_settings'
423 __tablename__ = 'repo_rhodecode_settings'
425 __table_args__ = (
424 __table_args__ = (
426 UniqueConstraint(
425 UniqueConstraint(
427 'app_settings_name', 'repository_id',
426 'app_settings_name', 'repository_id',
428 name='uq_repo_rhodecode_setting_name_repo_id'),
427 name='uq_repo_rhodecode_setting_name_repo_id'),
429 {'extend_existing': True, 'mysql_engine': 'InnoDB',
428 {'extend_existing': True, 'mysql_engine': 'InnoDB',
430 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
429 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
431 )
430 )
432
431
433 repository_id = Column(
432 repository_id = Column(
434 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
433 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
435 nullable=False)
434 nullable=False)
436 app_settings_id = Column(
435 app_settings_id = Column(
437 "app_settings_id", Integer(), nullable=False, unique=True,
436 "app_settings_id", Integer(), nullable=False, unique=True,
438 default=None, primary_key=True)
437 default=None, primary_key=True)
439 app_settings_name = Column(
438 app_settings_name = Column(
440 "app_settings_name", String(255), nullable=True, unique=None,
439 "app_settings_name", String(255), nullable=True, unique=None,
441 default=None)
440 default=None)
442 _app_settings_value = Column(
441 _app_settings_value = Column(
443 "app_settings_value", String(4096), nullable=True, unique=None,
442 "app_settings_value", String(4096), nullable=True, unique=None,
444 default=None)
443 default=None)
445 _app_settings_type = Column(
444 _app_settings_type = Column(
446 "app_settings_type", String(255), nullable=True, unique=None,
445 "app_settings_type", String(255), nullable=True, unique=None,
447 default=None)
446 default=None)
448
447
449 repository = relationship('Repository')
448 repository = relationship('Repository')
450
449
451 def __init__(self, repository_id, key='', val='', type='unicode'):
450 def __init__(self, repository_id, key='', val='', type='unicode'):
452 self.repository_id = repository_id
451 self.repository_id = repository_id
453 self.app_settings_name = key
452 self.app_settings_name = key
454 self.app_settings_type = type
453 self.app_settings_type = type
455 self.app_settings_value = val
454 self.app_settings_value = val
456
455
457 @validates('_app_settings_value')
456 @validates('_app_settings_value')
458 def validate_settings_value(self, key, val):
457 def validate_settings_value(self, key, val):
459 assert type(val) == unicode
458 assert type(val) == unicode
460 return val
459 return val
461
460
462 @hybrid_property
461 @hybrid_property
463 def app_settings_value(self):
462 def app_settings_value(self):
464 v = self._app_settings_value
463 v = self._app_settings_value
465 type_ = self.app_settings_type
464 type_ = self.app_settings_type
466 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
465 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
467 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
466 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
468 return converter(v)
467 return converter(v)
469
468
470 @app_settings_value.setter
469 @app_settings_value.setter
471 def app_settings_value(self, val):
470 def app_settings_value(self, val):
472 """
471 """
473 Setter that will always make sure we use unicode in app_settings_value
472 Setter that will always make sure we use unicode in app_settings_value
474
473
475 :param val:
474 :param val:
476 """
475 """
477 self._app_settings_value = safe_unicode(val)
476 self._app_settings_value = safe_unicode(val)
478
477
479 @hybrid_property
478 @hybrid_property
480 def app_settings_type(self):
479 def app_settings_type(self):
481 return self._app_settings_type
480 return self._app_settings_type
482
481
483 @app_settings_type.setter
482 @app_settings_type.setter
484 def app_settings_type(self, val):
483 def app_settings_type(self, val):
485 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
484 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
486 if val not in SETTINGS_TYPES:
485 if val not in SETTINGS_TYPES:
487 raise Exception('type must be one of %s got %s'
486 raise Exception('type must be one of %s got %s'
488 % (SETTINGS_TYPES.keys(), val))
487 % (SETTINGS_TYPES.keys(), val))
489 self._app_settings_type = val
488 self._app_settings_type = val
490
489
491 def __unicode__(self):
490 def __unicode__(self):
492 return u"<%s('%s:%s:%s[%s]')>" % (
491 return u"<%s('%s:%s:%s[%s]')>" % (
493 self.__class__.__name__, self.repository.repo_name,
492 self.__class__.__name__, self.repository.repo_name,
494 self.app_settings_name, self.app_settings_value,
493 self.app_settings_name, self.app_settings_value,
495 self.app_settings_type
494 self.app_settings_type
496 )
495 )
497
496
498
497
499 class RepoRhodeCodeUi(Base, BaseModel):
498 class RepoRhodeCodeUi(Base, BaseModel):
500 __tablename__ = 'repo_rhodecode_ui'
499 __tablename__ = 'repo_rhodecode_ui'
501 __table_args__ = (
500 __table_args__ = (
502 UniqueConstraint(
501 UniqueConstraint(
503 'repository_id', 'ui_section', 'ui_key',
502 'repository_id', 'ui_section', 'ui_key',
504 name='uq_repo_rhodecode_ui_repository_id_section_key'),
503 name='uq_repo_rhodecode_ui_repository_id_section_key'),
505 {'extend_existing': True, 'mysql_engine': 'InnoDB',
504 {'extend_existing': True, 'mysql_engine': 'InnoDB',
506 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
505 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
507 )
506 )
508
507
509 repository_id = Column(
508 repository_id = Column(
510 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
509 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
511 nullable=False)
510 nullable=False)
512 ui_id = Column(
511 ui_id = Column(
513 "ui_id", Integer(), nullable=False, unique=True, default=None,
512 "ui_id", Integer(), nullable=False, unique=True, default=None,
514 primary_key=True)
513 primary_key=True)
515 ui_section = Column(
514 ui_section = Column(
516 "ui_section", String(255), nullable=True, unique=None, default=None)
515 "ui_section", String(255), nullable=True, unique=None, default=None)
517 ui_key = Column(
516 ui_key = Column(
518 "ui_key", String(255), nullable=True, unique=None, default=None)
517 "ui_key", String(255), nullable=True, unique=None, default=None)
519 ui_value = Column(
518 ui_value = Column(
520 "ui_value", String(255), nullable=True, unique=None, default=None)
519 "ui_value", String(255), nullable=True, unique=None, default=None)
521 ui_active = Column(
520 ui_active = Column(
522 "ui_active", Boolean(), nullable=True, unique=None, default=True)
521 "ui_active", Boolean(), nullable=True, unique=None, default=True)
523
522
524 repository = relationship('Repository')
523 repository = relationship('Repository')
525
524
526 def __repr__(self):
525 def __repr__(self):
527 return '<%s[%s:%s]%s=>%s]>' % (
526 return '<%s[%s:%s]%s=>%s]>' % (
528 self.__class__.__name__, self.repository.repo_name,
527 self.__class__.__name__, self.repository.repo_name,
529 self.ui_section, self.ui_key, self.ui_value)
528 self.ui_section, self.ui_key, self.ui_value)
530
529
531
530
532 class User(Base, BaseModel):
531 class User(Base, BaseModel):
533 __tablename__ = 'users'
532 __tablename__ = 'users'
534 __table_args__ = (
533 __table_args__ = (
535 UniqueConstraint('username'), UniqueConstraint('email'),
534 UniqueConstraint('username'), UniqueConstraint('email'),
536 Index('u_username_idx', 'username'),
535 Index('u_username_idx', 'username'),
537 Index('u_email_idx', 'email'),
536 Index('u_email_idx', 'email'),
538 {'extend_existing': True, 'mysql_engine': 'InnoDB',
537 {'extend_existing': True, 'mysql_engine': 'InnoDB',
539 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
538 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
540 )
539 )
541 DEFAULT_USER = 'default'
540 DEFAULT_USER = 'default'
542 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
541 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
543 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
542 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
544
543
545 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
544 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
546 username = Column("username", String(255), nullable=True, unique=None, default=None)
545 username = Column("username", String(255), nullable=True, unique=None, default=None)
547 password = Column("password", String(255), nullable=True, unique=None, default=None)
546 password = Column("password", String(255), nullable=True, unique=None, default=None)
548 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
547 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
549 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
548 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
550 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
549 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
551 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
550 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
552 _email = Column("email", String(255), nullable=True, unique=None, default=None)
551 _email = Column("email", String(255), nullable=True, unique=None, default=None)
553 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
552 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
554 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
553 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
555
554
556 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
555 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
557 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
556 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
558 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
557 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
559 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
558 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
560 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
559 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
561 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
560 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
562
561
563 user_log = relationship('UserLog')
562 user_log = relationship('UserLog')
564 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
563 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
565
564
566 repositories = relationship('Repository')
565 repositories = relationship('Repository')
567 repository_groups = relationship('RepoGroup')
566 repository_groups = relationship('RepoGroup')
568 user_groups = relationship('UserGroup')
567 user_groups = relationship('UserGroup')
569
568
570 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
569 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
571 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
570 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
572
571
573 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
572 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
574 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
573 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
575 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
574 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
576
575
577 group_member = relationship('UserGroupMember', cascade='all')
576 group_member = relationship('UserGroupMember', cascade='all')
578
577
579 notifications = relationship('UserNotification', cascade='all')
578 notifications = relationship('UserNotification', cascade='all')
580 # notifications assigned to this user
579 # notifications assigned to this user
581 user_created_notifications = relationship('Notification', cascade='all')
580 user_created_notifications = relationship('Notification', cascade='all')
582 # comments created by this user
581 # comments created by this user
583 user_comments = relationship('ChangesetComment', cascade='all')
582 user_comments = relationship('ChangesetComment', cascade='all')
584 # user profile extra info
583 # user profile extra info
585 user_emails = relationship('UserEmailMap', cascade='all')
584 user_emails = relationship('UserEmailMap', cascade='all')
586 user_ip_map = relationship('UserIpMap', cascade='all')
585 user_ip_map = relationship('UserIpMap', cascade='all')
587 user_auth_tokens = relationship('UserApiKeys', cascade='all')
586 user_auth_tokens = relationship('UserApiKeys', cascade='all')
588 user_ssh_keys = relationship('UserSshKeys', cascade='all')
587 user_ssh_keys = relationship('UserSshKeys', cascade='all')
589
588
590 # gists
589 # gists
591 user_gists = relationship('Gist', cascade='all')
590 user_gists = relationship('Gist', cascade='all')
592 # user pull requests
591 # user pull requests
593 user_pull_requests = relationship('PullRequest', cascade='all')
592 user_pull_requests = relationship('PullRequest', cascade='all')
594 # external identities
593 # external identities
595 extenal_identities = relationship(
594 extenal_identities = relationship(
596 'ExternalIdentity',
595 'ExternalIdentity',
597 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
596 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
598 cascade='all')
597 cascade='all')
599 # review rules
598 # review rules
600 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
599 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
601
600
602 def __unicode__(self):
601 def __unicode__(self):
603 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
602 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
604 self.user_id, self.username)
603 self.user_id, self.username)
605
604
606 @hybrid_property
605 @hybrid_property
607 def email(self):
606 def email(self):
608 return self._email
607 return self._email
609
608
610 @email.setter
609 @email.setter
611 def email(self, val):
610 def email(self, val):
612 self._email = val.lower() if val else None
611 self._email = val.lower() if val else None
613
612
614 @hybrid_property
613 @hybrid_property
615 def first_name(self):
614 def first_name(self):
616 from rhodecode.lib import helpers as h
615 from rhodecode.lib import helpers as h
617 if self.name:
616 if self.name:
618 return h.escape(self.name)
617 return h.escape(self.name)
619 return self.name
618 return self.name
620
619
621 @hybrid_property
620 @hybrid_property
622 def last_name(self):
621 def last_name(self):
623 from rhodecode.lib import helpers as h
622 from rhodecode.lib import helpers as h
624 if self.lastname:
623 if self.lastname:
625 return h.escape(self.lastname)
624 return h.escape(self.lastname)
626 return self.lastname
625 return self.lastname
627
626
628 @hybrid_property
627 @hybrid_property
629 def api_key(self):
628 def api_key(self):
630 """
629 """
631 Fetch if exist an auth-token with role ALL connected to this user
630 Fetch if exist an auth-token with role ALL connected to this user
632 """
631 """
633 user_auth_token = UserApiKeys.query()\
632 user_auth_token = UserApiKeys.query()\
634 .filter(UserApiKeys.user_id == self.user_id)\
633 .filter(UserApiKeys.user_id == self.user_id)\
635 .filter(or_(UserApiKeys.expires == -1,
634 .filter(or_(UserApiKeys.expires == -1,
636 UserApiKeys.expires >= time.time()))\
635 UserApiKeys.expires >= time.time()))\
637 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
636 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
638 if user_auth_token:
637 if user_auth_token:
639 user_auth_token = user_auth_token.api_key
638 user_auth_token = user_auth_token.api_key
640
639
641 return user_auth_token
640 return user_auth_token
642
641
643 @api_key.setter
642 @api_key.setter
644 def api_key(self, val):
643 def api_key(self, val):
645 # don't allow to set API key this is deprecated for now
644 # don't allow to set API key this is deprecated for now
646 self._api_key = None
645 self._api_key = None
647
646
648 @property
647 @property
649 def reviewer_pull_requests(self):
648 def reviewer_pull_requests(self):
650 return PullRequestReviewers.query() \
649 return PullRequestReviewers.query() \
651 .options(joinedload(PullRequestReviewers.pull_request)) \
650 .options(joinedload(PullRequestReviewers.pull_request)) \
652 .filter(PullRequestReviewers.user_id == self.user_id) \
651 .filter(PullRequestReviewers.user_id == self.user_id) \
653 .all()
652 .all()
654
653
655 @property
654 @property
656 def firstname(self):
655 def firstname(self):
657 # alias for future
656 # alias for future
658 return self.name
657 return self.name
659
658
660 @property
659 @property
661 def emails(self):
660 def emails(self):
662 other = UserEmailMap.query()\
661 other = UserEmailMap.query()\
663 .filter(UserEmailMap.user == self) \
662 .filter(UserEmailMap.user == self) \
664 .order_by(UserEmailMap.email_id.asc()) \
663 .order_by(UserEmailMap.email_id.asc()) \
665 .all()
664 .all()
666 return [self.email] + [x.email for x in other]
665 return [self.email] + [x.email for x in other]
667
666
668 @property
667 @property
669 def auth_tokens(self):
668 def auth_tokens(self):
670 auth_tokens = self.get_auth_tokens()
669 auth_tokens = self.get_auth_tokens()
671 return [x.api_key for x in auth_tokens]
670 return [x.api_key for x in auth_tokens]
672
671
673 def get_auth_tokens(self):
672 def get_auth_tokens(self):
674 return UserApiKeys.query()\
673 return UserApiKeys.query()\
675 .filter(UserApiKeys.user == self)\
674 .filter(UserApiKeys.user == self)\
676 .order_by(UserApiKeys.user_api_key_id.asc())\
675 .order_by(UserApiKeys.user_api_key_id.asc())\
677 .all()
676 .all()
678
677
679 @LazyProperty
678 @LazyProperty
680 def feed_token(self):
679 def feed_token(self):
681 return self.get_feed_token()
680 return self.get_feed_token()
682
681
683 def get_feed_token(self, cache=True):
682 def get_feed_token(self, cache=True):
684 feed_tokens = UserApiKeys.query()\
683 feed_tokens = UserApiKeys.query()\
685 .filter(UserApiKeys.user == self)\
684 .filter(UserApiKeys.user == self)\
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
685 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
687 if cache:
686 if cache:
688 feed_tokens = feed_tokens.options(
687 feed_tokens = feed_tokens.options(
689 FromCache("long_term", "get_user_feed_token_%s" % self.user_id))
688 FromCache("long_term", "get_user_feed_token_%s" % self.user_id))
690
689
691 feed_tokens = feed_tokens.all()
690 feed_tokens = feed_tokens.all()
692 if feed_tokens:
691 if feed_tokens:
693 return feed_tokens[0].api_key
692 return feed_tokens[0].api_key
694 return 'NO_FEED_TOKEN_AVAILABLE'
693 return 'NO_FEED_TOKEN_AVAILABLE'
695
694
696 @classmethod
695 @classmethod
697 def get(cls, user_id, cache=False):
696 def get(cls, user_id, cache=False):
698 if not user_id:
697 if not user_id:
699 return
698 return
700
699
701 user = cls.query()
700 user = cls.query()
702 if cache:
701 if cache:
703 user = user.options(
702 user = user.options(
704 FromCache("sql_cache_short", "get_users_%s" % user_id))
703 FromCache("sql_cache_short", "get_users_%s" % user_id))
705 return user.get(user_id)
704 return user.get(user_id)
706
705
707 @classmethod
706 @classmethod
708 def extra_valid_auth_tokens(cls, user, role=None):
707 def extra_valid_auth_tokens(cls, user, role=None):
709 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
708 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
710 .filter(or_(UserApiKeys.expires == -1,
709 .filter(or_(UserApiKeys.expires == -1,
711 UserApiKeys.expires >= time.time()))
710 UserApiKeys.expires >= time.time()))
712 if role:
711 if role:
713 tokens = tokens.filter(or_(UserApiKeys.role == role,
712 tokens = tokens.filter(or_(UserApiKeys.role == role,
714 UserApiKeys.role == UserApiKeys.ROLE_ALL))
713 UserApiKeys.role == UserApiKeys.ROLE_ALL))
715 return tokens.all()
714 return tokens.all()
716
715
717 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
716 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
718 from rhodecode.lib import auth
717 from rhodecode.lib import auth
719
718
720 log.debug('Trying to authenticate user: %s via auth-token, '
719 log.debug('Trying to authenticate user: %s via auth-token, '
721 'and roles: %s', self, roles)
720 'and roles: %s', self, roles)
722
721
723 if not auth_token:
722 if not auth_token:
724 return False
723 return False
725
724
726 crypto_backend = auth.crypto_backend()
725 crypto_backend = auth.crypto_backend()
727
726
728 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
727 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
729 tokens_q = UserApiKeys.query()\
728 tokens_q = UserApiKeys.query()\
730 .filter(UserApiKeys.user_id == self.user_id)\
729 .filter(UserApiKeys.user_id == self.user_id)\
731 .filter(or_(UserApiKeys.expires == -1,
730 .filter(or_(UserApiKeys.expires == -1,
732 UserApiKeys.expires >= time.time()))
731 UserApiKeys.expires >= time.time()))
733
732
734 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
733 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
735
734
736 plain_tokens = []
735 plain_tokens = []
737 hash_tokens = []
736 hash_tokens = []
738
737
739 for token in tokens_q.all():
738 for token in tokens_q.all():
740 # verify scope first
739 # verify scope first
741 if token.repo_id:
740 if token.repo_id:
742 # token has a scope, we need to verify it
741 # token has a scope, we need to verify it
743 if scope_repo_id != token.repo_id:
742 if scope_repo_id != token.repo_id:
744 log.debug(
743 log.debug(
745 'Scope mismatch: token has a set repo scope: %s, '
744 'Scope mismatch: token has a set repo scope: %s, '
746 'and calling scope is:%s, skipping further checks',
745 'and calling scope is:%s, skipping further checks',
747 token.repo, scope_repo_id)
746 token.repo, scope_repo_id)
748 # token has a scope, and it doesn't match, skip token
747 # token has a scope, and it doesn't match, skip token
749 continue
748 continue
750
749
751 if token.api_key.startswith(crypto_backend.ENC_PREF):
750 if token.api_key.startswith(crypto_backend.ENC_PREF):
752 hash_tokens.append(token.api_key)
751 hash_tokens.append(token.api_key)
753 else:
752 else:
754 plain_tokens.append(token.api_key)
753 plain_tokens.append(token.api_key)
755
754
756 is_plain_match = auth_token in plain_tokens
755 is_plain_match = auth_token in plain_tokens
757 if is_plain_match:
756 if is_plain_match:
758 return True
757 return True
759
758
760 for hashed in hash_tokens:
759 for hashed in hash_tokens:
761 # TODO(marcink): this is expensive to calculate, but most secure
760 # TODO(marcink): this is expensive to calculate, but most secure
762 match = crypto_backend.hash_check(auth_token, hashed)
761 match = crypto_backend.hash_check(auth_token, hashed)
763 if match:
762 if match:
764 return True
763 return True
765
764
766 return False
765 return False
767
766
768 @property
767 @property
769 def ip_addresses(self):
768 def ip_addresses(self):
770 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
769 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
771 return [x.ip_addr for x in ret]
770 return [x.ip_addr for x in ret]
772
771
773 @property
772 @property
774 def username_and_name(self):
773 def username_and_name(self):
775 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
774 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
776
775
777 @property
776 @property
778 def username_or_name_or_email(self):
777 def username_or_name_or_email(self):
779 full_name = self.full_name if self.full_name is not ' ' else None
778 full_name = self.full_name if self.full_name is not ' ' else None
780 return self.username or full_name or self.email
779 return self.username or full_name or self.email
781
780
782 @property
781 @property
783 def full_name(self):
782 def full_name(self):
784 return '%s %s' % (self.first_name, self.last_name)
783 return '%s %s' % (self.first_name, self.last_name)
785
784
786 @property
785 @property
787 def full_name_or_username(self):
786 def full_name_or_username(self):
788 return ('%s %s' % (self.first_name, self.last_name)
787 return ('%s %s' % (self.first_name, self.last_name)
789 if (self.first_name and self.last_name) else self.username)
788 if (self.first_name and self.last_name) else self.username)
790
789
791 @property
790 @property
792 def full_contact(self):
791 def full_contact(self):
793 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
792 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
794
793
795 @property
794 @property
796 def short_contact(self):
795 def short_contact(self):
797 return '%s %s' % (self.first_name, self.last_name)
796 return '%s %s' % (self.first_name, self.last_name)
798
797
799 @property
798 @property
800 def is_admin(self):
799 def is_admin(self):
801 return self.admin
800 return self.admin
802
801
803 def AuthUser(self, **kwargs):
802 def AuthUser(self, **kwargs):
804 """
803 """
805 Returns instance of AuthUser for this user
804 Returns instance of AuthUser for this user
806 """
805 """
807 from rhodecode.lib.auth import AuthUser
806 from rhodecode.lib.auth import AuthUser
808 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
807 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
809
808
810 @hybrid_property
809 @hybrid_property
811 def user_data(self):
810 def user_data(self):
812 if not self._user_data:
811 if not self._user_data:
813 return {}
812 return {}
814
813
815 try:
814 try:
816 return json.loads(self._user_data)
815 return json.loads(self._user_data)
817 except TypeError:
816 except TypeError:
818 return {}
817 return {}
819
818
820 @user_data.setter
819 @user_data.setter
821 def user_data(self, val):
820 def user_data(self, val):
822 if not isinstance(val, dict):
821 if not isinstance(val, dict):
823 raise Exception('user_data must be dict, got %s' % type(val))
822 raise Exception('user_data must be dict, got %s' % type(val))
824 try:
823 try:
825 self._user_data = json.dumps(val)
824 self._user_data = json.dumps(val)
826 except Exception:
825 except Exception:
827 log.error(traceback.format_exc())
826 log.error(traceback.format_exc())
828
827
829 @classmethod
828 @classmethod
830 def get_by_username(cls, username, case_insensitive=False,
829 def get_by_username(cls, username, case_insensitive=False,
831 cache=False, identity_cache=False):
830 cache=False, identity_cache=False):
832 session = Session()
831 session = Session()
833
832
834 if case_insensitive:
833 if case_insensitive:
835 q = cls.query().filter(
834 q = cls.query().filter(
836 func.lower(cls.username) == func.lower(username))
835 func.lower(cls.username) == func.lower(username))
837 else:
836 else:
838 q = cls.query().filter(cls.username == username)
837 q = cls.query().filter(cls.username == username)
839
838
840 if cache:
839 if cache:
841 if identity_cache:
840 if identity_cache:
842 val = cls.identity_cache(session, 'username', username)
841 val = cls.identity_cache(session, 'username', username)
843 if val:
842 if val:
844 return val
843 return val
845 else:
844 else:
846 cache_key = "get_user_by_name_%s" % _hash_key(username)
845 cache_key = "get_user_by_name_%s" % _hash_key(username)
847 q = q.options(
846 q = q.options(
848 FromCache("sql_cache_short", cache_key))
847 FromCache("sql_cache_short", cache_key))
849
848
850 return q.scalar()
849 return q.scalar()
851
850
852 @classmethod
851 @classmethod
853 def get_by_auth_token(cls, auth_token, cache=False):
852 def get_by_auth_token(cls, auth_token, cache=False):
854 q = UserApiKeys.query()\
853 q = UserApiKeys.query()\
855 .filter(UserApiKeys.api_key == auth_token)\
854 .filter(UserApiKeys.api_key == auth_token)\
856 .filter(or_(UserApiKeys.expires == -1,
855 .filter(or_(UserApiKeys.expires == -1,
857 UserApiKeys.expires >= time.time()))
856 UserApiKeys.expires >= time.time()))
858 if cache:
857 if cache:
859 q = q.options(
858 q = q.options(
860 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
859 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
861
860
862 match = q.first()
861 match = q.first()
863 if match:
862 if match:
864 return match.user
863 return match.user
865
864
866 @classmethod
865 @classmethod
867 def get_by_email(cls, email, case_insensitive=False, cache=False):
866 def get_by_email(cls, email, case_insensitive=False, cache=False):
868
867
869 if case_insensitive:
868 if case_insensitive:
870 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
869 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
871
870
872 else:
871 else:
873 q = cls.query().filter(cls.email == email)
872 q = cls.query().filter(cls.email == email)
874
873
875 email_key = _hash_key(email)
874 email_key = _hash_key(email)
876 if cache:
875 if cache:
877 q = q.options(
876 q = q.options(
878 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
877 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
879
878
880 ret = q.scalar()
879 ret = q.scalar()
881 if ret is None:
880 if ret is None:
882 q = UserEmailMap.query()
881 q = UserEmailMap.query()
883 # try fetching in alternate email map
882 # try fetching in alternate email map
884 if case_insensitive:
883 if case_insensitive:
885 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
884 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
886 else:
885 else:
887 q = q.filter(UserEmailMap.email == email)
886 q = q.filter(UserEmailMap.email == email)
888 q = q.options(joinedload(UserEmailMap.user))
887 q = q.options(joinedload(UserEmailMap.user))
889 if cache:
888 if cache:
890 q = q.options(
889 q = q.options(
891 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
890 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
892 ret = getattr(q.scalar(), 'user', None)
891 ret = getattr(q.scalar(), 'user', None)
893
892
894 return ret
893 return ret
895
894
896 @classmethod
895 @classmethod
897 def get_from_cs_author(cls, author):
896 def get_from_cs_author(cls, author):
898 """
897 """
899 Tries to get User objects out of commit author string
898 Tries to get User objects out of commit author string
900
899
901 :param author:
900 :param author:
902 """
901 """
903 from rhodecode.lib.helpers import email, author_name
902 from rhodecode.lib.helpers import email, author_name
904 # Valid email in the attribute passed, see if they're in the system
903 # Valid email in the attribute passed, see if they're in the system
905 _email = email(author)
904 _email = email(author)
906 if _email:
905 if _email:
907 user = cls.get_by_email(_email, case_insensitive=True)
906 user = cls.get_by_email(_email, case_insensitive=True)
908 if user:
907 if user:
909 return user
908 return user
910 # Maybe we can match by username?
909 # Maybe we can match by username?
911 _author = author_name(author)
910 _author = author_name(author)
912 user = cls.get_by_username(_author, case_insensitive=True)
911 user = cls.get_by_username(_author, case_insensitive=True)
913 if user:
912 if user:
914 return user
913 return user
915
914
916 def update_userdata(self, **kwargs):
915 def update_userdata(self, **kwargs):
917 usr = self
916 usr = self
918 old = usr.user_data
917 old = usr.user_data
919 old.update(**kwargs)
918 old.update(**kwargs)
920 usr.user_data = old
919 usr.user_data = old
921 Session().add(usr)
920 Session().add(usr)
922 log.debug('updated userdata with ', kwargs)
921 log.debug('updated userdata with ', kwargs)
923
922
924 def update_lastlogin(self):
923 def update_lastlogin(self):
925 """Update user lastlogin"""
924 """Update user lastlogin"""
926 self.last_login = datetime.datetime.now()
925 self.last_login = datetime.datetime.now()
927 Session().add(self)
926 Session().add(self)
928 log.debug('updated user %s lastlogin', self.username)
927 log.debug('updated user %s lastlogin', self.username)
929
928
930 def update_lastactivity(self):
929 def update_lastactivity(self):
931 """Update user lastactivity"""
930 """Update user lastactivity"""
932 self.last_activity = datetime.datetime.now()
931 self.last_activity = datetime.datetime.now()
933 Session().add(self)
932 Session().add(self)
934 log.debug('updated user `%s` last activity', self.username)
933 log.debug('updated user `%s` last activity', self.username)
935
934
936 def update_password(self, new_password):
935 def update_password(self, new_password):
937 from rhodecode.lib.auth import get_crypt_password
936 from rhodecode.lib.auth import get_crypt_password
938
937
939 self.password = get_crypt_password(new_password)
938 self.password = get_crypt_password(new_password)
940 Session().add(self)
939 Session().add(self)
941
940
942 @classmethod
941 @classmethod
943 def get_first_super_admin(cls):
942 def get_first_super_admin(cls):
944 user = User.query().filter(User.admin == true()).first()
943 user = User.query().filter(User.admin == true()).first()
945 if user is None:
944 if user is None:
946 raise Exception('FATAL: Missing administrative account!')
945 raise Exception('FATAL: Missing administrative account!')
947 return user
946 return user
948
947
949 @classmethod
948 @classmethod
950 def get_all_super_admins(cls):
949 def get_all_super_admins(cls):
951 """
950 """
952 Returns all admin accounts sorted by username
951 Returns all admin accounts sorted by username
953 """
952 """
954 return User.query().filter(User.admin == true())\
953 return User.query().filter(User.admin == true())\
955 .order_by(User.username.asc()).all()
954 .order_by(User.username.asc()).all()
956
955
957 @classmethod
956 @classmethod
958 def get_default_user(cls, cache=False, refresh=False):
957 def get_default_user(cls, cache=False, refresh=False):
959 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
958 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
960 if user is None:
959 if user is None:
961 raise Exception('FATAL: Missing default account!')
960 raise Exception('FATAL: Missing default account!')
962 if refresh:
961 if refresh:
963 # The default user might be based on outdated state which
962 # The default user might be based on outdated state which
964 # has been loaded from the cache.
963 # has been loaded from the cache.
965 # A call to refresh() ensures that the
964 # A call to refresh() ensures that the
966 # latest state from the database is used.
965 # latest state from the database is used.
967 Session().refresh(user)
966 Session().refresh(user)
968 return user
967 return user
969
968
970 def _get_default_perms(self, user, suffix=''):
969 def _get_default_perms(self, user, suffix=''):
971 from rhodecode.model.permission import PermissionModel
970 from rhodecode.model.permission import PermissionModel
972 return PermissionModel().get_default_perms(user.user_perms, suffix)
971 return PermissionModel().get_default_perms(user.user_perms, suffix)
973
972
974 def get_default_perms(self, suffix=''):
973 def get_default_perms(self, suffix=''):
975 return self._get_default_perms(self, suffix)
974 return self._get_default_perms(self, suffix)
976
975
977 def get_api_data(self, include_secrets=False, details='full'):
976 def get_api_data(self, include_secrets=False, details='full'):
978 """
977 """
979 Common function for generating user related data for API
978 Common function for generating user related data for API
980
979
981 :param include_secrets: By default secrets in the API data will be replaced
980 :param include_secrets: By default secrets in the API data will be replaced
982 by a placeholder value to prevent exposing this data by accident. In case
981 by a placeholder value to prevent exposing this data by accident. In case
983 this data shall be exposed, set this flag to ``True``.
982 this data shall be exposed, set this flag to ``True``.
984
983
985 :param details: details can be 'basic|full' basic gives only a subset of
984 :param details: details can be 'basic|full' basic gives only a subset of
986 the available user information that includes user_id, name and emails.
985 the available user information that includes user_id, name and emails.
987 """
986 """
988 user = self
987 user = self
989 user_data = self.user_data
988 user_data = self.user_data
990 data = {
989 data = {
991 'user_id': user.user_id,
990 'user_id': user.user_id,
992 'username': user.username,
991 'username': user.username,
993 'firstname': user.name,
992 'firstname': user.name,
994 'lastname': user.lastname,
993 'lastname': user.lastname,
995 'email': user.email,
994 'email': user.email,
996 'emails': user.emails,
995 'emails': user.emails,
997 }
996 }
998 if details == 'basic':
997 if details == 'basic':
999 return data
998 return data
1000
999
1001 auth_token_length = 40
1000 auth_token_length = 40
1002 auth_token_replacement = '*' * auth_token_length
1001 auth_token_replacement = '*' * auth_token_length
1003
1002
1004 extras = {
1003 extras = {
1005 'auth_tokens': [auth_token_replacement],
1004 'auth_tokens': [auth_token_replacement],
1006 'active': user.active,
1005 'active': user.active,
1007 'admin': user.admin,
1006 'admin': user.admin,
1008 'extern_type': user.extern_type,
1007 'extern_type': user.extern_type,
1009 'extern_name': user.extern_name,
1008 'extern_name': user.extern_name,
1010 'last_login': user.last_login,
1009 'last_login': user.last_login,
1011 'last_activity': user.last_activity,
1010 'last_activity': user.last_activity,
1012 'ip_addresses': user.ip_addresses,
1011 'ip_addresses': user.ip_addresses,
1013 'language': user_data.get('language')
1012 'language': user_data.get('language')
1014 }
1013 }
1015 data.update(extras)
1014 data.update(extras)
1016
1015
1017 if include_secrets:
1016 if include_secrets:
1018 data['auth_tokens'] = user.auth_tokens
1017 data['auth_tokens'] = user.auth_tokens
1019 return data
1018 return data
1020
1019
1021 def __json__(self):
1020 def __json__(self):
1022 data = {
1021 data = {
1023 'full_name': self.full_name,
1022 'full_name': self.full_name,
1024 'full_name_or_username': self.full_name_or_username,
1023 'full_name_or_username': self.full_name_or_username,
1025 'short_contact': self.short_contact,
1024 'short_contact': self.short_contact,
1026 'full_contact': self.full_contact,
1025 'full_contact': self.full_contact,
1027 }
1026 }
1028 data.update(self.get_api_data())
1027 data.update(self.get_api_data())
1029 return data
1028 return data
1030
1029
1031
1030
1032 class UserApiKeys(Base, BaseModel):
1031 class UserApiKeys(Base, BaseModel):
1033 __tablename__ = 'user_api_keys'
1032 __tablename__ = 'user_api_keys'
1034 __table_args__ = (
1033 __table_args__ = (
1035 Index('uak_api_key_idx', 'api_key', unique=True),
1034 Index('uak_api_key_idx', 'api_key', unique=True),
1036 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1035 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1037 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1036 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1038 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1037 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1039 )
1038 )
1040 __mapper_args__ = {}
1039 __mapper_args__ = {}
1041
1040
1042 # ApiKey role
1041 # ApiKey role
1043 ROLE_ALL = 'token_role_all'
1042 ROLE_ALL = 'token_role_all'
1044 ROLE_HTTP = 'token_role_http'
1043 ROLE_HTTP = 'token_role_http'
1045 ROLE_VCS = 'token_role_vcs'
1044 ROLE_VCS = 'token_role_vcs'
1046 ROLE_API = 'token_role_api'
1045 ROLE_API = 'token_role_api'
1047 ROLE_FEED = 'token_role_feed'
1046 ROLE_FEED = 'token_role_feed'
1048 ROLE_PASSWORD_RESET = 'token_password_reset'
1047 ROLE_PASSWORD_RESET = 'token_password_reset'
1049
1048
1050 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1049 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1051
1050
1052 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1051 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1053 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1052 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1054 api_key = Column("api_key", String(255), nullable=False, unique=True)
1053 api_key = Column("api_key", String(255), nullable=False, unique=True)
1055 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1054 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1056 expires = Column('expires', Float(53), nullable=False)
1055 expires = Column('expires', Float(53), nullable=False)
1057 role = Column('role', String(255), nullable=True)
1056 role = Column('role', String(255), nullable=True)
1058 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1057 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1059
1058
1060 # scope columns
1059 # scope columns
1061 repo_id = Column(
1060 repo_id = Column(
1062 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1061 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1063 nullable=True, unique=None, default=None)
1062 nullable=True, unique=None, default=None)
1064 repo = relationship('Repository', lazy='joined')
1063 repo = relationship('Repository', lazy='joined')
1065
1064
1066 repo_group_id = Column(
1065 repo_group_id = Column(
1067 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1066 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1068 nullable=True, unique=None, default=None)
1067 nullable=True, unique=None, default=None)
1069 repo_group = relationship('RepoGroup', lazy='joined')
1068 repo_group = relationship('RepoGroup', lazy='joined')
1070
1069
1071 user = relationship('User', lazy='joined')
1070 user = relationship('User', lazy='joined')
1072
1071
1073 def __unicode__(self):
1072 def __unicode__(self):
1074 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1073 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1075
1074
1076 def __json__(self):
1075 def __json__(self):
1077 data = {
1076 data = {
1078 'auth_token': self.api_key,
1077 'auth_token': self.api_key,
1079 'role': self.role,
1078 'role': self.role,
1080 'scope': self.scope_humanized,
1079 'scope': self.scope_humanized,
1081 'expired': self.expired
1080 'expired': self.expired
1082 }
1081 }
1083 return data
1082 return data
1084
1083
1085 def get_api_data(self, include_secrets=False):
1084 def get_api_data(self, include_secrets=False):
1086 data = self.__json__()
1085 data = self.__json__()
1087 if include_secrets:
1086 if include_secrets:
1088 return data
1087 return data
1089 else:
1088 else:
1090 data['auth_token'] = self.token_obfuscated
1089 data['auth_token'] = self.token_obfuscated
1091 return data
1090 return data
1092
1091
1093 @hybrid_property
1092 @hybrid_property
1094 def description_safe(self):
1093 def description_safe(self):
1095 from rhodecode.lib import helpers as h
1094 from rhodecode.lib import helpers as h
1096 return h.escape(self.description)
1095 return h.escape(self.description)
1097
1096
1098 @property
1097 @property
1099 def expired(self):
1098 def expired(self):
1100 if self.expires == -1:
1099 if self.expires == -1:
1101 return False
1100 return False
1102 return time.time() > self.expires
1101 return time.time() > self.expires
1103
1102
1104 @classmethod
1103 @classmethod
1105 def _get_role_name(cls, role):
1104 def _get_role_name(cls, role):
1106 return {
1105 return {
1107 cls.ROLE_ALL: _('all'),
1106 cls.ROLE_ALL: _('all'),
1108 cls.ROLE_HTTP: _('http/web interface'),
1107 cls.ROLE_HTTP: _('http/web interface'),
1109 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1108 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1110 cls.ROLE_API: _('api calls'),
1109 cls.ROLE_API: _('api calls'),
1111 cls.ROLE_FEED: _('feed access'),
1110 cls.ROLE_FEED: _('feed access'),
1112 }.get(role, role)
1111 }.get(role, role)
1113
1112
1114 @property
1113 @property
1115 def role_humanized(self):
1114 def role_humanized(self):
1116 return self._get_role_name(self.role)
1115 return self._get_role_name(self.role)
1117
1116
1118 def _get_scope(self):
1117 def _get_scope(self):
1119 if self.repo:
1118 if self.repo:
1120 return repr(self.repo)
1119 return repr(self.repo)
1121 if self.repo_group:
1120 if self.repo_group:
1122 return repr(self.repo_group) + ' (recursive)'
1121 return repr(self.repo_group) + ' (recursive)'
1123 return 'global'
1122 return 'global'
1124
1123
1125 @property
1124 @property
1126 def scope_humanized(self):
1125 def scope_humanized(self):
1127 return self._get_scope()
1126 return self._get_scope()
1128
1127
1129 @property
1128 @property
1130 def token_obfuscated(self):
1129 def token_obfuscated(self):
1131 if self.api_key:
1130 if self.api_key:
1132 return self.api_key[:4] + "****"
1131 return self.api_key[:4] + "****"
1133
1132
1134
1133
1135 class UserEmailMap(Base, BaseModel):
1134 class UserEmailMap(Base, BaseModel):
1136 __tablename__ = 'user_email_map'
1135 __tablename__ = 'user_email_map'
1137 __table_args__ = (
1136 __table_args__ = (
1138 Index('uem_email_idx', 'email'),
1137 Index('uem_email_idx', 'email'),
1139 UniqueConstraint('email'),
1138 UniqueConstraint('email'),
1140 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1139 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1141 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1140 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1142 )
1141 )
1143 __mapper_args__ = {}
1142 __mapper_args__ = {}
1144
1143
1145 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1144 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1146 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1145 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1147 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1146 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1148 user = relationship('User', lazy='joined')
1147 user = relationship('User', lazy='joined')
1149
1148
1150 @validates('_email')
1149 @validates('_email')
1151 def validate_email(self, key, email):
1150 def validate_email(self, key, email):
1152 # check if this email is not main one
1151 # check if this email is not main one
1153 main_email = Session().query(User).filter(User.email == email).scalar()
1152 main_email = Session().query(User).filter(User.email == email).scalar()
1154 if main_email is not None:
1153 if main_email is not None:
1155 raise AttributeError('email %s is present is user table' % email)
1154 raise AttributeError('email %s is present is user table' % email)
1156 return email
1155 return email
1157
1156
1158 @hybrid_property
1157 @hybrid_property
1159 def email(self):
1158 def email(self):
1160 return self._email
1159 return self._email
1161
1160
1162 @email.setter
1161 @email.setter
1163 def email(self, val):
1162 def email(self, val):
1164 self._email = val.lower() if val else None
1163 self._email = val.lower() if val else None
1165
1164
1166
1165
1167 class UserIpMap(Base, BaseModel):
1166 class UserIpMap(Base, BaseModel):
1168 __tablename__ = 'user_ip_map'
1167 __tablename__ = 'user_ip_map'
1169 __table_args__ = (
1168 __table_args__ = (
1170 UniqueConstraint('user_id', 'ip_addr'),
1169 UniqueConstraint('user_id', 'ip_addr'),
1171 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1170 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1172 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1171 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1173 )
1172 )
1174 __mapper_args__ = {}
1173 __mapper_args__ = {}
1175
1174
1176 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1175 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1177 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1176 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1178 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1177 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1179 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1178 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1180 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1179 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1181 user = relationship('User', lazy='joined')
1180 user = relationship('User', lazy='joined')
1182
1181
1183 @hybrid_property
1182 @hybrid_property
1184 def description_safe(self):
1183 def description_safe(self):
1185 from rhodecode.lib import helpers as h
1184 from rhodecode.lib import helpers as h
1186 return h.escape(self.description)
1185 return h.escape(self.description)
1187
1186
1188 @classmethod
1187 @classmethod
1189 def _get_ip_range(cls, ip_addr):
1188 def _get_ip_range(cls, ip_addr):
1190 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1189 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1191 return [str(net.network_address), str(net.broadcast_address)]
1190 return [str(net.network_address), str(net.broadcast_address)]
1192
1191
1193 def __json__(self):
1192 def __json__(self):
1194 return {
1193 return {
1195 'ip_addr': self.ip_addr,
1194 'ip_addr': self.ip_addr,
1196 'ip_range': self._get_ip_range(self.ip_addr),
1195 'ip_range': self._get_ip_range(self.ip_addr),
1197 }
1196 }
1198
1197
1199 def __unicode__(self):
1198 def __unicode__(self):
1200 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1199 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1201 self.user_id, self.ip_addr)
1200 self.user_id, self.ip_addr)
1202
1201
1203
1202
1204 class UserSshKeys(Base, BaseModel):
1203 class UserSshKeys(Base, BaseModel):
1205 __tablename__ = 'user_ssh_keys'
1204 __tablename__ = 'user_ssh_keys'
1206 __table_args__ = (
1205 __table_args__ = (
1207 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1206 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1208
1207
1209 UniqueConstraint('ssh_key_fingerprint'),
1208 UniqueConstraint('ssh_key_fingerprint'),
1210
1209
1211 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1210 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1212 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1211 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1213 )
1212 )
1214 __mapper_args__ = {}
1213 __mapper_args__ = {}
1215
1214
1216 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1215 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1217 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1216 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1218 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1217 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1219
1218
1220 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1219 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1221
1220
1222 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1221 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1223 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1222 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1224 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1223 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1225
1224
1226 user = relationship('User', lazy='joined')
1225 user = relationship('User', lazy='joined')
1227
1226
1228 def __json__(self):
1227 def __json__(self):
1229 data = {
1228 data = {
1230 'ssh_fingerprint': self.ssh_key_fingerprint,
1229 'ssh_fingerprint': self.ssh_key_fingerprint,
1231 'description': self.description,
1230 'description': self.description,
1232 'created_on': self.created_on
1231 'created_on': self.created_on
1233 }
1232 }
1234 return data
1233 return data
1235
1234
1236 def get_api_data(self):
1235 def get_api_data(self):
1237 data = self.__json__()
1236 data = self.__json__()
1238 return data
1237 return data
1239
1238
1240
1239
1241 class UserLog(Base, BaseModel):
1240 class UserLog(Base, BaseModel):
1242 __tablename__ = 'user_logs'
1241 __tablename__ = 'user_logs'
1243 __table_args__ = (
1242 __table_args__ = (
1244 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1245 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1246 )
1245 )
1247 VERSION_1 = 'v1'
1246 VERSION_1 = 'v1'
1248 VERSION_2 = 'v2'
1247 VERSION_2 = 'v2'
1249 VERSIONS = [VERSION_1, VERSION_2]
1248 VERSIONS = [VERSION_1, VERSION_2]
1250
1249
1251 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1250 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1252 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1251 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1253 username = Column("username", String(255), nullable=True, unique=None, default=None)
1252 username = Column("username", String(255), nullable=True, unique=None, default=None)
1254 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1253 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1255 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1254 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1256 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1255 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1257 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1256 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1258 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1257 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1259
1258
1260 version = Column("version", String(255), nullable=True, default=VERSION_1)
1259 version = Column("version", String(255), nullable=True, default=VERSION_1)
1261 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1260 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1262 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1261 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1263
1262
1264 def __unicode__(self):
1263 def __unicode__(self):
1265 return u"<%s('id:%s:%s')>" % (
1264 return u"<%s('id:%s:%s')>" % (
1266 self.__class__.__name__, self.repository_name, self.action)
1265 self.__class__.__name__, self.repository_name, self.action)
1267
1266
1268 def __json__(self):
1267 def __json__(self):
1269 return {
1268 return {
1270 'user_id': self.user_id,
1269 'user_id': self.user_id,
1271 'username': self.username,
1270 'username': self.username,
1272 'repository_id': self.repository_id,
1271 'repository_id': self.repository_id,
1273 'repository_name': self.repository_name,
1272 'repository_name': self.repository_name,
1274 'user_ip': self.user_ip,
1273 'user_ip': self.user_ip,
1275 'action_date': self.action_date,
1274 'action_date': self.action_date,
1276 'action': self.action,
1275 'action': self.action,
1277 }
1276 }
1278
1277
1279 @hybrid_property
1278 @hybrid_property
1280 def entry_id(self):
1279 def entry_id(self):
1281 return self.user_log_id
1280 return self.user_log_id
1282
1281
1283 @property
1282 @property
1284 def action_as_day(self):
1283 def action_as_day(self):
1285 return datetime.date(*self.action_date.timetuple()[:3])
1284 return datetime.date(*self.action_date.timetuple()[:3])
1286
1285
1287 user = relationship('User')
1286 user = relationship('User')
1288 repository = relationship('Repository', cascade='')
1287 repository = relationship('Repository', cascade='')
1289
1288
1290
1289
1291 class UserGroup(Base, BaseModel):
1290 class UserGroup(Base, BaseModel):
1292 __tablename__ = 'users_groups'
1291 __tablename__ = 'users_groups'
1293 __table_args__ = (
1292 __table_args__ = (
1294 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1293 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1295 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1294 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1296 )
1295 )
1297
1296
1298 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1297 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1299 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1298 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1300 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1299 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1301 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1300 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1302 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1301 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1303 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1302 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1304 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1303 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1305 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1304 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1306
1305
1307 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1306 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1308 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1307 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1309 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1308 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1310 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1309 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1311 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1310 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1312 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1311 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1313
1312
1314 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1313 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1315 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1314 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1316
1315
1317 @classmethod
1316 @classmethod
1318 def _load_group_data(cls, column):
1317 def _load_group_data(cls, column):
1319 if not column:
1318 if not column:
1320 return {}
1319 return {}
1321
1320
1322 try:
1321 try:
1323 return json.loads(column) or {}
1322 return json.loads(column) or {}
1324 except TypeError:
1323 except TypeError:
1325 return {}
1324 return {}
1326
1325
1327 @hybrid_property
1326 @hybrid_property
1328 def description_safe(self):
1327 def description_safe(self):
1329 from rhodecode.lib import helpers as h
1328 from rhodecode.lib import helpers as h
1330 return h.escape(self.description)
1329 return h.escape(self.user_group_description)
1331
1330
1332 @hybrid_property
1331 @hybrid_property
1333 def group_data(self):
1332 def group_data(self):
1334 return self._load_group_data(self._group_data)
1333 return self._load_group_data(self._group_data)
1335
1334
1336 @group_data.expression
1335 @group_data.expression
1337 def group_data(self, **kwargs):
1336 def group_data(self, **kwargs):
1338 return self._group_data
1337 return self._group_data
1339
1338
1340 @group_data.setter
1339 @group_data.setter
1341 def group_data(self, val):
1340 def group_data(self, val):
1342 try:
1341 try:
1343 self._group_data = json.dumps(val)
1342 self._group_data = json.dumps(val)
1344 except Exception:
1343 except Exception:
1345 log.error(traceback.format_exc())
1344 log.error(traceback.format_exc())
1346
1345
1347 def __unicode__(self):
1346 def __unicode__(self):
1348 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1347 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1349 self.users_group_id,
1348 self.users_group_id,
1350 self.users_group_name)
1349 self.users_group_name)
1351
1350
1352 @classmethod
1351 @classmethod
1353 def get_by_group_name(cls, group_name, cache=False,
1352 def get_by_group_name(cls, group_name, cache=False,
1354 case_insensitive=False):
1353 case_insensitive=False):
1355 if case_insensitive:
1354 if case_insensitive:
1356 q = cls.query().filter(func.lower(cls.users_group_name) ==
1355 q = cls.query().filter(func.lower(cls.users_group_name) ==
1357 func.lower(group_name))
1356 func.lower(group_name))
1358
1357
1359 else:
1358 else:
1360 q = cls.query().filter(cls.users_group_name == group_name)
1359 q = cls.query().filter(cls.users_group_name == group_name)
1361 if cache:
1360 if cache:
1362 q = q.options(
1361 q = q.options(
1363 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1362 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1364 return q.scalar()
1363 return q.scalar()
1365
1364
1366 @classmethod
1365 @classmethod
1367 def get(cls, user_group_id, cache=False):
1366 def get(cls, user_group_id, cache=False):
1368 if not user_group_id:
1367 if not user_group_id:
1369 return
1368 return
1370
1369
1371 user_group = cls.query()
1370 user_group = cls.query()
1372 if cache:
1371 if cache:
1373 user_group = user_group.options(
1372 user_group = user_group.options(
1374 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1373 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1375 return user_group.get(user_group_id)
1374 return user_group.get(user_group_id)
1376
1375
1377 def permissions(self, with_admins=True, with_owner=True):
1376 def permissions(self, with_admins=True, with_owner=True):
1378 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1377 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1379 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1378 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1380 joinedload(UserUserGroupToPerm.user),
1379 joinedload(UserUserGroupToPerm.user),
1381 joinedload(UserUserGroupToPerm.permission),)
1380 joinedload(UserUserGroupToPerm.permission),)
1382
1381
1383 # get owners and admins and permissions. We do a trick of re-writing
1382 # get owners and admins and permissions. We do a trick of re-writing
1384 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1383 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1385 # has a global reference and changing one object propagates to all
1384 # has a global reference and changing one object propagates to all
1386 # others. This means if admin is also an owner admin_row that change
1385 # others. This means if admin is also an owner admin_row that change
1387 # would propagate to both objects
1386 # would propagate to both objects
1388 perm_rows = []
1387 perm_rows = []
1389 for _usr in q.all():
1388 for _usr in q.all():
1390 usr = AttributeDict(_usr.user.get_dict())
1389 usr = AttributeDict(_usr.user.get_dict())
1391 usr.permission = _usr.permission.permission_name
1390 usr.permission = _usr.permission.permission_name
1392 perm_rows.append(usr)
1391 perm_rows.append(usr)
1393
1392
1394 # filter the perm rows by 'default' first and then sort them by
1393 # filter the perm rows by 'default' first and then sort them by
1395 # admin,write,read,none permissions sorted again alphabetically in
1394 # admin,write,read,none permissions sorted again alphabetically in
1396 # each group
1395 # each group
1397 perm_rows = sorted(perm_rows, key=display_user_sort)
1396 perm_rows = sorted(perm_rows, key=display_user_sort)
1398
1397
1399 _admin_perm = 'usergroup.admin'
1398 _admin_perm = 'usergroup.admin'
1400 owner_row = []
1399 owner_row = []
1401 if with_owner:
1400 if with_owner:
1402 usr = AttributeDict(self.user.get_dict())
1401 usr = AttributeDict(self.user.get_dict())
1403 usr.owner_row = True
1402 usr.owner_row = True
1404 usr.permission = _admin_perm
1403 usr.permission = _admin_perm
1405 owner_row.append(usr)
1404 owner_row.append(usr)
1406
1405
1407 super_admin_rows = []
1406 super_admin_rows = []
1408 if with_admins:
1407 if with_admins:
1409 for usr in User.get_all_super_admins():
1408 for usr in User.get_all_super_admins():
1410 # if this admin is also owner, don't double the record
1409 # if this admin is also owner, don't double the record
1411 if usr.user_id == owner_row[0].user_id:
1410 if usr.user_id == owner_row[0].user_id:
1412 owner_row[0].admin_row = True
1411 owner_row[0].admin_row = True
1413 else:
1412 else:
1414 usr = AttributeDict(usr.get_dict())
1413 usr = AttributeDict(usr.get_dict())
1415 usr.admin_row = True
1414 usr.admin_row = True
1416 usr.permission = _admin_perm
1415 usr.permission = _admin_perm
1417 super_admin_rows.append(usr)
1416 super_admin_rows.append(usr)
1418
1417
1419 return super_admin_rows + owner_row + perm_rows
1418 return super_admin_rows + owner_row + perm_rows
1420
1419
1421 def permission_user_groups(self):
1420 def permission_user_groups(self):
1422 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1421 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1423 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1422 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1424 joinedload(UserGroupUserGroupToPerm.target_user_group),
1423 joinedload(UserGroupUserGroupToPerm.target_user_group),
1425 joinedload(UserGroupUserGroupToPerm.permission),)
1424 joinedload(UserGroupUserGroupToPerm.permission),)
1426
1425
1427 perm_rows = []
1426 perm_rows = []
1428 for _user_group in q.all():
1427 for _user_group in q.all():
1429 usr = AttributeDict(_user_group.user_group.get_dict())
1428 usr = AttributeDict(_user_group.user_group.get_dict())
1430 usr.permission = _user_group.permission.permission_name
1429 usr.permission = _user_group.permission.permission_name
1431 perm_rows.append(usr)
1430 perm_rows.append(usr)
1432
1431
1433 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1432 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1434 return perm_rows
1433 return perm_rows
1435
1434
1436 def _get_default_perms(self, user_group, suffix=''):
1435 def _get_default_perms(self, user_group, suffix=''):
1437 from rhodecode.model.permission import PermissionModel
1436 from rhodecode.model.permission import PermissionModel
1438 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1437 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1439
1438
1440 def get_default_perms(self, suffix=''):
1439 def get_default_perms(self, suffix=''):
1441 return self._get_default_perms(self, suffix)
1440 return self._get_default_perms(self, suffix)
1442
1441
1443 def get_api_data(self, with_group_members=True, include_secrets=False):
1442 def get_api_data(self, with_group_members=True, include_secrets=False):
1444 """
1443 """
1445 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1444 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1446 basically forwarded.
1445 basically forwarded.
1447
1446
1448 """
1447 """
1449 user_group = self
1448 user_group = self
1450 data = {
1449 data = {
1451 'users_group_id': user_group.users_group_id,
1450 'users_group_id': user_group.users_group_id,
1452 'group_name': user_group.users_group_name,
1451 'group_name': user_group.users_group_name,
1453 'group_description': user_group.user_group_description,
1452 'group_description': user_group.user_group_description,
1454 'active': user_group.users_group_active,
1453 'active': user_group.users_group_active,
1455 'owner': user_group.user.username,
1454 'owner': user_group.user.username,
1456 'owner_email': user_group.user.email,
1455 'owner_email': user_group.user.email,
1457 }
1456 }
1458
1457
1459 if with_group_members:
1458 if with_group_members:
1460 users = []
1459 users = []
1461 for user in user_group.members:
1460 for user in user_group.members:
1462 user = user.user
1461 user = user.user
1463 users.append(user.get_api_data(include_secrets=include_secrets))
1462 users.append(user.get_api_data(include_secrets=include_secrets))
1464 data['users'] = users
1463 data['users'] = users
1465
1464
1466 return data
1465 return data
1467
1466
1468
1467
1469 class UserGroupMember(Base, BaseModel):
1468 class UserGroupMember(Base, BaseModel):
1470 __tablename__ = 'users_groups_members'
1469 __tablename__ = 'users_groups_members'
1471 __table_args__ = (
1470 __table_args__ = (
1472 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1471 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1473 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1472 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1474 )
1473 )
1475
1474
1476 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1475 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1477 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1476 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1478 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1477 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1479
1478
1480 user = relationship('User', lazy='joined')
1479 user = relationship('User', lazy='joined')
1481 users_group = relationship('UserGroup')
1480 users_group = relationship('UserGroup')
1482
1481
1483 def __init__(self, gr_id='', u_id=''):
1482 def __init__(self, gr_id='', u_id=''):
1484 self.users_group_id = gr_id
1483 self.users_group_id = gr_id
1485 self.user_id = u_id
1484 self.user_id = u_id
1486
1485
1487
1486
1488 class RepositoryField(Base, BaseModel):
1487 class RepositoryField(Base, BaseModel):
1489 __tablename__ = 'repositories_fields'
1488 __tablename__ = 'repositories_fields'
1490 __table_args__ = (
1489 __table_args__ = (
1491 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1490 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1492 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1491 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1493 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1492 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1494 )
1493 )
1495 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1494 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1496
1495
1497 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1496 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1498 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1497 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1499 field_key = Column("field_key", String(250))
1498 field_key = Column("field_key", String(250))
1500 field_label = Column("field_label", String(1024), nullable=False)
1499 field_label = Column("field_label", String(1024), nullable=False)
1501 field_value = Column("field_value", String(10000), nullable=False)
1500 field_value = Column("field_value", String(10000), nullable=False)
1502 field_desc = Column("field_desc", String(1024), nullable=False)
1501 field_desc = Column("field_desc", String(1024), nullable=False)
1503 field_type = Column("field_type", String(255), nullable=False, unique=None)
1502 field_type = Column("field_type", String(255), nullable=False, unique=None)
1504 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1503 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1505
1504
1506 repository = relationship('Repository')
1505 repository = relationship('Repository')
1507
1506
1508 @property
1507 @property
1509 def field_key_prefixed(self):
1508 def field_key_prefixed(self):
1510 return 'ex_%s' % self.field_key
1509 return 'ex_%s' % self.field_key
1511
1510
1512 @classmethod
1511 @classmethod
1513 def un_prefix_key(cls, key):
1512 def un_prefix_key(cls, key):
1514 if key.startswith(cls.PREFIX):
1513 if key.startswith(cls.PREFIX):
1515 return key[len(cls.PREFIX):]
1514 return key[len(cls.PREFIX):]
1516 return key
1515 return key
1517
1516
1518 @classmethod
1517 @classmethod
1519 def get_by_key_name(cls, key, repo):
1518 def get_by_key_name(cls, key, repo):
1520 row = cls.query()\
1519 row = cls.query()\
1521 .filter(cls.repository == repo)\
1520 .filter(cls.repository == repo)\
1522 .filter(cls.field_key == key).scalar()
1521 .filter(cls.field_key == key).scalar()
1523 return row
1522 return row
1524
1523
1525
1524
1526 class Repository(Base, BaseModel):
1525 class Repository(Base, BaseModel):
1527 __tablename__ = 'repositories'
1526 __tablename__ = 'repositories'
1528 __table_args__ = (
1527 __table_args__ = (
1529 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1528 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1531 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1530 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1532 )
1531 )
1533 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1532 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1534 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1533 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1535
1534
1536 STATE_CREATED = 'repo_state_created'
1535 STATE_CREATED = 'repo_state_created'
1537 STATE_PENDING = 'repo_state_pending'
1536 STATE_PENDING = 'repo_state_pending'
1538 STATE_ERROR = 'repo_state_error'
1537 STATE_ERROR = 'repo_state_error'
1539
1538
1540 LOCK_AUTOMATIC = 'lock_auto'
1539 LOCK_AUTOMATIC = 'lock_auto'
1541 LOCK_API = 'lock_api'
1540 LOCK_API = 'lock_api'
1542 LOCK_WEB = 'lock_web'
1541 LOCK_WEB = 'lock_web'
1543 LOCK_PULL = 'lock_pull'
1542 LOCK_PULL = 'lock_pull'
1544
1543
1545 NAME_SEP = URL_SEP
1544 NAME_SEP = URL_SEP
1546
1545
1547 repo_id = Column(
1546 repo_id = Column(
1548 "repo_id", Integer(), nullable=False, unique=True, default=None,
1547 "repo_id", Integer(), nullable=False, unique=True, default=None,
1549 primary_key=True)
1548 primary_key=True)
1550 _repo_name = Column(
1549 _repo_name = Column(
1551 "repo_name", Text(), nullable=False, default=None)
1550 "repo_name", Text(), nullable=False, default=None)
1552 _repo_name_hash = Column(
1551 _repo_name_hash = Column(
1553 "repo_name_hash", String(255), nullable=False, unique=True)
1552 "repo_name_hash", String(255), nullable=False, unique=True)
1554 repo_state = Column("repo_state", String(255), nullable=True)
1553 repo_state = Column("repo_state", String(255), nullable=True)
1555
1554
1556 clone_uri = Column(
1555 clone_uri = Column(
1557 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1556 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1558 default=None)
1557 default=None)
1559 repo_type = Column(
1558 repo_type = Column(
1560 "repo_type", String(255), nullable=False, unique=False, default=None)
1559 "repo_type", String(255), nullable=False, unique=False, default=None)
1561 user_id = Column(
1560 user_id = Column(
1562 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1561 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1563 unique=False, default=None)
1562 unique=False, default=None)
1564 private = Column(
1563 private = Column(
1565 "private", Boolean(), nullable=True, unique=None, default=None)
1564 "private", Boolean(), nullable=True, unique=None, default=None)
1566 enable_statistics = Column(
1565 enable_statistics = Column(
1567 "statistics", Boolean(), nullable=True, unique=None, default=True)
1566 "statistics", Boolean(), nullable=True, unique=None, default=True)
1568 enable_downloads = Column(
1567 enable_downloads = Column(
1569 "downloads", Boolean(), nullable=True, unique=None, default=True)
1568 "downloads", Boolean(), nullable=True, unique=None, default=True)
1570 description = Column(
1569 description = Column(
1571 "description", String(10000), nullable=True, unique=None, default=None)
1570 "description", String(10000), nullable=True, unique=None, default=None)
1572 created_on = Column(
1571 created_on = Column(
1573 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1572 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1574 default=datetime.datetime.now)
1573 default=datetime.datetime.now)
1575 updated_on = Column(
1574 updated_on = Column(
1576 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1575 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1577 default=datetime.datetime.now)
1576 default=datetime.datetime.now)
1578 _landing_revision = Column(
1577 _landing_revision = Column(
1579 "landing_revision", String(255), nullable=False, unique=False,
1578 "landing_revision", String(255), nullable=False, unique=False,
1580 default=None)
1579 default=None)
1581 enable_locking = Column(
1580 enable_locking = Column(
1582 "enable_locking", Boolean(), nullable=False, unique=None,
1581 "enable_locking", Boolean(), nullable=False, unique=None,
1583 default=False)
1582 default=False)
1584 _locked = Column(
1583 _locked = Column(
1585 "locked", String(255), nullable=True, unique=False, default=None)
1584 "locked", String(255), nullable=True, unique=False, default=None)
1586 _changeset_cache = Column(
1585 _changeset_cache = Column(
1587 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1586 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1588
1587
1589 fork_id = Column(
1588 fork_id = Column(
1590 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1589 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1591 nullable=True, unique=False, default=None)
1590 nullable=True, unique=False, default=None)
1592 group_id = Column(
1591 group_id = Column(
1593 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1592 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1594 unique=False, default=None)
1593 unique=False, default=None)
1595
1594
1596 user = relationship('User', lazy='joined')
1595 user = relationship('User', lazy='joined')
1597 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1596 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1598 group = relationship('RepoGroup', lazy='joined')
1597 group = relationship('RepoGroup', lazy='joined')
1599 repo_to_perm = relationship(
1598 repo_to_perm = relationship(
1600 'UserRepoToPerm', cascade='all',
1599 'UserRepoToPerm', cascade='all',
1601 order_by='UserRepoToPerm.repo_to_perm_id')
1600 order_by='UserRepoToPerm.repo_to_perm_id')
1602 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1601 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1603 stats = relationship('Statistics', cascade='all', uselist=False)
1602 stats = relationship('Statistics', cascade='all', uselist=False)
1604
1603
1605 followers = relationship(
1604 followers = relationship(
1606 'UserFollowing',
1605 'UserFollowing',
1607 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1606 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1608 cascade='all')
1607 cascade='all')
1609 extra_fields = relationship(
1608 extra_fields = relationship(
1610 'RepositoryField', cascade="all, delete, delete-orphan")
1609 'RepositoryField', cascade="all, delete, delete-orphan")
1611 logs = relationship('UserLog')
1610 logs = relationship('UserLog')
1612 comments = relationship(
1611 comments = relationship(
1613 'ChangesetComment', cascade="all, delete, delete-orphan")
1612 'ChangesetComment', cascade="all, delete, delete-orphan")
1614 pull_requests_source = relationship(
1613 pull_requests_source = relationship(
1615 'PullRequest',
1614 'PullRequest',
1616 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1615 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1617 cascade="all, delete, delete-orphan")
1616 cascade="all, delete, delete-orphan")
1618 pull_requests_target = relationship(
1617 pull_requests_target = relationship(
1619 'PullRequest',
1618 'PullRequest',
1620 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1619 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1621 cascade="all, delete, delete-orphan")
1620 cascade="all, delete, delete-orphan")
1622 ui = relationship('RepoRhodeCodeUi', cascade="all")
1621 ui = relationship('RepoRhodeCodeUi', cascade="all")
1623 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1622 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1624 integrations = relationship('Integration',
1623 integrations = relationship('Integration',
1625 cascade="all, delete, delete-orphan")
1624 cascade="all, delete, delete-orphan")
1626
1625
1627 scoped_tokens = relationship('UserApiKeys', cascade="all")
1626 scoped_tokens = relationship('UserApiKeys', cascade="all")
1628
1627
1629 def __unicode__(self):
1628 def __unicode__(self):
1630 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1629 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1631 safe_unicode(self.repo_name))
1630 safe_unicode(self.repo_name))
1632
1631
1633 @hybrid_property
1632 @hybrid_property
1634 def description_safe(self):
1633 def description_safe(self):
1635 from rhodecode.lib import helpers as h
1634 from rhodecode.lib import helpers as h
1636 return h.escape(self.description)
1635 return h.escape(self.description)
1637
1636
1638 @hybrid_property
1637 @hybrid_property
1639 def landing_rev(self):
1638 def landing_rev(self):
1640 # always should return [rev_type, rev]
1639 # always should return [rev_type, rev]
1641 if self._landing_revision:
1640 if self._landing_revision:
1642 _rev_info = self._landing_revision.split(':')
1641 _rev_info = self._landing_revision.split(':')
1643 if len(_rev_info) < 2:
1642 if len(_rev_info) < 2:
1644 _rev_info.insert(0, 'rev')
1643 _rev_info.insert(0, 'rev')
1645 return [_rev_info[0], _rev_info[1]]
1644 return [_rev_info[0], _rev_info[1]]
1646 return [None, None]
1645 return [None, None]
1647
1646
1648 @landing_rev.setter
1647 @landing_rev.setter
1649 def landing_rev(self, val):
1648 def landing_rev(self, val):
1650 if ':' not in val:
1649 if ':' not in val:
1651 raise ValueError('value must be delimited with `:` and consist '
1650 raise ValueError('value must be delimited with `:` and consist '
1652 'of <rev_type>:<rev>, got %s instead' % val)
1651 'of <rev_type>:<rev>, got %s instead' % val)
1653 self._landing_revision = val
1652 self._landing_revision = val
1654
1653
1655 @hybrid_property
1654 @hybrid_property
1656 def locked(self):
1655 def locked(self):
1657 if self._locked:
1656 if self._locked:
1658 user_id, timelocked, reason = self._locked.split(':')
1657 user_id, timelocked, reason = self._locked.split(':')
1659 lock_values = int(user_id), timelocked, reason
1658 lock_values = int(user_id), timelocked, reason
1660 else:
1659 else:
1661 lock_values = [None, None, None]
1660 lock_values = [None, None, None]
1662 return lock_values
1661 return lock_values
1663
1662
1664 @locked.setter
1663 @locked.setter
1665 def locked(self, val):
1664 def locked(self, val):
1666 if val and isinstance(val, (list, tuple)):
1665 if val and isinstance(val, (list, tuple)):
1667 self._locked = ':'.join(map(str, val))
1666 self._locked = ':'.join(map(str, val))
1668 else:
1667 else:
1669 self._locked = None
1668 self._locked = None
1670
1669
1671 @hybrid_property
1670 @hybrid_property
1672 def changeset_cache(self):
1671 def changeset_cache(self):
1673 from rhodecode.lib.vcs.backends.base import EmptyCommit
1672 from rhodecode.lib.vcs.backends.base import EmptyCommit
1674 dummy = EmptyCommit().__json__()
1673 dummy = EmptyCommit().__json__()
1675 if not self._changeset_cache:
1674 if not self._changeset_cache:
1676 return dummy
1675 return dummy
1677 try:
1676 try:
1678 return json.loads(self._changeset_cache)
1677 return json.loads(self._changeset_cache)
1679 except TypeError:
1678 except TypeError:
1680 return dummy
1679 return dummy
1681 except Exception:
1680 except Exception:
1682 log.error(traceback.format_exc())
1681 log.error(traceback.format_exc())
1683 return dummy
1682 return dummy
1684
1683
1685 @changeset_cache.setter
1684 @changeset_cache.setter
1686 def changeset_cache(self, val):
1685 def changeset_cache(self, val):
1687 try:
1686 try:
1688 self._changeset_cache = json.dumps(val)
1687 self._changeset_cache = json.dumps(val)
1689 except Exception:
1688 except Exception:
1690 log.error(traceback.format_exc())
1689 log.error(traceback.format_exc())
1691
1690
1692 @hybrid_property
1691 @hybrid_property
1693 def repo_name(self):
1692 def repo_name(self):
1694 return self._repo_name
1693 return self._repo_name
1695
1694
1696 @repo_name.setter
1695 @repo_name.setter
1697 def repo_name(self, value):
1696 def repo_name(self, value):
1698 self._repo_name = value
1697 self._repo_name = value
1699 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1698 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1700
1699
1701 @classmethod
1700 @classmethod
1702 def normalize_repo_name(cls, repo_name):
1701 def normalize_repo_name(cls, repo_name):
1703 """
1702 """
1704 Normalizes os specific repo_name to the format internally stored inside
1703 Normalizes os specific repo_name to the format internally stored inside
1705 database using URL_SEP
1704 database using URL_SEP
1706
1705
1707 :param cls:
1706 :param cls:
1708 :param repo_name:
1707 :param repo_name:
1709 """
1708 """
1710 return cls.NAME_SEP.join(repo_name.split(os.sep))
1709 return cls.NAME_SEP.join(repo_name.split(os.sep))
1711
1710
1712 @classmethod
1711 @classmethod
1713 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1712 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1714 session = Session()
1713 session = Session()
1715 q = session.query(cls).filter(cls.repo_name == repo_name)
1714 q = session.query(cls).filter(cls.repo_name == repo_name)
1716
1715
1717 if cache:
1716 if cache:
1718 if identity_cache:
1717 if identity_cache:
1719 val = cls.identity_cache(session, 'repo_name', repo_name)
1718 val = cls.identity_cache(session, 'repo_name', repo_name)
1720 if val:
1719 if val:
1721 return val
1720 return val
1722 else:
1721 else:
1723 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1722 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1724 q = q.options(
1723 q = q.options(
1725 FromCache("sql_cache_short", cache_key))
1724 FromCache("sql_cache_short", cache_key))
1726
1725
1727 return q.scalar()
1726 return q.scalar()
1728
1727
1729 @classmethod
1728 @classmethod
1730 def get_by_id_or_repo_name(cls, repoid):
1729 def get_by_id_or_repo_name(cls, repoid):
1731 if isinstance(repoid, (int, long)):
1730 if isinstance(repoid, (int, long)):
1732 try:
1731 try:
1733 repo = cls.get(repoid)
1732 repo = cls.get(repoid)
1734 except ValueError:
1733 except ValueError:
1735 repo = None
1734 repo = None
1736 else:
1735 else:
1737 repo = cls.get_by_repo_name(repoid)
1736 repo = cls.get_by_repo_name(repoid)
1738 return repo
1737 return repo
1739
1738
1740 @classmethod
1739 @classmethod
1741 def get_by_full_path(cls, repo_full_path):
1740 def get_by_full_path(cls, repo_full_path):
1742 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1741 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1743 repo_name = cls.normalize_repo_name(repo_name)
1742 repo_name = cls.normalize_repo_name(repo_name)
1744 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1743 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1745
1744
1746 @classmethod
1745 @classmethod
1747 def get_repo_forks(cls, repo_id):
1746 def get_repo_forks(cls, repo_id):
1748 return cls.query().filter(Repository.fork_id == repo_id)
1747 return cls.query().filter(Repository.fork_id == repo_id)
1749
1748
1750 @classmethod
1749 @classmethod
1751 def base_path(cls):
1750 def base_path(cls):
1752 """
1751 """
1753 Returns base path when all repos are stored
1752 Returns base path when all repos are stored
1754
1753
1755 :param cls:
1754 :param cls:
1756 """
1755 """
1757 q = Session().query(RhodeCodeUi)\
1756 q = Session().query(RhodeCodeUi)\
1758 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1757 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1759 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1758 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1760 return q.one().ui_value
1759 return q.one().ui_value
1761
1760
1762 @classmethod
1761 @classmethod
1763 def is_valid(cls, repo_name):
1762 def is_valid(cls, repo_name):
1764 """
1763 """
1765 returns True if given repo name is a valid filesystem repository
1764 returns True if given repo name is a valid filesystem repository
1766
1765
1767 :param cls:
1766 :param cls:
1768 :param repo_name:
1767 :param repo_name:
1769 """
1768 """
1770 from rhodecode.lib.utils import is_valid_repo
1769 from rhodecode.lib.utils import is_valid_repo
1771
1770
1772 return is_valid_repo(repo_name, cls.base_path())
1771 return is_valid_repo(repo_name, cls.base_path())
1773
1772
1774 @classmethod
1773 @classmethod
1775 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1774 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1776 case_insensitive=True):
1775 case_insensitive=True):
1777 q = Repository.query()
1776 q = Repository.query()
1778
1777
1779 if not isinstance(user_id, Optional):
1778 if not isinstance(user_id, Optional):
1780 q = q.filter(Repository.user_id == user_id)
1779 q = q.filter(Repository.user_id == user_id)
1781
1780
1782 if not isinstance(group_id, Optional):
1781 if not isinstance(group_id, Optional):
1783 q = q.filter(Repository.group_id == group_id)
1782 q = q.filter(Repository.group_id == group_id)
1784
1783
1785 if case_insensitive:
1784 if case_insensitive:
1786 q = q.order_by(func.lower(Repository.repo_name))
1785 q = q.order_by(func.lower(Repository.repo_name))
1787 else:
1786 else:
1788 q = q.order_by(Repository.repo_name)
1787 q = q.order_by(Repository.repo_name)
1789 return q.all()
1788 return q.all()
1790
1789
1791 @property
1790 @property
1792 def forks(self):
1791 def forks(self):
1793 """
1792 """
1794 Return forks of this repo
1793 Return forks of this repo
1795 """
1794 """
1796 return Repository.get_repo_forks(self.repo_id)
1795 return Repository.get_repo_forks(self.repo_id)
1797
1796
1798 @property
1797 @property
1799 def parent(self):
1798 def parent(self):
1800 """
1799 """
1801 Returns fork parent
1800 Returns fork parent
1802 """
1801 """
1803 return self.fork
1802 return self.fork
1804
1803
1805 @property
1804 @property
1806 def just_name(self):
1805 def just_name(self):
1807 return self.repo_name.split(self.NAME_SEP)[-1]
1806 return self.repo_name.split(self.NAME_SEP)[-1]
1808
1807
1809 @property
1808 @property
1810 def groups_with_parents(self):
1809 def groups_with_parents(self):
1811 groups = []
1810 groups = []
1812 if self.group is None:
1811 if self.group is None:
1813 return groups
1812 return groups
1814
1813
1815 cur_gr = self.group
1814 cur_gr = self.group
1816 groups.insert(0, cur_gr)
1815 groups.insert(0, cur_gr)
1817 while 1:
1816 while 1:
1818 gr = getattr(cur_gr, 'parent_group', None)
1817 gr = getattr(cur_gr, 'parent_group', None)
1819 cur_gr = cur_gr.parent_group
1818 cur_gr = cur_gr.parent_group
1820 if gr is None:
1819 if gr is None:
1821 break
1820 break
1822 groups.insert(0, gr)
1821 groups.insert(0, gr)
1823
1822
1824 return groups
1823 return groups
1825
1824
1826 @property
1825 @property
1827 def groups_and_repo(self):
1826 def groups_and_repo(self):
1828 return self.groups_with_parents, self
1827 return self.groups_with_parents, self
1829
1828
1830 @LazyProperty
1829 @LazyProperty
1831 def repo_path(self):
1830 def repo_path(self):
1832 """
1831 """
1833 Returns base full path for that repository means where it actually
1832 Returns base full path for that repository means where it actually
1834 exists on a filesystem
1833 exists on a filesystem
1835 """
1834 """
1836 q = Session().query(RhodeCodeUi).filter(
1835 q = Session().query(RhodeCodeUi).filter(
1837 RhodeCodeUi.ui_key == self.NAME_SEP)
1836 RhodeCodeUi.ui_key == self.NAME_SEP)
1838 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1837 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1839 return q.one().ui_value
1838 return q.one().ui_value
1840
1839
1841 @property
1840 @property
1842 def repo_full_path(self):
1841 def repo_full_path(self):
1843 p = [self.repo_path]
1842 p = [self.repo_path]
1844 # we need to split the name by / since this is how we store the
1843 # we need to split the name by / since this is how we store the
1845 # names in the database, but that eventually needs to be converted
1844 # names in the database, but that eventually needs to be converted
1846 # into a valid system path
1845 # into a valid system path
1847 p += self.repo_name.split(self.NAME_SEP)
1846 p += self.repo_name.split(self.NAME_SEP)
1848 return os.path.join(*map(safe_unicode, p))
1847 return os.path.join(*map(safe_unicode, p))
1849
1848
1850 @property
1849 @property
1851 def cache_keys(self):
1850 def cache_keys(self):
1852 """
1851 """
1853 Returns associated cache keys for that repo
1852 Returns associated cache keys for that repo
1854 """
1853 """
1855 return CacheKey.query()\
1854 return CacheKey.query()\
1856 .filter(CacheKey.cache_args == self.repo_name)\
1855 .filter(CacheKey.cache_args == self.repo_name)\
1857 .order_by(CacheKey.cache_key)\
1856 .order_by(CacheKey.cache_key)\
1858 .all()
1857 .all()
1859
1858
1860 def get_new_name(self, repo_name):
1859 def get_new_name(self, repo_name):
1861 """
1860 """
1862 returns new full repository name based on assigned group and new new
1861 returns new full repository name based on assigned group and new new
1863
1862
1864 :param group_name:
1863 :param group_name:
1865 """
1864 """
1866 path_prefix = self.group.full_path_splitted if self.group else []
1865 path_prefix = self.group.full_path_splitted if self.group else []
1867 return self.NAME_SEP.join(path_prefix + [repo_name])
1866 return self.NAME_SEP.join(path_prefix + [repo_name])
1868
1867
1869 @property
1868 @property
1870 def _config(self):
1869 def _config(self):
1871 """
1870 """
1872 Returns db based config object.
1871 Returns db based config object.
1873 """
1872 """
1874 from rhodecode.lib.utils import make_db_config
1873 from rhodecode.lib.utils import make_db_config
1875 return make_db_config(clear_session=False, repo=self)
1874 return make_db_config(clear_session=False, repo=self)
1876
1875
1877 def permissions(self, with_admins=True, with_owner=True):
1876 def permissions(self, with_admins=True, with_owner=True):
1878 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1877 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1879 q = q.options(joinedload(UserRepoToPerm.repository),
1878 q = q.options(joinedload(UserRepoToPerm.repository),
1880 joinedload(UserRepoToPerm.user),
1879 joinedload(UserRepoToPerm.user),
1881 joinedload(UserRepoToPerm.permission),)
1880 joinedload(UserRepoToPerm.permission),)
1882
1881
1883 # get owners and admins and permissions. We do a trick of re-writing
1882 # get owners and admins and permissions. We do a trick of re-writing
1884 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1883 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1885 # has a global reference and changing one object propagates to all
1884 # has a global reference and changing one object propagates to all
1886 # others. This means if admin is also an owner admin_row that change
1885 # others. This means if admin is also an owner admin_row that change
1887 # would propagate to both objects
1886 # would propagate to both objects
1888 perm_rows = []
1887 perm_rows = []
1889 for _usr in q.all():
1888 for _usr in q.all():
1890 usr = AttributeDict(_usr.user.get_dict())
1889 usr = AttributeDict(_usr.user.get_dict())
1891 usr.permission = _usr.permission.permission_name
1890 usr.permission = _usr.permission.permission_name
1892 perm_rows.append(usr)
1891 perm_rows.append(usr)
1893
1892
1894 # filter the perm rows by 'default' first and then sort them by
1893 # filter the perm rows by 'default' first and then sort them by
1895 # admin,write,read,none permissions sorted again alphabetically in
1894 # admin,write,read,none permissions sorted again alphabetically in
1896 # each group
1895 # each group
1897 perm_rows = sorted(perm_rows, key=display_user_sort)
1896 perm_rows = sorted(perm_rows, key=display_user_sort)
1898
1897
1899 _admin_perm = 'repository.admin'
1898 _admin_perm = 'repository.admin'
1900 owner_row = []
1899 owner_row = []
1901 if with_owner:
1900 if with_owner:
1902 usr = AttributeDict(self.user.get_dict())
1901 usr = AttributeDict(self.user.get_dict())
1903 usr.owner_row = True
1902 usr.owner_row = True
1904 usr.permission = _admin_perm
1903 usr.permission = _admin_perm
1905 owner_row.append(usr)
1904 owner_row.append(usr)
1906
1905
1907 super_admin_rows = []
1906 super_admin_rows = []
1908 if with_admins:
1907 if with_admins:
1909 for usr in User.get_all_super_admins():
1908 for usr in User.get_all_super_admins():
1910 # if this admin is also owner, don't double the record
1909 # if this admin is also owner, don't double the record
1911 if usr.user_id == owner_row[0].user_id:
1910 if usr.user_id == owner_row[0].user_id:
1912 owner_row[0].admin_row = True
1911 owner_row[0].admin_row = True
1913 else:
1912 else:
1914 usr = AttributeDict(usr.get_dict())
1913 usr = AttributeDict(usr.get_dict())
1915 usr.admin_row = True
1914 usr.admin_row = True
1916 usr.permission = _admin_perm
1915 usr.permission = _admin_perm
1917 super_admin_rows.append(usr)
1916 super_admin_rows.append(usr)
1918
1917
1919 return super_admin_rows + owner_row + perm_rows
1918 return super_admin_rows + owner_row + perm_rows
1920
1919
1921 def permission_user_groups(self):
1920 def permission_user_groups(self):
1922 q = UserGroupRepoToPerm.query().filter(
1921 q = UserGroupRepoToPerm.query().filter(
1923 UserGroupRepoToPerm.repository == self)
1922 UserGroupRepoToPerm.repository == self)
1924 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1923 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1925 joinedload(UserGroupRepoToPerm.users_group),
1924 joinedload(UserGroupRepoToPerm.users_group),
1926 joinedload(UserGroupRepoToPerm.permission),)
1925 joinedload(UserGroupRepoToPerm.permission),)
1927
1926
1928 perm_rows = []
1927 perm_rows = []
1929 for _user_group in q.all():
1928 for _user_group in q.all():
1930 usr = AttributeDict(_user_group.users_group.get_dict())
1929 usr = AttributeDict(_user_group.users_group.get_dict())
1931 usr.permission = _user_group.permission.permission_name
1930 usr.permission = _user_group.permission.permission_name
1932 perm_rows.append(usr)
1931 perm_rows.append(usr)
1933
1932
1934 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1933 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1935 return perm_rows
1934 return perm_rows
1936
1935
1937 def get_api_data(self, include_secrets=False):
1936 def get_api_data(self, include_secrets=False):
1938 """
1937 """
1939 Common function for generating repo api data
1938 Common function for generating repo api data
1940
1939
1941 :param include_secrets: See :meth:`User.get_api_data`.
1940 :param include_secrets: See :meth:`User.get_api_data`.
1942
1941
1943 """
1942 """
1944 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1943 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1945 # move this methods on models level.
1944 # move this methods on models level.
1946 from rhodecode.model.settings import SettingsModel
1945 from rhodecode.model.settings import SettingsModel
1947 from rhodecode.model.repo import RepoModel
1946 from rhodecode.model.repo import RepoModel
1948
1947
1949 repo = self
1948 repo = self
1950 _user_id, _time, _reason = self.locked
1949 _user_id, _time, _reason = self.locked
1951
1950
1952 data = {
1951 data = {
1953 'repo_id': repo.repo_id,
1952 'repo_id': repo.repo_id,
1954 'repo_name': repo.repo_name,
1953 'repo_name': repo.repo_name,
1955 'repo_type': repo.repo_type,
1954 'repo_type': repo.repo_type,
1956 'clone_uri': repo.clone_uri or '',
1955 'clone_uri': repo.clone_uri or '',
1957 'url': RepoModel().get_url(self),
1956 'url': RepoModel().get_url(self),
1958 'private': repo.private,
1957 'private': repo.private,
1959 'created_on': repo.created_on,
1958 'created_on': repo.created_on,
1960 'description': repo.description_safe,
1959 'description': repo.description_safe,
1961 'landing_rev': repo.landing_rev,
1960 'landing_rev': repo.landing_rev,
1962 'owner': repo.user.username,
1961 'owner': repo.user.username,
1963 'fork_of': repo.fork.repo_name if repo.fork else None,
1962 'fork_of': repo.fork.repo_name if repo.fork else None,
1964 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1963 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1965 'enable_statistics': repo.enable_statistics,
1964 'enable_statistics': repo.enable_statistics,
1966 'enable_locking': repo.enable_locking,
1965 'enable_locking': repo.enable_locking,
1967 'enable_downloads': repo.enable_downloads,
1966 'enable_downloads': repo.enable_downloads,
1968 'last_changeset': repo.changeset_cache,
1967 'last_changeset': repo.changeset_cache,
1969 'locked_by': User.get(_user_id).get_api_data(
1968 'locked_by': User.get(_user_id).get_api_data(
1970 include_secrets=include_secrets) if _user_id else None,
1969 include_secrets=include_secrets) if _user_id else None,
1971 'locked_date': time_to_datetime(_time) if _time else None,
1970 'locked_date': time_to_datetime(_time) if _time else None,
1972 'lock_reason': _reason if _reason else None,
1971 'lock_reason': _reason if _reason else None,
1973 }
1972 }
1974
1973
1975 # TODO: mikhail: should be per-repo settings here
1974 # TODO: mikhail: should be per-repo settings here
1976 rc_config = SettingsModel().get_all_settings()
1975 rc_config = SettingsModel().get_all_settings()
1977 repository_fields = str2bool(
1976 repository_fields = str2bool(
1978 rc_config.get('rhodecode_repository_fields'))
1977 rc_config.get('rhodecode_repository_fields'))
1979 if repository_fields:
1978 if repository_fields:
1980 for f in self.extra_fields:
1979 for f in self.extra_fields:
1981 data[f.field_key_prefixed] = f.field_value
1980 data[f.field_key_prefixed] = f.field_value
1982
1981
1983 return data
1982 return data
1984
1983
1985 @classmethod
1984 @classmethod
1986 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1985 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1987 if not lock_time:
1986 if not lock_time:
1988 lock_time = time.time()
1987 lock_time = time.time()
1989 if not lock_reason:
1988 if not lock_reason:
1990 lock_reason = cls.LOCK_AUTOMATIC
1989 lock_reason = cls.LOCK_AUTOMATIC
1991 repo.locked = [user_id, lock_time, lock_reason]
1990 repo.locked = [user_id, lock_time, lock_reason]
1992 Session().add(repo)
1991 Session().add(repo)
1993 Session().commit()
1992 Session().commit()
1994
1993
1995 @classmethod
1994 @classmethod
1996 def unlock(cls, repo):
1995 def unlock(cls, repo):
1997 repo.locked = None
1996 repo.locked = None
1998 Session().add(repo)
1997 Session().add(repo)
1999 Session().commit()
1998 Session().commit()
2000
1999
2001 @classmethod
2000 @classmethod
2002 def getlock(cls, repo):
2001 def getlock(cls, repo):
2003 return repo.locked
2002 return repo.locked
2004
2003
2005 def is_user_lock(self, user_id):
2004 def is_user_lock(self, user_id):
2006 if self.lock[0]:
2005 if self.lock[0]:
2007 lock_user_id = safe_int(self.lock[0])
2006 lock_user_id = safe_int(self.lock[0])
2008 user_id = safe_int(user_id)
2007 user_id = safe_int(user_id)
2009 # both are ints, and they are equal
2008 # both are ints, and they are equal
2010 return all([lock_user_id, user_id]) and lock_user_id == user_id
2009 return all([lock_user_id, user_id]) and lock_user_id == user_id
2011
2010
2012 return False
2011 return False
2013
2012
2014 def get_locking_state(self, action, user_id, only_when_enabled=True):
2013 def get_locking_state(self, action, user_id, only_when_enabled=True):
2015 """
2014 """
2016 Checks locking on this repository, if locking is enabled and lock is
2015 Checks locking on this repository, if locking is enabled and lock is
2017 present returns a tuple of make_lock, locked, locked_by.
2016 present returns a tuple of make_lock, locked, locked_by.
2018 make_lock can have 3 states None (do nothing) True, make lock
2017 make_lock can have 3 states None (do nothing) True, make lock
2019 False release lock, This value is later propagated to hooks, which
2018 False release lock, This value is later propagated to hooks, which
2020 do the locking. Think about this as signals passed to hooks what to do.
2019 do the locking. Think about this as signals passed to hooks what to do.
2021
2020
2022 """
2021 """
2023 # TODO: johbo: This is part of the business logic and should be moved
2022 # TODO: johbo: This is part of the business logic and should be moved
2024 # into the RepositoryModel.
2023 # into the RepositoryModel.
2025
2024
2026 if action not in ('push', 'pull'):
2025 if action not in ('push', 'pull'):
2027 raise ValueError("Invalid action value: %s" % repr(action))
2026 raise ValueError("Invalid action value: %s" % repr(action))
2028
2027
2029 # defines if locked error should be thrown to user
2028 # defines if locked error should be thrown to user
2030 currently_locked = False
2029 currently_locked = False
2031 # defines if new lock should be made, tri-state
2030 # defines if new lock should be made, tri-state
2032 make_lock = None
2031 make_lock = None
2033 repo = self
2032 repo = self
2034 user = User.get(user_id)
2033 user = User.get(user_id)
2035
2034
2036 lock_info = repo.locked
2035 lock_info = repo.locked
2037
2036
2038 if repo and (repo.enable_locking or not only_when_enabled):
2037 if repo and (repo.enable_locking or not only_when_enabled):
2039 if action == 'push':
2038 if action == 'push':
2040 # check if it's already locked !, if it is compare users
2039 # check if it's already locked !, if it is compare users
2041 locked_by_user_id = lock_info[0]
2040 locked_by_user_id = lock_info[0]
2042 if user.user_id == locked_by_user_id:
2041 if user.user_id == locked_by_user_id:
2043 log.debug(
2042 log.debug(
2044 'Got `push` action from user %s, now unlocking', user)
2043 'Got `push` action from user %s, now unlocking', user)
2045 # unlock if we have push from user who locked
2044 # unlock if we have push from user who locked
2046 make_lock = False
2045 make_lock = False
2047 else:
2046 else:
2048 # we're not the same user who locked, ban with
2047 # we're not the same user who locked, ban with
2049 # code defined in settings (default is 423 HTTP Locked) !
2048 # code defined in settings (default is 423 HTTP Locked) !
2050 log.debug('Repo %s is currently locked by %s', repo, user)
2049 log.debug('Repo %s is currently locked by %s', repo, user)
2051 currently_locked = True
2050 currently_locked = True
2052 elif action == 'pull':
2051 elif action == 'pull':
2053 # [0] user [1] date
2052 # [0] user [1] date
2054 if lock_info[0] and lock_info[1]:
2053 if lock_info[0] and lock_info[1]:
2055 log.debug('Repo %s is currently locked by %s', repo, user)
2054 log.debug('Repo %s is currently locked by %s', repo, user)
2056 currently_locked = True
2055 currently_locked = True
2057 else:
2056 else:
2058 log.debug('Setting lock on repo %s by %s', repo, user)
2057 log.debug('Setting lock on repo %s by %s', repo, user)
2059 make_lock = True
2058 make_lock = True
2060
2059
2061 else:
2060 else:
2062 log.debug('Repository %s do not have locking enabled', repo)
2061 log.debug('Repository %s do not have locking enabled', repo)
2063
2062
2064 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2063 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2065 make_lock, currently_locked, lock_info)
2064 make_lock, currently_locked, lock_info)
2066
2065
2067 from rhodecode.lib.auth import HasRepoPermissionAny
2066 from rhodecode.lib.auth import HasRepoPermissionAny
2068 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2067 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2069 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2068 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2070 # if we don't have at least write permission we cannot make a lock
2069 # if we don't have at least write permission we cannot make a lock
2071 log.debug('lock state reset back to FALSE due to lack '
2070 log.debug('lock state reset back to FALSE due to lack '
2072 'of at least read permission')
2071 'of at least read permission')
2073 make_lock = False
2072 make_lock = False
2074
2073
2075 return make_lock, currently_locked, lock_info
2074 return make_lock, currently_locked, lock_info
2076
2075
2077 @property
2076 @property
2078 def last_db_change(self):
2077 def last_db_change(self):
2079 return self.updated_on
2078 return self.updated_on
2080
2079
2081 @property
2080 @property
2082 def clone_uri_hidden(self):
2081 def clone_uri_hidden(self):
2083 clone_uri = self.clone_uri
2082 clone_uri = self.clone_uri
2084 if clone_uri:
2083 if clone_uri:
2085 import urlobject
2084 import urlobject
2086 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2085 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2087 if url_obj.password:
2086 if url_obj.password:
2088 clone_uri = url_obj.with_password('*****')
2087 clone_uri = url_obj.with_password('*****')
2089 return clone_uri
2088 return clone_uri
2090
2089
2091 def clone_url(self, **override):
2090 def clone_url(self, **override):
2092 from rhodecode.model.settings import SettingsModel
2091 from rhodecode.model.settings import SettingsModel
2093
2092
2094 uri_tmpl = None
2093 uri_tmpl = None
2095 if 'with_id' in override:
2094 if 'with_id' in override:
2096 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2095 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2097 del override['with_id']
2096 del override['with_id']
2098
2097
2099 if 'uri_tmpl' in override:
2098 if 'uri_tmpl' in override:
2100 uri_tmpl = override['uri_tmpl']
2099 uri_tmpl = override['uri_tmpl']
2101 del override['uri_tmpl']
2100 del override['uri_tmpl']
2102
2101
2103 # we didn't override our tmpl from **overrides
2102 # we didn't override our tmpl from **overrides
2104 if not uri_tmpl:
2103 if not uri_tmpl:
2105 rc_config = SettingsModel().get_all_settings(cache=True)
2104 rc_config = SettingsModel().get_all_settings(cache=True)
2106 uri_tmpl = rc_config.get(
2105 uri_tmpl = rc_config.get(
2107 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2106 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2108
2107
2109 request = get_current_request()
2108 request = get_current_request()
2110 return get_clone_url(request=request,
2109 return get_clone_url(request=request,
2111 uri_tmpl=uri_tmpl,
2110 uri_tmpl=uri_tmpl,
2112 repo_name=self.repo_name,
2111 repo_name=self.repo_name,
2113 repo_id=self.repo_id, **override)
2112 repo_id=self.repo_id, **override)
2114
2113
2115 def set_state(self, state):
2114 def set_state(self, state):
2116 self.repo_state = state
2115 self.repo_state = state
2117 Session().add(self)
2116 Session().add(self)
2118 #==========================================================================
2117 #==========================================================================
2119 # SCM PROPERTIES
2118 # SCM PROPERTIES
2120 #==========================================================================
2119 #==========================================================================
2121
2120
2122 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2121 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2123 return get_commit_safe(
2122 return get_commit_safe(
2124 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2123 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2125
2124
2126 def get_changeset(self, rev=None, pre_load=None):
2125 def get_changeset(self, rev=None, pre_load=None):
2127 warnings.warn("Use get_commit", DeprecationWarning)
2126 warnings.warn("Use get_commit", DeprecationWarning)
2128 commit_id = None
2127 commit_id = None
2129 commit_idx = None
2128 commit_idx = None
2130 if isinstance(rev, basestring):
2129 if isinstance(rev, basestring):
2131 commit_id = rev
2130 commit_id = rev
2132 else:
2131 else:
2133 commit_idx = rev
2132 commit_idx = rev
2134 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2133 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2135 pre_load=pre_load)
2134 pre_load=pre_load)
2136
2135
2137 def get_landing_commit(self):
2136 def get_landing_commit(self):
2138 """
2137 """
2139 Returns landing commit, or if that doesn't exist returns the tip
2138 Returns landing commit, or if that doesn't exist returns the tip
2140 """
2139 """
2141 _rev_type, _rev = self.landing_rev
2140 _rev_type, _rev = self.landing_rev
2142 commit = self.get_commit(_rev)
2141 commit = self.get_commit(_rev)
2143 if isinstance(commit, EmptyCommit):
2142 if isinstance(commit, EmptyCommit):
2144 return self.get_commit()
2143 return self.get_commit()
2145 return commit
2144 return commit
2146
2145
2147 def update_commit_cache(self, cs_cache=None, config=None):
2146 def update_commit_cache(self, cs_cache=None, config=None):
2148 """
2147 """
2149 Update cache of last changeset for repository, keys should be::
2148 Update cache of last changeset for repository, keys should be::
2150
2149
2151 short_id
2150 short_id
2152 raw_id
2151 raw_id
2153 revision
2152 revision
2154 parents
2153 parents
2155 message
2154 message
2156 date
2155 date
2157 author
2156 author
2158
2157
2159 :param cs_cache:
2158 :param cs_cache:
2160 """
2159 """
2161 from rhodecode.lib.vcs.backends.base import BaseChangeset
2160 from rhodecode.lib.vcs.backends.base import BaseChangeset
2162 if cs_cache is None:
2161 if cs_cache is None:
2163 # use no-cache version here
2162 # use no-cache version here
2164 scm_repo = self.scm_instance(cache=False, config=config)
2163 scm_repo = self.scm_instance(cache=False, config=config)
2165 if scm_repo:
2164 if scm_repo:
2166 cs_cache = scm_repo.get_commit(
2165 cs_cache = scm_repo.get_commit(
2167 pre_load=["author", "date", "message", "parents"])
2166 pre_load=["author", "date", "message", "parents"])
2168 else:
2167 else:
2169 cs_cache = EmptyCommit()
2168 cs_cache = EmptyCommit()
2170
2169
2171 if isinstance(cs_cache, BaseChangeset):
2170 if isinstance(cs_cache, BaseChangeset):
2172 cs_cache = cs_cache.__json__()
2171 cs_cache = cs_cache.__json__()
2173
2172
2174 def is_outdated(new_cs_cache):
2173 def is_outdated(new_cs_cache):
2175 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2174 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2176 new_cs_cache['revision'] != self.changeset_cache['revision']):
2175 new_cs_cache['revision'] != self.changeset_cache['revision']):
2177 return True
2176 return True
2178 return False
2177 return False
2179
2178
2180 # check if we have maybe already latest cached revision
2179 # check if we have maybe already latest cached revision
2181 if is_outdated(cs_cache) or not self.changeset_cache:
2180 if is_outdated(cs_cache) or not self.changeset_cache:
2182 _default = datetime.datetime.fromtimestamp(0)
2181 _default = datetime.datetime.fromtimestamp(0)
2183 last_change = cs_cache.get('date') or _default
2182 last_change = cs_cache.get('date') or _default
2184 log.debug('updated repo %s with new cs cache %s',
2183 log.debug('updated repo %s with new cs cache %s',
2185 self.repo_name, cs_cache)
2184 self.repo_name, cs_cache)
2186 self.updated_on = last_change
2185 self.updated_on = last_change
2187 self.changeset_cache = cs_cache
2186 self.changeset_cache = cs_cache
2188 Session().add(self)
2187 Session().add(self)
2189 Session().commit()
2188 Session().commit()
2190 else:
2189 else:
2191 log.debug('Skipping update_commit_cache for repo:`%s` '
2190 log.debug('Skipping update_commit_cache for repo:`%s` '
2192 'commit already with latest changes', self.repo_name)
2191 'commit already with latest changes', self.repo_name)
2193
2192
2194 @property
2193 @property
2195 def tip(self):
2194 def tip(self):
2196 return self.get_commit('tip')
2195 return self.get_commit('tip')
2197
2196
2198 @property
2197 @property
2199 def author(self):
2198 def author(self):
2200 return self.tip.author
2199 return self.tip.author
2201
2200
2202 @property
2201 @property
2203 def last_change(self):
2202 def last_change(self):
2204 return self.scm_instance().last_change
2203 return self.scm_instance().last_change
2205
2204
2206 def get_comments(self, revisions=None):
2205 def get_comments(self, revisions=None):
2207 """
2206 """
2208 Returns comments for this repository grouped by revisions
2207 Returns comments for this repository grouped by revisions
2209
2208
2210 :param revisions: filter query by revisions only
2209 :param revisions: filter query by revisions only
2211 """
2210 """
2212 cmts = ChangesetComment.query()\
2211 cmts = ChangesetComment.query()\
2213 .filter(ChangesetComment.repo == self)
2212 .filter(ChangesetComment.repo == self)
2214 if revisions:
2213 if revisions:
2215 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2214 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2216 grouped = collections.defaultdict(list)
2215 grouped = collections.defaultdict(list)
2217 for cmt in cmts.all():
2216 for cmt in cmts.all():
2218 grouped[cmt.revision].append(cmt)
2217 grouped[cmt.revision].append(cmt)
2219 return grouped
2218 return grouped
2220
2219
2221 def statuses(self, revisions=None):
2220 def statuses(self, revisions=None):
2222 """
2221 """
2223 Returns statuses for this repository
2222 Returns statuses for this repository
2224
2223
2225 :param revisions: list of revisions to get statuses for
2224 :param revisions: list of revisions to get statuses for
2226 """
2225 """
2227 statuses = ChangesetStatus.query()\
2226 statuses = ChangesetStatus.query()\
2228 .filter(ChangesetStatus.repo == self)\
2227 .filter(ChangesetStatus.repo == self)\
2229 .filter(ChangesetStatus.version == 0)
2228 .filter(ChangesetStatus.version == 0)
2230
2229
2231 if revisions:
2230 if revisions:
2232 # Try doing the filtering in chunks to avoid hitting limits
2231 # Try doing the filtering in chunks to avoid hitting limits
2233 size = 500
2232 size = 500
2234 status_results = []
2233 status_results = []
2235 for chunk in xrange(0, len(revisions), size):
2234 for chunk in xrange(0, len(revisions), size):
2236 status_results += statuses.filter(
2235 status_results += statuses.filter(
2237 ChangesetStatus.revision.in_(
2236 ChangesetStatus.revision.in_(
2238 revisions[chunk: chunk+size])
2237 revisions[chunk: chunk+size])
2239 ).all()
2238 ).all()
2240 else:
2239 else:
2241 status_results = statuses.all()
2240 status_results = statuses.all()
2242
2241
2243 grouped = {}
2242 grouped = {}
2244
2243
2245 # maybe we have open new pullrequest without a status?
2244 # maybe we have open new pullrequest without a status?
2246 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2245 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2247 status_lbl = ChangesetStatus.get_status_lbl(stat)
2246 status_lbl = ChangesetStatus.get_status_lbl(stat)
2248 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2247 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2249 for rev in pr.revisions:
2248 for rev in pr.revisions:
2250 pr_id = pr.pull_request_id
2249 pr_id = pr.pull_request_id
2251 pr_repo = pr.target_repo.repo_name
2250 pr_repo = pr.target_repo.repo_name
2252 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2251 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2253
2252
2254 for stat in status_results:
2253 for stat in status_results:
2255 pr_id = pr_repo = None
2254 pr_id = pr_repo = None
2256 if stat.pull_request:
2255 if stat.pull_request:
2257 pr_id = stat.pull_request.pull_request_id
2256 pr_id = stat.pull_request.pull_request_id
2258 pr_repo = stat.pull_request.target_repo.repo_name
2257 pr_repo = stat.pull_request.target_repo.repo_name
2259 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2258 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2260 pr_id, pr_repo]
2259 pr_id, pr_repo]
2261 return grouped
2260 return grouped
2262
2261
2263 # ==========================================================================
2262 # ==========================================================================
2264 # SCM CACHE INSTANCE
2263 # SCM CACHE INSTANCE
2265 # ==========================================================================
2264 # ==========================================================================
2266
2265
2267 def scm_instance(self, **kwargs):
2266 def scm_instance(self, **kwargs):
2268 import rhodecode
2267 import rhodecode
2269
2268
2270 # Passing a config will not hit the cache currently only used
2269 # Passing a config will not hit the cache currently only used
2271 # for repo2dbmapper
2270 # for repo2dbmapper
2272 config = kwargs.pop('config', None)
2271 config = kwargs.pop('config', None)
2273 cache = kwargs.pop('cache', None)
2272 cache = kwargs.pop('cache', None)
2274 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2273 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2275 # if cache is NOT defined use default global, else we have a full
2274 # if cache is NOT defined use default global, else we have a full
2276 # control over cache behaviour
2275 # control over cache behaviour
2277 if cache is None and full_cache and not config:
2276 if cache is None and full_cache and not config:
2278 return self._get_instance_cached()
2277 return self._get_instance_cached()
2279 return self._get_instance(cache=bool(cache), config=config)
2278 return self._get_instance(cache=bool(cache), config=config)
2280
2279
2281 def _get_instance_cached(self):
2280 def _get_instance_cached(self):
2282 @cache_region('long_term')
2281 @cache_region('long_term')
2283 def _get_repo(cache_key):
2282 def _get_repo(cache_key):
2284 return self._get_instance()
2283 return self._get_instance()
2285
2284
2286 invalidator_context = CacheKey.repo_context_cache(
2285 invalidator_context = CacheKey.repo_context_cache(
2287 _get_repo, self.repo_name, None, thread_scoped=True)
2286 _get_repo, self.repo_name, None, thread_scoped=True)
2288
2287
2289 with invalidator_context as context:
2288 with invalidator_context as context:
2290 context.invalidate()
2289 context.invalidate()
2291 repo = context.compute()
2290 repo = context.compute()
2292
2291
2293 return repo
2292 return repo
2294
2293
2295 def _get_instance(self, cache=True, config=None):
2294 def _get_instance(self, cache=True, config=None):
2296 config = config or self._config
2295 config = config or self._config
2297 custom_wire = {
2296 custom_wire = {
2298 'cache': cache # controls the vcs.remote cache
2297 'cache': cache # controls the vcs.remote cache
2299 }
2298 }
2300 repo = get_vcs_instance(
2299 repo = get_vcs_instance(
2301 repo_path=safe_str(self.repo_full_path),
2300 repo_path=safe_str(self.repo_full_path),
2302 config=config,
2301 config=config,
2303 with_wire=custom_wire,
2302 with_wire=custom_wire,
2304 create=False,
2303 create=False,
2305 _vcs_alias=self.repo_type)
2304 _vcs_alias=self.repo_type)
2306
2305
2307 return repo
2306 return repo
2308
2307
2309 def __json__(self):
2308 def __json__(self):
2310 return {'landing_rev': self.landing_rev}
2309 return {'landing_rev': self.landing_rev}
2311
2310
2312 def get_dict(self):
2311 def get_dict(self):
2313
2312
2314 # Since we transformed `repo_name` to a hybrid property, we need to
2313 # Since we transformed `repo_name` to a hybrid property, we need to
2315 # keep compatibility with the code which uses `repo_name` field.
2314 # keep compatibility with the code which uses `repo_name` field.
2316
2315
2317 result = super(Repository, self).get_dict()
2316 result = super(Repository, self).get_dict()
2318 result['repo_name'] = result.pop('_repo_name', None)
2317 result['repo_name'] = result.pop('_repo_name', None)
2319 return result
2318 return result
2320
2319
2321
2320
2322 class RepoGroup(Base, BaseModel):
2321 class RepoGroup(Base, BaseModel):
2323 __tablename__ = 'groups'
2322 __tablename__ = 'groups'
2324 __table_args__ = (
2323 __table_args__ = (
2325 UniqueConstraint('group_name', 'group_parent_id'),
2324 UniqueConstraint('group_name', 'group_parent_id'),
2326 CheckConstraint('group_id != group_parent_id'),
2325 CheckConstraint('group_id != group_parent_id'),
2327 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2326 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2328 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2327 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2329 )
2328 )
2330 __mapper_args__ = {'order_by': 'group_name'}
2329 __mapper_args__ = {'order_by': 'group_name'}
2331
2330
2332 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2331 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2333
2332
2334 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2333 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2335 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2334 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2336 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2335 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2337 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2336 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2338 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2337 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2339 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2338 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2340 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2339 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2341 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2340 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2342 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2341 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2343
2342
2344 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2343 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2345 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2344 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2346 parent_group = relationship('RepoGroup', remote_side=group_id)
2345 parent_group = relationship('RepoGroup', remote_side=group_id)
2347 user = relationship('User')
2346 user = relationship('User')
2348 integrations = relationship('Integration',
2347 integrations = relationship('Integration',
2349 cascade="all, delete, delete-orphan")
2348 cascade="all, delete, delete-orphan")
2350
2349
2351 def __init__(self, group_name='', parent_group=None):
2350 def __init__(self, group_name='', parent_group=None):
2352 self.group_name = group_name
2351 self.group_name = group_name
2353 self.parent_group = parent_group
2352 self.parent_group = parent_group
2354
2353
2355 def __unicode__(self):
2354 def __unicode__(self):
2356 return u"<%s('id:%s:%s')>" % (
2355 return u"<%s('id:%s:%s')>" % (
2357 self.__class__.__name__, self.group_id, self.group_name)
2356 self.__class__.__name__, self.group_id, self.group_name)
2358
2357
2359 @hybrid_property
2358 @hybrid_property
2360 def description_safe(self):
2359 def description_safe(self):
2361 from rhodecode.lib import helpers as h
2360 from rhodecode.lib import helpers as h
2362 return h.escape(self.group_description)
2361 return h.escape(self.group_description)
2363
2362
2364 @classmethod
2363 @classmethod
2365 def _generate_choice(cls, repo_group):
2364 def _generate_choice(cls, repo_group):
2366 from webhelpers.html import literal as _literal
2365 from webhelpers.html import literal as _literal
2367 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2366 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2368 return repo_group.group_id, _name(repo_group.full_path_splitted)
2367 return repo_group.group_id, _name(repo_group.full_path_splitted)
2369
2368
2370 @classmethod
2369 @classmethod
2371 def groups_choices(cls, groups=None, show_empty_group=True):
2370 def groups_choices(cls, groups=None, show_empty_group=True):
2372 if not groups:
2371 if not groups:
2373 groups = cls.query().all()
2372 groups = cls.query().all()
2374
2373
2375 repo_groups = []
2374 repo_groups = []
2376 if show_empty_group:
2375 if show_empty_group:
2377 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2376 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2378
2377
2379 repo_groups.extend([cls._generate_choice(x) for x in groups])
2378 repo_groups.extend([cls._generate_choice(x) for x in groups])
2380
2379
2381 repo_groups = sorted(
2380 repo_groups = sorted(
2382 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2381 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2383 return repo_groups
2382 return repo_groups
2384
2383
2385 @classmethod
2384 @classmethod
2386 def url_sep(cls):
2385 def url_sep(cls):
2387 return URL_SEP
2386 return URL_SEP
2388
2387
2389 @classmethod
2388 @classmethod
2390 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2389 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2391 if case_insensitive:
2390 if case_insensitive:
2392 gr = cls.query().filter(func.lower(cls.group_name)
2391 gr = cls.query().filter(func.lower(cls.group_name)
2393 == func.lower(group_name))
2392 == func.lower(group_name))
2394 else:
2393 else:
2395 gr = cls.query().filter(cls.group_name == group_name)
2394 gr = cls.query().filter(cls.group_name == group_name)
2396 if cache:
2395 if cache:
2397 name_key = _hash_key(group_name)
2396 name_key = _hash_key(group_name)
2398 gr = gr.options(
2397 gr = gr.options(
2399 FromCache("sql_cache_short", "get_group_%s" % name_key))
2398 FromCache("sql_cache_short", "get_group_%s" % name_key))
2400 return gr.scalar()
2399 return gr.scalar()
2401
2400
2402 @classmethod
2401 @classmethod
2403 def get_user_personal_repo_group(cls, user_id):
2402 def get_user_personal_repo_group(cls, user_id):
2404 user = User.get(user_id)
2403 user = User.get(user_id)
2405 if user.username == User.DEFAULT_USER:
2404 if user.username == User.DEFAULT_USER:
2406 return None
2405 return None
2407
2406
2408 return cls.query()\
2407 return cls.query()\
2409 .filter(cls.personal == true()) \
2408 .filter(cls.personal == true()) \
2410 .filter(cls.user == user).scalar()
2409 .filter(cls.user == user).scalar()
2411
2410
2412 @classmethod
2411 @classmethod
2413 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2412 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2414 case_insensitive=True):
2413 case_insensitive=True):
2415 q = RepoGroup.query()
2414 q = RepoGroup.query()
2416
2415
2417 if not isinstance(user_id, Optional):
2416 if not isinstance(user_id, Optional):
2418 q = q.filter(RepoGroup.user_id == user_id)
2417 q = q.filter(RepoGroup.user_id == user_id)
2419
2418
2420 if not isinstance(group_id, Optional):
2419 if not isinstance(group_id, Optional):
2421 q = q.filter(RepoGroup.group_parent_id == group_id)
2420 q = q.filter(RepoGroup.group_parent_id == group_id)
2422
2421
2423 if case_insensitive:
2422 if case_insensitive:
2424 q = q.order_by(func.lower(RepoGroup.group_name))
2423 q = q.order_by(func.lower(RepoGroup.group_name))
2425 else:
2424 else:
2426 q = q.order_by(RepoGroup.group_name)
2425 q = q.order_by(RepoGroup.group_name)
2427 return q.all()
2426 return q.all()
2428
2427
2429 @property
2428 @property
2430 def parents(self):
2429 def parents(self):
2431 parents_recursion_limit = 10
2430 parents_recursion_limit = 10
2432 groups = []
2431 groups = []
2433 if self.parent_group is None:
2432 if self.parent_group is None:
2434 return groups
2433 return groups
2435 cur_gr = self.parent_group
2434 cur_gr = self.parent_group
2436 groups.insert(0, cur_gr)
2435 groups.insert(0, cur_gr)
2437 cnt = 0
2436 cnt = 0
2438 while 1:
2437 while 1:
2439 cnt += 1
2438 cnt += 1
2440 gr = getattr(cur_gr, 'parent_group', None)
2439 gr = getattr(cur_gr, 'parent_group', None)
2441 cur_gr = cur_gr.parent_group
2440 cur_gr = cur_gr.parent_group
2442 if gr is None:
2441 if gr is None:
2443 break
2442 break
2444 if cnt == parents_recursion_limit:
2443 if cnt == parents_recursion_limit:
2445 # this will prevent accidental infinit loops
2444 # this will prevent accidental infinit loops
2446 log.error(('more than %s parents found for group %s, stopping '
2445 log.error(('more than %s parents found for group %s, stopping '
2447 'recursive parent fetching' % (parents_recursion_limit, self)))
2446 'recursive parent fetching' % (parents_recursion_limit, self)))
2448 break
2447 break
2449
2448
2450 groups.insert(0, gr)
2449 groups.insert(0, gr)
2451 return groups
2450 return groups
2452
2451
2453 @property
2452 @property
2454 def last_db_change(self):
2453 def last_db_change(self):
2455 return self.updated_on
2454 return self.updated_on
2456
2455
2457 @property
2456 @property
2458 def children(self):
2457 def children(self):
2459 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2458 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2460
2459
2461 @property
2460 @property
2462 def name(self):
2461 def name(self):
2463 return self.group_name.split(RepoGroup.url_sep())[-1]
2462 return self.group_name.split(RepoGroup.url_sep())[-1]
2464
2463
2465 @property
2464 @property
2466 def full_path(self):
2465 def full_path(self):
2467 return self.group_name
2466 return self.group_name
2468
2467
2469 @property
2468 @property
2470 def full_path_splitted(self):
2469 def full_path_splitted(self):
2471 return self.group_name.split(RepoGroup.url_sep())
2470 return self.group_name.split(RepoGroup.url_sep())
2472
2471
2473 @property
2472 @property
2474 def repositories(self):
2473 def repositories(self):
2475 return Repository.query()\
2474 return Repository.query()\
2476 .filter(Repository.group == self)\
2475 .filter(Repository.group == self)\
2477 .order_by(Repository.repo_name)
2476 .order_by(Repository.repo_name)
2478
2477
2479 @property
2478 @property
2480 def repositories_recursive_count(self):
2479 def repositories_recursive_count(self):
2481 cnt = self.repositories.count()
2480 cnt = self.repositories.count()
2482
2481
2483 def children_count(group):
2482 def children_count(group):
2484 cnt = 0
2483 cnt = 0
2485 for child in group.children:
2484 for child in group.children:
2486 cnt += child.repositories.count()
2485 cnt += child.repositories.count()
2487 cnt += children_count(child)
2486 cnt += children_count(child)
2488 return cnt
2487 return cnt
2489
2488
2490 return cnt + children_count(self)
2489 return cnt + children_count(self)
2491
2490
2492 def _recursive_objects(self, include_repos=True):
2491 def _recursive_objects(self, include_repos=True):
2493 all_ = []
2492 all_ = []
2494
2493
2495 def _get_members(root_gr):
2494 def _get_members(root_gr):
2496 if include_repos:
2495 if include_repos:
2497 for r in root_gr.repositories:
2496 for r in root_gr.repositories:
2498 all_.append(r)
2497 all_.append(r)
2499 childs = root_gr.children.all()
2498 childs = root_gr.children.all()
2500 if childs:
2499 if childs:
2501 for gr in childs:
2500 for gr in childs:
2502 all_.append(gr)
2501 all_.append(gr)
2503 _get_members(gr)
2502 _get_members(gr)
2504
2503
2505 _get_members(self)
2504 _get_members(self)
2506 return [self] + all_
2505 return [self] + all_
2507
2506
2508 def recursive_groups_and_repos(self):
2507 def recursive_groups_and_repos(self):
2509 """
2508 """
2510 Recursive return all groups, with repositories in those groups
2509 Recursive return all groups, with repositories in those groups
2511 """
2510 """
2512 return self._recursive_objects()
2511 return self._recursive_objects()
2513
2512
2514 def recursive_groups(self):
2513 def recursive_groups(self):
2515 """
2514 """
2516 Returns all children groups for this group including children of children
2515 Returns all children groups for this group including children of children
2517 """
2516 """
2518 return self._recursive_objects(include_repos=False)
2517 return self._recursive_objects(include_repos=False)
2519
2518
2520 def get_new_name(self, group_name):
2519 def get_new_name(self, group_name):
2521 """
2520 """
2522 returns new full group name based on parent and new name
2521 returns new full group name based on parent and new name
2523
2522
2524 :param group_name:
2523 :param group_name:
2525 """
2524 """
2526 path_prefix = (self.parent_group.full_path_splitted if
2525 path_prefix = (self.parent_group.full_path_splitted if
2527 self.parent_group else [])
2526 self.parent_group else [])
2528 return RepoGroup.url_sep().join(path_prefix + [group_name])
2527 return RepoGroup.url_sep().join(path_prefix + [group_name])
2529
2528
2530 def permissions(self, with_admins=True, with_owner=True):
2529 def permissions(self, with_admins=True, with_owner=True):
2531 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2530 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2532 q = q.options(joinedload(UserRepoGroupToPerm.group),
2531 q = q.options(joinedload(UserRepoGroupToPerm.group),
2533 joinedload(UserRepoGroupToPerm.user),
2532 joinedload(UserRepoGroupToPerm.user),
2534 joinedload(UserRepoGroupToPerm.permission),)
2533 joinedload(UserRepoGroupToPerm.permission),)
2535
2534
2536 # get owners and admins and permissions. We do a trick of re-writing
2535 # get owners and admins and permissions. We do a trick of re-writing
2537 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2536 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2538 # has a global reference and changing one object propagates to all
2537 # has a global reference and changing one object propagates to all
2539 # others. This means if admin is also an owner admin_row that change
2538 # others. This means if admin is also an owner admin_row that change
2540 # would propagate to both objects
2539 # would propagate to both objects
2541 perm_rows = []
2540 perm_rows = []
2542 for _usr in q.all():
2541 for _usr in q.all():
2543 usr = AttributeDict(_usr.user.get_dict())
2542 usr = AttributeDict(_usr.user.get_dict())
2544 usr.permission = _usr.permission.permission_name
2543 usr.permission = _usr.permission.permission_name
2545 perm_rows.append(usr)
2544 perm_rows.append(usr)
2546
2545
2547 # filter the perm rows by 'default' first and then sort them by
2546 # filter the perm rows by 'default' first and then sort them by
2548 # admin,write,read,none permissions sorted again alphabetically in
2547 # admin,write,read,none permissions sorted again alphabetically in
2549 # each group
2548 # each group
2550 perm_rows = sorted(perm_rows, key=display_user_sort)
2549 perm_rows = sorted(perm_rows, key=display_user_sort)
2551
2550
2552 _admin_perm = 'group.admin'
2551 _admin_perm = 'group.admin'
2553 owner_row = []
2552 owner_row = []
2554 if with_owner:
2553 if with_owner:
2555 usr = AttributeDict(self.user.get_dict())
2554 usr = AttributeDict(self.user.get_dict())
2556 usr.owner_row = True
2555 usr.owner_row = True
2557 usr.permission = _admin_perm
2556 usr.permission = _admin_perm
2558 owner_row.append(usr)
2557 owner_row.append(usr)
2559
2558
2560 super_admin_rows = []
2559 super_admin_rows = []
2561 if with_admins:
2560 if with_admins:
2562 for usr in User.get_all_super_admins():
2561 for usr in User.get_all_super_admins():
2563 # if this admin is also owner, don't double the record
2562 # if this admin is also owner, don't double the record
2564 if usr.user_id == owner_row[0].user_id:
2563 if usr.user_id == owner_row[0].user_id:
2565 owner_row[0].admin_row = True
2564 owner_row[0].admin_row = True
2566 else:
2565 else:
2567 usr = AttributeDict(usr.get_dict())
2566 usr = AttributeDict(usr.get_dict())
2568 usr.admin_row = True
2567 usr.admin_row = True
2569 usr.permission = _admin_perm
2568 usr.permission = _admin_perm
2570 super_admin_rows.append(usr)
2569 super_admin_rows.append(usr)
2571
2570
2572 return super_admin_rows + owner_row + perm_rows
2571 return super_admin_rows + owner_row + perm_rows
2573
2572
2574 def permission_user_groups(self):
2573 def permission_user_groups(self):
2575 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2574 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2576 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2575 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2577 joinedload(UserGroupRepoGroupToPerm.users_group),
2576 joinedload(UserGroupRepoGroupToPerm.users_group),
2578 joinedload(UserGroupRepoGroupToPerm.permission),)
2577 joinedload(UserGroupRepoGroupToPerm.permission),)
2579
2578
2580 perm_rows = []
2579 perm_rows = []
2581 for _user_group in q.all():
2580 for _user_group in q.all():
2582 usr = AttributeDict(_user_group.users_group.get_dict())
2581 usr = AttributeDict(_user_group.users_group.get_dict())
2583 usr.permission = _user_group.permission.permission_name
2582 usr.permission = _user_group.permission.permission_name
2584 perm_rows.append(usr)
2583 perm_rows.append(usr)
2585
2584
2586 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2585 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2587 return perm_rows
2586 return perm_rows
2588
2587
2589 def get_api_data(self):
2588 def get_api_data(self):
2590 """
2589 """
2591 Common function for generating api data
2590 Common function for generating api data
2592
2591
2593 """
2592 """
2594 group = self
2593 group = self
2595 data = {
2594 data = {
2596 'group_id': group.group_id,
2595 'group_id': group.group_id,
2597 'group_name': group.group_name,
2596 'group_name': group.group_name,
2598 'group_description': group.description_safe,
2597 'group_description': group.description_safe,
2599 'parent_group': group.parent_group.group_name if group.parent_group else None,
2598 'parent_group': group.parent_group.group_name if group.parent_group else None,
2600 'repositories': [x.repo_name for x in group.repositories],
2599 'repositories': [x.repo_name for x in group.repositories],
2601 'owner': group.user.username,
2600 'owner': group.user.username,
2602 }
2601 }
2603 return data
2602 return data
2604
2603
2605
2604
2606 class Permission(Base, BaseModel):
2605 class Permission(Base, BaseModel):
2607 __tablename__ = 'permissions'
2606 __tablename__ = 'permissions'
2608 __table_args__ = (
2607 __table_args__ = (
2609 Index('p_perm_name_idx', 'permission_name'),
2608 Index('p_perm_name_idx', 'permission_name'),
2610 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2611 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2612 )
2611 )
2613 PERMS = [
2612 PERMS = [
2614 ('hg.admin', _('RhodeCode Super Administrator')),
2613 ('hg.admin', _('RhodeCode Super Administrator')),
2615
2614
2616 ('repository.none', _('Repository no access')),
2615 ('repository.none', _('Repository no access')),
2617 ('repository.read', _('Repository read access')),
2616 ('repository.read', _('Repository read access')),
2618 ('repository.write', _('Repository write access')),
2617 ('repository.write', _('Repository write access')),
2619 ('repository.admin', _('Repository admin access')),
2618 ('repository.admin', _('Repository admin access')),
2620
2619
2621 ('group.none', _('Repository group no access')),
2620 ('group.none', _('Repository group no access')),
2622 ('group.read', _('Repository group read access')),
2621 ('group.read', _('Repository group read access')),
2623 ('group.write', _('Repository group write access')),
2622 ('group.write', _('Repository group write access')),
2624 ('group.admin', _('Repository group admin access')),
2623 ('group.admin', _('Repository group admin access')),
2625
2624
2626 ('usergroup.none', _('User group no access')),
2625 ('usergroup.none', _('User group no access')),
2627 ('usergroup.read', _('User group read access')),
2626 ('usergroup.read', _('User group read access')),
2628 ('usergroup.write', _('User group write access')),
2627 ('usergroup.write', _('User group write access')),
2629 ('usergroup.admin', _('User group admin access')),
2628 ('usergroup.admin', _('User group admin access')),
2630
2629
2631 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2630 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2632 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2631 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2633
2632
2634 ('hg.usergroup.create.false', _('User Group creation disabled')),
2633 ('hg.usergroup.create.false', _('User Group creation disabled')),
2635 ('hg.usergroup.create.true', _('User Group creation enabled')),
2634 ('hg.usergroup.create.true', _('User Group creation enabled')),
2636
2635
2637 ('hg.create.none', _('Repository creation disabled')),
2636 ('hg.create.none', _('Repository creation disabled')),
2638 ('hg.create.repository', _('Repository creation enabled')),
2637 ('hg.create.repository', _('Repository creation enabled')),
2639 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2638 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2640 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2639 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2641
2640
2642 ('hg.fork.none', _('Repository forking disabled')),
2641 ('hg.fork.none', _('Repository forking disabled')),
2643 ('hg.fork.repository', _('Repository forking enabled')),
2642 ('hg.fork.repository', _('Repository forking enabled')),
2644
2643
2645 ('hg.register.none', _('Registration disabled')),
2644 ('hg.register.none', _('Registration disabled')),
2646 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2645 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2647 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2646 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2648
2647
2649 ('hg.password_reset.enabled', _('Password reset enabled')),
2648 ('hg.password_reset.enabled', _('Password reset enabled')),
2650 ('hg.password_reset.hidden', _('Password reset hidden')),
2649 ('hg.password_reset.hidden', _('Password reset hidden')),
2651 ('hg.password_reset.disabled', _('Password reset disabled')),
2650 ('hg.password_reset.disabled', _('Password reset disabled')),
2652
2651
2653 ('hg.extern_activate.manual', _('Manual activation of external account')),
2652 ('hg.extern_activate.manual', _('Manual activation of external account')),
2654 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2653 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2655
2654
2656 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2655 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2657 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2656 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2658 ]
2657 ]
2659
2658
2660 # definition of system default permissions for DEFAULT user
2659 # definition of system default permissions for DEFAULT user
2661 DEFAULT_USER_PERMISSIONS = [
2660 DEFAULT_USER_PERMISSIONS = [
2662 'repository.read',
2661 'repository.read',
2663 'group.read',
2662 'group.read',
2664 'usergroup.read',
2663 'usergroup.read',
2665 'hg.create.repository',
2664 'hg.create.repository',
2666 'hg.repogroup.create.false',
2665 'hg.repogroup.create.false',
2667 'hg.usergroup.create.false',
2666 'hg.usergroup.create.false',
2668 'hg.create.write_on_repogroup.true',
2667 'hg.create.write_on_repogroup.true',
2669 'hg.fork.repository',
2668 'hg.fork.repository',
2670 'hg.register.manual_activate',
2669 'hg.register.manual_activate',
2671 'hg.password_reset.enabled',
2670 'hg.password_reset.enabled',
2672 'hg.extern_activate.auto',
2671 'hg.extern_activate.auto',
2673 'hg.inherit_default_perms.true',
2672 'hg.inherit_default_perms.true',
2674 ]
2673 ]
2675
2674
2676 # defines which permissions are more important higher the more important
2675 # defines which permissions are more important higher the more important
2677 # Weight defines which permissions are more important.
2676 # Weight defines which permissions are more important.
2678 # The higher number the more important.
2677 # The higher number the more important.
2679 PERM_WEIGHTS = {
2678 PERM_WEIGHTS = {
2680 'repository.none': 0,
2679 'repository.none': 0,
2681 'repository.read': 1,
2680 'repository.read': 1,
2682 'repository.write': 3,
2681 'repository.write': 3,
2683 'repository.admin': 4,
2682 'repository.admin': 4,
2684
2683
2685 'group.none': 0,
2684 'group.none': 0,
2686 'group.read': 1,
2685 'group.read': 1,
2687 'group.write': 3,
2686 'group.write': 3,
2688 'group.admin': 4,
2687 'group.admin': 4,
2689
2688
2690 'usergroup.none': 0,
2689 'usergroup.none': 0,
2691 'usergroup.read': 1,
2690 'usergroup.read': 1,
2692 'usergroup.write': 3,
2691 'usergroup.write': 3,
2693 'usergroup.admin': 4,
2692 'usergroup.admin': 4,
2694
2693
2695 'hg.repogroup.create.false': 0,
2694 'hg.repogroup.create.false': 0,
2696 'hg.repogroup.create.true': 1,
2695 'hg.repogroup.create.true': 1,
2697
2696
2698 'hg.usergroup.create.false': 0,
2697 'hg.usergroup.create.false': 0,
2699 'hg.usergroup.create.true': 1,
2698 'hg.usergroup.create.true': 1,
2700
2699
2701 'hg.fork.none': 0,
2700 'hg.fork.none': 0,
2702 'hg.fork.repository': 1,
2701 'hg.fork.repository': 1,
2703 'hg.create.none': 0,
2702 'hg.create.none': 0,
2704 'hg.create.repository': 1
2703 'hg.create.repository': 1
2705 }
2704 }
2706
2705
2707 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2706 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2708 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2707 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2709 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2708 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2710
2709
2711 def __unicode__(self):
2710 def __unicode__(self):
2712 return u"<%s('%s:%s')>" % (
2711 return u"<%s('%s:%s')>" % (
2713 self.__class__.__name__, self.permission_id, self.permission_name
2712 self.__class__.__name__, self.permission_id, self.permission_name
2714 )
2713 )
2715
2714
2716 @classmethod
2715 @classmethod
2717 def get_by_key(cls, key):
2716 def get_by_key(cls, key):
2718 return cls.query().filter(cls.permission_name == key).scalar()
2717 return cls.query().filter(cls.permission_name == key).scalar()
2719
2718
2720 @classmethod
2719 @classmethod
2721 def get_default_repo_perms(cls, user_id, repo_id=None):
2720 def get_default_repo_perms(cls, user_id, repo_id=None):
2722 q = Session().query(UserRepoToPerm, Repository, Permission)\
2721 q = Session().query(UserRepoToPerm, Repository, Permission)\
2723 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2722 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2724 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2723 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2725 .filter(UserRepoToPerm.user_id == user_id)
2724 .filter(UserRepoToPerm.user_id == user_id)
2726 if repo_id:
2725 if repo_id:
2727 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2726 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2728 return q.all()
2727 return q.all()
2729
2728
2730 @classmethod
2729 @classmethod
2731 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2730 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2732 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2731 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2733 .join(
2732 .join(
2734 Permission,
2733 Permission,
2735 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2734 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2736 .join(
2735 .join(
2737 Repository,
2736 Repository,
2738 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2737 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2739 .join(
2738 .join(
2740 UserGroup,
2739 UserGroup,
2741 UserGroupRepoToPerm.users_group_id ==
2740 UserGroupRepoToPerm.users_group_id ==
2742 UserGroup.users_group_id)\
2741 UserGroup.users_group_id)\
2743 .join(
2742 .join(
2744 UserGroupMember,
2743 UserGroupMember,
2745 UserGroupRepoToPerm.users_group_id ==
2744 UserGroupRepoToPerm.users_group_id ==
2746 UserGroupMember.users_group_id)\
2745 UserGroupMember.users_group_id)\
2747 .filter(
2746 .filter(
2748 UserGroupMember.user_id == user_id,
2747 UserGroupMember.user_id == user_id,
2749 UserGroup.users_group_active == true())
2748 UserGroup.users_group_active == true())
2750 if repo_id:
2749 if repo_id:
2751 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2750 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2752 return q.all()
2751 return q.all()
2753
2752
2754 @classmethod
2753 @classmethod
2755 def get_default_group_perms(cls, user_id, repo_group_id=None):
2754 def get_default_group_perms(cls, user_id, repo_group_id=None):
2756 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2755 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2757 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2756 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2758 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2757 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2759 .filter(UserRepoGroupToPerm.user_id == user_id)
2758 .filter(UserRepoGroupToPerm.user_id == user_id)
2760 if repo_group_id:
2759 if repo_group_id:
2761 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2760 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2762 return q.all()
2761 return q.all()
2763
2762
2764 @classmethod
2763 @classmethod
2765 def get_default_group_perms_from_user_group(
2764 def get_default_group_perms_from_user_group(
2766 cls, user_id, repo_group_id=None):
2765 cls, user_id, repo_group_id=None):
2767 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2766 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2768 .join(
2767 .join(
2769 Permission,
2768 Permission,
2770 UserGroupRepoGroupToPerm.permission_id ==
2769 UserGroupRepoGroupToPerm.permission_id ==
2771 Permission.permission_id)\
2770 Permission.permission_id)\
2772 .join(
2771 .join(
2773 RepoGroup,
2772 RepoGroup,
2774 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2773 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2775 .join(
2774 .join(
2776 UserGroup,
2775 UserGroup,
2777 UserGroupRepoGroupToPerm.users_group_id ==
2776 UserGroupRepoGroupToPerm.users_group_id ==
2778 UserGroup.users_group_id)\
2777 UserGroup.users_group_id)\
2779 .join(
2778 .join(
2780 UserGroupMember,
2779 UserGroupMember,
2781 UserGroupRepoGroupToPerm.users_group_id ==
2780 UserGroupRepoGroupToPerm.users_group_id ==
2782 UserGroupMember.users_group_id)\
2781 UserGroupMember.users_group_id)\
2783 .filter(
2782 .filter(
2784 UserGroupMember.user_id == user_id,
2783 UserGroupMember.user_id == user_id,
2785 UserGroup.users_group_active == true())
2784 UserGroup.users_group_active == true())
2786 if repo_group_id:
2785 if repo_group_id:
2787 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2786 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2788 return q.all()
2787 return q.all()
2789
2788
2790 @classmethod
2789 @classmethod
2791 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2790 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2792 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2791 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2793 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2792 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2794 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2793 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2795 .filter(UserUserGroupToPerm.user_id == user_id)
2794 .filter(UserUserGroupToPerm.user_id == user_id)
2796 if user_group_id:
2795 if user_group_id:
2797 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2796 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2798 return q.all()
2797 return q.all()
2799
2798
2800 @classmethod
2799 @classmethod
2801 def get_default_user_group_perms_from_user_group(
2800 def get_default_user_group_perms_from_user_group(
2802 cls, user_id, user_group_id=None):
2801 cls, user_id, user_group_id=None):
2803 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2802 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2804 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2803 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2805 .join(
2804 .join(
2806 Permission,
2805 Permission,
2807 UserGroupUserGroupToPerm.permission_id ==
2806 UserGroupUserGroupToPerm.permission_id ==
2808 Permission.permission_id)\
2807 Permission.permission_id)\
2809 .join(
2808 .join(
2810 TargetUserGroup,
2809 TargetUserGroup,
2811 UserGroupUserGroupToPerm.target_user_group_id ==
2810 UserGroupUserGroupToPerm.target_user_group_id ==
2812 TargetUserGroup.users_group_id)\
2811 TargetUserGroup.users_group_id)\
2813 .join(
2812 .join(
2814 UserGroup,
2813 UserGroup,
2815 UserGroupUserGroupToPerm.user_group_id ==
2814 UserGroupUserGroupToPerm.user_group_id ==
2816 UserGroup.users_group_id)\
2815 UserGroup.users_group_id)\
2817 .join(
2816 .join(
2818 UserGroupMember,
2817 UserGroupMember,
2819 UserGroupUserGroupToPerm.user_group_id ==
2818 UserGroupUserGroupToPerm.user_group_id ==
2820 UserGroupMember.users_group_id)\
2819 UserGroupMember.users_group_id)\
2821 .filter(
2820 .filter(
2822 UserGroupMember.user_id == user_id,
2821 UserGroupMember.user_id == user_id,
2823 UserGroup.users_group_active == true())
2822 UserGroup.users_group_active == true())
2824 if user_group_id:
2823 if user_group_id:
2825 q = q.filter(
2824 q = q.filter(
2826 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2825 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2827
2826
2828 return q.all()
2827 return q.all()
2829
2828
2830
2829
2831 class UserRepoToPerm(Base, BaseModel):
2830 class UserRepoToPerm(Base, BaseModel):
2832 __tablename__ = 'repo_to_perm'
2831 __tablename__ = 'repo_to_perm'
2833 __table_args__ = (
2832 __table_args__ = (
2834 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2833 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2834 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2835 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2837 )
2836 )
2838 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2837 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2839 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2838 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2840 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2839 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2841 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2840 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2842
2841
2843 user = relationship('User')
2842 user = relationship('User')
2844 repository = relationship('Repository')
2843 repository = relationship('Repository')
2845 permission = relationship('Permission')
2844 permission = relationship('Permission')
2846
2845
2847 @classmethod
2846 @classmethod
2848 def create(cls, user, repository, permission):
2847 def create(cls, user, repository, permission):
2849 n = cls()
2848 n = cls()
2850 n.user = user
2849 n.user = user
2851 n.repository = repository
2850 n.repository = repository
2852 n.permission = permission
2851 n.permission = permission
2853 Session().add(n)
2852 Session().add(n)
2854 return n
2853 return n
2855
2854
2856 def __unicode__(self):
2855 def __unicode__(self):
2857 return u'<%s => %s >' % (self.user, self.repository)
2856 return u'<%s => %s >' % (self.user, self.repository)
2858
2857
2859
2858
2860 class UserUserGroupToPerm(Base, BaseModel):
2859 class UserUserGroupToPerm(Base, BaseModel):
2861 __tablename__ = 'user_user_group_to_perm'
2860 __tablename__ = 'user_user_group_to_perm'
2862 __table_args__ = (
2861 __table_args__ = (
2863 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2862 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2864 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2863 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2865 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2864 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2866 )
2865 )
2867 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2866 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2868 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2867 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2869 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2868 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2870 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2869 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2871
2870
2872 user = relationship('User')
2871 user = relationship('User')
2873 user_group = relationship('UserGroup')
2872 user_group = relationship('UserGroup')
2874 permission = relationship('Permission')
2873 permission = relationship('Permission')
2875
2874
2876 @classmethod
2875 @classmethod
2877 def create(cls, user, user_group, permission):
2876 def create(cls, user, user_group, permission):
2878 n = cls()
2877 n = cls()
2879 n.user = user
2878 n.user = user
2880 n.user_group = user_group
2879 n.user_group = user_group
2881 n.permission = permission
2880 n.permission = permission
2882 Session().add(n)
2881 Session().add(n)
2883 return n
2882 return n
2884
2883
2885 def __unicode__(self):
2884 def __unicode__(self):
2886 return u'<%s => %s >' % (self.user, self.user_group)
2885 return u'<%s => %s >' % (self.user, self.user_group)
2887
2886
2888
2887
2889 class UserToPerm(Base, BaseModel):
2888 class UserToPerm(Base, BaseModel):
2890 __tablename__ = 'user_to_perm'
2889 __tablename__ = 'user_to_perm'
2891 __table_args__ = (
2890 __table_args__ = (
2892 UniqueConstraint('user_id', 'permission_id'),
2891 UniqueConstraint('user_id', 'permission_id'),
2893 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2892 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2894 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2893 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2895 )
2894 )
2896 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2895 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2897 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2896 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2898 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2897 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2899
2898
2900 user = relationship('User')
2899 user = relationship('User')
2901 permission = relationship('Permission', lazy='joined')
2900 permission = relationship('Permission', lazy='joined')
2902
2901
2903 def __unicode__(self):
2902 def __unicode__(self):
2904 return u'<%s => %s >' % (self.user, self.permission)
2903 return u'<%s => %s >' % (self.user, self.permission)
2905
2904
2906
2905
2907 class UserGroupRepoToPerm(Base, BaseModel):
2906 class UserGroupRepoToPerm(Base, BaseModel):
2908 __tablename__ = 'users_group_repo_to_perm'
2907 __tablename__ = 'users_group_repo_to_perm'
2909 __table_args__ = (
2908 __table_args__ = (
2910 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2909 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2911 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2910 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2912 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2911 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2913 )
2912 )
2914 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2913 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2915 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2914 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2915 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2917 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2916 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2918
2917
2919 users_group = relationship('UserGroup')
2918 users_group = relationship('UserGroup')
2920 permission = relationship('Permission')
2919 permission = relationship('Permission')
2921 repository = relationship('Repository')
2920 repository = relationship('Repository')
2922
2921
2923 @classmethod
2922 @classmethod
2924 def create(cls, users_group, repository, permission):
2923 def create(cls, users_group, repository, permission):
2925 n = cls()
2924 n = cls()
2926 n.users_group = users_group
2925 n.users_group = users_group
2927 n.repository = repository
2926 n.repository = repository
2928 n.permission = permission
2927 n.permission = permission
2929 Session().add(n)
2928 Session().add(n)
2930 return n
2929 return n
2931
2930
2932 def __unicode__(self):
2931 def __unicode__(self):
2933 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2932 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2934
2933
2935
2934
2936 class UserGroupUserGroupToPerm(Base, BaseModel):
2935 class UserGroupUserGroupToPerm(Base, BaseModel):
2937 __tablename__ = 'user_group_user_group_to_perm'
2936 __tablename__ = 'user_group_user_group_to_perm'
2938 __table_args__ = (
2937 __table_args__ = (
2939 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2938 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2940 CheckConstraint('target_user_group_id != user_group_id'),
2939 CheckConstraint('target_user_group_id != user_group_id'),
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2940 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2941 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 )
2942 )
2944 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2943 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2945 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2944 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2946 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2945 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2947 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2946 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2948
2947
2949 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2948 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2950 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2949 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2951 permission = relationship('Permission')
2950 permission = relationship('Permission')
2952
2951
2953 @classmethod
2952 @classmethod
2954 def create(cls, target_user_group, user_group, permission):
2953 def create(cls, target_user_group, user_group, permission):
2955 n = cls()
2954 n = cls()
2956 n.target_user_group = target_user_group
2955 n.target_user_group = target_user_group
2957 n.user_group = user_group
2956 n.user_group = user_group
2958 n.permission = permission
2957 n.permission = permission
2959 Session().add(n)
2958 Session().add(n)
2960 return n
2959 return n
2961
2960
2962 def __unicode__(self):
2961 def __unicode__(self):
2963 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2962 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2964
2963
2965
2964
2966 class UserGroupToPerm(Base, BaseModel):
2965 class UserGroupToPerm(Base, BaseModel):
2967 __tablename__ = 'users_group_to_perm'
2966 __tablename__ = 'users_group_to_perm'
2968 __table_args__ = (
2967 __table_args__ = (
2969 UniqueConstraint('users_group_id', 'permission_id',),
2968 UniqueConstraint('users_group_id', 'permission_id',),
2970 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2969 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2971 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2970 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2972 )
2971 )
2973 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2972 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2974 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2973 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2975 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2974 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2976
2975
2977 users_group = relationship('UserGroup')
2976 users_group = relationship('UserGroup')
2978 permission = relationship('Permission')
2977 permission = relationship('Permission')
2979
2978
2980
2979
2981 class UserRepoGroupToPerm(Base, BaseModel):
2980 class UserRepoGroupToPerm(Base, BaseModel):
2982 __tablename__ = 'user_repo_group_to_perm'
2981 __tablename__ = 'user_repo_group_to_perm'
2983 __table_args__ = (
2982 __table_args__ = (
2984 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2983 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2985 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2984 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2986 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2985 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2987 )
2986 )
2988
2987
2989 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2988 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2990 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2989 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2991 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2990 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2992 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2991 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2993
2992
2994 user = relationship('User')
2993 user = relationship('User')
2995 group = relationship('RepoGroup')
2994 group = relationship('RepoGroup')
2996 permission = relationship('Permission')
2995 permission = relationship('Permission')
2997
2996
2998 @classmethod
2997 @classmethod
2999 def create(cls, user, repository_group, permission):
2998 def create(cls, user, repository_group, permission):
3000 n = cls()
2999 n = cls()
3001 n.user = user
3000 n.user = user
3002 n.group = repository_group
3001 n.group = repository_group
3003 n.permission = permission
3002 n.permission = permission
3004 Session().add(n)
3003 Session().add(n)
3005 return n
3004 return n
3006
3005
3007
3006
3008 class UserGroupRepoGroupToPerm(Base, BaseModel):
3007 class UserGroupRepoGroupToPerm(Base, BaseModel):
3009 __tablename__ = 'users_group_repo_group_to_perm'
3008 __tablename__ = 'users_group_repo_group_to_perm'
3010 __table_args__ = (
3009 __table_args__ = (
3011 UniqueConstraint('users_group_id', 'group_id'),
3010 UniqueConstraint('users_group_id', 'group_id'),
3012 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3011 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3013 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3012 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3014 )
3013 )
3015
3014
3016 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3015 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3017 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3016 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3018 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3017 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3019 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3018 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3020
3019
3021 users_group = relationship('UserGroup')
3020 users_group = relationship('UserGroup')
3022 permission = relationship('Permission')
3021 permission = relationship('Permission')
3023 group = relationship('RepoGroup')
3022 group = relationship('RepoGroup')
3024
3023
3025 @classmethod
3024 @classmethod
3026 def create(cls, user_group, repository_group, permission):
3025 def create(cls, user_group, repository_group, permission):
3027 n = cls()
3026 n = cls()
3028 n.users_group = user_group
3027 n.users_group = user_group
3029 n.group = repository_group
3028 n.group = repository_group
3030 n.permission = permission
3029 n.permission = permission
3031 Session().add(n)
3030 Session().add(n)
3032 return n
3031 return n
3033
3032
3034 def __unicode__(self):
3033 def __unicode__(self):
3035 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3034 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3036
3035
3037
3036
3038 class Statistics(Base, BaseModel):
3037 class Statistics(Base, BaseModel):
3039 __tablename__ = 'statistics'
3038 __tablename__ = 'statistics'
3040 __table_args__ = (
3039 __table_args__ = (
3041 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3040 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3042 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3041 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3043 )
3042 )
3044 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3043 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3045 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3044 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3046 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3045 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3047 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3046 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3048 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3047 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3049 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3048 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3050
3049
3051 repository = relationship('Repository', single_parent=True)
3050 repository = relationship('Repository', single_parent=True)
3052
3051
3053
3052
3054 class UserFollowing(Base, BaseModel):
3053 class UserFollowing(Base, BaseModel):
3055 __tablename__ = 'user_followings'
3054 __tablename__ = 'user_followings'
3056 __table_args__ = (
3055 __table_args__ = (
3057 UniqueConstraint('user_id', 'follows_repository_id'),
3056 UniqueConstraint('user_id', 'follows_repository_id'),
3058 UniqueConstraint('user_id', 'follows_user_id'),
3057 UniqueConstraint('user_id', 'follows_user_id'),
3059 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3058 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3060 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3059 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3061 )
3060 )
3062
3061
3063 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3062 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3064 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3063 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3065 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3064 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3066 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3065 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3067 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3066 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3068
3067
3069 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3068 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3070
3069
3071 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3070 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3072 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3071 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3073
3072
3074 @classmethod
3073 @classmethod
3075 def get_repo_followers(cls, repo_id):
3074 def get_repo_followers(cls, repo_id):
3076 return cls.query().filter(cls.follows_repo_id == repo_id)
3075 return cls.query().filter(cls.follows_repo_id == repo_id)
3077
3076
3078
3077
3079 class CacheKey(Base, BaseModel):
3078 class CacheKey(Base, BaseModel):
3080 __tablename__ = 'cache_invalidation'
3079 __tablename__ = 'cache_invalidation'
3081 __table_args__ = (
3080 __table_args__ = (
3082 UniqueConstraint('cache_key'),
3081 UniqueConstraint('cache_key'),
3083 Index('key_idx', 'cache_key'),
3082 Index('key_idx', 'cache_key'),
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3083 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3084 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 )
3085 )
3087 CACHE_TYPE_ATOM = 'ATOM'
3086 CACHE_TYPE_ATOM = 'ATOM'
3088 CACHE_TYPE_RSS = 'RSS'
3087 CACHE_TYPE_RSS = 'RSS'
3089 CACHE_TYPE_README = 'README'
3088 CACHE_TYPE_README = 'README'
3090
3089
3091 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3090 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3092 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3091 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3093 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3092 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3094 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3093 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3095
3094
3096 def __init__(self, cache_key, cache_args=''):
3095 def __init__(self, cache_key, cache_args=''):
3097 self.cache_key = cache_key
3096 self.cache_key = cache_key
3098 self.cache_args = cache_args
3097 self.cache_args = cache_args
3099 self.cache_active = False
3098 self.cache_active = False
3100
3099
3101 def __unicode__(self):
3100 def __unicode__(self):
3102 return u"<%s('%s:%s[%s]')>" % (
3101 return u"<%s('%s:%s[%s]')>" % (
3103 self.__class__.__name__,
3102 self.__class__.__name__,
3104 self.cache_id, self.cache_key, self.cache_active)
3103 self.cache_id, self.cache_key, self.cache_active)
3105
3104
3106 def _cache_key_partition(self):
3105 def _cache_key_partition(self):
3107 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3106 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3108 return prefix, repo_name, suffix
3107 return prefix, repo_name, suffix
3109
3108
3110 def get_prefix(self):
3109 def get_prefix(self):
3111 """
3110 """
3112 Try to extract prefix from existing cache key. The key could consist
3111 Try to extract prefix from existing cache key. The key could consist
3113 of prefix, repo_name, suffix
3112 of prefix, repo_name, suffix
3114 """
3113 """
3115 # this returns prefix, repo_name, suffix
3114 # this returns prefix, repo_name, suffix
3116 return self._cache_key_partition()[0]
3115 return self._cache_key_partition()[0]
3117
3116
3118 def get_suffix(self):
3117 def get_suffix(self):
3119 """
3118 """
3120 get suffix that might have been used in _get_cache_key to
3119 get suffix that might have been used in _get_cache_key to
3121 generate self.cache_key. Only used for informational purposes
3120 generate self.cache_key. Only used for informational purposes
3122 in repo_edit.mako.
3121 in repo_edit.mako.
3123 """
3122 """
3124 # prefix, repo_name, suffix
3123 # prefix, repo_name, suffix
3125 return self._cache_key_partition()[2]
3124 return self._cache_key_partition()[2]
3126
3125
3127 @classmethod
3126 @classmethod
3128 def delete_all_cache(cls):
3127 def delete_all_cache(cls):
3129 """
3128 """
3130 Delete all cache keys from database.
3129 Delete all cache keys from database.
3131 Should only be run when all instances are down and all entries
3130 Should only be run when all instances are down and all entries
3132 thus stale.
3131 thus stale.
3133 """
3132 """
3134 cls.query().delete()
3133 cls.query().delete()
3135 Session().commit()
3134 Session().commit()
3136
3135
3137 @classmethod
3136 @classmethod
3138 def get_cache_key(cls, repo_name, cache_type):
3137 def get_cache_key(cls, repo_name, cache_type):
3139 """
3138 """
3140
3139
3141 Generate a cache key for this process of RhodeCode instance.
3140 Generate a cache key for this process of RhodeCode instance.
3142 Prefix most likely will be process id or maybe explicitly set
3141 Prefix most likely will be process id or maybe explicitly set
3143 instance_id from .ini file.
3142 instance_id from .ini file.
3144 """
3143 """
3145 import rhodecode
3144 import rhodecode
3146 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3145 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3147
3146
3148 repo_as_unicode = safe_unicode(repo_name)
3147 repo_as_unicode = safe_unicode(repo_name)
3149 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3148 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3150 if cache_type else repo_as_unicode
3149 if cache_type else repo_as_unicode
3151
3150
3152 return u'{}{}'.format(prefix, key)
3151 return u'{}{}'.format(prefix, key)
3153
3152
3154 @classmethod
3153 @classmethod
3155 def set_invalidate(cls, repo_name, delete=False):
3154 def set_invalidate(cls, repo_name, delete=False):
3156 """
3155 """
3157 Mark all caches of a repo as invalid in the database.
3156 Mark all caches of a repo as invalid in the database.
3158 """
3157 """
3159
3158
3160 try:
3159 try:
3161 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3160 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3162 if delete:
3161 if delete:
3163 log.debug('cache objects deleted for repo %s',
3162 log.debug('cache objects deleted for repo %s',
3164 safe_str(repo_name))
3163 safe_str(repo_name))
3165 qry.delete()
3164 qry.delete()
3166 else:
3165 else:
3167 log.debug('cache objects marked as invalid for repo %s',
3166 log.debug('cache objects marked as invalid for repo %s',
3168 safe_str(repo_name))
3167 safe_str(repo_name))
3169 qry.update({"cache_active": False})
3168 qry.update({"cache_active": False})
3170
3169
3171 Session().commit()
3170 Session().commit()
3172 except Exception:
3171 except Exception:
3173 log.exception(
3172 log.exception(
3174 'Cache key invalidation failed for repository %s',
3173 'Cache key invalidation failed for repository %s',
3175 safe_str(repo_name))
3174 safe_str(repo_name))
3176 Session().rollback()
3175 Session().rollback()
3177
3176
3178 @classmethod
3177 @classmethod
3179 def get_active_cache(cls, cache_key):
3178 def get_active_cache(cls, cache_key):
3180 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3179 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3181 if inv_obj:
3180 if inv_obj:
3182 return inv_obj
3181 return inv_obj
3183 return None
3182 return None
3184
3183
3185 @classmethod
3184 @classmethod
3186 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3185 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3187 thread_scoped=False):
3186 thread_scoped=False):
3188 """
3187 """
3189 @cache_region('long_term')
3188 @cache_region('long_term')
3190 def _heavy_calculation(cache_key):
3189 def _heavy_calculation(cache_key):
3191 return 'result'
3190 return 'result'
3192
3191
3193 cache_context = CacheKey.repo_context_cache(
3192 cache_context = CacheKey.repo_context_cache(
3194 _heavy_calculation, repo_name, cache_type)
3193 _heavy_calculation, repo_name, cache_type)
3195
3194
3196 with cache_context as context:
3195 with cache_context as context:
3197 context.invalidate()
3196 context.invalidate()
3198 computed = context.compute()
3197 computed = context.compute()
3199
3198
3200 assert computed == 'result'
3199 assert computed == 'result'
3201 """
3200 """
3202 from rhodecode.lib import caches
3201 from rhodecode.lib import caches
3203 return caches.InvalidationContext(
3202 return caches.InvalidationContext(
3204 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3203 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3205
3204
3206
3205
3207 class ChangesetComment(Base, BaseModel):
3206 class ChangesetComment(Base, BaseModel):
3208 __tablename__ = 'changeset_comments'
3207 __tablename__ = 'changeset_comments'
3209 __table_args__ = (
3208 __table_args__ = (
3210 Index('cc_revision_idx', 'revision'),
3209 Index('cc_revision_idx', 'revision'),
3211 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3210 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3212 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3211 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3213 )
3212 )
3214
3213
3215 COMMENT_OUTDATED = u'comment_outdated'
3214 COMMENT_OUTDATED = u'comment_outdated'
3216 COMMENT_TYPE_NOTE = u'note'
3215 COMMENT_TYPE_NOTE = u'note'
3217 COMMENT_TYPE_TODO = u'todo'
3216 COMMENT_TYPE_TODO = u'todo'
3218 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3217 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3219
3218
3220 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3219 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3221 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3220 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3222 revision = Column('revision', String(40), nullable=True)
3221 revision = Column('revision', String(40), nullable=True)
3223 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3222 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3224 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3223 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3225 line_no = Column('line_no', Unicode(10), nullable=True)
3224 line_no = Column('line_no', Unicode(10), nullable=True)
3226 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3225 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3227 f_path = Column('f_path', Unicode(1000), nullable=True)
3226 f_path = Column('f_path', Unicode(1000), nullable=True)
3228 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3227 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3229 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3228 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3230 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3229 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3231 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3230 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3232 renderer = Column('renderer', Unicode(64), nullable=True)
3231 renderer = Column('renderer', Unicode(64), nullable=True)
3233 display_state = Column('display_state', Unicode(128), nullable=True)
3232 display_state = Column('display_state', Unicode(128), nullable=True)
3234
3233
3235 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3234 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3236 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3235 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3237 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3236 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3238 author = relationship('User', lazy='joined')
3237 author = relationship('User', lazy='joined')
3239 repo = relationship('Repository')
3238 repo = relationship('Repository')
3240 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3239 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3241 pull_request = relationship('PullRequest', lazy='joined')
3240 pull_request = relationship('PullRequest', lazy='joined')
3242 pull_request_version = relationship('PullRequestVersion')
3241 pull_request_version = relationship('PullRequestVersion')
3243
3242
3244 @classmethod
3243 @classmethod
3245 def get_users(cls, revision=None, pull_request_id=None):
3244 def get_users(cls, revision=None, pull_request_id=None):
3246 """
3245 """
3247 Returns user associated with this ChangesetComment. ie those
3246 Returns user associated with this ChangesetComment. ie those
3248 who actually commented
3247 who actually commented
3249
3248
3250 :param cls:
3249 :param cls:
3251 :param revision:
3250 :param revision:
3252 """
3251 """
3253 q = Session().query(User)\
3252 q = Session().query(User)\
3254 .join(ChangesetComment.author)
3253 .join(ChangesetComment.author)
3255 if revision:
3254 if revision:
3256 q = q.filter(cls.revision == revision)
3255 q = q.filter(cls.revision == revision)
3257 elif pull_request_id:
3256 elif pull_request_id:
3258 q = q.filter(cls.pull_request_id == pull_request_id)
3257 q = q.filter(cls.pull_request_id == pull_request_id)
3259 return q.all()
3258 return q.all()
3260
3259
3261 @classmethod
3260 @classmethod
3262 def get_index_from_version(cls, pr_version, versions):
3261 def get_index_from_version(cls, pr_version, versions):
3263 num_versions = [x.pull_request_version_id for x in versions]
3262 num_versions = [x.pull_request_version_id for x in versions]
3264 try:
3263 try:
3265 return num_versions.index(pr_version) +1
3264 return num_versions.index(pr_version) +1
3266 except (IndexError, ValueError):
3265 except (IndexError, ValueError):
3267 return
3266 return
3268
3267
3269 @property
3268 @property
3270 def outdated(self):
3269 def outdated(self):
3271 return self.display_state == self.COMMENT_OUTDATED
3270 return self.display_state == self.COMMENT_OUTDATED
3272
3271
3273 def outdated_at_version(self, version):
3272 def outdated_at_version(self, version):
3274 """
3273 """
3275 Checks if comment is outdated for given pull request version
3274 Checks if comment is outdated for given pull request version
3276 """
3275 """
3277 return self.outdated and self.pull_request_version_id != version
3276 return self.outdated and self.pull_request_version_id != version
3278
3277
3279 def older_than_version(self, version):
3278 def older_than_version(self, version):
3280 """
3279 """
3281 Checks if comment is made from previous version than given
3280 Checks if comment is made from previous version than given
3282 """
3281 """
3283 if version is None:
3282 if version is None:
3284 return self.pull_request_version_id is not None
3283 return self.pull_request_version_id is not None
3285
3284
3286 return self.pull_request_version_id < version
3285 return self.pull_request_version_id < version
3287
3286
3288 @property
3287 @property
3289 def resolved(self):
3288 def resolved(self):
3290 return self.resolved_by[0] if self.resolved_by else None
3289 return self.resolved_by[0] if self.resolved_by else None
3291
3290
3292 @property
3291 @property
3293 def is_todo(self):
3292 def is_todo(self):
3294 return self.comment_type == self.COMMENT_TYPE_TODO
3293 return self.comment_type == self.COMMENT_TYPE_TODO
3295
3294
3296 @property
3295 @property
3297 def is_inline(self):
3296 def is_inline(self):
3298 return self.line_no and self.f_path
3297 return self.line_no and self.f_path
3299
3298
3300 def get_index_version(self, versions):
3299 def get_index_version(self, versions):
3301 return self.get_index_from_version(
3300 return self.get_index_from_version(
3302 self.pull_request_version_id, versions)
3301 self.pull_request_version_id, versions)
3303
3302
3304 def __repr__(self):
3303 def __repr__(self):
3305 if self.comment_id:
3304 if self.comment_id:
3306 return '<DB:Comment #%s>' % self.comment_id
3305 return '<DB:Comment #%s>' % self.comment_id
3307 else:
3306 else:
3308 return '<DB:Comment at %#x>' % id(self)
3307 return '<DB:Comment at %#x>' % id(self)
3309
3308
3310 def get_api_data(self):
3309 def get_api_data(self):
3311 comment = self
3310 comment = self
3312 data = {
3311 data = {
3313 'comment_id': comment.comment_id,
3312 'comment_id': comment.comment_id,
3314 'comment_type': comment.comment_type,
3313 'comment_type': comment.comment_type,
3315 'comment_text': comment.text,
3314 'comment_text': comment.text,
3316 'comment_status': comment.status_change,
3315 'comment_status': comment.status_change,
3317 'comment_f_path': comment.f_path,
3316 'comment_f_path': comment.f_path,
3318 'comment_lineno': comment.line_no,
3317 'comment_lineno': comment.line_no,
3319 'comment_author': comment.author,
3318 'comment_author': comment.author,
3320 'comment_created_on': comment.created_on
3319 'comment_created_on': comment.created_on
3321 }
3320 }
3322 return data
3321 return data
3323
3322
3324 def __json__(self):
3323 def __json__(self):
3325 data = dict()
3324 data = dict()
3326 data.update(self.get_api_data())
3325 data.update(self.get_api_data())
3327 return data
3326 return data
3328
3327
3329
3328
3330 class ChangesetStatus(Base, BaseModel):
3329 class ChangesetStatus(Base, BaseModel):
3331 __tablename__ = 'changeset_statuses'
3330 __tablename__ = 'changeset_statuses'
3332 __table_args__ = (
3331 __table_args__ = (
3333 Index('cs_revision_idx', 'revision'),
3332 Index('cs_revision_idx', 'revision'),
3334 Index('cs_version_idx', 'version'),
3333 Index('cs_version_idx', 'version'),
3335 UniqueConstraint('repo_id', 'revision', 'version'),
3334 UniqueConstraint('repo_id', 'revision', 'version'),
3336 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3335 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3337 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3336 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3338 )
3337 )
3339 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3338 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3340 STATUS_APPROVED = 'approved'
3339 STATUS_APPROVED = 'approved'
3341 STATUS_REJECTED = 'rejected'
3340 STATUS_REJECTED = 'rejected'
3342 STATUS_UNDER_REVIEW = 'under_review'
3341 STATUS_UNDER_REVIEW = 'under_review'
3343
3342
3344 STATUSES = [
3343 STATUSES = [
3345 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3344 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3346 (STATUS_APPROVED, _("Approved")),
3345 (STATUS_APPROVED, _("Approved")),
3347 (STATUS_REJECTED, _("Rejected")),
3346 (STATUS_REJECTED, _("Rejected")),
3348 (STATUS_UNDER_REVIEW, _("Under Review")),
3347 (STATUS_UNDER_REVIEW, _("Under Review")),
3349 ]
3348 ]
3350
3349
3351 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3350 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3352 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3351 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3353 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3352 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3354 revision = Column('revision', String(40), nullable=False)
3353 revision = Column('revision', String(40), nullable=False)
3355 status = Column('status', String(128), nullable=False, default=DEFAULT)
3354 status = Column('status', String(128), nullable=False, default=DEFAULT)
3356 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3355 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3357 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3356 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3358 version = Column('version', Integer(), nullable=False, default=0)
3357 version = Column('version', Integer(), nullable=False, default=0)
3359 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3358 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3360
3359
3361 author = relationship('User', lazy='joined')
3360 author = relationship('User', lazy='joined')
3362 repo = relationship('Repository')
3361 repo = relationship('Repository')
3363 comment = relationship('ChangesetComment', lazy='joined')
3362 comment = relationship('ChangesetComment', lazy='joined')
3364 pull_request = relationship('PullRequest', lazy='joined')
3363 pull_request = relationship('PullRequest', lazy='joined')
3365
3364
3366 def __unicode__(self):
3365 def __unicode__(self):
3367 return u"<%s('%s[v%s]:%s')>" % (
3366 return u"<%s('%s[v%s]:%s')>" % (
3368 self.__class__.__name__,
3367 self.__class__.__name__,
3369 self.status, self.version, self.author
3368 self.status, self.version, self.author
3370 )
3369 )
3371
3370
3372 @classmethod
3371 @classmethod
3373 def get_status_lbl(cls, value):
3372 def get_status_lbl(cls, value):
3374 return dict(cls.STATUSES).get(value)
3373 return dict(cls.STATUSES).get(value)
3375
3374
3376 @property
3375 @property
3377 def status_lbl(self):
3376 def status_lbl(self):
3378 return ChangesetStatus.get_status_lbl(self.status)
3377 return ChangesetStatus.get_status_lbl(self.status)
3379
3378
3380 def get_api_data(self):
3379 def get_api_data(self):
3381 status = self
3380 status = self
3382 data = {
3381 data = {
3383 'status_id': status.changeset_status_id,
3382 'status_id': status.changeset_status_id,
3384 'status': status.status,
3383 'status': status.status,
3385 }
3384 }
3386 return data
3385 return data
3387
3386
3388 def __json__(self):
3387 def __json__(self):
3389 data = dict()
3388 data = dict()
3390 data.update(self.get_api_data())
3389 data.update(self.get_api_data())
3391 return data
3390 return data
3392
3391
3393
3392
3394 class _PullRequestBase(BaseModel):
3393 class _PullRequestBase(BaseModel):
3395 """
3394 """
3396 Common attributes of pull request and version entries.
3395 Common attributes of pull request and version entries.
3397 """
3396 """
3398
3397
3399 # .status values
3398 # .status values
3400 STATUS_NEW = u'new'
3399 STATUS_NEW = u'new'
3401 STATUS_OPEN = u'open'
3400 STATUS_OPEN = u'open'
3402 STATUS_CLOSED = u'closed'
3401 STATUS_CLOSED = u'closed'
3403
3402
3404 title = Column('title', Unicode(255), nullable=True)
3403 title = Column('title', Unicode(255), nullable=True)
3405 description = Column(
3404 description = Column(
3406 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3405 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3407 nullable=True)
3406 nullable=True)
3408 # new/open/closed status of pull request (not approve/reject/etc)
3407 # new/open/closed status of pull request (not approve/reject/etc)
3409 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3408 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3410 created_on = Column(
3409 created_on = Column(
3411 'created_on', DateTime(timezone=False), nullable=False,
3410 'created_on', DateTime(timezone=False), nullable=False,
3412 default=datetime.datetime.now)
3411 default=datetime.datetime.now)
3413 updated_on = Column(
3412 updated_on = Column(
3414 'updated_on', DateTime(timezone=False), nullable=False,
3413 'updated_on', DateTime(timezone=False), nullable=False,
3415 default=datetime.datetime.now)
3414 default=datetime.datetime.now)
3416
3415
3417 @declared_attr
3416 @declared_attr
3418 def user_id(cls):
3417 def user_id(cls):
3419 return Column(
3418 return Column(
3420 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3419 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3421 unique=None)
3420 unique=None)
3422
3421
3423 # 500 revisions max
3422 # 500 revisions max
3424 _revisions = Column(
3423 _revisions = Column(
3425 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3424 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3426
3425
3427 @declared_attr
3426 @declared_attr
3428 def source_repo_id(cls):
3427 def source_repo_id(cls):
3429 # TODO: dan: rename column to source_repo_id
3428 # TODO: dan: rename column to source_repo_id
3430 return Column(
3429 return Column(
3431 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3430 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3432 nullable=False)
3431 nullable=False)
3433
3432
3434 source_ref = Column('org_ref', Unicode(255), nullable=False)
3433 source_ref = Column('org_ref', Unicode(255), nullable=False)
3435
3434
3436 @declared_attr
3435 @declared_attr
3437 def target_repo_id(cls):
3436 def target_repo_id(cls):
3438 # TODO: dan: rename column to target_repo_id
3437 # TODO: dan: rename column to target_repo_id
3439 return Column(
3438 return Column(
3440 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3439 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3441 nullable=False)
3440 nullable=False)
3442
3441
3443 target_ref = Column('other_ref', Unicode(255), nullable=False)
3442 target_ref = Column('other_ref', Unicode(255), nullable=False)
3444 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3443 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3445
3444
3446 # TODO: dan: rename column to last_merge_source_rev
3445 # TODO: dan: rename column to last_merge_source_rev
3447 _last_merge_source_rev = Column(
3446 _last_merge_source_rev = Column(
3448 'last_merge_org_rev', String(40), nullable=True)
3447 'last_merge_org_rev', String(40), nullable=True)
3449 # TODO: dan: rename column to last_merge_target_rev
3448 # TODO: dan: rename column to last_merge_target_rev
3450 _last_merge_target_rev = Column(
3449 _last_merge_target_rev = Column(
3451 'last_merge_other_rev', String(40), nullable=True)
3450 'last_merge_other_rev', String(40), nullable=True)
3452 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3451 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3453 merge_rev = Column('merge_rev', String(40), nullable=True)
3452 merge_rev = Column('merge_rev', String(40), nullable=True)
3454
3453
3455 reviewer_data = Column(
3454 reviewer_data = Column(
3456 'reviewer_data_json', MutationObj.as_mutable(
3455 'reviewer_data_json', MutationObj.as_mutable(
3457 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3456 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3458
3457
3459 @property
3458 @property
3460 def reviewer_data_json(self):
3459 def reviewer_data_json(self):
3461 return json.dumps(self.reviewer_data)
3460 return json.dumps(self.reviewer_data)
3462
3461
3463 @hybrid_property
3462 @hybrid_property
3464 def description_safe(self):
3463 def description_safe(self):
3465 from rhodecode.lib import helpers as h
3464 from rhodecode.lib import helpers as h
3466 return h.escape(self.description)
3465 return h.escape(self.description)
3467
3466
3468 @hybrid_property
3467 @hybrid_property
3469 def revisions(self):
3468 def revisions(self):
3470 return self._revisions.split(':') if self._revisions else []
3469 return self._revisions.split(':') if self._revisions else []
3471
3470
3472 @revisions.setter
3471 @revisions.setter
3473 def revisions(self, val):
3472 def revisions(self, val):
3474 self._revisions = ':'.join(val)
3473 self._revisions = ':'.join(val)
3475
3474
3476 @hybrid_property
3475 @hybrid_property
3477 def last_merge_status(self):
3476 def last_merge_status(self):
3478 return safe_int(self._last_merge_status)
3477 return safe_int(self._last_merge_status)
3479
3478
3480 @last_merge_status.setter
3479 @last_merge_status.setter
3481 def last_merge_status(self, val):
3480 def last_merge_status(self, val):
3482 self._last_merge_status = val
3481 self._last_merge_status = val
3483
3482
3484 @declared_attr
3483 @declared_attr
3485 def author(cls):
3484 def author(cls):
3486 return relationship('User', lazy='joined')
3485 return relationship('User', lazy='joined')
3487
3486
3488 @declared_attr
3487 @declared_attr
3489 def source_repo(cls):
3488 def source_repo(cls):
3490 return relationship(
3489 return relationship(
3491 'Repository',
3490 'Repository',
3492 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3491 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3493
3492
3494 @property
3493 @property
3495 def source_ref_parts(self):
3494 def source_ref_parts(self):
3496 return self.unicode_to_reference(self.source_ref)
3495 return self.unicode_to_reference(self.source_ref)
3497
3496
3498 @declared_attr
3497 @declared_attr
3499 def target_repo(cls):
3498 def target_repo(cls):
3500 return relationship(
3499 return relationship(
3501 'Repository',
3500 'Repository',
3502 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3501 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3503
3502
3504 @property
3503 @property
3505 def target_ref_parts(self):
3504 def target_ref_parts(self):
3506 return self.unicode_to_reference(self.target_ref)
3505 return self.unicode_to_reference(self.target_ref)
3507
3506
3508 @property
3507 @property
3509 def shadow_merge_ref(self):
3508 def shadow_merge_ref(self):
3510 return self.unicode_to_reference(self._shadow_merge_ref)
3509 return self.unicode_to_reference(self._shadow_merge_ref)
3511
3510
3512 @shadow_merge_ref.setter
3511 @shadow_merge_ref.setter
3513 def shadow_merge_ref(self, ref):
3512 def shadow_merge_ref(self, ref):
3514 self._shadow_merge_ref = self.reference_to_unicode(ref)
3513 self._shadow_merge_ref = self.reference_to_unicode(ref)
3515
3514
3516 def unicode_to_reference(self, raw):
3515 def unicode_to_reference(self, raw):
3517 """
3516 """
3518 Convert a unicode (or string) to a reference object.
3517 Convert a unicode (or string) to a reference object.
3519 If unicode evaluates to False it returns None.
3518 If unicode evaluates to False it returns None.
3520 """
3519 """
3521 if raw:
3520 if raw:
3522 refs = raw.split(':')
3521 refs = raw.split(':')
3523 return Reference(*refs)
3522 return Reference(*refs)
3524 else:
3523 else:
3525 return None
3524 return None
3526
3525
3527 def reference_to_unicode(self, ref):
3526 def reference_to_unicode(self, ref):
3528 """
3527 """
3529 Convert a reference object to unicode.
3528 Convert a reference object to unicode.
3530 If reference is None it returns None.
3529 If reference is None it returns None.
3531 """
3530 """
3532 if ref:
3531 if ref:
3533 return u':'.join(ref)
3532 return u':'.join(ref)
3534 else:
3533 else:
3535 return None
3534 return None
3536
3535
3537 def get_api_data(self, with_merge_state=True):
3536 def get_api_data(self, with_merge_state=True):
3538 from rhodecode.model.pull_request import PullRequestModel
3537 from rhodecode.model.pull_request import PullRequestModel
3539
3538
3540 pull_request = self
3539 pull_request = self
3541 if with_merge_state:
3540 if with_merge_state:
3542 merge_status = PullRequestModel().merge_status(pull_request)
3541 merge_status = PullRequestModel().merge_status(pull_request)
3543 merge_state = {
3542 merge_state = {
3544 'status': merge_status[0],
3543 'status': merge_status[0],
3545 'message': safe_unicode(merge_status[1]),
3544 'message': safe_unicode(merge_status[1]),
3546 }
3545 }
3547 else:
3546 else:
3548 merge_state = {'status': 'not_available',
3547 merge_state = {'status': 'not_available',
3549 'message': 'not_available'}
3548 'message': 'not_available'}
3550
3549
3551 merge_data = {
3550 merge_data = {
3552 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3551 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3553 'reference': (
3552 'reference': (
3554 pull_request.shadow_merge_ref._asdict()
3553 pull_request.shadow_merge_ref._asdict()
3555 if pull_request.shadow_merge_ref else None),
3554 if pull_request.shadow_merge_ref else None),
3556 }
3555 }
3557
3556
3558 data = {
3557 data = {
3559 'pull_request_id': pull_request.pull_request_id,
3558 'pull_request_id': pull_request.pull_request_id,
3560 'url': PullRequestModel().get_url(pull_request),
3559 'url': PullRequestModel().get_url(pull_request),
3561 'title': pull_request.title,
3560 'title': pull_request.title,
3562 'description': pull_request.description,
3561 'description': pull_request.description,
3563 'status': pull_request.status,
3562 'status': pull_request.status,
3564 'created_on': pull_request.created_on,
3563 'created_on': pull_request.created_on,
3565 'updated_on': pull_request.updated_on,
3564 'updated_on': pull_request.updated_on,
3566 'commit_ids': pull_request.revisions,
3565 'commit_ids': pull_request.revisions,
3567 'review_status': pull_request.calculated_review_status(),
3566 'review_status': pull_request.calculated_review_status(),
3568 'mergeable': merge_state,
3567 'mergeable': merge_state,
3569 'source': {
3568 'source': {
3570 'clone_url': pull_request.source_repo.clone_url(),
3569 'clone_url': pull_request.source_repo.clone_url(),
3571 'repository': pull_request.source_repo.repo_name,
3570 'repository': pull_request.source_repo.repo_name,
3572 'reference': {
3571 'reference': {
3573 'name': pull_request.source_ref_parts.name,
3572 'name': pull_request.source_ref_parts.name,
3574 'type': pull_request.source_ref_parts.type,
3573 'type': pull_request.source_ref_parts.type,
3575 'commit_id': pull_request.source_ref_parts.commit_id,
3574 'commit_id': pull_request.source_ref_parts.commit_id,
3576 },
3575 },
3577 },
3576 },
3578 'target': {
3577 'target': {
3579 'clone_url': pull_request.target_repo.clone_url(),
3578 'clone_url': pull_request.target_repo.clone_url(),
3580 'repository': pull_request.target_repo.repo_name,
3579 'repository': pull_request.target_repo.repo_name,
3581 'reference': {
3580 'reference': {
3582 'name': pull_request.target_ref_parts.name,
3581 'name': pull_request.target_ref_parts.name,
3583 'type': pull_request.target_ref_parts.type,
3582 'type': pull_request.target_ref_parts.type,
3584 'commit_id': pull_request.target_ref_parts.commit_id,
3583 'commit_id': pull_request.target_ref_parts.commit_id,
3585 },
3584 },
3586 },
3585 },
3587 'merge': merge_data,
3586 'merge': merge_data,
3588 'author': pull_request.author.get_api_data(include_secrets=False,
3587 'author': pull_request.author.get_api_data(include_secrets=False,
3589 details='basic'),
3588 details='basic'),
3590 'reviewers': [
3589 'reviewers': [
3591 {
3590 {
3592 'user': reviewer.get_api_data(include_secrets=False,
3591 'user': reviewer.get_api_data(include_secrets=False,
3593 details='basic'),
3592 details='basic'),
3594 'reasons': reasons,
3593 'reasons': reasons,
3595 'review_status': st[0][1].status if st else 'not_reviewed',
3594 'review_status': st[0][1].status if st else 'not_reviewed',
3596 }
3595 }
3597 for reviewer, reasons, mandatory, st in
3596 for obj, reviewer, reasons, mandatory, st in
3598 pull_request.reviewers_statuses()
3597 pull_request.reviewers_statuses()
3599 ]
3598 ]
3600 }
3599 }
3601
3600
3602 return data
3601 return data
3603
3602
3604
3603
3605 class PullRequest(Base, _PullRequestBase):
3604 class PullRequest(Base, _PullRequestBase):
3606 __tablename__ = 'pull_requests'
3605 __tablename__ = 'pull_requests'
3607 __table_args__ = (
3606 __table_args__ = (
3608 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3607 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3609 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3608 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3610 )
3609 )
3611
3610
3612 pull_request_id = Column(
3611 pull_request_id = Column(
3613 'pull_request_id', Integer(), nullable=False, primary_key=True)
3612 'pull_request_id', Integer(), nullable=False, primary_key=True)
3614
3613
3615 def __repr__(self):
3614 def __repr__(self):
3616 if self.pull_request_id:
3615 if self.pull_request_id:
3617 return '<DB:PullRequest #%s>' % self.pull_request_id
3616 return '<DB:PullRequest #%s>' % self.pull_request_id
3618 else:
3617 else:
3619 return '<DB:PullRequest at %#x>' % id(self)
3618 return '<DB:PullRequest at %#x>' % id(self)
3620
3619
3621 reviewers = relationship('PullRequestReviewers',
3620 reviewers = relationship('PullRequestReviewers',
3622 cascade="all, delete, delete-orphan")
3621 cascade="all, delete, delete-orphan")
3623 statuses = relationship('ChangesetStatus',
3622 statuses = relationship('ChangesetStatus',
3624 cascade="all, delete, delete-orphan")
3623 cascade="all, delete, delete-orphan")
3625 comments = relationship('ChangesetComment',
3624 comments = relationship('ChangesetComment',
3626 cascade="all, delete, delete-orphan")
3625 cascade="all, delete, delete-orphan")
3627 versions = relationship('PullRequestVersion',
3626 versions = relationship('PullRequestVersion',
3628 cascade="all, delete, delete-orphan",
3627 cascade="all, delete, delete-orphan",
3629 lazy='dynamic')
3628 lazy='dynamic')
3630
3629
3631 @classmethod
3630 @classmethod
3632 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3631 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3633 internal_methods=None):
3632 internal_methods=None):
3634
3633
3635 class PullRequestDisplay(object):
3634 class PullRequestDisplay(object):
3636 """
3635 """
3637 Special object wrapper for showing PullRequest data via Versions
3636 Special object wrapper for showing PullRequest data via Versions
3638 It mimics PR object as close as possible. This is read only object
3637 It mimics PR object as close as possible. This is read only object
3639 just for display
3638 just for display
3640 """
3639 """
3641
3640
3642 def __init__(self, attrs, internal=None):
3641 def __init__(self, attrs, internal=None):
3643 self.attrs = attrs
3642 self.attrs = attrs
3644 # internal have priority over the given ones via attrs
3643 # internal have priority over the given ones via attrs
3645 self.internal = internal or ['versions']
3644 self.internal = internal or ['versions']
3646
3645
3647 def __getattr__(self, item):
3646 def __getattr__(self, item):
3648 if item in self.internal:
3647 if item in self.internal:
3649 return getattr(self, item)
3648 return getattr(self, item)
3650 try:
3649 try:
3651 return self.attrs[item]
3650 return self.attrs[item]
3652 except KeyError:
3651 except KeyError:
3653 raise AttributeError(
3652 raise AttributeError(
3654 '%s object has no attribute %s' % (self, item))
3653 '%s object has no attribute %s' % (self, item))
3655
3654
3656 def __repr__(self):
3655 def __repr__(self):
3657 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3656 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3658
3657
3659 def versions(self):
3658 def versions(self):
3660 return pull_request_obj.versions.order_by(
3659 return pull_request_obj.versions.order_by(
3661 PullRequestVersion.pull_request_version_id).all()
3660 PullRequestVersion.pull_request_version_id).all()
3662
3661
3663 def is_closed(self):
3662 def is_closed(self):
3664 return pull_request_obj.is_closed()
3663 return pull_request_obj.is_closed()
3665
3664
3666 @property
3665 @property
3667 def pull_request_version_id(self):
3666 def pull_request_version_id(self):
3668 return getattr(pull_request_obj, 'pull_request_version_id', None)
3667 return getattr(pull_request_obj, 'pull_request_version_id', None)
3669
3668
3670 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3669 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3671
3670
3672 attrs.author = StrictAttributeDict(
3671 attrs.author = StrictAttributeDict(
3673 pull_request_obj.author.get_api_data())
3672 pull_request_obj.author.get_api_data())
3674 if pull_request_obj.target_repo:
3673 if pull_request_obj.target_repo:
3675 attrs.target_repo = StrictAttributeDict(
3674 attrs.target_repo = StrictAttributeDict(
3676 pull_request_obj.target_repo.get_api_data())
3675 pull_request_obj.target_repo.get_api_data())
3677 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3676 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3678
3677
3679 if pull_request_obj.source_repo:
3678 if pull_request_obj.source_repo:
3680 attrs.source_repo = StrictAttributeDict(
3679 attrs.source_repo = StrictAttributeDict(
3681 pull_request_obj.source_repo.get_api_data())
3680 pull_request_obj.source_repo.get_api_data())
3682 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3681 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3683
3682
3684 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3683 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3685 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3684 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3686 attrs.revisions = pull_request_obj.revisions
3685 attrs.revisions = pull_request_obj.revisions
3687
3686
3688 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3687 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3689 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3688 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3690 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3689 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3691
3690
3692 return PullRequestDisplay(attrs, internal=internal_methods)
3691 return PullRequestDisplay(attrs, internal=internal_methods)
3693
3692
3694 def is_closed(self):
3693 def is_closed(self):
3695 return self.status == self.STATUS_CLOSED
3694 return self.status == self.STATUS_CLOSED
3696
3695
3697 def __json__(self):
3696 def __json__(self):
3698 return {
3697 return {
3699 'revisions': self.revisions,
3698 'revisions': self.revisions,
3700 }
3699 }
3701
3700
3702 def calculated_review_status(self):
3701 def calculated_review_status(self):
3703 from rhodecode.model.changeset_status import ChangesetStatusModel
3702 from rhodecode.model.changeset_status import ChangesetStatusModel
3704 return ChangesetStatusModel().calculated_review_status(self)
3703 return ChangesetStatusModel().calculated_review_status(self)
3705
3704
3706 def reviewers_statuses(self):
3705 def reviewers_statuses(self):
3707 from rhodecode.model.changeset_status import ChangesetStatusModel
3706 from rhodecode.model.changeset_status import ChangesetStatusModel
3708 return ChangesetStatusModel().reviewers_statuses(self)
3707 return ChangesetStatusModel().reviewers_statuses(self)
3709
3708
3710 @property
3709 @property
3711 def workspace_id(self):
3710 def workspace_id(self):
3712 from rhodecode.model.pull_request import PullRequestModel
3711 from rhodecode.model.pull_request import PullRequestModel
3713 return PullRequestModel()._workspace_id(self)
3712 return PullRequestModel()._workspace_id(self)
3714
3713
3715 def get_shadow_repo(self):
3714 def get_shadow_repo(self):
3716 workspace_id = self.workspace_id
3715 workspace_id = self.workspace_id
3717 vcs_obj = self.target_repo.scm_instance()
3716 vcs_obj = self.target_repo.scm_instance()
3718 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3717 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3719 workspace_id)
3718 workspace_id)
3720 return vcs_obj._get_shadow_instance(shadow_repository_path)
3719 return vcs_obj._get_shadow_instance(shadow_repository_path)
3721
3720
3722
3721
3723 class PullRequestVersion(Base, _PullRequestBase):
3722 class PullRequestVersion(Base, _PullRequestBase):
3724 __tablename__ = 'pull_request_versions'
3723 __tablename__ = 'pull_request_versions'
3725 __table_args__ = (
3724 __table_args__ = (
3726 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3725 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3727 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3726 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3728 )
3727 )
3729
3728
3730 pull_request_version_id = Column(
3729 pull_request_version_id = Column(
3731 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3730 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3732 pull_request_id = Column(
3731 pull_request_id = Column(
3733 'pull_request_id', Integer(),
3732 'pull_request_id', Integer(),
3734 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3733 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3735 pull_request = relationship('PullRequest')
3734 pull_request = relationship('PullRequest')
3736
3735
3737 def __repr__(self):
3736 def __repr__(self):
3738 if self.pull_request_version_id:
3737 if self.pull_request_version_id:
3739 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3738 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3740 else:
3739 else:
3741 return '<DB:PullRequestVersion at %#x>' % id(self)
3740 return '<DB:PullRequestVersion at %#x>' % id(self)
3742
3741
3743 @property
3742 @property
3744 def reviewers(self):
3743 def reviewers(self):
3745 return self.pull_request.reviewers
3744 return self.pull_request.reviewers
3746
3745
3747 @property
3746 @property
3748 def versions(self):
3747 def versions(self):
3749 return self.pull_request.versions
3748 return self.pull_request.versions
3750
3749
3751 def is_closed(self):
3750 def is_closed(self):
3752 # calculate from original
3751 # calculate from original
3753 return self.pull_request.status == self.STATUS_CLOSED
3752 return self.pull_request.status == self.STATUS_CLOSED
3754
3753
3755 def calculated_review_status(self):
3754 def calculated_review_status(self):
3756 return self.pull_request.calculated_review_status()
3755 return self.pull_request.calculated_review_status()
3757
3756
3758 def reviewers_statuses(self):
3757 def reviewers_statuses(self):
3759 return self.pull_request.reviewers_statuses()
3758 return self.pull_request.reviewers_statuses()
3760
3759
3761
3760
3762 class PullRequestReviewers(Base, BaseModel):
3761 class PullRequestReviewers(Base, BaseModel):
3763 __tablename__ = 'pull_request_reviewers'
3762 __tablename__ = 'pull_request_reviewers'
3764 __table_args__ = (
3763 __table_args__ = (
3765 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3764 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3766 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3765 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3767 )
3766 )
3768
3767
3769 @hybrid_property
3768 @hybrid_property
3770 def reasons(self):
3769 def reasons(self):
3771 if not self._reasons:
3770 if not self._reasons:
3772 return []
3771 return []
3773 return self._reasons
3772 return self._reasons
3774
3773
3775 @reasons.setter
3774 @reasons.setter
3776 def reasons(self, val):
3775 def reasons(self, val):
3777 val = val or []
3776 val = val or []
3778 if any(not isinstance(x, basestring) for x in val):
3777 if any(not isinstance(x, basestring) for x in val):
3779 raise Exception('invalid reasons type, must be list of strings')
3778 raise Exception('invalid reasons type, must be list of strings')
3780 self._reasons = val
3779 self._reasons = val
3781
3780
3782 pull_requests_reviewers_id = Column(
3781 pull_requests_reviewers_id = Column(
3783 'pull_requests_reviewers_id', Integer(), nullable=False,
3782 'pull_requests_reviewers_id', Integer(), nullable=False,
3784 primary_key=True)
3783 primary_key=True)
3785 pull_request_id = Column(
3784 pull_request_id = Column(
3786 "pull_request_id", Integer(),
3785 "pull_request_id", Integer(),
3787 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3786 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3788 user_id = Column(
3787 user_id = Column(
3789 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3788 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3790 _reasons = Column(
3789 _reasons = Column(
3791 'reason', MutationList.as_mutable(
3790 'reason', MutationList.as_mutable(
3792 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3791 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3792
3793 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3793 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3794 user = relationship('User')
3794 user = relationship('User')
3795 pull_request = relationship('PullRequest')
3795 pull_request = relationship('PullRequest')
3796
3796
3797 rule_data = Column(
3798 'rule_data_json',
3799 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
3800
3801 def rule_user_group_data(self):
3802 """
3803 Returns the voting user group rule data for this reviewer
3804 """
3805
3806 if self.rule_data and 'vote_rule' in self.rule_data:
3807 user_group_data = {}
3808 if 'rule_user_group_entry_id' in self.rule_data:
3809 # means a group with voting rules !
3810 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
3811 user_group_data['name'] = self.rule_data['rule_name']
3812 user_group_data['vote_rule'] = self.rule_data['vote_rule']
3813
3814 return user_group_data
3815
3816 def __unicode__(self):
3817 return u"<%s('id:%s')>" % (self.__class__.__name__,
3818 self.pull_requests_reviewers_id)
3819
3797
3820
3798 class Notification(Base, BaseModel):
3821 class Notification(Base, BaseModel):
3799 __tablename__ = 'notifications'
3822 __tablename__ = 'notifications'
3800 __table_args__ = (
3823 __table_args__ = (
3801 Index('notification_type_idx', 'type'),
3824 Index('notification_type_idx', 'type'),
3802 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3825 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3803 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3826 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3804 )
3827 )
3805
3828
3806 TYPE_CHANGESET_COMMENT = u'cs_comment'
3829 TYPE_CHANGESET_COMMENT = u'cs_comment'
3807 TYPE_MESSAGE = u'message'
3830 TYPE_MESSAGE = u'message'
3808 TYPE_MENTION = u'mention'
3831 TYPE_MENTION = u'mention'
3809 TYPE_REGISTRATION = u'registration'
3832 TYPE_REGISTRATION = u'registration'
3810 TYPE_PULL_REQUEST = u'pull_request'
3833 TYPE_PULL_REQUEST = u'pull_request'
3811 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3834 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3812
3835
3813 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3836 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3814 subject = Column('subject', Unicode(512), nullable=True)
3837 subject = Column('subject', Unicode(512), nullable=True)
3815 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3838 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3816 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3839 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3817 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3840 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3818 type_ = Column('type', Unicode(255))
3841 type_ = Column('type', Unicode(255))
3819
3842
3820 created_by_user = relationship('User')
3843 created_by_user = relationship('User')
3821 notifications_to_users = relationship('UserNotification', lazy='joined',
3844 notifications_to_users = relationship('UserNotification', lazy='joined',
3822 cascade="all, delete, delete-orphan")
3845 cascade="all, delete, delete-orphan")
3823
3846
3824 @property
3847 @property
3825 def recipients(self):
3848 def recipients(self):
3826 return [x.user for x in UserNotification.query()\
3849 return [x.user for x in UserNotification.query()\
3827 .filter(UserNotification.notification == self)\
3850 .filter(UserNotification.notification == self)\
3828 .order_by(UserNotification.user_id.asc()).all()]
3851 .order_by(UserNotification.user_id.asc()).all()]
3829
3852
3830 @classmethod
3853 @classmethod
3831 def create(cls, created_by, subject, body, recipients, type_=None):
3854 def create(cls, created_by, subject, body, recipients, type_=None):
3832 if type_ is None:
3855 if type_ is None:
3833 type_ = Notification.TYPE_MESSAGE
3856 type_ = Notification.TYPE_MESSAGE
3834
3857
3835 notification = cls()
3858 notification = cls()
3836 notification.created_by_user = created_by
3859 notification.created_by_user = created_by
3837 notification.subject = subject
3860 notification.subject = subject
3838 notification.body = body
3861 notification.body = body
3839 notification.type_ = type_
3862 notification.type_ = type_
3840 notification.created_on = datetime.datetime.now()
3863 notification.created_on = datetime.datetime.now()
3841
3864
3842 for u in recipients:
3865 for u in recipients:
3843 assoc = UserNotification()
3866 assoc = UserNotification()
3844 assoc.notification = notification
3867 assoc.notification = notification
3845
3868
3846 # if created_by is inside recipients mark his notification
3869 # if created_by is inside recipients mark his notification
3847 # as read
3870 # as read
3848 if u.user_id == created_by.user_id:
3871 if u.user_id == created_by.user_id:
3849 assoc.read = True
3872 assoc.read = True
3850
3873
3851 u.notifications.append(assoc)
3874 u.notifications.append(assoc)
3852 Session().add(notification)
3875 Session().add(notification)
3853
3876
3854 return notification
3877 return notification
3855
3878
3856
3879
3857 class UserNotification(Base, BaseModel):
3880 class UserNotification(Base, BaseModel):
3858 __tablename__ = 'user_to_notification'
3881 __tablename__ = 'user_to_notification'
3859 __table_args__ = (
3882 __table_args__ = (
3860 UniqueConstraint('user_id', 'notification_id'),
3883 UniqueConstraint('user_id', 'notification_id'),
3861 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3862 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3863 )
3886 )
3864 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3887 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3865 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3888 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3866 read = Column('read', Boolean, default=False)
3889 read = Column('read', Boolean, default=False)
3867 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3890 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3868
3891
3869 user = relationship('User', lazy="joined")
3892 user = relationship('User', lazy="joined")
3870 notification = relationship('Notification', lazy="joined",
3893 notification = relationship('Notification', lazy="joined",
3871 order_by=lambda: Notification.created_on.desc(),)
3894 order_by=lambda: Notification.created_on.desc(),)
3872
3895
3873 def mark_as_read(self):
3896 def mark_as_read(self):
3874 self.read = True
3897 self.read = True
3875 Session().add(self)
3898 Session().add(self)
3876
3899
3877
3900
3878 class Gist(Base, BaseModel):
3901 class Gist(Base, BaseModel):
3879 __tablename__ = 'gists'
3902 __tablename__ = 'gists'
3880 __table_args__ = (
3903 __table_args__ = (
3881 Index('g_gist_access_id_idx', 'gist_access_id'),
3904 Index('g_gist_access_id_idx', 'gist_access_id'),
3882 Index('g_created_on_idx', 'created_on'),
3905 Index('g_created_on_idx', 'created_on'),
3883 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3906 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3884 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3907 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3885 )
3908 )
3886 GIST_PUBLIC = u'public'
3909 GIST_PUBLIC = u'public'
3887 GIST_PRIVATE = u'private'
3910 GIST_PRIVATE = u'private'
3888 DEFAULT_FILENAME = u'gistfile1.txt'
3911 DEFAULT_FILENAME = u'gistfile1.txt'
3889
3912
3890 ACL_LEVEL_PUBLIC = u'acl_public'
3913 ACL_LEVEL_PUBLIC = u'acl_public'
3891 ACL_LEVEL_PRIVATE = u'acl_private'
3914 ACL_LEVEL_PRIVATE = u'acl_private'
3892
3915
3893 gist_id = Column('gist_id', Integer(), primary_key=True)
3916 gist_id = Column('gist_id', Integer(), primary_key=True)
3894 gist_access_id = Column('gist_access_id', Unicode(250))
3917 gist_access_id = Column('gist_access_id', Unicode(250))
3895 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3918 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3896 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3919 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3897 gist_expires = Column('gist_expires', Float(53), nullable=False)
3920 gist_expires = Column('gist_expires', Float(53), nullable=False)
3898 gist_type = Column('gist_type', Unicode(128), nullable=False)
3921 gist_type = Column('gist_type', Unicode(128), nullable=False)
3899 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3922 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3900 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3923 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3901 acl_level = Column('acl_level', Unicode(128), nullable=True)
3924 acl_level = Column('acl_level', Unicode(128), nullable=True)
3902
3925
3903 owner = relationship('User')
3926 owner = relationship('User')
3904
3927
3905 def __repr__(self):
3928 def __repr__(self):
3906 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3929 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3907
3930
3908 @hybrid_property
3931 @hybrid_property
3909 def description_safe(self):
3932 def description_safe(self):
3910 from rhodecode.lib import helpers as h
3933 from rhodecode.lib import helpers as h
3911 return h.escape(self.gist_description)
3934 return h.escape(self.gist_description)
3912
3935
3913 @classmethod
3936 @classmethod
3914 def get_or_404(cls, id_):
3937 def get_or_404(cls, id_):
3915 from pyramid.httpexceptions import HTTPNotFound
3938 from pyramid.httpexceptions import HTTPNotFound
3916
3939
3917 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3940 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3918 if not res:
3941 if not res:
3919 raise HTTPNotFound()
3942 raise HTTPNotFound()
3920 return res
3943 return res
3921
3944
3922 @classmethod
3945 @classmethod
3923 def get_by_access_id(cls, gist_access_id):
3946 def get_by_access_id(cls, gist_access_id):
3924 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3947 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3925
3948
3926 def gist_url(self):
3949 def gist_url(self):
3927 from rhodecode.model.gist import GistModel
3950 from rhodecode.model.gist import GistModel
3928 return GistModel().get_url(self)
3951 return GistModel().get_url(self)
3929
3952
3930 @classmethod
3953 @classmethod
3931 def base_path(cls):
3954 def base_path(cls):
3932 """
3955 """
3933 Returns base path when all gists are stored
3956 Returns base path when all gists are stored
3934
3957
3935 :param cls:
3958 :param cls:
3936 """
3959 """
3937 from rhodecode.model.gist import GIST_STORE_LOC
3960 from rhodecode.model.gist import GIST_STORE_LOC
3938 q = Session().query(RhodeCodeUi)\
3961 q = Session().query(RhodeCodeUi)\
3939 .filter(RhodeCodeUi.ui_key == URL_SEP)
3962 .filter(RhodeCodeUi.ui_key == URL_SEP)
3940 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3963 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3941 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3964 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3942
3965
3943 def get_api_data(self):
3966 def get_api_data(self):
3944 """
3967 """
3945 Common function for generating gist related data for API
3968 Common function for generating gist related data for API
3946 """
3969 """
3947 gist = self
3970 gist = self
3948 data = {
3971 data = {
3949 'gist_id': gist.gist_id,
3972 'gist_id': gist.gist_id,
3950 'type': gist.gist_type,
3973 'type': gist.gist_type,
3951 'access_id': gist.gist_access_id,
3974 'access_id': gist.gist_access_id,
3952 'description': gist.gist_description,
3975 'description': gist.gist_description,
3953 'url': gist.gist_url(),
3976 'url': gist.gist_url(),
3954 'expires': gist.gist_expires,
3977 'expires': gist.gist_expires,
3955 'created_on': gist.created_on,
3978 'created_on': gist.created_on,
3956 'modified_at': gist.modified_at,
3979 'modified_at': gist.modified_at,
3957 'content': None,
3980 'content': None,
3958 'acl_level': gist.acl_level,
3981 'acl_level': gist.acl_level,
3959 }
3982 }
3960 return data
3983 return data
3961
3984
3962 def __json__(self):
3985 def __json__(self):
3963 data = dict(
3986 data = dict(
3964 )
3987 )
3965 data.update(self.get_api_data())
3988 data.update(self.get_api_data())
3966 return data
3989 return data
3967 # SCM functions
3990 # SCM functions
3968
3991
3969 def scm_instance(self, **kwargs):
3992 def scm_instance(self, **kwargs):
3970 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3993 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3971 return get_vcs_instance(
3994 return get_vcs_instance(
3972 repo_path=safe_str(full_repo_path), create=False)
3995 repo_path=safe_str(full_repo_path), create=False)
3973
3996
3974
3997
3975 class ExternalIdentity(Base, BaseModel):
3998 class ExternalIdentity(Base, BaseModel):
3976 __tablename__ = 'external_identities'
3999 __tablename__ = 'external_identities'
3977 __table_args__ = (
4000 __table_args__ = (
3978 Index('local_user_id_idx', 'local_user_id'),
4001 Index('local_user_id_idx', 'local_user_id'),
3979 Index('external_id_idx', 'external_id'),
4002 Index('external_id_idx', 'external_id'),
3980 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4003 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3981 'mysql_charset': 'utf8'})
4004 'mysql_charset': 'utf8'})
3982
4005
3983 external_id = Column('external_id', Unicode(255), default=u'',
4006 external_id = Column('external_id', Unicode(255), default=u'',
3984 primary_key=True)
4007 primary_key=True)
3985 external_username = Column('external_username', Unicode(1024), default=u'')
4008 external_username = Column('external_username', Unicode(1024), default=u'')
3986 local_user_id = Column('local_user_id', Integer(),
4009 local_user_id = Column('local_user_id', Integer(),
3987 ForeignKey('users.user_id'), primary_key=True)
4010 ForeignKey('users.user_id'), primary_key=True)
3988 provider_name = Column('provider_name', Unicode(255), default=u'',
4011 provider_name = Column('provider_name', Unicode(255), default=u'',
3989 primary_key=True)
4012 primary_key=True)
3990 access_token = Column('access_token', String(1024), default=u'')
4013 access_token = Column('access_token', String(1024), default=u'')
3991 alt_token = Column('alt_token', String(1024), default=u'')
4014 alt_token = Column('alt_token', String(1024), default=u'')
3992 token_secret = Column('token_secret', String(1024), default=u'')
4015 token_secret = Column('token_secret', String(1024), default=u'')
3993
4016
3994 @classmethod
4017 @classmethod
3995 def by_external_id_and_provider(cls, external_id, provider_name,
4018 def by_external_id_and_provider(cls, external_id, provider_name,
3996 local_user_id=None):
4019 local_user_id=None):
3997 """
4020 """
3998 Returns ExternalIdentity instance based on search params
4021 Returns ExternalIdentity instance based on search params
3999
4022
4000 :param external_id:
4023 :param external_id:
4001 :param provider_name:
4024 :param provider_name:
4002 :return: ExternalIdentity
4025 :return: ExternalIdentity
4003 """
4026 """
4004 query = cls.query()
4027 query = cls.query()
4005 query = query.filter(cls.external_id == external_id)
4028 query = query.filter(cls.external_id == external_id)
4006 query = query.filter(cls.provider_name == provider_name)
4029 query = query.filter(cls.provider_name == provider_name)
4007 if local_user_id:
4030 if local_user_id:
4008 query = query.filter(cls.local_user_id == local_user_id)
4031 query = query.filter(cls.local_user_id == local_user_id)
4009 return query.first()
4032 return query.first()
4010
4033
4011 @classmethod
4034 @classmethod
4012 def user_by_external_id_and_provider(cls, external_id, provider_name):
4035 def user_by_external_id_and_provider(cls, external_id, provider_name):
4013 """
4036 """
4014 Returns User instance based on search params
4037 Returns User instance based on search params
4015
4038
4016 :param external_id:
4039 :param external_id:
4017 :param provider_name:
4040 :param provider_name:
4018 :return: User
4041 :return: User
4019 """
4042 """
4020 query = User.query()
4043 query = User.query()
4021 query = query.filter(cls.external_id == external_id)
4044 query = query.filter(cls.external_id == external_id)
4022 query = query.filter(cls.provider_name == provider_name)
4045 query = query.filter(cls.provider_name == provider_name)
4023 query = query.filter(User.user_id == cls.local_user_id)
4046 query = query.filter(User.user_id == cls.local_user_id)
4024 return query.first()
4047 return query.first()
4025
4048
4026 @classmethod
4049 @classmethod
4027 def by_local_user_id(cls, local_user_id):
4050 def by_local_user_id(cls, local_user_id):
4028 """
4051 """
4029 Returns all tokens for user
4052 Returns all tokens for user
4030
4053
4031 :param local_user_id:
4054 :param local_user_id:
4032 :return: ExternalIdentity
4055 :return: ExternalIdentity
4033 """
4056 """
4034 query = cls.query()
4057 query = cls.query()
4035 query = query.filter(cls.local_user_id == local_user_id)
4058 query = query.filter(cls.local_user_id == local_user_id)
4036 return query
4059 return query
4037
4060
4038
4061
4039 class Integration(Base, BaseModel):
4062 class Integration(Base, BaseModel):
4040 __tablename__ = 'integrations'
4063 __tablename__ = 'integrations'
4041 __table_args__ = (
4064 __table_args__ = (
4042 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4065 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4043 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4066 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4044 )
4067 )
4045
4068
4046 integration_id = Column('integration_id', Integer(), primary_key=True)
4069 integration_id = Column('integration_id', Integer(), primary_key=True)
4047 integration_type = Column('integration_type', String(255))
4070 integration_type = Column('integration_type', String(255))
4048 enabled = Column('enabled', Boolean(), nullable=False)
4071 enabled = Column('enabled', Boolean(), nullable=False)
4049 name = Column('name', String(255), nullable=False)
4072 name = Column('name', String(255), nullable=False)
4050 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4073 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4051 default=False)
4074 default=False)
4052
4075
4053 settings = Column(
4076 settings = Column(
4054 'settings_json', MutationObj.as_mutable(
4077 'settings_json', MutationObj.as_mutable(
4055 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4078 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4056 repo_id = Column(
4079 repo_id = Column(
4057 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4080 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4058 nullable=True, unique=None, default=None)
4081 nullable=True, unique=None, default=None)
4059 repo = relationship('Repository', lazy='joined')
4082 repo = relationship('Repository', lazy='joined')
4060
4083
4061 repo_group_id = Column(
4084 repo_group_id = Column(
4062 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4085 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4063 nullable=True, unique=None, default=None)
4086 nullable=True, unique=None, default=None)
4064 repo_group = relationship('RepoGroup', lazy='joined')
4087 repo_group = relationship('RepoGroup', lazy='joined')
4065
4088
4066 @property
4089 @property
4067 def scope(self):
4090 def scope(self):
4068 if self.repo:
4091 if self.repo:
4069 return repr(self.repo)
4092 return repr(self.repo)
4070 if self.repo_group:
4093 if self.repo_group:
4071 if self.child_repos_only:
4094 if self.child_repos_only:
4072 return repr(self.repo_group) + ' (child repos only)'
4095 return repr(self.repo_group) + ' (child repos only)'
4073 else:
4096 else:
4074 return repr(self.repo_group) + ' (recursive)'
4097 return repr(self.repo_group) + ' (recursive)'
4075 if self.child_repos_only:
4098 if self.child_repos_only:
4076 return 'root_repos'
4099 return 'root_repos'
4077 return 'global'
4100 return 'global'
4078
4101
4079 def __repr__(self):
4102 def __repr__(self):
4080 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4103 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4081
4104
4082
4105
4083 class RepoReviewRuleUser(Base, BaseModel):
4106 class RepoReviewRuleUser(Base, BaseModel):
4084 __tablename__ = 'repo_review_rules_users'
4107 __tablename__ = 'repo_review_rules_users'
4085 __table_args__ = (
4108 __table_args__ = (
4086 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4109 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4087 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4110 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4088 )
4111 )
4112
4089 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4113 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4090 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4114 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4091 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4115 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4092 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4116 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4093 user = relationship('User')
4117 user = relationship('User')
4094
4118
4095 def rule_data(self):
4119 def rule_data(self):
4096 return {
4120 return {
4097 'mandatory': self.mandatory
4121 'mandatory': self.mandatory
4098 }
4122 }
4099
4123
4100
4124
4101 class RepoReviewRuleUserGroup(Base, BaseModel):
4125 class RepoReviewRuleUserGroup(Base, BaseModel):
4102 __tablename__ = 'repo_review_rules_users_groups'
4126 __tablename__ = 'repo_review_rules_users_groups'
4103 __table_args__ = (
4127 __table_args__ = (
4104 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4128 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4105 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4129 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4106 )
4130 )
4131 VOTE_RULE_ALL = -1
4132
4107 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4133 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4108 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4134 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4109 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4135 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4110 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4136 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4137 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4111 users_group = relationship('UserGroup')
4138 users_group = relationship('UserGroup')
4112
4139
4113 def rule_data(self):
4140 def rule_data(self):
4114 return {
4141 return {
4115 'mandatory': self.mandatory
4142 'mandatory': self.mandatory,
4143 'vote_rule': self.vote_rule
4116 }
4144 }
4117
4145
4146 @property
4147 def vote_rule_label(self):
4148 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4149 return 'all must vote'
4150 else:
4151 return 'min. vote {}'.format(self.vote_rule)
4152
4118
4153
4119 class RepoReviewRule(Base, BaseModel):
4154 class RepoReviewRule(Base, BaseModel):
4120 __tablename__ = 'repo_review_rules'
4155 __tablename__ = 'repo_review_rules'
4121 __table_args__ = (
4156 __table_args__ = (
4122 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4157 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4123 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4158 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4124 )
4159 )
4125
4160
4126 repo_review_rule_id = Column(
4161 repo_review_rule_id = Column(
4127 'repo_review_rule_id', Integer(), primary_key=True)
4162 'repo_review_rule_id', Integer(), primary_key=True)
4128 repo_id = Column(
4163 repo_id = Column(
4129 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4164 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4130 repo = relationship('Repository', backref='review_rules')
4165 repo = relationship('Repository', backref='review_rules')
4131
4166
4132 review_rule_name = Column('review_rule_name', String(255))
4167 review_rule_name = Column('review_rule_name', String(255))
4133 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4168 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4134 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4169 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4135 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4170 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4136
4171
4137 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4172 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4138 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4173 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4139 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4174 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4140 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4175 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4141
4176
4142 rule_users = relationship('RepoReviewRuleUser')
4177 rule_users = relationship('RepoReviewRuleUser')
4143 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4178 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4144
4179
4145 def _validate_glob(self, value):
4180 def _validate_glob(self, value):
4146 re.compile('^' + glob2re(value) + '$')
4181 re.compile('^' + glob2re(value) + '$')
4147
4182
4148 @hybrid_property
4183 @hybrid_property
4149 def source_branch_pattern(self):
4184 def source_branch_pattern(self):
4150 return self._branch_pattern or '*'
4185 return self._branch_pattern or '*'
4151
4186
4152 @source_branch_pattern.setter
4187 @source_branch_pattern.setter
4153 def source_branch_pattern(self, value):
4188 def source_branch_pattern(self, value):
4154 self._validate_glob(value)
4189 self._validate_glob(value)
4155 self._branch_pattern = value or '*'
4190 self._branch_pattern = value or '*'
4156
4191
4157 @hybrid_property
4192 @hybrid_property
4158 def target_branch_pattern(self):
4193 def target_branch_pattern(self):
4159 return self._target_branch_pattern or '*'
4194 return self._target_branch_pattern or '*'
4160
4195
4161 @target_branch_pattern.setter
4196 @target_branch_pattern.setter
4162 def target_branch_pattern(self, value):
4197 def target_branch_pattern(self, value):
4163 self._validate_glob(value)
4198 self._validate_glob(value)
4164 self._target_branch_pattern = value or '*'
4199 self._target_branch_pattern = value or '*'
4165
4200
4166 @hybrid_property
4201 @hybrid_property
4167 def file_pattern(self):
4202 def file_pattern(self):
4168 return self._file_pattern or '*'
4203 return self._file_pattern or '*'
4169
4204
4170 @file_pattern.setter
4205 @file_pattern.setter
4171 def file_pattern(self, value):
4206 def file_pattern(self, value):
4172 self._validate_glob(value)
4207 self._validate_glob(value)
4173 self._file_pattern = value or '*'
4208 self._file_pattern = value or '*'
4174
4209
4175 def matches(self, source_branch, target_branch, files_changed):
4210 def matches(self, source_branch, target_branch, files_changed):
4176 """
4211 """
4177 Check if this review rule matches a branch/files in a pull request
4212 Check if this review rule matches a branch/files in a pull request
4178
4213
4179 :param branch: branch name for the commit
4214 :param branch: branch name for the commit
4180 :param files_changed: list of file paths changed in the pull request
4215 :param files_changed: list of file paths changed in the pull request
4181 """
4216 """
4182
4217
4183 source_branch = source_branch or ''
4218 source_branch = source_branch or ''
4184 target_branch = target_branch or ''
4219 target_branch = target_branch or ''
4185 files_changed = files_changed or []
4220 files_changed = files_changed or []
4186
4221
4187 branch_matches = True
4222 branch_matches = True
4188 if source_branch or target_branch:
4223 if source_branch or target_branch:
4189 source_branch_regex = re.compile(
4224 source_branch_regex = re.compile(
4190 '^' + glob2re(self.source_branch_pattern) + '$')
4225 '^' + glob2re(self.source_branch_pattern) + '$')
4191 target_branch_regex = re.compile(
4226 target_branch_regex = re.compile(
4192 '^' + glob2re(self.target_branch_pattern) + '$')
4227 '^' + glob2re(self.target_branch_pattern) + '$')
4193
4228
4194 branch_matches = (
4229 branch_matches = (
4195 bool(source_branch_regex.search(source_branch)) and
4230 bool(source_branch_regex.search(source_branch)) and
4196 bool(target_branch_regex.search(target_branch))
4231 bool(target_branch_regex.search(target_branch))
4197 )
4232 )
4198
4233
4199 files_matches = True
4234 files_matches = True
4200 if self.file_pattern != '*':
4235 if self.file_pattern != '*':
4201 files_matches = False
4236 files_matches = False
4202 file_regex = re.compile(glob2re(self.file_pattern))
4237 file_regex = re.compile(glob2re(self.file_pattern))
4203 for filename in files_changed:
4238 for filename in files_changed:
4204 if file_regex.search(filename):
4239 if file_regex.search(filename):
4205 files_matches = True
4240 files_matches = True
4206 break
4241 break
4207
4242
4208 return branch_matches and files_matches
4243 return branch_matches and files_matches
4209
4244
4210 @property
4245 @property
4211 def review_users(self):
4246 def review_users(self):
4212 """ Returns the users which this rule applies to """
4247 """ Returns the users which this rule applies to """
4213
4248
4214 users = collections.OrderedDict()
4249 users = collections.OrderedDict()
4215
4250
4216 for rule_user in self.rule_users:
4251 for rule_user in self.rule_users:
4217 if rule_user.user.active:
4252 if rule_user.user.active:
4218 if rule_user.user not in users:
4253 if rule_user.user not in users:
4219 users[rule_user.user.username] = {
4254 users[rule_user.user.username] = {
4220 'user': rule_user.user,
4255 'user': rule_user.user,
4221 'source': 'user',
4256 'source': 'user',
4222 'source_data': {},
4257 'source_data': {},
4223 'data': rule_user.rule_data()
4258 'data': rule_user.rule_data()
4224 }
4259 }
4225
4260
4226 for rule_user_group in self.rule_user_groups:
4261 for rule_user_group in self.rule_user_groups:
4227 source_data = {
4262 source_data = {
4263 'user_group_id': rule_user_group.users_group.users_group_id,
4228 'name': rule_user_group.users_group.users_group_name,
4264 'name': rule_user_group.users_group.users_group_name,
4229 'members': len(rule_user_group.users_group.members)
4265 'members': len(rule_user_group.users_group.members)
4230 }
4266 }
4231 for member in rule_user_group.users_group.members:
4267 for member in rule_user_group.users_group.members:
4232 if member.user.active:
4268 if member.user.active:
4233 users[member.user.username] = {
4269 key = member.user.username
4270 if key in users:
4271 # skip this member as we have him already
4272 # this prevents from override the "first" matched
4273 # users with duplicates in multiple groups
4274 continue
4275
4276 users[key] = {
4234 'user': member.user,
4277 'user': member.user,
4235 'source': 'user_group',
4278 'source': 'user_group',
4236 'source_data': source_data,
4279 'source_data': source_data,
4237 'data': rule_user_group.rule_data()
4280 'data': rule_user_group.rule_data()
4238 }
4281 }
4239
4282
4240 return users
4283 return users
4241
4284
4285 def user_group_vote_rule(self):
4286 rules = []
4287 if self.rule_user_groups:
4288 for user_group in self.rule_user_groups:
4289 rules.append(user_group)
4290 return rules
4291
4242 def __repr__(self):
4292 def __repr__(self):
4243 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4293 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4244 self.repo_review_rule_id, self.repo)
4294 self.repo_review_rule_id, self.repo)
4245
4295
4246
4296
4247 class ScheduleEntry(Base, BaseModel):
4297 class ScheduleEntry(Base, BaseModel):
4248 __tablename__ = 'schedule_entries'
4298 __tablename__ = 'schedule_entries'
4249 __table_args__ = (
4299 __table_args__ = (
4250 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4300 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4251 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4301 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4252 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4302 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4253 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4303 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4254 )
4304 )
4255 schedule_types = ['crontab', 'timedelta', 'integer']
4305 schedule_types = ['crontab', 'timedelta', 'integer']
4256 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4306 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4257
4307
4258 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4308 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4259 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4309 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4260 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4310 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4261
4311
4262 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4312 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4263 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4313 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4264
4314
4265 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4315 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4266 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4316 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4267
4317
4268 # task
4318 # task
4269 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4319 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4270 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4320 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4271 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4321 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4272 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4322 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4273
4323
4274 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4324 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4275 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4325 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4276
4326
4277 @hybrid_property
4327 @hybrid_property
4278 def schedule_type(self):
4328 def schedule_type(self):
4279 return self._schedule_type
4329 return self._schedule_type
4280
4330
4281 @schedule_type.setter
4331 @schedule_type.setter
4282 def schedule_type(self, val):
4332 def schedule_type(self, val):
4283 if val not in self.schedule_types:
4333 if val not in self.schedule_types:
4284 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4334 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4285 val, self.schedule_type))
4335 val, self.schedule_type))
4286
4336
4287 self._schedule_type = val
4337 self._schedule_type = val
4288
4338
4289 @classmethod
4339 @classmethod
4290 def get_uid(cls, obj):
4340 def get_uid(cls, obj):
4291 args = obj.task_args
4341 args = obj.task_args
4292 kwargs = obj.task_kwargs
4342 kwargs = obj.task_kwargs
4293 if isinstance(args, JsonRaw):
4343 if isinstance(args, JsonRaw):
4294 try:
4344 try:
4295 args = json.loads(args)
4345 args = json.loads(args)
4296 except ValueError:
4346 except ValueError:
4297 args = tuple()
4347 args = tuple()
4298
4348
4299 if isinstance(kwargs, JsonRaw):
4349 if isinstance(kwargs, JsonRaw):
4300 try:
4350 try:
4301 kwargs = json.loads(kwargs)
4351 kwargs = json.loads(kwargs)
4302 except ValueError:
4352 except ValueError:
4303 kwargs = dict()
4353 kwargs = dict()
4304
4354
4305 dot_notation = obj.task_dot_notation
4355 dot_notation = obj.task_dot_notation
4306 val = '.'.join(map(safe_str, [
4356 val = '.'.join(map(safe_str, [
4307 sorted(dot_notation), args, sorted(kwargs.items())]))
4357 sorted(dot_notation), args, sorted(kwargs.items())]))
4308 return hashlib.sha1(val).hexdigest()
4358 return hashlib.sha1(val).hexdigest()
4309
4359
4310 @classmethod
4360 @classmethod
4311 def get_by_schedule_name(cls, schedule_name):
4361 def get_by_schedule_name(cls, schedule_name):
4312 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4362 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4313
4363
4314 @classmethod
4364 @classmethod
4315 def get_by_schedule_id(cls, schedule_id):
4365 def get_by_schedule_id(cls, schedule_id):
4316 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4366 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4317
4367
4318 @property
4368 @property
4319 def task(self):
4369 def task(self):
4320 return self.task_dot_notation
4370 return self.task_dot_notation
4321
4371
4322 @property
4372 @property
4323 def schedule(self):
4373 def schedule(self):
4324 from rhodecode.lib.celerylib.utils import raw_2_schedule
4374 from rhodecode.lib.celerylib.utils import raw_2_schedule
4325 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4375 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4326 return schedule
4376 return schedule
4327
4377
4328 @property
4378 @property
4329 def args(self):
4379 def args(self):
4330 try:
4380 try:
4331 return list(self.task_args or [])
4381 return list(self.task_args or [])
4332 except ValueError:
4382 except ValueError:
4333 return list()
4383 return list()
4334
4384
4335 @property
4385 @property
4336 def kwargs(self):
4386 def kwargs(self):
4337 try:
4387 try:
4338 return dict(self.task_kwargs or {})
4388 return dict(self.task_kwargs or {})
4339 except ValueError:
4389 except ValueError:
4340 return dict()
4390 return dict()
4341
4391
4342 def _as_raw(self, val):
4392 def _as_raw(self, val):
4343 if hasattr(val, 'de_coerce'):
4393 if hasattr(val, 'de_coerce'):
4344 val = val.de_coerce()
4394 val = val.de_coerce()
4345 if val:
4395 if val:
4346 val = json.dumps(val)
4396 val = json.dumps(val)
4347
4397
4348 return val
4398 return val
4349
4399
4350 @property
4400 @property
4351 def schedule_definition_raw(self):
4401 def schedule_definition_raw(self):
4352 return self._as_raw(self.schedule_definition)
4402 return self._as_raw(self.schedule_definition)
4353
4403
4354 @property
4404 @property
4355 def args_raw(self):
4405 def args_raw(self):
4356 return self._as_raw(self.task_args)
4406 return self._as_raw(self.task_args)
4357
4407
4358 @property
4408 @property
4359 def kwargs_raw(self):
4409 def kwargs_raw(self):
4360 return self._as_raw(self.task_kwargs)
4410 return self._as_raw(self.task_kwargs)
4361
4411
4362 def __repr__(self):
4412 def __repr__(self):
4363 return '<DB:ScheduleEntry({}:{})>'.format(
4413 return '<DB:ScheduleEntry({}:{})>'.format(
4364 self.schedule_entry_id, self.schedule_name)
4414 self.schedule_entry_id, self.schedule_name)
4365
4415
4366
4416
4367 @event.listens_for(ScheduleEntry, 'before_update')
4417 @event.listens_for(ScheduleEntry, 'before_update')
4368 def update_task_uid(mapper, connection, target):
4418 def update_task_uid(mapper, connection, target):
4369 target.task_uid = ScheduleEntry.get_uid(target)
4419 target.task_uid = ScheduleEntry.get_uid(target)
4370
4420
4371
4421
4372 @event.listens_for(ScheduleEntry, 'before_insert')
4422 @event.listens_for(ScheduleEntry, 'before_insert')
4373 def set_task_uid(mapper, connection, target):
4423 def set_task_uid(mapper, connection, target):
4374 target.task_uid = ScheduleEntry.get_uid(target)
4424 target.task_uid = ScheduleEntry.get_uid(target)
4375
4425
4376
4426
4377 class DbMigrateVersion(Base, BaseModel):
4427 class DbMigrateVersion(Base, BaseModel):
4378 __tablename__ = 'db_migrate_version'
4428 __tablename__ = 'db_migrate_version'
4379 __table_args__ = (
4429 __table_args__ = (
4380 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4430 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4381 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4431 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4382 )
4432 )
4383 repository_id = Column('repository_id', String(250), primary_key=True)
4433 repository_id = Column('repository_id', String(250), primary_key=True)
4384 repository_path = Column('repository_path', Text)
4434 repository_path = Column('repository_path', Text)
4385 version = Column('version', Integer)
4435 version = Column('version', Integer)
4386
4436
4387
4437
4388 class DbSession(Base, BaseModel):
4438 class DbSession(Base, BaseModel):
4389 __tablename__ = 'db_session'
4439 __tablename__ = 'db_session'
4390 __table_args__ = (
4440 __table_args__ = (
4391 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4441 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4392 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4442 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4393 )
4443 )
4394
4444
4395 def __repr__(self):
4445 def __repr__(self):
4396 return '<DB:DbSession({})>'.format(self.id)
4446 return '<DB:DbSession({})>'.format(self.id)
4397
4447
4398 id = Column('id', Integer())
4448 id = Column('id', Integer())
4399 namespace = Column('namespace', String(255), primary_key=True)
4449 namespace = Column('namespace', String(255), primary_key=True)
4400 accessed = Column('accessed', DateTime, nullable=False)
4450 accessed = Column('accessed', DateTime, nullable=False)
4401 created = Column('created', DateTime, nullable=False)
4451 created = Column('created', DateTime, nullable=False)
4402 data = Column('data', PickleType, nullable=False)
4452 data = Column('data', PickleType, nullable=False)
@@ -1,614 +1,615 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
35
36
36
37 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pyramid.threadlocal import get_current_request
51 from pyramid.threadlocal import get_current_request
52
52
53 from rhodecode import BACKENDS
53 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
54 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
55 from rhodecode.model import validators as v
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 deform_templates = resource_filename('deform', 'templates')
60 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
62 search_path = (rhodecode_templates, deform_templates)
63
63
64
64
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
67 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
68 kw['h'] = helpers
69 kw['request'] = get_current_request()
69 kw['request'] = get_current_request()
70 return self.load(template_name)(**kw)
70 return self.load(template_name)(**kw)
71
71
72
72
73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 deform.Form.set_default_renderer(form_renderer)
74 deform.Form.set_default_renderer(form_renderer)
75
75
76
76
77 def LoginForm(localizer):
77 def LoginForm(localizer):
78 _ = localizer
78 _ = localizer
79
79
80 class _LoginForm(formencode.Schema):
80 class _LoginForm(formencode.Schema):
81 allow_extra_fields = True
81 allow_extra_fields = True
82 filter_extra_fields = True
82 filter_extra_fields = True
83 username = v.UnicodeString(
83 username = v.UnicodeString(
84 strip=True,
84 strip=True,
85 min=1,
85 min=1,
86 not_empty=True,
86 not_empty=True,
87 messages={
87 messages={
88 'empty': _(u'Please enter a login'),
88 'empty': _(u'Please enter a login'),
89 'tooShort': _(u'Enter a value %(min)i characters long or more')
89 'tooShort': _(u'Enter a value %(min)i characters long or more')
90 }
90 }
91 )
91 )
92
92
93 password = v.UnicodeString(
93 password = v.UnicodeString(
94 strip=False,
94 strip=False,
95 min=3,
95 min=3,
96 max=72,
96 max=72,
97 not_empty=True,
97 not_empty=True,
98 messages={
98 messages={
99 'empty': _(u'Please enter a password'),
99 'empty': _(u'Please enter a password'),
100 'tooShort': _(u'Enter %(min)i characters or more')}
100 'tooShort': _(u'Enter %(min)i characters or more')}
101 )
101 )
102
102
103 remember = v.StringBoolean(if_missing=False)
103 remember = v.StringBoolean(if_missing=False)
104
104
105 chained_validators = [v.ValidAuth(localizer)]
105 chained_validators = [v.ValidAuth(localizer)]
106 return _LoginForm
106 return _LoginForm
107
107
108
108
109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
110 old_data = old_data or {}
110 old_data = old_data or {}
111 available_languages = available_languages or []
111 available_languages = available_languages or []
112 _ = localizer
112 _ = localizer
113
113
114 class _UserForm(formencode.Schema):
114 class _UserForm(formencode.Schema):
115 allow_extra_fields = True
115 allow_extra_fields = True
116 filter_extra_fields = True
116 filter_extra_fields = True
117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
118 v.ValidUsername(localizer, edit, old_data))
118 v.ValidUsername(localizer, edit, old_data))
119 if edit:
119 if edit:
120 new_password = All(
120 new_password = All(
121 v.ValidPassword(localizer),
121 v.ValidPassword(localizer),
122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
123 )
123 )
124 password_confirmation = All(
124 password_confirmation = All(
125 v.ValidPassword(localizer),
125 v.ValidPassword(localizer),
126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
127 )
127 )
128 admin = v.StringBoolean(if_missing=False)
128 admin = v.StringBoolean(if_missing=False)
129 else:
129 else:
130 password = All(
130 password = All(
131 v.ValidPassword(localizer),
131 v.ValidPassword(localizer),
132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
133 )
133 )
134 password_confirmation = All(
134 password_confirmation = All(
135 v.ValidPassword(localizer),
135 v.ValidPassword(localizer),
136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
137 )
137 )
138
138
139 password_change = v.StringBoolean(if_missing=False)
139 password_change = v.StringBoolean(if_missing=False)
140 create_repo_group = v.StringBoolean(if_missing=False)
140 create_repo_group = v.StringBoolean(if_missing=False)
141
141
142 active = v.StringBoolean(if_missing=False)
142 active = v.StringBoolean(if_missing=False)
143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
146 extern_name = v.UnicodeString(strip=True)
146 extern_name = v.UnicodeString(strip=True)
147 extern_type = v.UnicodeString(strip=True)
147 extern_type = v.UnicodeString(strip=True)
148 language = v.OneOf(available_languages, hideList=False,
148 language = v.OneOf(available_languages, hideList=False,
149 testValueList=True, if_missing=None)
149 testValueList=True, if_missing=None)
150 chained_validators = [v.ValidPasswordsMatch(localizer)]
150 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 return _UserForm
151 return _UserForm
152
152
153
153
154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 old_data = old_data or {}
155 old_data = old_data or {}
156 _ = localizer
156 _ = localizer
157
157
158 class _UserGroupForm(formencode.Schema):
158 class _UserGroupForm(formencode.Schema):
159 allow_extra_fields = True
159 allow_extra_fields = True
160 filter_extra_fields = True
160 filter_extra_fields = True
161
161
162 users_group_name = All(
162 users_group_name = All(
163 v.UnicodeString(strip=True, min=1, not_empty=True),
163 v.UnicodeString(strip=True, min=1, not_empty=True),
164 v.ValidUserGroup(localizer, edit, old_data)
164 v.ValidUserGroup(localizer, edit, old_data)
165 )
165 )
166 user_group_description = v.UnicodeString(strip=True, min=1,
166 user_group_description = v.UnicodeString(strip=True, min=1,
167 not_empty=False)
167 not_empty=False)
168
168
169 users_group_active = v.StringBoolean(if_missing=False)
169 users_group_active = v.StringBoolean(if_missing=False)
170
170
171 if edit:
171 if edit:
172 # this is user group owner
172 # this is user group owner
173 user = All(
173 user = All(
174 v.UnicodeString(not_empty=True),
174 v.UnicodeString(not_empty=True),
175 v.ValidRepoUser(localizer, allow_disabled))
175 v.ValidRepoUser(localizer, allow_disabled))
176 return _UserGroupForm
176 return _UserGroupForm
177
177
178
178
179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 can_create_in_root=False, allow_disabled=False):
180 can_create_in_root=False, allow_disabled=False):
181 _ = localizer
181 _ = localizer
182 old_data = old_data or {}
182 old_data = old_data or {}
183 available_groups = available_groups or []
183 available_groups = available_groups or []
184
184
185 class _RepoGroupForm(formencode.Schema):
185 class _RepoGroupForm(formencode.Schema):
186 allow_extra_fields = True
186 allow_extra_fields = True
187 filter_extra_fields = False
187 filter_extra_fields = False
188
188
189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 v.SlugifyName(localizer),)
190 v.SlugifyName(localizer),)
191 group_description = v.UnicodeString(strip=True, min=1,
191 group_description = v.UnicodeString(strip=True, min=1,
192 not_empty=False)
192 not_empty=False)
193 group_copy_permissions = v.StringBoolean(if_missing=False)
193 group_copy_permissions = v.StringBoolean(if_missing=False)
194
194
195 group_parent_id = v.OneOf(available_groups, hideList=False,
195 group_parent_id = v.OneOf(available_groups, hideList=False,
196 testValueList=True, not_empty=True)
196 testValueList=True, not_empty=True)
197 enable_locking = v.StringBoolean(if_missing=False)
197 enable_locking = v.StringBoolean(if_missing=False)
198 chained_validators = [
198 chained_validators = [
199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200
200
201 if edit:
201 if edit:
202 # this is repo group owner
202 # this is repo group owner
203 user = All(
203 user = All(
204 v.UnicodeString(not_empty=True),
204 v.UnicodeString(not_empty=True),
205 v.ValidRepoUser(localizer, allow_disabled))
205 v.ValidRepoUser(localizer, allow_disabled))
206 return _RepoGroupForm
206 return _RepoGroupForm
207
207
208
208
209 def RegisterForm(localizer, edit=False, old_data=None):
209 def RegisterForm(localizer, edit=False, old_data=None):
210 _ = localizer
210 _ = localizer
211 old_data = old_data or {}
211 old_data = old_data or {}
212
212
213 class _RegisterForm(formencode.Schema):
213 class _RegisterForm(formencode.Schema):
214 allow_extra_fields = True
214 allow_extra_fields = True
215 filter_extra_fields = True
215 filter_extra_fields = True
216 username = All(
216 username = All(
217 v.ValidUsername(localizer, edit, old_data),
217 v.ValidUsername(localizer, edit, old_data),
218 v.UnicodeString(strip=True, min=1, not_empty=True)
218 v.UnicodeString(strip=True, min=1, not_empty=True)
219 )
219 )
220 password = All(
220 password = All(
221 v.ValidPassword(localizer),
221 v.ValidPassword(localizer),
222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 )
223 )
224 password_confirmation = All(
224 password_confirmation = All(
225 v.ValidPassword(localizer),
225 v.ValidPassword(localizer),
226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 )
227 )
228 active = v.StringBoolean(if_missing=False)
228 active = v.StringBoolean(if_missing=False)
229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232
232
233 chained_validators = [v.ValidPasswordsMatch(localizer)]
233 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 return _RegisterForm
234 return _RegisterForm
235
235
236
236
237 def PasswordResetForm(localizer):
237 def PasswordResetForm(localizer):
238 _ = localizer
238 _ = localizer
239
239
240 class _PasswordResetForm(formencode.Schema):
240 class _PasswordResetForm(formencode.Schema):
241 allow_extra_fields = True
241 allow_extra_fields = True
242 filter_extra_fields = True
242 filter_extra_fields = True
243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 return _PasswordResetForm
244 return _PasswordResetForm
245
245
246
246
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None,
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None,
248 landing_revs=None, allow_disabled=False):
248 landing_revs=None, allow_disabled=False):
249 _ = localizer
249 _ = localizer
250 old_data = old_data or {}
250 old_data = old_data or {}
251 repo_groups = repo_groups or []
251 repo_groups = repo_groups or []
252 landing_revs = landing_revs or []
252 landing_revs = landing_revs or []
253 supported_backends = BACKENDS.keys()
253 supported_backends = BACKENDS.keys()
254
254
255 class _RepoForm(formencode.Schema):
255 class _RepoForm(formencode.Schema):
256 allow_extra_fields = True
256 allow_extra_fields = True
257 filter_extra_fields = False
257 filter_extra_fields = False
258 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
258 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
259 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
259 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
260 repo_group = All(v.CanWriteGroup(localizer, old_data),
260 repo_group = All(v.CanWriteGroup(localizer, old_data),
261 v.OneOf(repo_groups, hideList=True))
261 v.OneOf(repo_groups, hideList=True))
262 repo_type = v.OneOf(supported_backends, required=False,
262 repo_type = v.OneOf(supported_backends, required=False,
263 if_missing=old_data.get('repo_type'))
263 if_missing=old_data.get('repo_type'))
264 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
264 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
265 repo_private = v.StringBoolean(if_missing=False)
265 repo_private = v.StringBoolean(if_missing=False)
266 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
266 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
267 repo_copy_permissions = v.StringBoolean(if_missing=False)
267 repo_copy_permissions = v.StringBoolean(if_missing=False)
268 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
268 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
269
269
270 repo_enable_statistics = v.StringBoolean(if_missing=False)
270 repo_enable_statistics = v.StringBoolean(if_missing=False)
271 repo_enable_downloads = v.StringBoolean(if_missing=False)
271 repo_enable_downloads = v.StringBoolean(if_missing=False)
272 repo_enable_locking = v.StringBoolean(if_missing=False)
272 repo_enable_locking = v.StringBoolean(if_missing=False)
273
273
274 if edit:
274 if edit:
275 # this is repo owner
275 # this is repo owner
276 user = All(
276 user = All(
277 v.UnicodeString(not_empty=True),
277 v.UnicodeString(not_empty=True),
278 v.ValidRepoUser(localizer, allow_disabled))
278 v.ValidRepoUser(localizer, allow_disabled))
279 clone_uri_change = v.UnicodeString(
279 clone_uri_change = v.UnicodeString(
280 not_empty=False, if_missing=v.Missing)
280 not_empty=False, if_missing=v.Missing)
281
281
282 chained_validators = [v.ValidCloneUri(localizer),
282 chained_validators = [v.ValidCloneUri(localizer),
283 v.ValidRepoName(localizer, edit, old_data)]
283 v.ValidRepoName(localizer, edit, old_data)]
284 return _RepoForm
284 return _RepoForm
285
285
286
286
287 def RepoPermsForm(localizer):
287 def RepoPermsForm(localizer):
288 _ = localizer
288 _ = localizer
289
289
290 class _RepoPermsForm(formencode.Schema):
290 class _RepoPermsForm(formencode.Schema):
291 allow_extra_fields = True
291 allow_extra_fields = True
292 filter_extra_fields = False
292 filter_extra_fields = False
293 chained_validators = [v.ValidPerms(localizer, type_='repo')]
293 chained_validators = [v.ValidPerms(localizer, type_='repo')]
294 return _RepoPermsForm
294 return _RepoPermsForm
295
295
296
296
297 def RepoGroupPermsForm(localizer, valid_recursive_choices):
297 def RepoGroupPermsForm(localizer, valid_recursive_choices):
298 _ = localizer
298 _ = localizer
299
299
300 class _RepoGroupPermsForm(formencode.Schema):
300 class _RepoGroupPermsForm(formencode.Schema):
301 allow_extra_fields = True
301 allow_extra_fields = True
302 filter_extra_fields = False
302 filter_extra_fields = False
303 recursive = v.OneOf(valid_recursive_choices)
303 recursive = v.OneOf(valid_recursive_choices)
304 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
304 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
305 return _RepoGroupPermsForm
305 return _RepoGroupPermsForm
306
306
307
307
308 def UserGroupPermsForm(localizer):
308 def UserGroupPermsForm(localizer):
309 _ = localizer
309 _ = localizer
310
310
311 class _UserPermsForm(formencode.Schema):
311 class _UserPermsForm(formencode.Schema):
312 allow_extra_fields = True
312 allow_extra_fields = True
313 filter_extra_fields = False
313 filter_extra_fields = False
314 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
314 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
315 return _UserPermsForm
315 return _UserPermsForm
316
316
317
317
318 def RepoFieldForm(localizer):
318 def RepoFieldForm(localizer):
319 _ = localizer
319 _ = localizer
320
320
321 class _RepoFieldForm(formencode.Schema):
321 class _RepoFieldForm(formencode.Schema):
322 filter_extra_fields = True
322 filter_extra_fields = True
323 allow_extra_fields = True
323 allow_extra_fields = True
324
324
325 new_field_key = All(v.FieldKey(localizer),
325 new_field_key = All(v.FieldKey(localizer),
326 v.UnicodeString(strip=True, min=3, not_empty=True))
326 v.UnicodeString(strip=True, min=3, not_empty=True))
327 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
327 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
328 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
328 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
329 if_missing='str')
329 if_missing='str')
330 new_field_label = v.UnicodeString(not_empty=False)
330 new_field_label = v.UnicodeString(not_empty=False)
331 new_field_desc = v.UnicodeString(not_empty=False)
331 new_field_desc = v.UnicodeString(not_empty=False)
332 return _RepoFieldForm
332 return _RepoFieldForm
333
333
334
334
335 def RepoForkForm(localizer, edit=False, old_data=None,
335 def RepoForkForm(localizer, edit=False, old_data=None,
336 supported_backends=BACKENDS.keys(), repo_groups=None,
336 supported_backends=BACKENDS.keys(), repo_groups=None,
337 landing_revs=None):
337 landing_revs=None):
338 _ = localizer
338 _ = localizer
339 old_data = old_data or {}
339 old_data = old_data or {}
340 repo_groups = repo_groups or []
340 repo_groups = repo_groups or []
341 landing_revs = landing_revs or []
341 landing_revs = landing_revs or []
342
342
343 class _RepoForkForm(formencode.Schema):
343 class _RepoForkForm(formencode.Schema):
344 allow_extra_fields = True
344 allow_extra_fields = True
345 filter_extra_fields = False
345 filter_extra_fields = False
346 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
346 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
347 v.SlugifyName(localizer))
347 v.SlugifyName(localizer))
348 repo_group = All(v.CanWriteGroup(localizer, ),
348 repo_group = All(v.CanWriteGroup(localizer, ),
349 v.OneOf(repo_groups, hideList=True))
349 v.OneOf(repo_groups, hideList=True))
350 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
350 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
351 description = v.UnicodeString(strip=True, min=1, not_empty=True)
351 description = v.UnicodeString(strip=True, min=1, not_empty=True)
352 private = v.StringBoolean(if_missing=False)
352 private = v.StringBoolean(if_missing=False)
353 copy_permissions = v.StringBoolean(if_missing=False)
353 copy_permissions = v.StringBoolean(if_missing=False)
354 fork_parent_id = v.UnicodeString()
354 fork_parent_id = v.UnicodeString()
355 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
355 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
356 landing_rev = v.OneOf(landing_revs, hideList=True)
356 landing_rev = v.OneOf(landing_revs, hideList=True)
357 return _RepoForkForm
357 return _RepoForkForm
358
358
359
359
360 def ApplicationSettingsForm(localizer):
360 def ApplicationSettingsForm(localizer):
361 _ = localizer
361 _ = localizer
362
362
363 class _ApplicationSettingsForm(formencode.Schema):
363 class _ApplicationSettingsForm(formencode.Schema):
364 allow_extra_fields = True
364 allow_extra_fields = True
365 filter_extra_fields = False
365 filter_extra_fields = False
366 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
366 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
367 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
367 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
368 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
368 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
369 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
369 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
370 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
370 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
371 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
371 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
372 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
372 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
373 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
373 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
374 return _ApplicationSettingsForm
374 return _ApplicationSettingsForm
375
375
376
376
377 def ApplicationVisualisationForm(localizer):
377 def ApplicationVisualisationForm(localizer):
378 _ = localizer
378 _ = localizer
379
379
380 class _ApplicationVisualisationForm(formencode.Schema):
380 class _ApplicationVisualisationForm(formencode.Schema):
381 allow_extra_fields = True
381 allow_extra_fields = True
382 filter_extra_fields = False
382 filter_extra_fields = False
383 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
383 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
384 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
384 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
385 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
385 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
386
386
387 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
387 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
388 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
388 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
389 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
389 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
390 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
390 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
391 rhodecode_show_version = v.StringBoolean(if_missing=False)
391 rhodecode_show_version = v.StringBoolean(if_missing=False)
392 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
392 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
393 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
393 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
394 rhodecode_gravatar_url = v.UnicodeString(min=3)
394 rhodecode_gravatar_url = v.UnicodeString(min=3)
395 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
395 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
396 rhodecode_support_url = v.UnicodeString()
396 rhodecode_support_url = v.UnicodeString()
397 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
397 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
398 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
398 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
399 return _ApplicationVisualisationForm
399 return _ApplicationVisualisationForm
400
400
401
401
402 class _BaseVcsSettingsForm(formencode.Schema):
402 class _BaseVcsSettingsForm(formencode.Schema):
403
403
404 allow_extra_fields = True
404 allow_extra_fields = True
405 filter_extra_fields = False
405 filter_extra_fields = False
406 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
406 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
407 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
407 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
408 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
408 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
409
409
410 # PR/Code-review
410 # PR/Code-review
411 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
411 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
412 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
412 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
413
413
414 # hg
414 # hg
415 extensions_largefiles = v.StringBoolean(if_missing=False)
415 extensions_largefiles = v.StringBoolean(if_missing=False)
416 extensions_evolve = v.StringBoolean(if_missing=False)
416 extensions_evolve = v.StringBoolean(if_missing=False)
417 phases_publish = v.StringBoolean(if_missing=False)
417 phases_publish = v.StringBoolean(if_missing=False)
418
418
419 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
419 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
420 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
420 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
421
421
422 # git
422 # git
423 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
423 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
424 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
424 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
425 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
425 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
426
426
427 # svn
427 # svn
428 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
428 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
429 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
429 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
430
430
431
431
432 def ApplicationUiSettingsForm(localizer):
432 def ApplicationUiSettingsForm(localizer):
433 _ = localizer
433 _ = localizer
434
434
435 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
435 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
436 web_push_ssl = v.StringBoolean(if_missing=False)
436 web_push_ssl = v.StringBoolean(if_missing=False)
437 paths_root_path = All(
437 paths_root_path = All(
438 v.ValidPath(localizer),
438 v.ValidPath(localizer),
439 v.UnicodeString(strip=True, min=1, not_empty=True)
439 v.UnicodeString(strip=True, min=1, not_empty=True)
440 )
440 )
441 largefiles_usercache = All(
441 largefiles_usercache = All(
442 v.ValidPath(localizer),
442 v.ValidPath(localizer),
443 v.UnicodeString(strip=True, min=2, not_empty=True))
443 v.UnicodeString(strip=True, min=2, not_empty=True))
444 vcs_git_lfs_store_location = All(
444 vcs_git_lfs_store_location = All(
445 v.ValidPath(localizer),
445 v.ValidPath(localizer),
446 v.UnicodeString(strip=True, min=2, not_empty=True))
446 v.UnicodeString(strip=True, min=2, not_empty=True))
447 extensions_hgsubversion = v.StringBoolean(if_missing=False)
447 extensions_hgsubversion = v.StringBoolean(if_missing=False)
448 extensions_hggit = v.StringBoolean(if_missing=False)
448 extensions_hggit = v.StringBoolean(if_missing=False)
449 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
449 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
450 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
450 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
451 return _ApplicationUiSettingsForm
451 return _ApplicationUiSettingsForm
452
452
453
453
454 def RepoVcsSettingsForm(localizer, repo_name):
454 def RepoVcsSettingsForm(localizer, repo_name):
455 _ = localizer
455 _ = localizer
456
456
457 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
457 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
458 inherit_global_settings = v.StringBoolean(if_missing=False)
458 inherit_global_settings = v.StringBoolean(if_missing=False)
459 new_svn_branch = v.ValidSvnPattern(localizer,
459 new_svn_branch = v.ValidSvnPattern(localizer,
460 section='vcs_svn_branch', repo_name=repo_name)
460 section='vcs_svn_branch', repo_name=repo_name)
461 new_svn_tag = v.ValidSvnPattern(localizer,
461 new_svn_tag = v.ValidSvnPattern(localizer,
462 section='vcs_svn_tag', repo_name=repo_name)
462 section='vcs_svn_tag', repo_name=repo_name)
463 return _RepoVcsSettingsForm
463 return _RepoVcsSettingsForm
464
464
465
465
466 def LabsSettingsForm(localizer):
466 def LabsSettingsForm(localizer):
467 _ = localizer
467 _ = localizer
468
468
469 class _LabSettingsForm(formencode.Schema):
469 class _LabSettingsForm(formencode.Schema):
470 allow_extra_fields = True
470 allow_extra_fields = True
471 filter_extra_fields = False
471 filter_extra_fields = False
472 return _LabSettingsForm
472 return _LabSettingsForm
473
473
474
474
475 def ApplicationPermissionsForm(
475 def ApplicationPermissionsForm(
476 localizer, register_choices, password_reset_choices,
476 localizer, register_choices, password_reset_choices,
477 extern_activate_choices):
477 extern_activate_choices):
478 _ = localizer
478 _ = localizer
479
479
480 class _DefaultPermissionsForm(formencode.Schema):
480 class _DefaultPermissionsForm(formencode.Schema):
481 allow_extra_fields = True
481 allow_extra_fields = True
482 filter_extra_fields = True
482 filter_extra_fields = True
483
483
484 anonymous = v.StringBoolean(if_missing=False)
484 anonymous = v.StringBoolean(if_missing=False)
485 default_register = v.OneOf(register_choices)
485 default_register = v.OneOf(register_choices)
486 default_register_message = v.UnicodeString()
486 default_register_message = v.UnicodeString()
487 default_password_reset = v.OneOf(password_reset_choices)
487 default_password_reset = v.OneOf(password_reset_choices)
488 default_extern_activate = v.OneOf(extern_activate_choices)
488 default_extern_activate = v.OneOf(extern_activate_choices)
489 return _DefaultPermissionsForm
489 return _DefaultPermissionsForm
490
490
491
491
492 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
492 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
493 user_group_perms_choices):
493 user_group_perms_choices):
494 _ = localizer
494 _ = localizer
495
495
496 class _ObjectPermissionsForm(formencode.Schema):
496 class _ObjectPermissionsForm(formencode.Schema):
497 allow_extra_fields = True
497 allow_extra_fields = True
498 filter_extra_fields = True
498 filter_extra_fields = True
499 overwrite_default_repo = v.StringBoolean(if_missing=False)
499 overwrite_default_repo = v.StringBoolean(if_missing=False)
500 overwrite_default_group = v.StringBoolean(if_missing=False)
500 overwrite_default_group = v.StringBoolean(if_missing=False)
501 overwrite_default_user_group = v.StringBoolean(if_missing=False)
501 overwrite_default_user_group = v.StringBoolean(if_missing=False)
502 default_repo_perm = v.OneOf(repo_perms_choices)
502 default_repo_perm = v.OneOf(repo_perms_choices)
503 default_group_perm = v.OneOf(group_perms_choices)
503 default_group_perm = v.OneOf(group_perms_choices)
504 default_user_group_perm = v.OneOf(user_group_perms_choices)
504 default_user_group_perm = v.OneOf(user_group_perms_choices)
505 return _ObjectPermissionsForm
505 return _ObjectPermissionsForm
506
506
507
507
508 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
508 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
509 repo_group_create_choices, user_group_create_choices,
509 repo_group_create_choices, user_group_create_choices,
510 fork_choices, inherit_default_permissions_choices):
510 fork_choices, inherit_default_permissions_choices):
511 _ = localizer
511 _ = localizer
512
512
513 class _DefaultPermissionsForm(formencode.Schema):
513 class _DefaultPermissionsForm(formencode.Schema):
514 allow_extra_fields = True
514 allow_extra_fields = True
515 filter_extra_fields = True
515 filter_extra_fields = True
516
516
517 anonymous = v.StringBoolean(if_missing=False)
517 anonymous = v.StringBoolean(if_missing=False)
518
518
519 default_repo_create = v.OneOf(create_choices)
519 default_repo_create = v.OneOf(create_choices)
520 default_repo_create_on_write = v.OneOf(create_on_write_choices)
520 default_repo_create_on_write = v.OneOf(create_on_write_choices)
521 default_user_group_create = v.OneOf(user_group_create_choices)
521 default_user_group_create = v.OneOf(user_group_create_choices)
522 default_repo_group_create = v.OneOf(repo_group_create_choices)
522 default_repo_group_create = v.OneOf(repo_group_create_choices)
523 default_fork_create = v.OneOf(fork_choices)
523 default_fork_create = v.OneOf(fork_choices)
524 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
524 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
525 return _DefaultPermissionsForm
525 return _DefaultPermissionsForm
526
526
527
527
528 def UserIndividualPermissionsForm(localizer):
528 def UserIndividualPermissionsForm(localizer):
529 _ = localizer
529 _ = localizer
530
530
531 class _DefaultPermissionsForm(formencode.Schema):
531 class _DefaultPermissionsForm(formencode.Schema):
532 allow_extra_fields = True
532 allow_extra_fields = True
533 filter_extra_fields = True
533 filter_extra_fields = True
534
534
535 inherit_default_permissions = v.StringBoolean(if_missing=False)
535 inherit_default_permissions = v.StringBoolean(if_missing=False)
536 return _DefaultPermissionsForm
536 return _DefaultPermissionsForm
537
537
538
538
539 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
539 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
540 _ = localizer
540 _ = localizer
541 old_data = old_data or {}
541 old_data = old_data or {}
542
542
543 class _DefaultsForm(formencode.Schema):
543 class _DefaultsForm(formencode.Schema):
544 allow_extra_fields = True
544 allow_extra_fields = True
545 filter_extra_fields = True
545 filter_extra_fields = True
546 default_repo_type = v.OneOf(supported_backends)
546 default_repo_type = v.OneOf(supported_backends)
547 default_repo_private = v.StringBoolean(if_missing=False)
547 default_repo_private = v.StringBoolean(if_missing=False)
548 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
548 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
549 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
549 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
550 default_repo_enable_locking = v.StringBoolean(if_missing=False)
550 default_repo_enable_locking = v.StringBoolean(if_missing=False)
551 return _DefaultsForm
551 return _DefaultsForm
552
552
553
553
554 def AuthSettingsForm(localizer):
554 def AuthSettingsForm(localizer):
555 _ = localizer
555 _ = localizer
556
556
557 class _AuthSettingsForm(formencode.Schema):
557 class _AuthSettingsForm(formencode.Schema):
558 allow_extra_fields = True
558 allow_extra_fields = True
559 filter_extra_fields = True
559 filter_extra_fields = True
560 auth_plugins = All(v.ValidAuthPlugins(localizer),
560 auth_plugins = All(v.ValidAuthPlugins(localizer),
561 v.UniqueListFromString(localizer)(not_empty=True))
561 v.UniqueListFromString(localizer)(not_empty=True))
562 return _AuthSettingsForm
562 return _AuthSettingsForm
563
563
564
564
565 def UserExtraEmailForm(localizer):
565 def UserExtraEmailForm(localizer):
566 _ = localizer
566 _ = localizer
567
567
568 class _UserExtraEmailForm(formencode.Schema):
568 class _UserExtraEmailForm(formencode.Schema):
569 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
569 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
570 return _UserExtraEmailForm
570 return _UserExtraEmailForm
571
571
572
572
573 def UserExtraIpForm(localizer):
573 def UserExtraIpForm(localizer):
574 _ = localizer
574 _ = localizer
575
575
576 class _UserExtraIpForm(formencode.Schema):
576 class _UserExtraIpForm(formencode.Schema):
577 ip = v.ValidIp(localizer)(not_empty=True)
577 ip = v.ValidIp(localizer)(not_empty=True)
578 return _UserExtraIpForm
578 return _UserExtraIpForm
579
579
580
580
581 def PullRequestForm(localizer, repo_id):
581 def PullRequestForm(localizer, repo_id):
582 _ = localizer
582 _ = localizer
583
583
584 class ReviewerForm(formencode.Schema):
584 class ReviewerForm(formencode.Schema):
585 user_id = v.Int(not_empty=True)
585 user_id = v.Int(not_empty=True)
586 reasons = All()
586 reasons = All()
587 rules = All(v.UniqueList(localizer, convert=int)())
587 mandatory = v.StringBoolean()
588 mandatory = v.StringBoolean()
588
589
589 class _PullRequestForm(formencode.Schema):
590 class _PullRequestForm(formencode.Schema):
590 allow_extra_fields = True
591 allow_extra_fields = True
591 filter_extra_fields = True
592 filter_extra_fields = True
592
593
593 common_ancestor = v.UnicodeString(strip=True, required=True)
594 common_ancestor = v.UnicodeString(strip=True, required=True)
594 source_repo = v.UnicodeString(strip=True, required=True)
595 source_repo = v.UnicodeString(strip=True, required=True)
595 source_ref = v.UnicodeString(strip=True, required=True)
596 source_ref = v.UnicodeString(strip=True, required=True)
596 target_repo = v.UnicodeString(strip=True, required=True)
597 target_repo = v.UnicodeString(strip=True, required=True)
597 target_ref = v.UnicodeString(strip=True, required=True)
598 target_ref = v.UnicodeString(strip=True, required=True)
598 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
599 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
599 v.UniqueList(localizer)(not_empty=True))
600 v.UniqueList(localizer)(not_empty=True))
600 review_members = formencode.ForEach(ReviewerForm())
601 review_members = formencode.ForEach(ReviewerForm())
601 pullrequest_title = v.UnicodeString(strip=True, required=True, min=3, max=255)
602 pullrequest_title = v.UnicodeString(strip=True, required=True, min=3, max=255)
602 pullrequest_desc = v.UnicodeString(strip=True, required=False)
603 pullrequest_desc = v.UnicodeString(strip=True, required=False)
603
604
604 return _PullRequestForm
605 return _PullRequestForm
605
606
606
607
607 def IssueTrackerPatternsForm(localizer):
608 def IssueTrackerPatternsForm(localizer):
608 _ = localizer
609 _ = localizer
609
610
610 class _IssueTrackerPatternsForm(formencode.Schema):
611 class _IssueTrackerPatternsForm(formencode.Schema):
611 allow_extra_fields = True
612 allow_extra_fields = True
612 filter_extra_fields = False
613 filter_extra_fields = False
613 chained_validators = [v.ValidPattern(localizer)]
614 chained_validators = [v.ValidPattern(localizer)]
614 return _IssueTrackerPatternsForm
615 return _IssueTrackerPatternsForm
@@ -1,1654 +1,1681 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = 3
77
77
78 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
88 ' failed.'),
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
112 'to sub repositories.'),
113 }
113 }
114
114
115 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
117 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
127 'reference is missing.'),
127 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
130 'reference is missing.'),
130 'reference is missing.'),
131 }
131 }
132
132
133 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
134 return self._get_instance((
134 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
136
136
137 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
138 if not api:
139 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
141 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
144
144
145 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
148
148
149 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
152
152
153 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
156
156
157 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
161
161
162 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
164 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
166
166
167 def check_user_comment(self, pull_request, user):
167 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
168 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
169 return self.check_user_read(pull_request, user) or owner
170
170
171 def get(self, pull_request):
171 def get(self, pull_request):
172 return self.__get_pull_request(pull_request)
172 return self.__get_pull_request(pull_request)
173
173
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 opened_by=None, order_by=None,
175 opened_by=None, order_by=None,
176 order_dir='desc'):
176 order_dir='desc'):
177 repo = None
177 repo = None
178 if repo_name:
178 if repo_name:
179 repo = self._get_repo(repo_name)
179 repo = self._get_repo(repo_name)
180
180
181 q = PullRequest.query()
181 q = PullRequest.query()
182
182
183 # source or target
183 # source or target
184 if repo and source:
184 if repo and source:
185 q = q.filter(PullRequest.source_repo == repo)
185 q = q.filter(PullRequest.source_repo == repo)
186 elif repo:
186 elif repo:
187 q = q.filter(PullRequest.target_repo == repo)
187 q = q.filter(PullRequest.target_repo == repo)
188
188
189 # closed,opened
189 # closed,opened
190 if statuses:
190 if statuses:
191 q = q.filter(PullRequest.status.in_(statuses))
191 q = q.filter(PullRequest.status.in_(statuses))
192
192
193 # opened by filter
193 # opened by filter
194 if opened_by:
194 if opened_by:
195 q = q.filter(PullRequest.user_id.in_(opened_by))
195 q = q.filter(PullRequest.user_id.in_(opened_by))
196
196
197 if order_by:
197 if order_by:
198 order_map = {
198 order_map = {
199 'name_raw': PullRequest.pull_request_id,
199 'name_raw': PullRequest.pull_request_id,
200 'title': PullRequest.title,
200 'title': PullRequest.title,
201 'updated_on_raw': PullRequest.updated_on,
201 'updated_on_raw': PullRequest.updated_on,
202 'target_repo': PullRequest.target_repo_id
202 'target_repo': PullRequest.target_repo_id
203 }
203 }
204 if order_dir == 'asc':
204 if order_dir == 'asc':
205 q = q.order_by(order_map[order_by].asc())
205 q = q.order_by(order_map[order_by].asc())
206 else:
206 else:
207 q = q.order_by(order_map[order_by].desc())
207 q = q.order_by(order_map[order_by].desc())
208
208
209 return q
209 return q
210
210
211 def count_all(self, repo_name, source=False, statuses=None,
211 def count_all(self, repo_name, source=False, statuses=None,
212 opened_by=None):
212 opened_by=None):
213 """
213 """
214 Count the number of pull requests for a specific repository.
214 Count the number of pull requests for a specific repository.
215
215
216 :param repo_name: target or source repo
216 :param repo_name: target or source repo
217 :param source: boolean flag to specify if repo_name refers to source
217 :param source: boolean flag to specify if repo_name refers to source
218 :param statuses: list of pull request statuses
218 :param statuses: list of pull request statuses
219 :param opened_by: author user of the pull request
219 :param opened_by: author user of the pull request
220 :returns: int number of pull requests
220 :returns: int number of pull requests
221 """
221 """
222 q = self._prepare_get_all_query(
222 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224
224
225 return q.count()
225 return q.count()
226
226
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 offset=0, length=None, order_by=None, order_dir='desc'):
228 offset=0, length=None, order_by=None, order_dir='desc'):
229 """
229 """
230 Get all pull requests for a specific repository.
230 Get all pull requests for a specific repository.
231
231
232 :param repo_name: target or source repo
232 :param repo_name: target or source repo
233 :param source: boolean flag to specify if repo_name refers to source
233 :param source: boolean flag to specify if repo_name refers to source
234 :param statuses: list of pull request statuses
234 :param statuses: list of pull request statuses
235 :param opened_by: author user of the pull request
235 :param opened_by: author user of the pull request
236 :param offset: pagination offset
236 :param offset: pagination offset
237 :param length: length of returned list
237 :param length: length of returned list
238 :param order_by: order of the returned list
238 :param order_by: order of the returned list
239 :param order_dir: 'asc' or 'desc' ordering direction
239 :param order_dir: 'asc' or 'desc' ordering direction
240 :returns: list of pull requests
240 :returns: list of pull requests
241 """
241 """
242 q = self._prepare_get_all_query(
242 q = self._prepare_get_all_query(
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 order_by=order_by, order_dir=order_dir)
244 order_by=order_by, order_dir=order_dir)
245
245
246 if length:
246 if length:
247 pull_requests = q.limit(length).offset(offset).all()
247 pull_requests = q.limit(length).offset(offset).all()
248 else:
248 else:
249 pull_requests = q.all()
249 pull_requests = q.all()
250
250
251 return pull_requests
251 return pull_requests
252
252
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 opened_by=None):
254 opened_by=None):
255 """
255 """
256 Count the number of pull requests for a specific repository that are
256 Count the number of pull requests for a specific repository that are
257 awaiting review.
257 awaiting review.
258
258
259 :param repo_name: target or source repo
259 :param repo_name: target or source repo
260 :param source: boolean flag to specify if repo_name refers to source
260 :param source: boolean flag to specify if repo_name refers to source
261 :param statuses: list of pull request statuses
261 :param statuses: list of pull request statuses
262 :param opened_by: author user of the pull request
262 :param opened_by: author user of the pull request
263 :returns: int number of pull requests
263 :returns: int number of pull requests
264 """
264 """
265 pull_requests = self.get_awaiting_review(
265 pull_requests = self.get_awaiting_review(
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267
267
268 return len(pull_requests)
268 return len(pull_requests)
269
269
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, offset=0, length=None,
271 opened_by=None, offset=0, length=None,
272 order_by=None, order_dir='desc'):
272 order_by=None, order_dir='desc'):
273 """
273 """
274 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
275 review.
275 review.
276
276
277 :param repo_name: target or source repo
277 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _filtered_pull_requests = []
291 _filtered_pull_requests = []
292 for pr in pull_requests:
292 for pr in pull_requests:
293 status = pr.calculated_review_status()
293 status = pr.calculated_review_status()
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 _filtered_pull_requests.append(pr)
296 _filtered_pull_requests.append(pr)
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 opened_by=None, user_id=None):
303 opened_by=None, user_id=None):
304 """
304 """
305 Count the number of pull requests for a specific repository that are
305 Count the number of pull requests for a specific repository that are
306 awaiting review from a specific user.
306 awaiting review from a specific user.
307
307
308 :param repo_name: target or source repo
308 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
309 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
310 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
311 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
312 :param user_id: reviewer user of the pull request
313 :returns: int number of pull requests
313 :returns: int number of pull requests
314 """
314 """
315 pull_requests = self.get_awaiting_my_review(
315 pull_requests = self.get_awaiting_my_review(
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 user_id=user_id)
317 user_id=user_id)
318
318
319 return len(pull_requests)
319 return len(pull_requests)
320
320
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 opened_by=None, user_id=None, offset=0,
322 opened_by=None, user_id=None, offset=0,
323 length=None, order_by=None, order_dir='desc'):
323 length=None, order_by=None, order_dir='desc'):
324 """
324 """
325 Get all pull requests for a specific repository that are awaiting
325 Get all pull requests for a specific repository that are awaiting
326 review from a specific user.
326 review from a specific user.
327
327
328 :param repo_name: target or source repo
328 :param repo_name: target or source repo
329 :param source: boolean flag to specify if repo_name refers to source
329 :param source: boolean flag to specify if repo_name refers to source
330 :param statuses: list of pull request statuses
330 :param statuses: list of pull request statuses
331 :param opened_by: author user of the pull request
331 :param opened_by: author user of the pull request
332 :param user_id: reviewer user of the pull request
332 :param user_id: reviewer user of the pull request
333 :param offset: pagination offset
333 :param offset: pagination offset
334 :param length: length of returned list
334 :param length: length of returned list
335 :param order_by: order of the returned list
335 :param order_by: order of the returned list
336 :param order_dir: 'asc' or 'desc' ordering direction
336 :param order_dir: 'asc' or 'desc' ordering direction
337 :returns: list of pull requests
337 :returns: list of pull requests
338 """
338 """
339 pull_requests = self.get_all(
339 pull_requests = self.get_all(
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 order_by=order_by, order_dir=order_dir)
341 order_by=order_by, order_dir=order_dir)
342
342
343 _my = PullRequestModel().get_not_reviewed(user_id)
343 _my = PullRequestModel().get_not_reviewed(user_id)
344 my_participation = []
344 my_participation = []
345 for pr in pull_requests:
345 for pr in pull_requests:
346 if pr in _my:
346 if pr in _my:
347 my_participation.append(pr)
347 my_participation.append(pr)
348 _filtered_pull_requests = my_participation
348 _filtered_pull_requests = my_participation
349 if length:
349 if length:
350 return _filtered_pull_requests[offset:offset+length]
350 return _filtered_pull_requests[offset:offset+length]
351 else:
351 else:
352 return _filtered_pull_requests
352 return _filtered_pull_requests
353
353
354 def get_not_reviewed(self, user_id):
354 def get_not_reviewed(self, user_id):
355 return [
355 return [
356 x.pull_request for x in PullRequestReviewers.query().filter(
356 x.pull_request for x in PullRequestReviewers.query().filter(
357 PullRequestReviewers.user_id == user_id).all()
357 PullRequestReviewers.user_id == user_id).all()
358 ]
358 ]
359
359
360 def _prepare_participating_query(self, user_id=None, statuses=None,
360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 order_by=None, order_dir='desc'):
361 order_by=None, order_dir='desc'):
362 q = PullRequest.query()
362 q = PullRequest.query()
363 if user_id:
363 if user_id:
364 reviewers_subquery = Session().query(
364 reviewers_subquery = Session().query(
365 PullRequestReviewers.pull_request_id).filter(
365 PullRequestReviewers.pull_request_id).filter(
366 PullRequestReviewers.user_id == user_id).subquery()
366 PullRequestReviewers.user_id == user_id).subquery()
367 user_filter = or_(
367 user_filter = or_(
368 PullRequest.user_id == user_id,
368 PullRequest.user_id == user_id,
369 PullRequest.pull_request_id.in_(reviewers_subquery)
369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 )
370 )
371 q = PullRequest.query().filter(user_filter)
371 q = PullRequest.query().filter(user_filter)
372
372
373 # closed,opened
373 # closed,opened
374 if statuses:
374 if statuses:
375 q = q.filter(PullRequest.status.in_(statuses))
375 q = q.filter(PullRequest.status.in_(statuses))
376
376
377 if order_by:
377 if order_by:
378 order_map = {
378 order_map = {
379 'name_raw': PullRequest.pull_request_id,
379 'name_raw': PullRequest.pull_request_id,
380 'title': PullRequest.title,
380 'title': PullRequest.title,
381 'updated_on_raw': PullRequest.updated_on,
381 'updated_on_raw': PullRequest.updated_on,
382 'target_repo': PullRequest.target_repo_id
382 'target_repo': PullRequest.target_repo_id
383 }
383 }
384 if order_dir == 'asc':
384 if order_dir == 'asc':
385 q = q.order_by(order_map[order_by].asc())
385 q = q.order_by(order_map[order_by].asc())
386 else:
386 else:
387 q = q.order_by(order_map[order_by].desc())
387 q = q.order_by(order_map[order_by].desc())
388
388
389 return q
389 return q
390
390
391 def count_im_participating_in(self, user_id=None, statuses=None):
391 def count_im_participating_in(self, user_id=None, statuses=None):
392 q = self._prepare_participating_query(user_id, statuses=statuses)
392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 return q.count()
393 return q.count()
394
394
395 def get_im_participating_in(
395 def get_im_participating_in(
396 self, user_id=None, statuses=None, offset=0,
396 self, user_id=None, statuses=None, offset=0,
397 length=None, order_by=None, order_dir='desc'):
397 length=None, order_by=None, order_dir='desc'):
398 """
398 """
399 Get all Pull requests that i'm participating in, or i have opened
399 Get all Pull requests that i'm participating in, or i have opened
400 """
400 """
401
401
402 q = self._prepare_participating_query(
402 q = self._prepare_participating_query(
403 user_id, statuses=statuses, order_by=order_by,
403 user_id, statuses=statuses, order_by=order_by,
404 order_dir=order_dir)
404 order_dir=order_dir)
405
405
406 if length:
406 if length:
407 pull_requests = q.limit(length).offset(offset).all()
407 pull_requests = q.limit(length).offset(offset).all()
408 else:
408 else:
409 pull_requests = q.all()
409 pull_requests = q.all()
410
410
411 return pull_requests
411 return pull_requests
412
412
413 def get_versions(self, pull_request):
413 def get_versions(self, pull_request):
414 """
414 """
415 returns version of pull request sorted by ID descending
415 returns version of pull request sorted by ID descending
416 """
416 """
417 return PullRequestVersion.query()\
417 return PullRequestVersion.query()\
418 .filter(PullRequestVersion.pull_request == pull_request)\
418 .filter(PullRequestVersion.pull_request == pull_request)\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 .all()
420 .all()
421
421
422 def get_pr_version(self, pull_request_id, version=None):
422 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
423 at_version = None
424
424
425 if version and version == 'latest':
425 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
426 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
427 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
428 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
429 at_version = 'latest'
430 elif version:
430 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
432 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
433 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
434 at_version = pull_request_ver.pull_request_version_id
435 else:
435 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
437 pull_request_id)
438
438
439 pull_request_display_obj = PullRequest.get_pr_display_object(
439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
440 pull_request_obj, _org_pull_request_obj)
441
441
442 return _org_pull_request_obj, pull_request_obj, \
442 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
443 pull_request_display_obj, at_version
444
444
445 def create(self, created_by, source_repo, source_ref, target_repo,
445 def create(self, created_by, source_repo, source_ref, target_repo,
446 target_ref, revisions, reviewers, title, description=None,
446 target_ref, revisions, reviewers, title, description=None,
447 reviewer_data=None, translator=None):
447 reviewer_data=None, translator=None):
448 translator = translator or get_current_request().translate
448 translator = translator or get_current_request().translate
449
449
450 created_by_user = self._get_user(created_by)
450 created_by_user = self._get_user(created_by)
451 source_repo = self._get_repo(source_repo)
451 source_repo = self._get_repo(source_repo)
452 target_repo = self._get_repo(target_repo)
452 target_repo = self._get_repo(target_repo)
453
453
454 pull_request = PullRequest()
454 pull_request = PullRequest()
455 pull_request.source_repo = source_repo
455 pull_request.source_repo = source_repo
456 pull_request.source_ref = source_ref
456 pull_request.source_ref = source_ref
457 pull_request.target_repo = target_repo
457 pull_request.target_repo = target_repo
458 pull_request.target_ref = target_ref
458 pull_request.target_ref = target_ref
459 pull_request.revisions = revisions
459 pull_request.revisions = revisions
460 pull_request.title = title
460 pull_request.title = title
461 pull_request.description = description
461 pull_request.description = description
462 pull_request.author = created_by_user
462 pull_request.author = created_by_user
463 pull_request.reviewer_data = reviewer_data
463 pull_request.reviewer_data = reviewer_data
464
464
465 Session().add(pull_request)
465 Session().add(pull_request)
466 Session().flush()
466 Session().flush()
467
467
468 reviewer_ids = set()
468 reviewer_ids = set()
469 # members / reviewers
469 # members / reviewers
470 for reviewer_object in reviewers:
470 for reviewer_object in reviewers:
471 user_id, reasons, mandatory = reviewer_object
471 user_id, reasons, mandatory, rules = reviewer_object
472 user = self._get_user(user_id)
472 user = self._get_user(user_id)
473
473
474 # skip duplicates
474 # skip duplicates
475 if user.user_id in reviewer_ids:
475 if user.user_id in reviewer_ids:
476 continue
476 continue
477
477
478 reviewer_ids.add(user.user_id)
478 reviewer_ids.add(user.user_id)
479
479
480 reviewer = PullRequestReviewers()
480 reviewer = PullRequestReviewers()
481 reviewer.user = user
481 reviewer.user = user
482 reviewer.pull_request = pull_request
482 reviewer.pull_request = pull_request
483 reviewer.reasons = reasons
483 reviewer.reasons = reasons
484 reviewer.mandatory = mandatory
484 reviewer.mandatory = mandatory
485
486 # NOTE(marcink): pick only first rule for now
487 rule_id = rules[0] if rules else None
488 rule = RepoReviewRule.get(rule_id) if rule_id else None
489 if rule:
490 review_group = rule.user_group_vote_rule()
491 if review_group:
492 # NOTE(marcink):
493 # again, can be that user is member of more,
494 # but we pick the first same, as default reviewers algo
495 review_group = review_group[0]
496
497 rule_data = {
498 'rule_name':
499 rule.review_rule_name,
500 'rule_user_group_entry_id':
501 review_group.repo_review_rule_users_group_id,
502 'rule_user_group_name':
503 review_group.users_group.users_group_name,
504 'rule_user_group_members':
505 [x.user.username for x in review_group.users_group.members],
506 }
507 # e.g {'vote_rule': -1, 'mandatory': True}
508 rule_data.update(review_group.rule_data())
509
510 reviewer.rule_data = rule_data
511
485 Session().add(reviewer)
512 Session().add(reviewer)
486
513
487 # Set approval status to "Under Review" for all commits which are
514 # Set approval status to "Under Review" for all commits which are
488 # part of this pull request.
515 # part of this pull request.
489 ChangesetStatusModel().set_status(
516 ChangesetStatusModel().set_status(
490 repo=target_repo,
517 repo=target_repo,
491 status=ChangesetStatus.STATUS_UNDER_REVIEW,
518 status=ChangesetStatus.STATUS_UNDER_REVIEW,
492 user=created_by_user,
519 user=created_by_user,
493 pull_request=pull_request
520 pull_request=pull_request
494 )
521 )
495
522
496 MergeCheck.validate(
523 MergeCheck.validate(
497 pull_request, user=created_by_user, translator=translator)
524 pull_request, user=created_by_user, translator=translator)
498
525
499 self.notify_reviewers(pull_request, reviewer_ids)
526 self.notify_reviewers(pull_request, reviewer_ids)
500 self._trigger_pull_request_hook(
527 self._trigger_pull_request_hook(
501 pull_request, created_by_user, 'create')
528 pull_request, created_by_user, 'create')
502
529
503 creation_data = pull_request.get_api_data(with_merge_state=False)
530 creation_data = pull_request.get_api_data(with_merge_state=False)
504 self._log_audit_action(
531 self._log_audit_action(
505 'repo.pull_request.create', {'data': creation_data},
532 'repo.pull_request.create', {'data': creation_data},
506 created_by_user, pull_request)
533 created_by_user, pull_request)
507
534
508 return pull_request
535 return pull_request
509
536
510 def _trigger_pull_request_hook(self, pull_request, user, action):
537 def _trigger_pull_request_hook(self, pull_request, user, action):
511 pull_request = self.__get_pull_request(pull_request)
538 pull_request = self.__get_pull_request(pull_request)
512 target_scm = pull_request.target_repo.scm_instance()
539 target_scm = pull_request.target_repo.scm_instance()
513 if action == 'create':
540 if action == 'create':
514 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
541 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
515 elif action == 'merge':
542 elif action == 'merge':
516 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
543 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
517 elif action == 'close':
544 elif action == 'close':
518 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
545 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
519 elif action == 'review_status_change':
546 elif action == 'review_status_change':
520 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
547 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
521 elif action == 'update':
548 elif action == 'update':
522 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
549 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
523 else:
550 else:
524 return
551 return
525
552
526 trigger_hook(
553 trigger_hook(
527 username=user.username,
554 username=user.username,
528 repo_name=pull_request.target_repo.repo_name,
555 repo_name=pull_request.target_repo.repo_name,
529 repo_alias=target_scm.alias,
556 repo_alias=target_scm.alias,
530 pull_request=pull_request)
557 pull_request=pull_request)
531
558
532 def _get_commit_ids(self, pull_request):
559 def _get_commit_ids(self, pull_request):
533 """
560 """
534 Return the commit ids of the merged pull request.
561 Return the commit ids of the merged pull request.
535
562
536 This method is not dealing correctly yet with the lack of autoupdates
563 This method is not dealing correctly yet with the lack of autoupdates
537 nor with the implicit target updates.
564 nor with the implicit target updates.
538 For example: if a commit in the source repo is already in the target it
565 For example: if a commit in the source repo is already in the target it
539 will be reported anyways.
566 will be reported anyways.
540 """
567 """
541 merge_rev = pull_request.merge_rev
568 merge_rev = pull_request.merge_rev
542 if merge_rev is None:
569 if merge_rev is None:
543 raise ValueError('This pull request was not merged yet')
570 raise ValueError('This pull request was not merged yet')
544
571
545 commit_ids = list(pull_request.revisions)
572 commit_ids = list(pull_request.revisions)
546 if merge_rev not in commit_ids:
573 if merge_rev not in commit_ids:
547 commit_ids.append(merge_rev)
574 commit_ids.append(merge_rev)
548
575
549 return commit_ids
576 return commit_ids
550
577
551 def merge(self, pull_request, user, extras):
578 def merge(self, pull_request, user, extras):
552 log.debug("Merging pull request %s", pull_request.pull_request_id)
579 log.debug("Merging pull request %s", pull_request.pull_request_id)
553 merge_state = self._merge_pull_request(pull_request, user, extras)
580 merge_state = self._merge_pull_request(pull_request, user, extras)
554 if merge_state.executed:
581 if merge_state.executed:
555 log.debug(
582 log.debug(
556 "Merge was successful, updating the pull request comments.")
583 "Merge was successful, updating the pull request comments.")
557 self._comment_and_close_pr(pull_request, user, merge_state)
584 self._comment_and_close_pr(pull_request, user, merge_state)
558
585
559 self._log_audit_action(
586 self._log_audit_action(
560 'repo.pull_request.merge',
587 'repo.pull_request.merge',
561 {'merge_state': merge_state.__dict__},
588 {'merge_state': merge_state.__dict__},
562 user, pull_request)
589 user, pull_request)
563
590
564 else:
591 else:
565 log.warn("Merge failed, not updating the pull request.")
592 log.warn("Merge failed, not updating the pull request.")
566 return merge_state
593 return merge_state
567
594
568 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
595 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
569 target_vcs = pull_request.target_repo.scm_instance()
596 target_vcs = pull_request.target_repo.scm_instance()
570 source_vcs = pull_request.source_repo.scm_instance()
597 source_vcs = pull_request.source_repo.scm_instance()
571 target_ref = self._refresh_reference(
598 target_ref = self._refresh_reference(
572 pull_request.target_ref_parts, target_vcs)
599 pull_request.target_ref_parts, target_vcs)
573
600
574 message = merge_msg or (
601 message = merge_msg or (
575 'Merge pull request #%(pr_id)s from '
602 'Merge pull request #%(pr_id)s from '
576 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
603 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
577 'pr_id': pull_request.pull_request_id,
604 'pr_id': pull_request.pull_request_id,
578 'source_repo': source_vcs.name,
605 'source_repo': source_vcs.name,
579 'source_ref_name': pull_request.source_ref_parts.name,
606 'source_ref_name': pull_request.source_ref_parts.name,
580 'pr_title': pull_request.title
607 'pr_title': pull_request.title
581 }
608 }
582
609
583 workspace_id = self._workspace_id(pull_request)
610 workspace_id = self._workspace_id(pull_request)
584 use_rebase = self._use_rebase_for_merging(pull_request)
611 use_rebase = self._use_rebase_for_merging(pull_request)
585 close_branch = self._close_branch_before_merging(pull_request)
612 close_branch = self._close_branch_before_merging(pull_request)
586
613
587 callback_daemon, extras = prepare_callback_daemon(
614 callback_daemon, extras = prepare_callback_daemon(
588 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
589 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
590
617
591 with callback_daemon:
618 with callback_daemon:
592 # TODO: johbo: Implement a clean way to run a config_override
619 # TODO: johbo: Implement a clean way to run a config_override
593 # for a single call.
620 # for a single call.
594 target_vcs.config.set(
621 target_vcs.config.set(
595 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
596 merge_state = target_vcs.merge(
623 merge_state = target_vcs.merge(
597 target_ref, source_vcs, pull_request.source_ref_parts,
624 target_ref, source_vcs, pull_request.source_ref_parts,
598 workspace_id, user_name=user.username,
625 workspace_id, user_name=user.username,
599 user_email=user.email, message=message, use_rebase=use_rebase,
626 user_email=user.email, message=message, use_rebase=use_rebase,
600 close_branch=close_branch)
627 close_branch=close_branch)
601 return merge_state
628 return merge_state
602
629
603 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
630 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
604 pull_request.merge_rev = merge_state.merge_ref.commit_id
631 pull_request.merge_rev = merge_state.merge_ref.commit_id
605 pull_request.updated_on = datetime.datetime.now()
632 pull_request.updated_on = datetime.datetime.now()
606 close_msg = close_msg or 'Pull request merged and closed'
633 close_msg = close_msg or 'Pull request merged and closed'
607
634
608 CommentsModel().create(
635 CommentsModel().create(
609 text=safe_unicode(close_msg),
636 text=safe_unicode(close_msg),
610 repo=pull_request.target_repo.repo_id,
637 repo=pull_request.target_repo.repo_id,
611 user=user.user_id,
638 user=user.user_id,
612 pull_request=pull_request.pull_request_id,
639 pull_request=pull_request.pull_request_id,
613 f_path=None,
640 f_path=None,
614 line_no=None,
641 line_no=None,
615 closing_pr=True
642 closing_pr=True
616 )
643 )
617
644
618 Session().add(pull_request)
645 Session().add(pull_request)
619 Session().flush()
646 Session().flush()
620 # TODO: paris: replace invalidation with less radical solution
647 # TODO: paris: replace invalidation with less radical solution
621 ScmModel().mark_for_invalidation(
648 ScmModel().mark_for_invalidation(
622 pull_request.target_repo.repo_name)
649 pull_request.target_repo.repo_name)
623 self._trigger_pull_request_hook(pull_request, user, 'merge')
650 self._trigger_pull_request_hook(pull_request, user, 'merge')
624
651
625 def has_valid_update_type(self, pull_request):
652 def has_valid_update_type(self, pull_request):
626 source_ref_type = pull_request.source_ref_parts.type
653 source_ref_type = pull_request.source_ref_parts.type
627 return source_ref_type in ['book', 'branch', 'tag']
654 return source_ref_type in ['book', 'branch', 'tag']
628
655
629 def update_commits(self, pull_request):
656 def update_commits(self, pull_request):
630 """
657 """
631 Get the updated list of commits for the pull request
658 Get the updated list of commits for the pull request
632 and return the new pull request version and the list
659 and return the new pull request version and the list
633 of commits processed by this update action
660 of commits processed by this update action
634 """
661 """
635 pull_request = self.__get_pull_request(pull_request)
662 pull_request = self.__get_pull_request(pull_request)
636 source_ref_type = pull_request.source_ref_parts.type
663 source_ref_type = pull_request.source_ref_parts.type
637 source_ref_name = pull_request.source_ref_parts.name
664 source_ref_name = pull_request.source_ref_parts.name
638 source_ref_id = pull_request.source_ref_parts.commit_id
665 source_ref_id = pull_request.source_ref_parts.commit_id
639
666
640 target_ref_type = pull_request.target_ref_parts.type
667 target_ref_type = pull_request.target_ref_parts.type
641 target_ref_name = pull_request.target_ref_parts.name
668 target_ref_name = pull_request.target_ref_parts.name
642 target_ref_id = pull_request.target_ref_parts.commit_id
669 target_ref_id = pull_request.target_ref_parts.commit_id
643
670
644 if not self.has_valid_update_type(pull_request):
671 if not self.has_valid_update_type(pull_request):
645 log.debug(
672 log.debug(
646 "Skipping update of pull request %s due to ref type: %s",
673 "Skipping update of pull request %s due to ref type: %s",
647 pull_request, source_ref_type)
674 pull_request, source_ref_type)
648 return UpdateResponse(
675 return UpdateResponse(
649 executed=False,
676 executed=False,
650 reason=UpdateFailureReason.WRONG_REF_TYPE,
677 reason=UpdateFailureReason.WRONG_REF_TYPE,
651 old=pull_request, new=None, changes=None,
678 old=pull_request, new=None, changes=None,
652 source_changed=False, target_changed=False)
679 source_changed=False, target_changed=False)
653
680
654 # source repo
681 # source repo
655 source_repo = pull_request.source_repo.scm_instance()
682 source_repo = pull_request.source_repo.scm_instance()
656 try:
683 try:
657 source_commit = source_repo.get_commit(commit_id=source_ref_name)
684 source_commit = source_repo.get_commit(commit_id=source_ref_name)
658 except CommitDoesNotExistError:
685 except CommitDoesNotExistError:
659 return UpdateResponse(
686 return UpdateResponse(
660 executed=False,
687 executed=False,
661 reason=UpdateFailureReason.MISSING_SOURCE_REF,
688 reason=UpdateFailureReason.MISSING_SOURCE_REF,
662 old=pull_request, new=None, changes=None,
689 old=pull_request, new=None, changes=None,
663 source_changed=False, target_changed=False)
690 source_changed=False, target_changed=False)
664
691
665 source_changed = source_ref_id != source_commit.raw_id
692 source_changed = source_ref_id != source_commit.raw_id
666
693
667 # target repo
694 # target repo
668 target_repo = pull_request.target_repo.scm_instance()
695 target_repo = pull_request.target_repo.scm_instance()
669 try:
696 try:
670 target_commit = target_repo.get_commit(commit_id=target_ref_name)
697 target_commit = target_repo.get_commit(commit_id=target_ref_name)
671 except CommitDoesNotExistError:
698 except CommitDoesNotExistError:
672 return UpdateResponse(
699 return UpdateResponse(
673 executed=False,
700 executed=False,
674 reason=UpdateFailureReason.MISSING_TARGET_REF,
701 reason=UpdateFailureReason.MISSING_TARGET_REF,
675 old=pull_request, new=None, changes=None,
702 old=pull_request, new=None, changes=None,
676 source_changed=False, target_changed=False)
703 source_changed=False, target_changed=False)
677 target_changed = target_ref_id != target_commit.raw_id
704 target_changed = target_ref_id != target_commit.raw_id
678
705
679 if not (source_changed or target_changed):
706 if not (source_changed or target_changed):
680 log.debug("Nothing changed in pull request %s", pull_request)
707 log.debug("Nothing changed in pull request %s", pull_request)
681 return UpdateResponse(
708 return UpdateResponse(
682 executed=False,
709 executed=False,
683 reason=UpdateFailureReason.NO_CHANGE,
710 reason=UpdateFailureReason.NO_CHANGE,
684 old=pull_request, new=None, changes=None,
711 old=pull_request, new=None, changes=None,
685 source_changed=target_changed, target_changed=source_changed)
712 source_changed=target_changed, target_changed=source_changed)
686
713
687 change_in_found = 'target repo' if target_changed else 'source repo'
714 change_in_found = 'target repo' if target_changed else 'source repo'
688 log.debug('Updating pull request because of change in %s detected',
715 log.debug('Updating pull request because of change in %s detected',
689 change_in_found)
716 change_in_found)
690
717
691 # Finally there is a need for an update, in case of source change
718 # Finally there is a need for an update, in case of source change
692 # we create a new version, else just an update
719 # we create a new version, else just an update
693 if source_changed:
720 if source_changed:
694 pull_request_version = self._create_version_from_snapshot(pull_request)
721 pull_request_version = self._create_version_from_snapshot(pull_request)
695 self._link_comments_to_version(pull_request_version)
722 self._link_comments_to_version(pull_request_version)
696 else:
723 else:
697 try:
724 try:
698 ver = pull_request.versions[-1]
725 ver = pull_request.versions[-1]
699 except IndexError:
726 except IndexError:
700 ver = None
727 ver = None
701
728
702 pull_request.pull_request_version_id = \
729 pull_request.pull_request_version_id = \
703 ver.pull_request_version_id if ver else None
730 ver.pull_request_version_id if ver else None
704 pull_request_version = pull_request
731 pull_request_version = pull_request
705
732
706 try:
733 try:
707 if target_ref_type in ('tag', 'branch', 'book'):
734 if target_ref_type in ('tag', 'branch', 'book'):
708 target_commit = target_repo.get_commit(target_ref_name)
735 target_commit = target_repo.get_commit(target_ref_name)
709 else:
736 else:
710 target_commit = target_repo.get_commit(target_ref_id)
737 target_commit = target_repo.get_commit(target_ref_id)
711 except CommitDoesNotExistError:
738 except CommitDoesNotExistError:
712 return UpdateResponse(
739 return UpdateResponse(
713 executed=False,
740 executed=False,
714 reason=UpdateFailureReason.MISSING_TARGET_REF,
741 reason=UpdateFailureReason.MISSING_TARGET_REF,
715 old=pull_request, new=None, changes=None,
742 old=pull_request, new=None, changes=None,
716 source_changed=source_changed, target_changed=target_changed)
743 source_changed=source_changed, target_changed=target_changed)
717
744
718 # re-compute commit ids
745 # re-compute commit ids
719 old_commit_ids = pull_request.revisions
746 old_commit_ids = pull_request.revisions
720 pre_load = ["author", "branch", "date", "message"]
747 pre_load = ["author", "branch", "date", "message"]
721 commit_ranges = target_repo.compare(
748 commit_ranges = target_repo.compare(
722 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
749 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
723 pre_load=pre_load)
750 pre_load=pre_load)
724
751
725 ancestor = target_repo.get_common_ancestor(
752 ancestor = target_repo.get_common_ancestor(
726 target_commit.raw_id, source_commit.raw_id, source_repo)
753 target_commit.raw_id, source_commit.raw_id, source_repo)
727
754
728 pull_request.source_ref = '%s:%s:%s' % (
755 pull_request.source_ref = '%s:%s:%s' % (
729 source_ref_type, source_ref_name, source_commit.raw_id)
756 source_ref_type, source_ref_name, source_commit.raw_id)
730 pull_request.target_ref = '%s:%s:%s' % (
757 pull_request.target_ref = '%s:%s:%s' % (
731 target_ref_type, target_ref_name, ancestor)
758 target_ref_type, target_ref_name, ancestor)
732
759
733 pull_request.revisions = [
760 pull_request.revisions = [
734 commit.raw_id for commit in reversed(commit_ranges)]
761 commit.raw_id for commit in reversed(commit_ranges)]
735 pull_request.updated_on = datetime.datetime.now()
762 pull_request.updated_on = datetime.datetime.now()
736 Session().add(pull_request)
763 Session().add(pull_request)
737 new_commit_ids = pull_request.revisions
764 new_commit_ids = pull_request.revisions
738
765
739 old_diff_data, new_diff_data = self._generate_update_diffs(
766 old_diff_data, new_diff_data = self._generate_update_diffs(
740 pull_request, pull_request_version)
767 pull_request, pull_request_version)
741
768
742 # calculate commit and file changes
769 # calculate commit and file changes
743 changes = self._calculate_commit_id_changes(
770 changes = self._calculate_commit_id_changes(
744 old_commit_ids, new_commit_ids)
771 old_commit_ids, new_commit_ids)
745 file_changes = self._calculate_file_changes(
772 file_changes = self._calculate_file_changes(
746 old_diff_data, new_diff_data)
773 old_diff_data, new_diff_data)
747
774
748 # set comments as outdated if DIFFS changed
775 # set comments as outdated if DIFFS changed
749 CommentsModel().outdate_comments(
776 CommentsModel().outdate_comments(
750 pull_request, old_diff_data=old_diff_data,
777 pull_request, old_diff_data=old_diff_data,
751 new_diff_data=new_diff_data)
778 new_diff_data=new_diff_data)
752
779
753 commit_changes = (changes.added or changes.removed)
780 commit_changes = (changes.added or changes.removed)
754 file_node_changes = (
781 file_node_changes = (
755 file_changes.added or file_changes.modified or file_changes.removed)
782 file_changes.added or file_changes.modified or file_changes.removed)
756 pr_has_changes = commit_changes or file_node_changes
783 pr_has_changes = commit_changes or file_node_changes
757
784
758 # Add an automatic comment to the pull request, in case
785 # Add an automatic comment to the pull request, in case
759 # anything has changed
786 # anything has changed
760 if pr_has_changes:
787 if pr_has_changes:
761 update_comment = CommentsModel().create(
788 update_comment = CommentsModel().create(
762 text=self._render_update_message(changes, file_changes),
789 text=self._render_update_message(changes, file_changes),
763 repo=pull_request.target_repo,
790 repo=pull_request.target_repo,
764 user=pull_request.author,
791 user=pull_request.author,
765 pull_request=pull_request,
792 pull_request=pull_request,
766 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
793 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
767
794
768 # Update status to "Under Review" for added commits
795 # Update status to "Under Review" for added commits
769 for commit_id in changes.added:
796 for commit_id in changes.added:
770 ChangesetStatusModel().set_status(
797 ChangesetStatusModel().set_status(
771 repo=pull_request.source_repo,
798 repo=pull_request.source_repo,
772 status=ChangesetStatus.STATUS_UNDER_REVIEW,
799 status=ChangesetStatus.STATUS_UNDER_REVIEW,
773 comment=update_comment,
800 comment=update_comment,
774 user=pull_request.author,
801 user=pull_request.author,
775 pull_request=pull_request,
802 pull_request=pull_request,
776 revision=commit_id)
803 revision=commit_id)
777
804
778 log.debug(
805 log.debug(
779 'Updated pull request %s, added_ids: %s, common_ids: %s, '
806 'Updated pull request %s, added_ids: %s, common_ids: %s, '
780 'removed_ids: %s', pull_request.pull_request_id,
807 'removed_ids: %s', pull_request.pull_request_id,
781 changes.added, changes.common, changes.removed)
808 changes.added, changes.common, changes.removed)
782 log.debug(
809 log.debug(
783 'Updated pull request with the following file changes: %s',
810 'Updated pull request with the following file changes: %s',
784 file_changes)
811 file_changes)
785
812
786 log.info(
813 log.info(
787 "Updated pull request %s from commit %s to commit %s, "
814 "Updated pull request %s from commit %s to commit %s, "
788 "stored new version %s of this pull request.",
815 "stored new version %s of this pull request.",
789 pull_request.pull_request_id, source_ref_id,
816 pull_request.pull_request_id, source_ref_id,
790 pull_request.source_ref_parts.commit_id,
817 pull_request.source_ref_parts.commit_id,
791 pull_request_version.pull_request_version_id)
818 pull_request_version.pull_request_version_id)
792 Session().commit()
819 Session().commit()
793 self._trigger_pull_request_hook(
820 self._trigger_pull_request_hook(
794 pull_request, pull_request.author, 'update')
821 pull_request, pull_request.author, 'update')
795
822
796 return UpdateResponse(
823 return UpdateResponse(
797 executed=True, reason=UpdateFailureReason.NONE,
824 executed=True, reason=UpdateFailureReason.NONE,
798 old=pull_request, new=pull_request_version, changes=changes,
825 old=pull_request, new=pull_request_version, changes=changes,
799 source_changed=source_changed, target_changed=target_changed)
826 source_changed=source_changed, target_changed=target_changed)
800
827
801 def _create_version_from_snapshot(self, pull_request):
828 def _create_version_from_snapshot(self, pull_request):
802 version = PullRequestVersion()
829 version = PullRequestVersion()
803 version.title = pull_request.title
830 version.title = pull_request.title
804 version.description = pull_request.description
831 version.description = pull_request.description
805 version.status = pull_request.status
832 version.status = pull_request.status
806 version.created_on = datetime.datetime.now()
833 version.created_on = datetime.datetime.now()
807 version.updated_on = pull_request.updated_on
834 version.updated_on = pull_request.updated_on
808 version.user_id = pull_request.user_id
835 version.user_id = pull_request.user_id
809 version.source_repo = pull_request.source_repo
836 version.source_repo = pull_request.source_repo
810 version.source_ref = pull_request.source_ref
837 version.source_ref = pull_request.source_ref
811 version.target_repo = pull_request.target_repo
838 version.target_repo = pull_request.target_repo
812 version.target_ref = pull_request.target_ref
839 version.target_ref = pull_request.target_ref
813
840
814 version._last_merge_source_rev = pull_request._last_merge_source_rev
841 version._last_merge_source_rev = pull_request._last_merge_source_rev
815 version._last_merge_target_rev = pull_request._last_merge_target_rev
842 version._last_merge_target_rev = pull_request._last_merge_target_rev
816 version.last_merge_status = pull_request.last_merge_status
843 version.last_merge_status = pull_request.last_merge_status
817 version.shadow_merge_ref = pull_request.shadow_merge_ref
844 version.shadow_merge_ref = pull_request.shadow_merge_ref
818 version.merge_rev = pull_request.merge_rev
845 version.merge_rev = pull_request.merge_rev
819 version.reviewer_data = pull_request.reviewer_data
846 version.reviewer_data = pull_request.reviewer_data
820
847
821 version.revisions = pull_request.revisions
848 version.revisions = pull_request.revisions
822 version.pull_request = pull_request
849 version.pull_request = pull_request
823 Session().add(version)
850 Session().add(version)
824 Session().flush()
851 Session().flush()
825
852
826 return version
853 return version
827
854
828 def _generate_update_diffs(self, pull_request, pull_request_version):
855 def _generate_update_diffs(self, pull_request, pull_request_version):
829
856
830 diff_context = (
857 diff_context = (
831 self.DIFF_CONTEXT +
858 self.DIFF_CONTEXT +
832 CommentsModel.needed_extra_diff_context())
859 CommentsModel.needed_extra_diff_context())
833
860
834 source_repo = pull_request_version.source_repo
861 source_repo = pull_request_version.source_repo
835 source_ref_id = pull_request_version.source_ref_parts.commit_id
862 source_ref_id = pull_request_version.source_ref_parts.commit_id
836 target_ref_id = pull_request_version.target_ref_parts.commit_id
863 target_ref_id = pull_request_version.target_ref_parts.commit_id
837 old_diff = self._get_diff_from_pr_or_version(
864 old_diff = self._get_diff_from_pr_or_version(
838 source_repo, source_ref_id, target_ref_id, context=diff_context)
865 source_repo, source_ref_id, target_ref_id, context=diff_context)
839
866
840 source_repo = pull_request.source_repo
867 source_repo = pull_request.source_repo
841 source_ref_id = pull_request.source_ref_parts.commit_id
868 source_ref_id = pull_request.source_ref_parts.commit_id
842 target_ref_id = pull_request.target_ref_parts.commit_id
869 target_ref_id = pull_request.target_ref_parts.commit_id
843
870
844 new_diff = self._get_diff_from_pr_or_version(
871 new_diff = self._get_diff_from_pr_or_version(
845 source_repo, source_ref_id, target_ref_id, context=diff_context)
872 source_repo, source_ref_id, target_ref_id, context=diff_context)
846
873
847 old_diff_data = diffs.DiffProcessor(old_diff)
874 old_diff_data = diffs.DiffProcessor(old_diff)
848 old_diff_data.prepare()
875 old_diff_data.prepare()
849 new_diff_data = diffs.DiffProcessor(new_diff)
876 new_diff_data = diffs.DiffProcessor(new_diff)
850 new_diff_data.prepare()
877 new_diff_data.prepare()
851
878
852 return old_diff_data, new_diff_data
879 return old_diff_data, new_diff_data
853
880
854 def _link_comments_to_version(self, pull_request_version):
881 def _link_comments_to_version(self, pull_request_version):
855 """
882 """
856 Link all unlinked comments of this pull request to the given version.
883 Link all unlinked comments of this pull request to the given version.
857
884
858 :param pull_request_version: The `PullRequestVersion` to which
885 :param pull_request_version: The `PullRequestVersion` to which
859 the comments shall be linked.
886 the comments shall be linked.
860
887
861 """
888 """
862 pull_request = pull_request_version.pull_request
889 pull_request = pull_request_version.pull_request
863 comments = ChangesetComment.query()\
890 comments = ChangesetComment.query()\
864 .filter(
891 .filter(
865 # TODO: johbo: Should we query for the repo at all here?
892 # TODO: johbo: Should we query for the repo at all here?
866 # Pending decision on how comments of PRs are to be related
893 # Pending decision on how comments of PRs are to be related
867 # to either the source repo, the target repo or no repo at all.
894 # to either the source repo, the target repo or no repo at all.
868 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
895 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
869 ChangesetComment.pull_request == pull_request,
896 ChangesetComment.pull_request == pull_request,
870 ChangesetComment.pull_request_version == None)\
897 ChangesetComment.pull_request_version == None)\
871 .order_by(ChangesetComment.comment_id.asc())
898 .order_by(ChangesetComment.comment_id.asc())
872
899
873 # TODO: johbo: Find out why this breaks if it is done in a bulk
900 # TODO: johbo: Find out why this breaks if it is done in a bulk
874 # operation.
901 # operation.
875 for comment in comments:
902 for comment in comments:
876 comment.pull_request_version_id = (
903 comment.pull_request_version_id = (
877 pull_request_version.pull_request_version_id)
904 pull_request_version.pull_request_version_id)
878 Session().add(comment)
905 Session().add(comment)
879
906
880 def _calculate_commit_id_changes(self, old_ids, new_ids):
907 def _calculate_commit_id_changes(self, old_ids, new_ids):
881 added = [x for x in new_ids if x not in old_ids]
908 added = [x for x in new_ids if x not in old_ids]
882 common = [x for x in new_ids if x in old_ids]
909 common = [x for x in new_ids if x in old_ids]
883 removed = [x for x in old_ids if x not in new_ids]
910 removed = [x for x in old_ids if x not in new_ids]
884 total = new_ids
911 total = new_ids
885 return ChangeTuple(added, common, removed, total)
912 return ChangeTuple(added, common, removed, total)
886
913
887 def _calculate_file_changes(self, old_diff_data, new_diff_data):
914 def _calculate_file_changes(self, old_diff_data, new_diff_data):
888
915
889 old_files = OrderedDict()
916 old_files = OrderedDict()
890 for diff_data in old_diff_data.parsed_diff:
917 for diff_data in old_diff_data.parsed_diff:
891 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
918 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
892
919
893 added_files = []
920 added_files = []
894 modified_files = []
921 modified_files = []
895 removed_files = []
922 removed_files = []
896 for diff_data in new_diff_data.parsed_diff:
923 for diff_data in new_diff_data.parsed_diff:
897 new_filename = diff_data['filename']
924 new_filename = diff_data['filename']
898 new_hash = md5_safe(diff_data['raw_diff'])
925 new_hash = md5_safe(diff_data['raw_diff'])
899
926
900 old_hash = old_files.get(new_filename)
927 old_hash = old_files.get(new_filename)
901 if not old_hash:
928 if not old_hash:
902 # file is not present in old diff, means it's added
929 # file is not present in old diff, means it's added
903 added_files.append(new_filename)
930 added_files.append(new_filename)
904 else:
931 else:
905 if new_hash != old_hash:
932 if new_hash != old_hash:
906 modified_files.append(new_filename)
933 modified_files.append(new_filename)
907 # now remove a file from old, since we have seen it already
934 # now remove a file from old, since we have seen it already
908 del old_files[new_filename]
935 del old_files[new_filename]
909
936
910 # removed files is when there are present in old, but not in NEW,
937 # removed files is when there are present in old, but not in NEW,
911 # since we remove old files that are present in new diff, left-overs
938 # since we remove old files that are present in new diff, left-overs
912 # if any should be the removed files
939 # if any should be the removed files
913 removed_files.extend(old_files.keys())
940 removed_files.extend(old_files.keys())
914
941
915 return FileChangeTuple(added_files, modified_files, removed_files)
942 return FileChangeTuple(added_files, modified_files, removed_files)
916
943
917 def _render_update_message(self, changes, file_changes):
944 def _render_update_message(self, changes, file_changes):
918 """
945 """
919 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
946 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
920 so it's always looking the same disregarding on which default
947 so it's always looking the same disregarding on which default
921 renderer system is using.
948 renderer system is using.
922
949
923 :param changes: changes named tuple
950 :param changes: changes named tuple
924 :param file_changes: file changes named tuple
951 :param file_changes: file changes named tuple
925
952
926 """
953 """
927 new_status = ChangesetStatus.get_status_lbl(
954 new_status = ChangesetStatus.get_status_lbl(
928 ChangesetStatus.STATUS_UNDER_REVIEW)
955 ChangesetStatus.STATUS_UNDER_REVIEW)
929
956
930 changed_files = (
957 changed_files = (
931 file_changes.added + file_changes.modified + file_changes.removed)
958 file_changes.added + file_changes.modified + file_changes.removed)
932
959
933 params = {
960 params = {
934 'under_review_label': new_status,
961 'under_review_label': new_status,
935 'added_commits': changes.added,
962 'added_commits': changes.added,
936 'removed_commits': changes.removed,
963 'removed_commits': changes.removed,
937 'changed_files': changed_files,
964 'changed_files': changed_files,
938 'added_files': file_changes.added,
965 'added_files': file_changes.added,
939 'modified_files': file_changes.modified,
966 'modified_files': file_changes.modified,
940 'removed_files': file_changes.removed,
967 'removed_files': file_changes.removed,
941 }
968 }
942 renderer = RstTemplateRenderer()
969 renderer = RstTemplateRenderer()
943 return renderer.render('pull_request_update.mako', **params)
970 return renderer.render('pull_request_update.mako', **params)
944
971
945 def edit(self, pull_request, title, description, user):
972 def edit(self, pull_request, title, description, user):
946 pull_request = self.__get_pull_request(pull_request)
973 pull_request = self.__get_pull_request(pull_request)
947 old_data = pull_request.get_api_data(with_merge_state=False)
974 old_data = pull_request.get_api_data(with_merge_state=False)
948 if pull_request.is_closed():
975 if pull_request.is_closed():
949 raise ValueError('This pull request is closed')
976 raise ValueError('This pull request is closed')
950 if title:
977 if title:
951 pull_request.title = title
978 pull_request.title = title
952 pull_request.description = description
979 pull_request.description = description
953 pull_request.updated_on = datetime.datetime.now()
980 pull_request.updated_on = datetime.datetime.now()
954 Session().add(pull_request)
981 Session().add(pull_request)
955 self._log_audit_action(
982 self._log_audit_action(
956 'repo.pull_request.edit', {'old_data': old_data},
983 'repo.pull_request.edit', {'old_data': old_data},
957 user, pull_request)
984 user, pull_request)
958
985
959 def update_reviewers(self, pull_request, reviewer_data, user):
986 def update_reviewers(self, pull_request, reviewer_data, user):
960 """
987 """
961 Update the reviewers in the pull request
988 Update the reviewers in the pull request
962
989
963 :param pull_request: the pr to update
990 :param pull_request: the pr to update
964 :param reviewer_data: list of tuples
991 :param reviewer_data: list of tuples
965 [(user, ['reason1', 'reason2'], mandatory_flag)]
992 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
966 """
993 """
967 pull_request = self.__get_pull_request(pull_request)
994 pull_request = self.__get_pull_request(pull_request)
968 if pull_request.is_closed():
995 if pull_request.is_closed():
969 raise ValueError('This pull request is closed')
996 raise ValueError('This pull request is closed')
970
997
971 reviewers = {}
998 reviewers = {}
972 for user_id, reasons, mandatory in reviewer_data:
999 for user_id, reasons, mandatory, rules in reviewer_data:
973 if isinstance(user_id, (int, basestring)):
1000 if isinstance(user_id, (int, basestring)):
974 user_id = self._get_user(user_id).user_id
1001 user_id = self._get_user(user_id).user_id
975 reviewers[user_id] = {
1002 reviewers[user_id] = {
976 'reasons': reasons, 'mandatory': mandatory}
1003 'reasons': reasons, 'mandatory': mandatory}
977
1004
978 reviewers_ids = set(reviewers.keys())
1005 reviewers_ids = set(reviewers.keys())
979 current_reviewers = PullRequestReviewers.query()\
1006 current_reviewers = PullRequestReviewers.query()\
980 .filter(PullRequestReviewers.pull_request ==
1007 .filter(PullRequestReviewers.pull_request ==
981 pull_request).all()
1008 pull_request).all()
982 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1009 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
983
1010
984 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1011 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
985 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1012 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
986
1013
987 log.debug("Adding %s reviewers", ids_to_add)
1014 log.debug("Adding %s reviewers", ids_to_add)
988 log.debug("Removing %s reviewers", ids_to_remove)
1015 log.debug("Removing %s reviewers", ids_to_remove)
989 changed = False
1016 changed = False
990 for uid in ids_to_add:
1017 for uid in ids_to_add:
991 changed = True
1018 changed = True
992 _usr = self._get_user(uid)
1019 _usr = self._get_user(uid)
993 reviewer = PullRequestReviewers()
1020 reviewer = PullRequestReviewers()
994 reviewer.user = _usr
1021 reviewer.user = _usr
995 reviewer.pull_request = pull_request
1022 reviewer.pull_request = pull_request
996 reviewer.reasons = reviewers[uid]['reasons']
1023 reviewer.reasons = reviewers[uid]['reasons']
997 # NOTE(marcink): mandatory shouldn't be changed now
1024 # NOTE(marcink): mandatory shouldn't be changed now
998 # reviewer.mandatory = reviewers[uid]['reasons']
1025 # reviewer.mandatory = reviewers[uid]['reasons']
999 Session().add(reviewer)
1026 Session().add(reviewer)
1000 self._log_audit_action(
1027 self._log_audit_action(
1001 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1028 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1002 user, pull_request)
1029 user, pull_request)
1003
1030
1004 for uid in ids_to_remove:
1031 for uid in ids_to_remove:
1005 changed = True
1032 changed = True
1006 reviewers = PullRequestReviewers.query()\
1033 reviewers = PullRequestReviewers.query()\
1007 .filter(PullRequestReviewers.user_id == uid,
1034 .filter(PullRequestReviewers.user_id == uid,
1008 PullRequestReviewers.pull_request == pull_request)\
1035 PullRequestReviewers.pull_request == pull_request)\
1009 .all()
1036 .all()
1010 # use .all() in case we accidentally added the same person twice
1037 # use .all() in case we accidentally added the same person twice
1011 # this CAN happen due to the lack of DB checks
1038 # this CAN happen due to the lack of DB checks
1012 for obj in reviewers:
1039 for obj in reviewers:
1013 old_data = obj.get_dict()
1040 old_data = obj.get_dict()
1014 Session().delete(obj)
1041 Session().delete(obj)
1015 self._log_audit_action(
1042 self._log_audit_action(
1016 'repo.pull_request.reviewer.delete',
1043 'repo.pull_request.reviewer.delete',
1017 {'old_data': old_data}, user, pull_request)
1044 {'old_data': old_data}, user, pull_request)
1018
1045
1019 if changed:
1046 if changed:
1020 pull_request.updated_on = datetime.datetime.now()
1047 pull_request.updated_on = datetime.datetime.now()
1021 Session().add(pull_request)
1048 Session().add(pull_request)
1022
1049
1023 self.notify_reviewers(pull_request, ids_to_add)
1050 self.notify_reviewers(pull_request, ids_to_add)
1024 return ids_to_add, ids_to_remove
1051 return ids_to_add, ids_to_remove
1025
1052
1026 def get_url(self, pull_request, request=None, permalink=False):
1053 def get_url(self, pull_request, request=None, permalink=False):
1027 if not request:
1054 if not request:
1028 request = get_current_request()
1055 request = get_current_request()
1029
1056
1030 if permalink:
1057 if permalink:
1031 return request.route_url(
1058 return request.route_url(
1032 'pull_requests_global',
1059 'pull_requests_global',
1033 pull_request_id=pull_request.pull_request_id,)
1060 pull_request_id=pull_request.pull_request_id,)
1034 else:
1061 else:
1035 return request.route_url('pullrequest_show',
1062 return request.route_url('pullrequest_show',
1036 repo_name=safe_str(pull_request.target_repo.repo_name),
1063 repo_name=safe_str(pull_request.target_repo.repo_name),
1037 pull_request_id=pull_request.pull_request_id,)
1064 pull_request_id=pull_request.pull_request_id,)
1038
1065
1039 def get_shadow_clone_url(self, pull_request):
1066 def get_shadow_clone_url(self, pull_request):
1040 """
1067 """
1041 Returns qualified url pointing to the shadow repository. If this pull
1068 Returns qualified url pointing to the shadow repository. If this pull
1042 request is closed there is no shadow repository and ``None`` will be
1069 request is closed there is no shadow repository and ``None`` will be
1043 returned.
1070 returned.
1044 """
1071 """
1045 if pull_request.is_closed():
1072 if pull_request.is_closed():
1046 return None
1073 return None
1047 else:
1074 else:
1048 pr_url = urllib.unquote(self.get_url(pull_request))
1075 pr_url = urllib.unquote(self.get_url(pull_request))
1049 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1076 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1050
1077
1051 def notify_reviewers(self, pull_request, reviewers_ids):
1078 def notify_reviewers(self, pull_request, reviewers_ids):
1052 # notification to reviewers
1079 # notification to reviewers
1053 if not reviewers_ids:
1080 if not reviewers_ids:
1054 return
1081 return
1055
1082
1056 pull_request_obj = pull_request
1083 pull_request_obj = pull_request
1057 # get the current participants of this pull request
1084 # get the current participants of this pull request
1058 recipients = reviewers_ids
1085 recipients = reviewers_ids
1059 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1086 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1060
1087
1061 pr_source_repo = pull_request_obj.source_repo
1088 pr_source_repo = pull_request_obj.source_repo
1062 pr_target_repo = pull_request_obj.target_repo
1089 pr_target_repo = pull_request_obj.target_repo
1063
1090
1064 pr_url = h.route_url('pullrequest_show',
1091 pr_url = h.route_url('pullrequest_show',
1065 repo_name=pr_target_repo.repo_name,
1092 repo_name=pr_target_repo.repo_name,
1066 pull_request_id=pull_request_obj.pull_request_id,)
1093 pull_request_id=pull_request_obj.pull_request_id,)
1067
1094
1068 # set some variables for email notification
1095 # set some variables for email notification
1069 pr_target_repo_url = h.route_url(
1096 pr_target_repo_url = h.route_url(
1070 'repo_summary', repo_name=pr_target_repo.repo_name)
1097 'repo_summary', repo_name=pr_target_repo.repo_name)
1071
1098
1072 pr_source_repo_url = h.route_url(
1099 pr_source_repo_url = h.route_url(
1073 'repo_summary', repo_name=pr_source_repo.repo_name)
1100 'repo_summary', repo_name=pr_source_repo.repo_name)
1074
1101
1075 # pull request specifics
1102 # pull request specifics
1076 pull_request_commits = [
1103 pull_request_commits = [
1077 (x.raw_id, x.message)
1104 (x.raw_id, x.message)
1078 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1105 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1079
1106
1080 kwargs = {
1107 kwargs = {
1081 'user': pull_request.author,
1108 'user': pull_request.author,
1082 'pull_request': pull_request_obj,
1109 'pull_request': pull_request_obj,
1083 'pull_request_commits': pull_request_commits,
1110 'pull_request_commits': pull_request_commits,
1084
1111
1085 'pull_request_target_repo': pr_target_repo,
1112 'pull_request_target_repo': pr_target_repo,
1086 'pull_request_target_repo_url': pr_target_repo_url,
1113 'pull_request_target_repo_url': pr_target_repo_url,
1087
1114
1088 'pull_request_source_repo': pr_source_repo,
1115 'pull_request_source_repo': pr_source_repo,
1089 'pull_request_source_repo_url': pr_source_repo_url,
1116 'pull_request_source_repo_url': pr_source_repo_url,
1090
1117
1091 'pull_request_url': pr_url,
1118 'pull_request_url': pr_url,
1092 }
1119 }
1093
1120
1094 # pre-generate the subject for notification itself
1121 # pre-generate the subject for notification itself
1095 (subject,
1122 (subject,
1096 _h, _e, # we don't care about those
1123 _h, _e, # we don't care about those
1097 body_plaintext) = EmailNotificationModel().render_email(
1124 body_plaintext) = EmailNotificationModel().render_email(
1098 notification_type, **kwargs)
1125 notification_type, **kwargs)
1099
1126
1100 # create notification objects, and emails
1127 # create notification objects, and emails
1101 NotificationModel().create(
1128 NotificationModel().create(
1102 created_by=pull_request.author,
1129 created_by=pull_request.author,
1103 notification_subject=subject,
1130 notification_subject=subject,
1104 notification_body=body_plaintext,
1131 notification_body=body_plaintext,
1105 notification_type=notification_type,
1132 notification_type=notification_type,
1106 recipients=recipients,
1133 recipients=recipients,
1107 email_kwargs=kwargs,
1134 email_kwargs=kwargs,
1108 )
1135 )
1109
1136
1110 def delete(self, pull_request, user):
1137 def delete(self, pull_request, user):
1111 pull_request = self.__get_pull_request(pull_request)
1138 pull_request = self.__get_pull_request(pull_request)
1112 old_data = pull_request.get_api_data(with_merge_state=False)
1139 old_data = pull_request.get_api_data(with_merge_state=False)
1113 self._cleanup_merge_workspace(pull_request)
1140 self._cleanup_merge_workspace(pull_request)
1114 self._log_audit_action(
1141 self._log_audit_action(
1115 'repo.pull_request.delete', {'old_data': old_data},
1142 'repo.pull_request.delete', {'old_data': old_data},
1116 user, pull_request)
1143 user, pull_request)
1117 Session().delete(pull_request)
1144 Session().delete(pull_request)
1118
1145
1119 def close_pull_request(self, pull_request, user):
1146 def close_pull_request(self, pull_request, user):
1120 pull_request = self.__get_pull_request(pull_request)
1147 pull_request = self.__get_pull_request(pull_request)
1121 self._cleanup_merge_workspace(pull_request)
1148 self._cleanup_merge_workspace(pull_request)
1122 pull_request.status = PullRequest.STATUS_CLOSED
1149 pull_request.status = PullRequest.STATUS_CLOSED
1123 pull_request.updated_on = datetime.datetime.now()
1150 pull_request.updated_on = datetime.datetime.now()
1124 Session().add(pull_request)
1151 Session().add(pull_request)
1125 self._trigger_pull_request_hook(
1152 self._trigger_pull_request_hook(
1126 pull_request, pull_request.author, 'close')
1153 pull_request, pull_request.author, 'close')
1127
1154
1128 pr_data = pull_request.get_api_data(with_merge_state=False)
1155 pr_data = pull_request.get_api_data(with_merge_state=False)
1129 self._log_audit_action(
1156 self._log_audit_action(
1130 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1157 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1131
1158
1132 def close_pull_request_with_comment(
1159 def close_pull_request_with_comment(
1133 self, pull_request, user, repo, message=None):
1160 self, pull_request, user, repo, message=None):
1134
1161
1135 pull_request_review_status = pull_request.calculated_review_status()
1162 pull_request_review_status = pull_request.calculated_review_status()
1136
1163
1137 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1164 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1138 # approved only if we have voting consent
1165 # approved only if we have voting consent
1139 status = ChangesetStatus.STATUS_APPROVED
1166 status = ChangesetStatus.STATUS_APPROVED
1140 else:
1167 else:
1141 status = ChangesetStatus.STATUS_REJECTED
1168 status = ChangesetStatus.STATUS_REJECTED
1142 status_lbl = ChangesetStatus.get_status_lbl(status)
1169 status_lbl = ChangesetStatus.get_status_lbl(status)
1143
1170
1144 default_message = (
1171 default_message = (
1145 'Closing with status change {transition_icon} {status}.'
1172 'Closing with status change {transition_icon} {status}.'
1146 ).format(transition_icon='>', status=status_lbl)
1173 ).format(transition_icon='>', status=status_lbl)
1147 text = message or default_message
1174 text = message or default_message
1148
1175
1149 # create a comment, and link it to new status
1176 # create a comment, and link it to new status
1150 comment = CommentsModel().create(
1177 comment = CommentsModel().create(
1151 text=text,
1178 text=text,
1152 repo=repo.repo_id,
1179 repo=repo.repo_id,
1153 user=user.user_id,
1180 user=user.user_id,
1154 pull_request=pull_request.pull_request_id,
1181 pull_request=pull_request.pull_request_id,
1155 status_change=status_lbl,
1182 status_change=status_lbl,
1156 status_change_type=status,
1183 status_change_type=status,
1157 closing_pr=True
1184 closing_pr=True
1158 )
1185 )
1159
1186
1160 # calculate old status before we change it
1187 # calculate old status before we change it
1161 old_calculated_status = pull_request.calculated_review_status()
1188 old_calculated_status = pull_request.calculated_review_status()
1162 ChangesetStatusModel().set_status(
1189 ChangesetStatusModel().set_status(
1163 repo.repo_id,
1190 repo.repo_id,
1164 status,
1191 status,
1165 user.user_id,
1192 user.user_id,
1166 comment=comment,
1193 comment=comment,
1167 pull_request=pull_request.pull_request_id
1194 pull_request=pull_request.pull_request_id
1168 )
1195 )
1169
1196
1170 Session().flush()
1197 Session().flush()
1171 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1198 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1172 # we now calculate the status of pull request again, and based on that
1199 # we now calculate the status of pull request again, and based on that
1173 # calculation trigger status change. This might happen in cases
1200 # calculation trigger status change. This might happen in cases
1174 # that non-reviewer admin closes a pr, which means his vote doesn't
1201 # that non-reviewer admin closes a pr, which means his vote doesn't
1175 # change the status, while if he's a reviewer this might change it.
1202 # change the status, while if he's a reviewer this might change it.
1176 calculated_status = pull_request.calculated_review_status()
1203 calculated_status = pull_request.calculated_review_status()
1177 if old_calculated_status != calculated_status:
1204 if old_calculated_status != calculated_status:
1178 self._trigger_pull_request_hook(
1205 self._trigger_pull_request_hook(
1179 pull_request, user, 'review_status_change')
1206 pull_request, user, 'review_status_change')
1180
1207
1181 # finally close the PR
1208 # finally close the PR
1182 PullRequestModel().close_pull_request(
1209 PullRequestModel().close_pull_request(
1183 pull_request.pull_request_id, user)
1210 pull_request.pull_request_id, user)
1184
1211
1185 return comment, status
1212 return comment, status
1186
1213
1187 def merge_status(self, pull_request, translator=None):
1214 def merge_status(self, pull_request, translator=None):
1188 _ = translator or get_current_request().translate
1215 _ = translator or get_current_request().translate
1189
1216
1190 if not self._is_merge_enabled(pull_request):
1217 if not self._is_merge_enabled(pull_request):
1191 return False, _('Server-side pull request merging is disabled.')
1218 return False, _('Server-side pull request merging is disabled.')
1192 if pull_request.is_closed():
1219 if pull_request.is_closed():
1193 return False, _('This pull request is closed.')
1220 return False, _('This pull request is closed.')
1194 merge_possible, msg = self._check_repo_requirements(
1221 merge_possible, msg = self._check_repo_requirements(
1195 target=pull_request.target_repo, source=pull_request.source_repo,
1222 target=pull_request.target_repo, source=pull_request.source_repo,
1196 translator=_)
1223 translator=_)
1197 if not merge_possible:
1224 if not merge_possible:
1198 return merge_possible, msg
1225 return merge_possible, msg
1199
1226
1200 try:
1227 try:
1201 resp = self._try_merge(pull_request)
1228 resp = self._try_merge(pull_request)
1202 log.debug("Merge response: %s", resp)
1229 log.debug("Merge response: %s", resp)
1203 status = resp.possible, self.merge_status_message(
1230 status = resp.possible, self.merge_status_message(
1204 resp.failure_reason)
1231 resp.failure_reason)
1205 except NotImplementedError:
1232 except NotImplementedError:
1206 status = False, _('Pull request merging is not supported.')
1233 status = False, _('Pull request merging is not supported.')
1207
1234
1208 return status
1235 return status
1209
1236
1210 def _check_repo_requirements(self, target, source, translator):
1237 def _check_repo_requirements(self, target, source, translator):
1211 """
1238 """
1212 Check if `target` and `source` have compatible requirements.
1239 Check if `target` and `source` have compatible requirements.
1213
1240
1214 Currently this is just checking for largefiles.
1241 Currently this is just checking for largefiles.
1215 """
1242 """
1216 _ = translator
1243 _ = translator
1217 target_has_largefiles = self._has_largefiles(target)
1244 target_has_largefiles = self._has_largefiles(target)
1218 source_has_largefiles = self._has_largefiles(source)
1245 source_has_largefiles = self._has_largefiles(source)
1219 merge_possible = True
1246 merge_possible = True
1220 message = u''
1247 message = u''
1221
1248
1222 if target_has_largefiles != source_has_largefiles:
1249 if target_has_largefiles != source_has_largefiles:
1223 merge_possible = False
1250 merge_possible = False
1224 if source_has_largefiles:
1251 if source_has_largefiles:
1225 message = _(
1252 message = _(
1226 'Target repository large files support is disabled.')
1253 'Target repository large files support is disabled.')
1227 else:
1254 else:
1228 message = _(
1255 message = _(
1229 'Source repository large files support is disabled.')
1256 'Source repository large files support is disabled.')
1230
1257
1231 return merge_possible, message
1258 return merge_possible, message
1232
1259
1233 def _has_largefiles(self, repo):
1260 def _has_largefiles(self, repo):
1234 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1261 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1235 'extensions', 'largefiles')
1262 'extensions', 'largefiles')
1236 return largefiles_ui and largefiles_ui[0].active
1263 return largefiles_ui and largefiles_ui[0].active
1237
1264
1238 def _try_merge(self, pull_request):
1265 def _try_merge(self, pull_request):
1239 """
1266 """
1240 Try to merge the pull request and return the merge status.
1267 Try to merge the pull request and return the merge status.
1241 """
1268 """
1242 log.debug(
1269 log.debug(
1243 "Trying out if the pull request %s can be merged.",
1270 "Trying out if the pull request %s can be merged.",
1244 pull_request.pull_request_id)
1271 pull_request.pull_request_id)
1245 target_vcs = pull_request.target_repo.scm_instance()
1272 target_vcs = pull_request.target_repo.scm_instance()
1246
1273
1247 # Refresh the target reference.
1274 # Refresh the target reference.
1248 try:
1275 try:
1249 target_ref = self._refresh_reference(
1276 target_ref = self._refresh_reference(
1250 pull_request.target_ref_parts, target_vcs)
1277 pull_request.target_ref_parts, target_vcs)
1251 except CommitDoesNotExistError:
1278 except CommitDoesNotExistError:
1252 merge_state = MergeResponse(
1279 merge_state = MergeResponse(
1253 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1280 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1254 return merge_state
1281 return merge_state
1255
1282
1256 target_locked = pull_request.target_repo.locked
1283 target_locked = pull_request.target_repo.locked
1257 if target_locked and target_locked[0]:
1284 if target_locked and target_locked[0]:
1258 log.debug("The target repository is locked.")
1285 log.debug("The target repository is locked.")
1259 merge_state = MergeResponse(
1286 merge_state = MergeResponse(
1260 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1287 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1261 elif self._needs_merge_state_refresh(pull_request, target_ref):
1288 elif self._needs_merge_state_refresh(pull_request, target_ref):
1262 log.debug("Refreshing the merge status of the repository.")
1289 log.debug("Refreshing the merge status of the repository.")
1263 merge_state = self._refresh_merge_state(
1290 merge_state = self._refresh_merge_state(
1264 pull_request, target_vcs, target_ref)
1291 pull_request, target_vcs, target_ref)
1265 else:
1292 else:
1266 possible = pull_request.\
1293 possible = pull_request.\
1267 last_merge_status == MergeFailureReason.NONE
1294 last_merge_status == MergeFailureReason.NONE
1268 merge_state = MergeResponse(
1295 merge_state = MergeResponse(
1269 possible, False, None, pull_request.last_merge_status)
1296 possible, False, None, pull_request.last_merge_status)
1270
1297
1271 return merge_state
1298 return merge_state
1272
1299
1273 def _refresh_reference(self, reference, vcs_repository):
1300 def _refresh_reference(self, reference, vcs_repository):
1274 if reference.type in ('branch', 'book'):
1301 if reference.type in ('branch', 'book'):
1275 name_or_id = reference.name
1302 name_or_id = reference.name
1276 else:
1303 else:
1277 name_or_id = reference.commit_id
1304 name_or_id = reference.commit_id
1278 refreshed_commit = vcs_repository.get_commit(name_or_id)
1305 refreshed_commit = vcs_repository.get_commit(name_or_id)
1279 refreshed_reference = Reference(
1306 refreshed_reference = Reference(
1280 reference.type, reference.name, refreshed_commit.raw_id)
1307 reference.type, reference.name, refreshed_commit.raw_id)
1281 return refreshed_reference
1308 return refreshed_reference
1282
1309
1283 def _needs_merge_state_refresh(self, pull_request, target_reference):
1310 def _needs_merge_state_refresh(self, pull_request, target_reference):
1284 return not(
1311 return not(
1285 pull_request.revisions and
1312 pull_request.revisions and
1286 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1313 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1287 target_reference.commit_id == pull_request._last_merge_target_rev)
1314 target_reference.commit_id == pull_request._last_merge_target_rev)
1288
1315
1289 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1316 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1290 workspace_id = self._workspace_id(pull_request)
1317 workspace_id = self._workspace_id(pull_request)
1291 source_vcs = pull_request.source_repo.scm_instance()
1318 source_vcs = pull_request.source_repo.scm_instance()
1292 use_rebase = self._use_rebase_for_merging(pull_request)
1319 use_rebase = self._use_rebase_for_merging(pull_request)
1293 close_branch = self._close_branch_before_merging(pull_request)
1320 close_branch = self._close_branch_before_merging(pull_request)
1294 merge_state = target_vcs.merge(
1321 merge_state = target_vcs.merge(
1295 target_reference, source_vcs, pull_request.source_ref_parts,
1322 target_reference, source_vcs, pull_request.source_ref_parts,
1296 workspace_id, dry_run=True, use_rebase=use_rebase,
1323 workspace_id, dry_run=True, use_rebase=use_rebase,
1297 close_branch=close_branch)
1324 close_branch=close_branch)
1298
1325
1299 # Do not store the response if there was an unknown error.
1326 # Do not store the response if there was an unknown error.
1300 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1327 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1301 pull_request._last_merge_source_rev = \
1328 pull_request._last_merge_source_rev = \
1302 pull_request.source_ref_parts.commit_id
1329 pull_request.source_ref_parts.commit_id
1303 pull_request._last_merge_target_rev = target_reference.commit_id
1330 pull_request._last_merge_target_rev = target_reference.commit_id
1304 pull_request.last_merge_status = merge_state.failure_reason
1331 pull_request.last_merge_status = merge_state.failure_reason
1305 pull_request.shadow_merge_ref = merge_state.merge_ref
1332 pull_request.shadow_merge_ref = merge_state.merge_ref
1306 Session().add(pull_request)
1333 Session().add(pull_request)
1307 Session().commit()
1334 Session().commit()
1308
1335
1309 return merge_state
1336 return merge_state
1310
1337
1311 def _workspace_id(self, pull_request):
1338 def _workspace_id(self, pull_request):
1312 workspace_id = 'pr-%s' % pull_request.pull_request_id
1339 workspace_id = 'pr-%s' % pull_request.pull_request_id
1313 return workspace_id
1340 return workspace_id
1314
1341
1315 def merge_status_message(self, status_code):
1342 def merge_status_message(self, status_code):
1316 """
1343 """
1317 Return a human friendly error message for the given merge status code.
1344 Return a human friendly error message for the given merge status code.
1318 """
1345 """
1319 return self.MERGE_STATUS_MESSAGES[status_code]
1346 return self.MERGE_STATUS_MESSAGES[status_code]
1320
1347
1321 def generate_repo_data(self, repo, commit_id=None, branch=None,
1348 def generate_repo_data(self, repo, commit_id=None, branch=None,
1322 bookmark=None, translator=None):
1349 bookmark=None, translator=None):
1323 from rhodecode.model.repo import RepoModel
1350 from rhodecode.model.repo import RepoModel
1324
1351
1325 all_refs, selected_ref = \
1352 all_refs, selected_ref = \
1326 self._get_repo_pullrequest_sources(
1353 self._get_repo_pullrequest_sources(
1327 repo.scm_instance(), commit_id=commit_id,
1354 repo.scm_instance(), commit_id=commit_id,
1328 branch=branch, bookmark=bookmark, translator=translator)
1355 branch=branch, bookmark=bookmark, translator=translator)
1329
1356
1330 refs_select2 = []
1357 refs_select2 = []
1331 for element in all_refs:
1358 for element in all_refs:
1332 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1359 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1333 refs_select2.append({'text': element[1], 'children': children})
1360 refs_select2.append({'text': element[1], 'children': children})
1334
1361
1335 return {
1362 return {
1336 'user': {
1363 'user': {
1337 'user_id': repo.user.user_id,
1364 'user_id': repo.user.user_id,
1338 'username': repo.user.username,
1365 'username': repo.user.username,
1339 'firstname': repo.user.first_name,
1366 'firstname': repo.user.first_name,
1340 'lastname': repo.user.last_name,
1367 'lastname': repo.user.last_name,
1341 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1368 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1342 },
1369 },
1343 'name': repo.repo_name,
1370 'name': repo.repo_name,
1344 'link': RepoModel().get_url(repo),
1371 'link': RepoModel().get_url(repo),
1345 'description': h.chop_at_smart(repo.description_safe, '\n'),
1372 'description': h.chop_at_smart(repo.description_safe, '\n'),
1346 'refs': {
1373 'refs': {
1347 'all_refs': all_refs,
1374 'all_refs': all_refs,
1348 'selected_ref': selected_ref,
1375 'selected_ref': selected_ref,
1349 'select2_refs': refs_select2
1376 'select2_refs': refs_select2
1350 }
1377 }
1351 }
1378 }
1352
1379
1353 def generate_pullrequest_title(self, source, source_ref, target):
1380 def generate_pullrequest_title(self, source, source_ref, target):
1354 return u'{source}#{at_ref} to {target}'.format(
1381 return u'{source}#{at_ref} to {target}'.format(
1355 source=source,
1382 source=source,
1356 at_ref=source_ref,
1383 at_ref=source_ref,
1357 target=target,
1384 target=target,
1358 )
1385 )
1359
1386
1360 def _cleanup_merge_workspace(self, pull_request):
1387 def _cleanup_merge_workspace(self, pull_request):
1361 # Merging related cleanup
1388 # Merging related cleanup
1362 target_scm = pull_request.target_repo.scm_instance()
1389 target_scm = pull_request.target_repo.scm_instance()
1363 workspace_id = 'pr-%s' % pull_request.pull_request_id
1390 workspace_id = 'pr-%s' % pull_request.pull_request_id
1364
1391
1365 try:
1392 try:
1366 target_scm.cleanup_merge_workspace(workspace_id)
1393 target_scm.cleanup_merge_workspace(workspace_id)
1367 except NotImplementedError:
1394 except NotImplementedError:
1368 pass
1395 pass
1369
1396
1370 def _get_repo_pullrequest_sources(
1397 def _get_repo_pullrequest_sources(
1371 self, repo, commit_id=None, branch=None, bookmark=None,
1398 self, repo, commit_id=None, branch=None, bookmark=None,
1372 translator=None):
1399 translator=None):
1373 """
1400 """
1374 Return a structure with repo's interesting commits, suitable for
1401 Return a structure with repo's interesting commits, suitable for
1375 the selectors in pullrequest controller
1402 the selectors in pullrequest controller
1376
1403
1377 :param commit_id: a commit that must be in the list somehow
1404 :param commit_id: a commit that must be in the list somehow
1378 and selected by default
1405 and selected by default
1379 :param branch: a branch that must be in the list and selected
1406 :param branch: a branch that must be in the list and selected
1380 by default - even if closed
1407 by default - even if closed
1381 :param bookmark: a bookmark that must be in the list and selected
1408 :param bookmark: a bookmark that must be in the list and selected
1382 """
1409 """
1383 _ = translator or get_current_request().translate
1410 _ = translator or get_current_request().translate
1384
1411
1385 commit_id = safe_str(commit_id) if commit_id else None
1412 commit_id = safe_str(commit_id) if commit_id else None
1386 branch = safe_str(branch) if branch else None
1413 branch = safe_str(branch) if branch else None
1387 bookmark = safe_str(bookmark) if bookmark else None
1414 bookmark = safe_str(bookmark) if bookmark else None
1388
1415
1389 selected = None
1416 selected = None
1390
1417
1391 # order matters: first source that has commit_id in it will be selected
1418 # order matters: first source that has commit_id in it will be selected
1392 sources = []
1419 sources = []
1393 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1420 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1394 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1421 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1395
1422
1396 if commit_id:
1423 if commit_id:
1397 ref_commit = (h.short_id(commit_id), commit_id)
1424 ref_commit = (h.short_id(commit_id), commit_id)
1398 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1425 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1399
1426
1400 sources.append(
1427 sources.append(
1401 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1428 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1402 )
1429 )
1403
1430
1404 groups = []
1431 groups = []
1405 for group_key, ref_list, group_name, match in sources:
1432 for group_key, ref_list, group_name, match in sources:
1406 group_refs = []
1433 group_refs = []
1407 for ref_name, ref_id in ref_list:
1434 for ref_name, ref_id in ref_list:
1408 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1435 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1409 group_refs.append((ref_key, ref_name))
1436 group_refs.append((ref_key, ref_name))
1410
1437
1411 if not selected:
1438 if not selected:
1412 if set([commit_id, match]) & set([ref_id, ref_name]):
1439 if set([commit_id, match]) & set([ref_id, ref_name]):
1413 selected = ref_key
1440 selected = ref_key
1414
1441
1415 if group_refs:
1442 if group_refs:
1416 groups.append((group_refs, group_name))
1443 groups.append((group_refs, group_name))
1417
1444
1418 if not selected:
1445 if not selected:
1419 ref = commit_id or branch or bookmark
1446 ref = commit_id or branch or bookmark
1420 if ref:
1447 if ref:
1421 raise CommitDoesNotExistError(
1448 raise CommitDoesNotExistError(
1422 'No commit refs could be found matching: %s' % ref)
1449 'No commit refs could be found matching: %s' % ref)
1423 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1450 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1424 selected = 'branch:%s:%s' % (
1451 selected = 'branch:%s:%s' % (
1425 repo.DEFAULT_BRANCH_NAME,
1452 repo.DEFAULT_BRANCH_NAME,
1426 repo.branches[repo.DEFAULT_BRANCH_NAME]
1453 repo.branches[repo.DEFAULT_BRANCH_NAME]
1427 )
1454 )
1428 elif repo.commit_ids:
1455 elif repo.commit_ids:
1429 # make the user select in this case
1456 # make the user select in this case
1430 selected = None
1457 selected = None
1431 else:
1458 else:
1432 raise EmptyRepositoryError()
1459 raise EmptyRepositoryError()
1433 return groups, selected
1460 return groups, selected
1434
1461
1435 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1462 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1436 return self._get_diff_from_pr_or_version(
1463 return self._get_diff_from_pr_or_version(
1437 source_repo, source_ref_id, target_ref_id, context=context)
1464 source_repo, source_ref_id, target_ref_id, context=context)
1438
1465
1439 def _get_diff_from_pr_or_version(
1466 def _get_diff_from_pr_or_version(
1440 self, source_repo, source_ref_id, target_ref_id, context):
1467 self, source_repo, source_ref_id, target_ref_id, context):
1441 target_commit = source_repo.get_commit(
1468 target_commit = source_repo.get_commit(
1442 commit_id=safe_str(target_ref_id))
1469 commit_id=safe_str(target_ref_id))
1443 source_commit = source_repo.get_commit(
1470 source_commit = source_repo.get_commit(
1444 commit_id=safe_str(source_ref_id))
1471 commit_id=safe_str(source_ref_id))
1445 if isinstance(source_repo, Repository):
1472 if isinstance(source_repo, Repository):
1446 vcs_repo = source_repo.scm_instance()
1473 vcs_repo = source_repo.scm_instance()
1447 else:
1474 else:
1448 vcs_repo = source_repo
1475 vcs_repo = source_repo
1449
1476
1450 # TODO: johbo: In the context of an update, we cannot reach
1477 # TODO: johbo: In the context of an update, we cannot reach
1451 # the old commit anymore with our normal mechanisms. It needs
1478 # the old commit anymore with our normal mechanisms. It needs
1452 # some sort of special support in the vcs layer to avoid this
1479 # some sort of special support in the vcs layer to avoid this
1453 # workaround.
1480 # workaround.
1454 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1481 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1455 vcs_repo.alias == 'git'):
1482 vcs_repo.alias == 'git'):
1456 source_commit.raw_id = safe_str(source_ref_id)
1483 source_commit.raw_id = safe_str(source_ref_id)
1457
1484
1458 log.debug('calculating diff between '
1485 log.debug('calculating diff between '
1459 'source_ref:%s and target_ref:%s for repo `%s`',
1486 'source_ref:%s and target_ref:%s for repo `%s`',
1460 target_ref_id, source_ref_id,
1487 target_ref_id, source_ref_id,
1461 safe_unicode(vcs_repo.path))
1488 safe_unicode(vcs_repo.path))
1462
1489
1463 vcs_diff = vcs_repo.get_diff(
1490 vcs_diff = vcs_repo.get_diff(
1464 commit1=target_commit, commit2=source_commit, context=context)
1491 commit1=target_commit, commit2=source_commit, context=context)
1465 return vcs_diff
1492 return vcs_diff
1466
1493
1467 def _is_merge_enabled(self, pull_request):
1494 def _is_merge_enabled(self, pull_request):
1468 return self._get_general_setting(
1495 return self._get_general_setting(
1469 pull_request, 'rhodecode_pr_merge_enabled')
1496 pull_request, 'rhodecode_pr_merge_enabled')
1470
1497
1471 def _use_rebase_for_merging(self, pull_request):
1498 def _use_rebase_for_merging(self, pull_request):
1472 repo_type = pull_request.target_repo.repo_type
1499 repo_type = pull_request.target_repo.repo_type
1473 if repo_type == 'hg':
1500 if repo_type == 'hg':
1474 return self._get_general_setting(
1501 return self._get_general_setting(
1475 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1502 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1476 elif repo_type == 'git':
1503 elif repo_type == 'git':
1477 return self._get_general_setting(
1504 return self._get_general_setting(
1478 pull_request, 'rhodecode_git_use_rebase_for_merging')
1505 pull_request, 'rhodecode_git_use_rebase_for_merging')
1479
1506
1480 return False
1507 return False
1481
1508
1482 def _close_branch_before_merging(self, pull_request):
1509 def _close_branch_before_merging(self, pull_request):
1483 repo_type = pull_request.target_repo.repo_type
1510 repo_type = pull_request.target_repo.repo_type
1484 if repo_type == 'hg':
1511 if repo_type == 'hg':
1485 return self._get_general_setting(
1512 return self._get_general_setting(
1486 pull_request, 'rhodecode_hg_close_branch_before_merging')
1513 pull_request, 'rhodecode_hg_close_branch_before_merging')
1487 elif repo_type == 'git':
1514 elif repo_type == 'git':
1488 return self._get_general_setting(
1515 return self._get_general_setting(
1489 pull_request, 'rhodecode_git_close_branch_before_merging')
1516 pull_request, 'rhodecode_git_close_branch_before_merging')
1490
1517
1491 return False
1518 return False
1492
1519
1493 def _get_general_setting(self, pull_request, settings_key, default=False):
1520 def _get_general_setting(self, pull_request, settings_key, default=False):
1494 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1521 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1495 settings = settings_model.get_general_settings()
1522 settings = settings_model.get_general_settings()
1496 return settings.get(settings_key, default)
1523 return settings.get(settings_key, default)
1497
1524
1498 def _log_audit_action(self, action, action_data, user, pull_request):
1525 def _log_audit_action(self, action, action_data, user, pull_request):
1499 audit_logger.store(
1526 audit_logger.store(
1500 action=action,
1527 action=action,
1501 action_data=action_data,
1528 action_data=action_data,
1502 user=user,
1529 user=user,
1503 repo=pull_request.target_repo)
1530 repo=pull_request.target_repo)
1504
1531
1505 def get_reviewer_functions(self):
1532 def get_reviewer_functions(self):
1506 """
1533 """
1507 Fetches functions for validation and fetching default reviewers.
1534 Fetches functions for validation and fetching default reviewers.
1508 If available we use the EE package, else we fallback to CE
1535 If available we use the EE package, else we fallback to CE
1509 package functions
1536 package functions
1510 """
1537 """
1511 try:
1538 try:
1512 from rc_reviewers.utils import get_default_reviewers_data
1539 from rc_reviewers.utils import get_default_reviewers_data
1513 from rc_reviewers.utils import validate_default_reviewers
1540 from rc_reviewers.utils import validate_default_reviewers
1514 except ImportError:
1541 except ImportError:
1515 from rhodecode.apps.repository.utils import \
1542 from rhodecode.apps.repository.utils import \
1516 get_default_reviewers_data
1543 get_default_reviewers_data
1517 from rhodecode.apps.repository.utils import \
1544 from rhodecode.apps.repository.utils import \
1518 validate_default_reviewers
1545 validate_default_reviewers
1519
1546
1520 return get_default_reviewers_data, validate_default_reviewers
1547 return get_default_reviewers_data, validate_default_reviewers
1521
1548
1522
1549
1523 class MergeCheck(object):
1550 class MergeCheck(object):
1524 """
1551 """
1525 Perform Merge Checks and returns a check object which stores information
1552 Perform Merge Checks and returns a check object which stores information
1526 about merge errors, and merge conditions
1553 about merge errors, and merge conditions
1527 """
1554 """
1528 TODO_CHECK = 'todo'
1555 TODO_CHECK = 'todo'
1529 PERM_CHECK = 'perm'
1556 PERM_CHECK = 'perm'
1530 REVIEW_CHECK = 'review'
1557 REVIEW_CHECK = 'review'
1531 MERGE_CHECK = 'merge'
1558 MERGE_CHECK = 'merge'
1532
1559
1533 def __init__(self):
1560 def __init__(self):
1534 self.review_status = None
1561 self.review_status = None
1535 self.merge_possible = None
1562 self.merge_possible = None
1536 self.merge_msg = ''
1563 self.merge_msg = ''
1537 self.failed = None
1564 self.failed = None
1538 self.errors = []
1565 self.errors = []
1539 self.error_details = OrderedDict()
1566 self.error_details = OrderedDict()
1540
1567
1541 def push_error(self, error_type, message, error_key, details):
1568 def push_error(self, error_type, message, error_key, details):
1542 self.failed = True
1569 self.failed = True
1543 self.errors.append([error_type, message])
1570 self.errors.append([error_type, message])
1544 self.error_details[error_key] = dict(
1571 self.error_details[error_key] = dict(
1545 details=details,
1572 details=details,
1546 error_type=error_type,
1573 error_type=error_type,
1547 message=message
1574 message=message
1548 )
1575 )
1549
1576
1550 @classmethod
1577 @classmethod
1551 def validate(cls, pull_request, user, translator, fail_early=False):
1578 def validate(cls, pull_request, user, translator, fail_early=False):
1552 _ = translator
1579 _ = translator
1553 merge_check = cls()
1580 merge_check = cls()
1554
1581
1555 # permissions to merge
1582 # permissions to merge
1556 user_allowed_to_merge = PullRequestModel().check_user_merge(
1583 user_allowed_to_merge = PullRequestModel().check_user_merge(
1557 pull_request, user)
1584 pull_request, user)
1558 if not user_allowed_to_merge:
1585 if not user_allowed_to_merge:
1559 log.debug("MergeCheck: cannot merge, approval is pending.")
1586 log.debug("MergeCheck: cannot merge, approval is pending.")
1560
1587
1561 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1588 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1562 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1589 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1563 if fail_early:
1590 if fail_early:
1564 return merge_check
1591 return merge_check
1565
1592
1566 # review status, must be always present
1593 # review status, must be always present
1567 review_status = pull_request.calculated_review_status()
1594 review_status = pull_request.calculated_review_status()
1568 merge_check.review_status = review_status
1595 merge_check.review_status = review_status
1569
1596
1570 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1597 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1571 if not status_approved:
1598 if not status_approved:
1572 log.debug("MergeCheck: cannot merge, approval is pending.")
1599 log.debug("MergeCheck: cannot merge, approval is pending.")
1573
1600
1574 msg = _('Pull request reviewer approval is pending.')
1601 msg = _('Pull request reviewer approval is pending.')
1575
1602
1576 merge_check.push_error(
1603 merge_check.push_error(
1577 'warning', msg, cls.REVIEW_CHECK, review_status)
1604 'warning', msg, cls.REVIEW_CHECK, review_status)
1578
1605
1579 if fail_early:
1606 if fail_early:
1580 return merge_check
1607 return merge_check
1581
1608
1582 # left over TODOs
1609 # left over TODOs
1583 todos = CommentsModel().get_unresolved_todos(pull_request)
1610 todos = CommentsModel().get_unresolved_todos(pull_request)
1584 if todos:
1611 if todos:
1585 log.debug("MergeCheck: cannot merge, {} "
1612 log.debug("MergeCheck: cannot merge, {} "
1586 "unresolved todos left.".format(len(todos)))
1613 "unresolved todos left.".format(len(todos)))
1587
1614
1588 if len(todos) == 1:
1615 if len(todos) == 1:
1589 msg = _('Cannot merge, {} TODO still not resolved.').format(
1616 msg = _('Cannot merge, {} TODO still not resolved.').format(
1590 len(todos))
1617 len(todos))
1591 else:
1618 else:
1592 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1619 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1593 len(todos))
1620 len(todos))
1594
1621
1595 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1622 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1596
1623
1597 if fail_early:
1624 if fail_early:
1598 return merge_check
1625 return merge_check
1599
1626
1600 # merge possible
1627 # merge possible
1601 merge_status, msg = PullRequestModel().merge_status(
1628 merge_status, msg = PullRequestModel().merge_status(
1602 pull_request, translator=translator)
1629 pull_request, translator=translator)
1603 merge_check.merge_possible = merge_status
1630 merge_check.merge_possible = merge_status
1604 merge_check.merge_msg = msg
1631 merge_check.merge_msg = msg
1605 if not merge_status:
1632 if not merge_status:
1606 log.debug(
1633 log.debug(
1607 "MergeCheck: cannot merge, pull request merge not possible.")
1634 "MergeCheck: cannot merge, pull request merge not possible.")
1608 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1635 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1609
1636
1610 if fail_early:
1637 if fail_early:
1611 return merge_check
1638 return merge_check
1612
1639
1613 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1640 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1614 return merge_check
1641 return merge_check
1615
1642
1616 @classmethod
1643 @classmethod
1617 def get_merge_conditions(cls, pull_request, translator):
1644 def get_merge_conditions(cls, pull_request, translator):
1618 _ = translator
1645 _ = translator
1619 merge_details = {}
1646 merge_details = {}
1620
1647
1621 model = PullRequestModel()
1648 model = PullRequestModel()
1622 use_rebase = model._use_rebase_for_merging(pull_request)
1649 use_rebase = model._use_rebase_for_merging(pull_request)
1623
1650
1624 if use_rebase:
1651 if use_rebase:
1625 merge_details['merge_strategy'] = dict(
1652 merge_details['merge_strategy'] = dict(
1626 details={},
1653 details={},
1627 message=_('Merge strategy: rebase')
1654 message=_('Merge strategy: rebase')
1628 )
1655 )
1629 else:
1656 else:
1630 merge_details['merge_strategy'] = dict(
1657 merge_details['merge_strategy'] = dict(
1631 details={},
1658 details={},
1632 message=_('Merge strategy: explicit merge commit')
1659 message=_('Merge strategy: explicit merge commit')
1633 )
1660 )
1634
1661
1635 close_branch = model._close_branch_before_merging(pull_request)
1662 close_branch = model._close_branch_before_merging(pull_request)
1636 if close_branch:
1663 if close_branch:
1637 repo_type = pull_request.target_repo.repo_type
1664 repo_type = pull_request.target_repo.repo_type
1638 if repo_type == 'hg':
1665 if repo_type == 'hg':
1639 close_msg = _('Source branch will be closed after merge.')
1666 close_msg = _('Source branch will be closed after merge.')
1640 elif repo_type == 'git':
1667 elif repo_type == 'git':
1641 close_msg = _('Source branch will be deleted after merge.')
1668 close_msg = _('Source branch will be deleted after merge.')
1642
1669
1643 merge_details['close_branch'] = dict(
1670 merge_details['close_branch'] = dict(
1644 details={},
1671 details={},
1645 message=close_msg
1672 message=close_msg
1646 )
1673 )
1647
1674
1648 return merge_details
1675 return merge_details
1649
1676
1650 ChangeTuple = collections.namedtuple(
1677 ChangeTuple = collections.namedtuple(
1651 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1678 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1652
1679
1653 FileChangeTuple = collections.namedtuple(
1680 FileChangeTuple = collections.namedtuple(
1654 'FileChangeTuple', ['added', 'modified', 'removed'])
1681 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,913 +1,914 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 users model for RhodeCode
22 users model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import datetime
27 import datetime
28 import ipaddress
28 import ipaddress
29
29
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.exc import DatabaseError
31 from sqlalchemy.exc import DatabaseError
32
32
33 from rhodecode import events
33 from rhodecode import events
34 from rhodecode.lib.user_log_filter import user_log_filter
34 from rhodecode.lib.user_log_filter import user_log_filter
35 from rhodecode.lib.utils2 import (
35 from rhodecode.lib.utils2 import (
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 AttributeDict, str2bool)
37 AttributeDict, str2bool)
38 from rhodecode.lib.exceptions import (
38 from rhodecode.lib.exceptions import (
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
41 from rhodecode.lib.caching_query import FromCache
41 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.model import BaseModel
42 from rhodecode.model import BaseModel
43 from rhodecode.model.auth_token import AuthTokenModel
43 from rhodecode.model.auth_token import AuthTokenModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 UserEmailMap, UserIpMap, UserLog)
46 UserEmailMap, UserIpMap, UserLog)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.repo_group import RepoGroupModel
48 from rhodecode.model.repo_group import RepoGroupModel
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class UserModel(BaseModel):
54 class UserModel(BaseModel):
55 cls = User
55 cls = User
56
56
57 def get(self, user_id, cache=False):
57 def get(self, user_id, cache=False):
58 user = self.sa.query(User)
58 user = self.sa.query(User)
59 if cache:
59 if cache:
60 user = user.options(
60 user = user.options(
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 return user.get(user_id)
62 return user.get(user_id)
63
63
64 def get_user(self, user):
64 def get_user(self, user):
65 return self._get_user(user)
65 return self._get_user(user)
66
66
67 def _serialize_user(self, user):
67 def _serialize_user(self, user):
68 import rhodecode.lib.helpers as h
68 import rhodecode.lib.helpers as h
69
69
70 return {
70 return {
71 'id': user.user_id,
71 'id': user.user_id,
72 'first_name': user.first_name,
72 'first_name': user.first_name,
73 'last_name': user.last_name,
73 'last_name': user.last_name,
74 'username': user.username,
74 'username': user.username,
75 'email': user.email,
75 'email': user.email,
76 'icon_link': h.gravatar_url(user.email, 30),
76 'icon_link': h.gravatar_url(user.email, 30),
77 'profile_link': h.link_to_user(user),
77 'value_display': h.escape(h.person(user)),
78 'value_display': h.escape(h.person(user)),
78 'value': user.username,
79 'value': user.username,
79 'value_type': 'user',
80 'value_type': 'user',
80 'active': user.active,
81 'active': user.active,
81 }
82 }
82
83
83 def get_users(self, name_contains=None, limit=20, only_active=True):
84 def get_users(self, name_contains=None, limit=20, only_active=True):
84
85
85 query = self.sa.query(User)
86 query = self.sa.query(User)
86 if only_active:
87 if only_active:
87 query = query.filter(User.active == true())
88 query = query.filter(User.active == true())
88
89
89 if name_contains:
90 if name_contains:
90 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
91 query = query.filter(
92 query = query.filter(
92 or_(
93 or_(
93 User.name.ilike(ilike_expression),
94 User.name.ilike(ilike_expression),
94 User.lastname.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
95 User.username.ilike(ilike_expression)
96 User.username.ilike(ilike_expression)
96 )
97 )
97 )
98 )
98 query = query.limit(limit)
99 query = query.limit(limit)
99 users = query.all()
100 users = query.all()
100
101
101 _users = [
102 _users = [
102 self._serialize_user(user) for user in users
103 self._serialize_user(user) for user in users
103 ]
104 ]
104 return _users
105 return _users
105
106
106 def get_by_username(self, username, cache=False, case_insensitive=False):
107 def get_by_username(self, username, cache=False, case_insensitive=False):
107
108
108 if case_insensitive:
109 if case_insensitive:
109 user = self.sa.query(User).filter(User.username.ilike(username))
110 user = self.sa.query(User).filter(User.username.ilike(username))
110 else:
111 else:
111 user = self.sa.query(User)\
112 user = self.sa.query(User)\
112 .filter(User.username == username)
113 .filter(User.username == username)
113 if cache:
114 if cache:
114 name_key = _hash_key(username)
115 name_key = _hash_key(username)
115 user = user.options(
116 user = user.options(
116 FromCache("sql_cache_short", "get_user_%s" % name_key))
117 FromCache("sql_cache_short", "get_user_%s" % name_key))
117 return user.scalar()
118 return user.scalar()
118
119
119 def get_by_email(self, email, cache=False, case_insensitive=False):
120 def get_by_email(self, email, cache=False, case_insensitive=False):
120 return User.get_by_email(email, case_insensitive, cache)
121 return User.get_by_email(email, case_insensitive, cache)
121
122
122 def get_by_auth_token(self, auth_token, cache=False):
123 def get_by_auth_token(self, auth_token, cache=False):
123 return User.get_by_auth_token(auth_token, cache)
124 return User.get_by_auth_token(auth_token, cache)
124
125
125 def get_active_user_count(self, cache=False):
126 def get_active_user_count(self, cache=False):
126 qry = User.query().filter(
127 qry = User.query().filter(
127 User.active == true()).filter(
128 User.active == true()).filter(
128 User.username != User.DEFAULT_USER)
129 User.username != User.DEFAULT_USER)
129 if cache:
130 if cache:
130 qry = qry.options(
131 qry = qry.options(
131 FromCache("sql_cache_short", "get_active_users"))
132 FromCache("sql_cache_short", "get_active_users"))
132 return qry.count()
133 return qry.count()
133
134
134 def create(self, form_data, cur_user=None):
135 def create(self, form_data, cur_user=None):
135 if not cur_user:
136 if not cur_user:
136 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
137
138
138 user_data = {
139 user_data = {
139 'username': form_data['username'],
140 'username': form_data['username'],
140 'password': form_data['password'],
141 'password': form_data['password'],
141 'email': form_data['email'],
142 'email': form_data['email'],
142 'firstname': form_data['firstname'],
143 'firstname': form_data['firstname'],
143 'lastname': form_data['lastname'],
144 'lastname': form_data['lastname'],
144 'active': form_data['active'],
145 'active': form_data['active'],
145 'extern_type': form_data['extern_type'],
146 'extern_type': form_data['extern_type'],
146 'extern_name': form_data['extern_name'],
147 'extern_name': form_data['extern_name'],
147 'admin': False,
148 'admin': False,
148 'cur_user': cur_user
149 'cur_user': cur_user
149 }
150 }
150
151
151 if 'create_repo_group' in form_data:
152 if 'create_repo_group' in form_data:
152 user_data['create_repo_group'] = str2bool(
153 user_data['create_repo_group'] = str2bool(
153 form_data.get('create_repo_group'))
154 form_data.get('create_repo_group'))
154
155
155 try:
156 try:
156 if form_data.get('password_change'):
157 if form_data.get('password_change'):
157 user_data['force_password_change'] = True
158 user_data['force_password_change'] = True
158 return UserModel().create_or_update(**user_data)
159 return UserModel().create_or_update(**user_data)
159 except Exception:
160 except Exception:
160 log.error(traceback.format_exc())
161 log.error(traceback.format_exc())
161 raise
162 raise
162
163
163 def update_user(self, user, skip_attrs=None, **kwargs):
164 def update_user(self, user, skip_attrs=None, **kwargs):
164 from rhodecode.lib.auth import get_crypt_password
165 from rhodecode.lib.auth import get_crypt_password
165
166
166 user = self._get_user(user)
167 user = self._get_user(user)
167 if user.username == User.DEFAULT_USER:
168 if user.username == User.DEFAULT_USER:
168 raise DefaultUserException(
169 raise DefaultUserException(
169 "You can't edit this user (`%(username)s`) since it's "
170 "You can't edit this user (`%(username)s`) since it's "
170 "crucial for entire application" % {
171 "crucial for entire application" % {
171 'username': user.username})
172 'username': user.username})
172
173
173 # first store only defaults
174 # first store only defaults
174 user_attrs = {
175 user_attrs = {
175 'updating_user_id': user.user_id,
176 'updating_user_id': user.user_id,
176 'username': user.username,
177 'username': user.username,
177 'password': user.password,
178 'password': user.password,
178 'email': user.email,
179 'email': user.email,
179 'firstname': user.name,
180 'firstname': user.name,
180 'lastname': user.lastname,
181 'lastname': user.lastname,
181 'active': user.active,
182 'active': user.active,
182 'admin': user.admin,
183 'admin': user.admin,
183 'extern_name': user.extern_name,
184 'extern_name': user.extern_name,
184 'extern_type': user.extern_type,
185 'extern_type': user.extern_type,
185 'language': user.user_data.get('language')
186 'language': user.user_data.get('language')
186 }
187 }
187
188
188 # in case there's new_password, that comes from form, use it to
189 # in case there's new_password, that comes from form, use it to
189 # store password
190 # store password
190 if kwargs.get('new_password'):
191 if kwargs.get('new_password'):
191 kwargs['password'] = kwargs['new_password']
192 kwargs['password'] = kwargs['new_password']
192
193
193 # cleanups, my_account password change form
194 # cleanups, my_account password change form
194 kwargs.pop('current_password', None)
195 kwargs.pop('current_password', None)
195 kwargs.pop('new_password', None)
196 kwargs.pop('new_password', None)
196
197
197 # cleanups, user edit password change form
198 # cleanups, user edit password change form
198 kwargs.pop('password_confirmation', None)
199 kwargs.pop('password_confirmation', None)
199 kwargs.pop('password_change', None)
200 kwargs.pop('password_change', None)
200
201
201 # create repo group on user creation
202 # create repo group on user creation
202 kwargs.pop('create_repo_group', None)
203 kwargs.pop('create_repo_group', None)
203
204
204 # legacy forms send name, which is the firstname
205 # legacy forms send name, which is the firstname
205 firstname = kwargs.pop('name', None)
206 firstname = kwargs.pop('name', None)
206 if firstname:
207 if firstname:
207 kwargs['firstname'] = firstname
208 kwargs['firstname'] = firstname
208
209
209 for k, v in kwargs.items():
210 for k, v in kwargs.items():
210 # skip if we don't want to update this
211 # skip if we don't want to update this
211 if skip_attrs and k in skip_attrs:
212 if skip_attrs and k in skip_attrs:
212 continue
213 continue
213
214
214 user_attrs[k] = v
215 user_attrs[k] = v
215
216
216 try:
217 try:
217 return self.create_or_update(**user_attrs)
218 return self.create_or_update(**user_attrs)
218 except Exception:
219 except Exception:
219 log.error(traceback.format_exc())
220 log.error(traceback.format_exc())
220 raise
221 raise
221
222
222 def create_or_update(
223 def create_or_update(
223 self, username, password, email, firstname='', lastname='',
224 self, username, password, email, firstname='', lastname='',
224 active=True, admin=False, extern_type=None, extern_name=None,
225 active=True, admin=False, extern_type=None, extern_name=None,
225 cur_user=None, plugin=None, force_password_change=False,
226 cur_user=None, plugin=None, force_password_change=False,
226 allow_to_create_user=True, create_repo_group=None,
227 allow_to_create_user=True, create_repo_group=None,
227 updating_user_id=None, language=None, strict_creation_check=True):
228 updating_user_id=None, language=None, strict_creation_check=True):
228 """
229 """
229 Creates a new instance if not found, or updates current one
230 Creates a new instance if not found, or updates current one
230
231
231 :param username:
232 :param username:
232 :param password:
233 :param password:
233 :param email:
234 :param email:
234 :param firstname:
235 :param firstname:
235 :param lastname:
236 :param lastname:
236 :param active:
237 :param active:
237 :param admin:
238 :param admin:
238 :param extern_type:
239 :param extern_type:
239 :param extern_name:
240 :param extern_name:
240 :param cur_user:
241 :param cur_user:
241 :param plugin: optional plugin this method was called from
242 :param plugin: optional plugin this method was called from
242 :param force_password_change: toggles new or existing user flag
243 :param force_password_change: toggles new or existing user flag
243 for password change
244 for password change
244 :param allow_to_create_user: Defines if the method can actually create
245 :param allow_to_create_user: Defines if the method can actually create
245 new users
246 new users
246 :param create_repo_group: Defines if the method should also
247 :param create_repo_group: Defines if the method should also
247 create an repo group with user name, and owner
248 create an repo group with user name, and owner
248 :param updating_user_id: if we set it up this is the user we want to
249 :param updating_user_id: if we set it up this is the user we want to
249 update this allows to editing username.
250 update this allows to editing username.
250 :param language: language of user from interface.
251 :param language: language of user from interface.
251
252
252 :returns: new User object with injected `is_new_user` attribute.
253 :returns: new User object with injected `is_new_user` attribute.
253 """
254 """
254
255
255 if not cur_user:
256 if not cur_user:
256 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
257 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
257
258
258 from rhodecode.lib.auth import (
259 from rhodecode.lib.auth import (
259 get_crypt_password, check_password, generate_auth_token)
260 get_crypt_password, check_password, generate_auth_token)
260 from rhodecode.lib.hooks_base import (
261 from rhodecode.lib.hooks_base import (
261 log_create_user, check_allowed_create_user)
262 log_create_user, check_allowed_create_user)
262
263
263 def _password_change(new_user, password):
264 def _password_change(new_user, password):
264 old_password = new_user.password or ''
265 old_password = new_user.password or ''
265 # empty password
266 # empty password
266 if not old_password:
267 if not old_password:
267 return False
268 return False
268
269
269 # password check is only needed for RhodeCode internal auth calls
270 # password check is only needed for RhodeCode internal auth calls
270 # in case it's a plugin we don't care
271 # in case it's a plugin we don't care
271 if not plugin:
272 if not plugin:
272
273
273 # first check if we gave crypted password back, and if it
274 # first check if we gave crypted password back, and if it
274 # matches it's not password change
275 # matches it's not password change
275 if new_user.password == password:
276 if new_user.password == password:
276 return False
277 return False
277
278
278 password_match = check_password(password, old_password)
279 password_match = check_password(password, old_password)
279 if not password_match:
280 if not password_match:
280 return True
281 return True
281
282
282 return False
283 return False
283
284
284 # read settings on default personal repo group creation
285 # read settings on default personal repo group creation
285 if create_repo_group is None:
286 if create_repo_group is None:
286 default_create_repo_group = RepoGroupModel()\
287 default_create_repo_group = RepoGroupModel()\
287 .get_default_create_personal_repo_group()
288 .get_default_create_personal_repo_group()
288 create_repo_group = default_create_repo_group
289 create_repo_group = default_create_repo_group
289
290
290 user_data = {
291 user_data = {
291 'username': username,
292 'username': username,
292 'password': password,
293 'password': password,
293 'email': email,
294 'email': email,
294 'firstname': firstname,
295 'firstname': firstname,
295 'lastname': lastname,
296 'lastname': lastname,
296 'active': active,
297 'active': active,
297 'admin': admin
298 'admin': admin
298 }
299 }
299
300
300 if updating_user_id:
301 if updating_user_id:
301 log.debug('Checking for existing account in RhodeCode '
302 log.debug('Checking for existing account in RhodeCode '
302 'database with user_id `%s` ' % (updating_user_id,))
303 'database with user_id `%s` ' % (updating_user_id,))
303 user = User.get(updating_user_id)
304 user = User.get(updating_user_id)
304 else:
305 else:
305 log.debug('Checking for existing account in RhodeCode '
306 log.debug('Checking for existing account in RhodeCode '
306 'database with username `%s` ' % (username,))
307 'database with username `%s` ' % (username,))
307 user = User.get_by_username(username, case_insensitive=True)
308 user = User.get_by_username(username, case_insensitive=True)
308
309
309 if user is None:
310 if user is None:
310 # we check internal flag if this method is actually allowed to
311 # we check internal flag if this method is actually allowed to
311 # create new user
312 # create new user
312 if not allow_to_create_user:
313 if not allow_to_create_user:
313 msg = ('Method wants to create new user, but it is not '
314 msg = ('Method wants to create new user, but it is not '
314 'allowed to do so')
315 'allowed to do so')
315 log.warning(msg)
316 log.warning(msg)
316 raise NotAllowedToCreateUserError(msg)
317 raise NotAllowedToCreateUserError(msg)
317
318
318 log.debug('Creating new user %s', username)
319 log.debug('Creating new user %s', username)
319
320
320 # only if we create user that is active
321 # only if we create user that is active
321 new_active_user = active
322 new_active_user = active
322 if new_active_user and strict_creation_check:
323 if new_active_user and strict_creation_check:
323 # raises UserCreationError if it's not allowed for any reason to
324 # raises UserCreationError if it's not allowed for any reason to
324 # create new active user, this also executes pre-create hooks
325 # create new active user, this also executes pre-create hooks
325 check_allowed_create_user(user_data, cur_user, strict_check=True)
326 check_allowed_create_user(user_data, cur_user, strict_check=True)
326 events.trigger(events.UserPreCreate(user_data))
327 events.trigger(events.UserPreCreate(user_data))
327 new_user = User()
328 new_user = User()
328 edit = False
329 edit = False
329 else:
330 else:
330 log.debug('updating user %s', username)
331 log.debug('updating user %s', username)
331 events.trigger(events.UserPreUpdate(user, user_data))
332 events.trigger(events.UserPreUpdate(user, user_data))
332 new_user = user
333 new_user = user
333 edit = True
334 edit = True
334
335
335 # we're not allowed to edit default user
336 # we're not allowed to edit default user
336 if user.username == User.DEFAULT_USER:
337 if user.username == User.DEFAULT_USER:
337 raise DefaultUserException(
338 raise DefaultUserException(
338 "You can't edit this user (`%(username)s`) since it's "
339 "You can't edit this user (`%(username)s`) since it's "
339 "crucial for entire application"
340 "crucial for entire application"
340 % {'username': user.username})
341 % {'username': user.username})
341
342
342 # inject special attribute that will tell us if User is new or old
343 # inject special attribute that will tell us if User is new or old
343 new_user.is_new_user = not edit
344 new_user.is_new_user = not edit
344 # for users that didn's specify auth type, we use RhodeCode built in
345 # for users that didn's specify auth type, we use RhodeCode built in
345 from rhodecode.authentication.plugins import auth_rhodecode
346 from rhodecode.authentication.plugins import auth_rhodecode
346 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
347 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
347 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
348 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
348
349
349 try:
350 try:
350 new_user.username = username
351 new_user.username = username
351 new_user.admin = admin
352 new_user.admin = admin
352 new_user.email = email
353 new_user.email = email
353 new_user.active = active
354 new_user.active = active
354 new_user.extern_name = safe_unicode(extern_name)
355 new_user.extern_name = safe_unicode(extern_name)
355 new_user.extern_type = safe_unicode(extern_type)
356 new_user.extern_type = safe_unicode(extern_type)
356 new_user.name = firstname
357 new_user.name = firstname
357 new_user.lastname = lastname
358 new_user.lastname = lastname
358
359
359 # set password only if creating an user or password is changed
360 # set password only if creating an user or password is changed
360 if not edit or _password_change(new_user, password):
361 if not edit or _password_change(new_user, password):
361 reason = 'new password' if edit else 'new user'
362 reason = 'new password' if edit else 'new user'
362 log.debug('Updating password reason=>%s', reason)
363 log.debug('Updating password reason=>%s', reason)
363 new_user.password = get_crypt_password(password) if password else None
364 new_user.password = get_crypt_password(password) if password else None
364
365
365 if force_password_change:
366 if force_password_change:
366 new_user.update_userdata(force_password_change=True)
367 new_user.update_userdata(force_password_change=True)
367 if language:
368 if language:
368 new_user.update_userdata(language=language)
369 new_user.update_userdata(language=language)
369 new_user.update_userdata(notification_status=True)
370 new_user.update_userdata(notification_status=True)
370
371
371 self.sa.add(new_user)
372 self.sa.add(new_user)
372
373
373 if not edit and create_repo_group:
374 if not edit and create_repo_group:
374 RepoGroupModel().create_personal_repo_group(
375 RepoGroupModel().create_personal_repo_group(
375 new_user, commit_early=False)
376 new_user, commit_early=False)
376
377
377 if not edit:
378 if not edit:
378 # add the RSS token
379 # add the RSS token
379 AuthTokenModel().create(username,
380 AuthTokenModel().create(username,
380 description=u'Generated feed token',
381 description=u'Generated feed token',
381 role=AuthTokenModel.cls.ROLE_FEED)
382 role=AuthTokenModel.cls.ROLE_FEED)
382 kwargs = new_user.get_dict()
383 kwargs = new_user.get_dict()
383 # backward compat, require api_keys present
384 # backward compat, require api_keys present
384 kwargs['api_keys'] = kwargs['auth_tokens']
385 kwargs['api_keys'] = kwargs['auth_tokens']
385 log_create_user(created_by=cur_user, **kwargs)
386 log_create_user(created_by=cur_user, **kwargs)
386 events.trigger(events.UserPostCreate(user_data))
387 events.trigger(events.UserPostCreate(user_data))
387 return new_user
388 return new_user
388 except (DatabaseError,):
389 except (DatabaseError,):
389 log.error(traceback.format_exc())
390 log.error(traceback.format_exc())
390 raise
391 raise
391
392
392 def create_registration(self, form_data):
393 def create_registration(self, form_data):
393 from rhodecode.model.notification import NotificationModel
394 from rhodecode.model.notification import NotificationModel
394 from rhodecode.model.notification import EmailNotificationModel
395 from rhodecode.model.notification import EmailNotificationModel
395
396
396 try:
397 try:
397 form_data['admin'] = False
398 form_data['admin'] = False
398 form_data['extern_name'] = 'rhodecode'
399 form_data['extern_name'] = 'rhodecode'
399 form_data['extern_type'] = 'rhodecode'
400 form_data['extern_type'] = 'rhodecode'
400 new_user = self.create(form_data)
401 new_user = self.create(form_data)
401
402
402 self.sa.add(new_user)
403 self.sa.add(new_user)
403 self.sa.flush()
404 self.sa.flush()
404
405
405 user_data = new_user.get_dict()
406 user_data = new_user.get_dict()
406 kwargs = {
407 kwargs = {
407 # use SQLALCHEMY safe dump of user data
408 # use SQLALCHEMY safe dump of user data
408 'user': AttributeDict(user_data),
409 'user': AttributeDict(user_data),
409 'date': datetime.datetime.now()
410 'date': datetime.datetime.now()
410 }
411 }
411 notification_type = EmailNotificationModel.TYPE_REGISTRATION
412 notification_type = EmailNotificationModel.TYPE_REGISTRATION
412 # pre-generate the subject for notification itself
413 # pre-generate the subject for notification itself
413 (subject,
414 (subject,
414 _h, _e, # we don't care about those
415 _h, _e, # we don't care about those
415 body_plaintext) = EmailNotificationModel().render_email(
416 body_plaintext) = EmailNotificationModel().render_email(
416 notification_type, **kwargs)
417 notification_type, **kwargs)
417
418
418 # create notification objects, and emails
419 # create notification objects, and emails
419 NotificationModel().create(
420 NotificationModel().create(
420 created_by=new_user,
421 created_by=new_user,
421 notification_subject=subject,
422 notification_subject=subject,
422 notification_body=body_plaintext,
423 notification_body=body_plaintext,
423 notification_type=notification_type,
424 notification_type=notification_type,
424 recipients=None, # all admins
425 recipients=None, # all admins
425 email_kwargs=kwargs,
426 email_kwargs=kwargs,
426 )
427 )
427
428
428 return new_user
429 return new_user
429 except Exception:
430 except Exception:
430 log.error(traceback.format_exc())
431 log.error(traceback.format_exc())
431 raise
432 raise
432
433
433 def _handle_user_repos(self, username, repositories, handle_mode=None):
434 def _handle_user_repos(self, username, repositories, handle_mode=None):
434 _superadmin = self.cls.get_first_super_admin()
435 _superadmin = self.cls.get_first_super_admin()
435 left_overs = True
436 left_overs = True
436
437
437 from rhodecode.model.repo import RepoModel
438 from rhodecode.model.repo import RepoModel
438
439
439 if handle_mode == 'detach':
440 if handle_mode == 'detach':
440 for obj in repositories:
441 for obj in repositories:
441 obj.user = _superadmin
442 obj.user = _superadmin
442 # set description we know why we super admin now owns
443 # set description we know why we super admin now owns
443 # additional repositories that were orphaned !
444 # additional repositories that were orphaned !
444 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
445 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
445 self.sa.add(obj)
446 self.sa.add(obj)
446 left_overs = False
447 left_overs = False
447 elif handle_mode == 'delete':
448 elif handle_mode == 'delete':
448 for obj in repositories:
449 for obj in repositories:
449 RepoModel().delete(obj, forks='detach')
450 RepoModel().delete(obj, forks='detach')
450 left_overs = False
451 left_overs = False
451
452
452 # if nothing is done we have left overs left
453 # if nothing is done we have left overs left
453 return left_overs
454 return left_overs
454
455
455 def _handle_user_repo_groups(self, username, repository_groups,
456 def _handle_user_repo_groups(self, username, repository_groups,
456 handle_mode=None):
457 handle_mode=None):
457 _superadmin = self.cls.get_first_super_admin()
458 _superadmin = self.cls.get_first_super_admin()
458 left_overs = True
459 left_overs = True
459
460
460 from rhodecode.model.repo_group import RepoGroupModel
461 from rhodecode.model.repo_group import RepoGroupModel
461
462
462 if handle_mode == 'detach':
463 if handle_mode == 'detach':
463 for r in repository_groups:
464 for r in repository_groups:
464 r.user = _superadmin
465 r.user = _superadmin
465 # set description we know why we super admin now owns
466 # set description we know why we super admin now owns
466 # additional repositories that were orphaned !
467 # additional repositories that were orphaned !
467 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
468 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
468 self.sa.add(r)
469 self.sa.add(r)
469 left_overs = False
470 left_overs = False
470 elif handle_mode == 'delete':
471 elif handle_mode == 'delete':
471 for r in repository_groups:
472 for r in repository_groups:
472 RepoGroupModel().delete(r)
473 RepoGroupModel().delete(r)
473 left_overs = False
474 left_overs = False
474
475
475 # if nothing is done we have left overs left
476 # if nothing is done we have left overs left
476 return left_overs
477 return left_overs
477
478
478 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
479 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
479 _superadmin = self.cls.get_first_super_admin()
480 _superadmin = self.cls.get_first_super_admin()
480 left_overs = True
481 left_overs = True
481
482
482 from rhodecode.model.user_group import UserGroupModel
483 from rhodecode.model.user_group import UserGroupModel
483
484
484 if handle_mode == 'detach':
485 if handle_mode == 'detach':
485 for r in user_groups:
486 for r in user_groups:
486 for user_user_group_to_perm in r.user_user_group_to_perm:
487 for user_user_group_to_perm in r.user_user_group_to_perm:
487 if user_user_group_to_perm.user.username == username:
488 if user_user_group_to_perm.user.username == username:
488 user_user_group_to_perm.user = _superadmin
489 user_user_group_to_perm.user = _superadmin
489 r.user = _superadmin
490 r.user = _superadmin
490 # set description we know why we super admin now owns
491 # set description we know why we super admin now owns
491 # additional repositories that were orphaned !
492 # additional repositories that were orphaned !
492 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
493 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
493 self.sa.add(r)
494 self.sa.add(r)
494 left_overs = False
495 left_overs = False
495 elif handle_mode == 'delete':
496 elif handle_mode == 'delete':
496 for r in user_groups:
497 for r in user_groups:
497 UserGroupModel().delete(r)
498 UserGroupModel().delete(r)
498 left_overs = False
499 left_overs = False
499
500
500 # if nothing is done we have left overs left
501 # if nothing is done we have left overs left
501 return left_overs
502 return left_overs
502
503
503 def delete(self, user, cur_user=None, handle_repos=None,
504 def delete(self, user, cur_user=None, handle_repos=None,
504 handle_repo_groups=None, handle_user_groups=None):
505 handle_repo_groups=None, handle_user_groups=None):
505 if not cur_user:
506 if not cur_user:
506 cur_user = getattr(
507 cur_user = getattr(
507 get_current_rhodecode_user(), 'username', None)
508 get_current_rhodecode_user(), 'username', None)
508 user = self._get_user(user)
509 user = self._get_user(user)
509
510
510 try:
511 try:
511 if user.username == User.DEFAULT_USER:
512 if user.username == User.DEFAULT_USER:
512 raise DefaultUserException(
513 raise DefaultUserException(
513 u"You can't remove this user since it's"
514 u"You can't remove this user since it's"
514 u" crucial for entire application")
515 u" crucial for entire application")
515
516
516 left_overs = self._handle_user_repos(
517 left_overs = self._handle_user_repos(
517 user.username, user.repositories, handle_repos)
518 user.username, user.repositories, handle_repos)
518 if left_overs and user.repositories:
519 if left_overs and user.repositories:
519 repos = [x.repo_name for x in user.repositories]
520 repos = [x.repo_name for x in user.repositories]
520 raise UserOwnsReposException(
521 raise UserOwnsReposException(
521 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
522 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
522 u'removed. Switch owners or remove those repositories:%(list_repos)s'
523 u'removed. Switch owners or remove those repositories:%(list_repos)s'
523 % {'username': user.username, 'len_repos': len(repos),
524 % {'username': user.username, 'len_repos': len(repos),
524 'list_repos': ', '.join(repos)})
525 'list_repos': ', '.join(repos)})
525
526
526 left_overs = self._handle_user_repo_groups(
527 left_overs = self._handle_user_repo_groups(
527 user.username, user.repository_groups, handle_repo_groups)
528 user.username, user.repository_groups, handle_repo_groups)
528 if left_overs and user.repository_groups:
529 if left_overs and user.repository_groups:
529 repo_groups = [x.group_name for x in user.repository_groups]
530 repo_groups = [x.group_name for x in user.repository_groups]
530 raise UserOwnsRepoGroupsException(
531 raise UserOwnsRepoGroupsException(
531 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
532 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
532 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
533 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
533 % {'username': user.username, 'len_repo_groups': len(repo_groups),
534 % {'username': user.username, 'len_repo_groups': len(repo_groups),
534 'list_repo_groups': ', '.join(repo_groups)})
535 'list_repo_groups': ', '.join(repo_groups)})
535
536
536 left_overs = self._handle_user_user_groups(
537 left_overs = self._handle_user_user_groups(
537 user.username, user.user_groups, handle_user_groups)
538 user.username, user.user_groups, handle_user_groups)
538 if left_overs and user.user_groups:
539 if left_overs and user.user_groups:
539 user_groups = [x.users_group_name for x in user.user_groups]
540 user_groups = [x.users_group_name for x in user.user_groups]
540 raise UserOwnsUserGroupsException(
541 raise UserOwnsUserGroupsException(
541 u'user "%s" still owns %s user groups and cannot be '
542 u'user "%s" still owns %s user groups and cannot be '
542 u'removed. Switch owners or remove those user groups:%s'
543 u'removed. Switch owners or remove those user groups:%s'
543 % (user.username, len(user_groups), ', '.join(user_groups)))
544 % (user.username, len(user_groups), ', '.join(user_groups)))
544
545
545 # we might change the user data with detach/delete, make sure
546 # we might change the user data with detach/delete, make sure
546 # the object is marked as expired before actually deleting !
547 # the object is marked as expired before actually deleting !
547 self.sa.expire(user)
548 self.sa.expire(user)
548 self.sa.delete(user)
549 self.sa.delete(user)
549 from rhodecode.lib.hooks_base import log_delete_user
550 from rhodecode.lib.hooks_base import log_delete_user
550 log_delete_user(deleted_by=cur_user, **user.get_dict())
551 log_delete_user(deleted_by=cur_user, **user.get_dict())
551 except Exception:
552 except Exception:
552 log.error(traceback.format_exc())
553 log.error(traceback.format_exc())
553 raise
554 raise
554
555
555 def reset_password_link(self, data, pwd_reset_url):
556 def reset_password_link(self, data, pwd_reset_url):
556 from rhodecode.lib.celerylib import tasks, run_task
557 from rhodecode.lib.celerylib import tasks, run_task
557 from rhodecode.model.notification import EmailNotificationModel
558 from rhodecode.model.notification import EmailNotificationModel
558 user_email = data['email']
559 user_email = data['email']
559 try:
560 try:
560 user = User.get_by_email(user_email)
561 user = User.get_by_email(user_email)
561 if user:
562 if user:
562 log.debug('password reset user found %s', user)
563 log.debug('password reset user found %s', user)
563
564
564 email_kwargs = {
565 email_kwargs = {
565 'password_reset_url': pwd_reset_url,
566 'password_reset_url': pwd_reset_url,
566 'user': user,
567 'user': user,
567 'email': user_email,
568 'email': user_email,
568 'date': datetime.datetime.now()
569 'date': datetime.datetime.now()
569 }
570 }
570
571
571 (subject, headers, email_body,
572 (subject, headers, email_body,
572 email_body_plaintext) = EmailNotificationModel().render_email(
573 email_body_plaintext) = EmailNotificationModel().render_email(
573 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
574 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
574
575
575 recipients = [user_email]
576 recipients = [user_email]
576
577
577 action_logger_generic(
578 action_logger_generic(
578 'sending password reset email to user: {}'.format(
579 'sending password reset email to user: {}'.format(
579 user), namespace='security.password_reset')
580 user), namespace='security.password_reset')
580
581
581 run_task(tasks.send_email, recipients, subject,
582 run_task(tasks.send_email, recipients, subject,
582 email_body_plaintext, email_body)
583 email_body_plaintext, email_body)
583
584
584 else:
585 else:
585 log.debug("password reset email %s not found", user_email)
586 log.debug("password reset email %s not found", user_email)
586 except Exception:
587 except Exception:
587 log.error(traceback.format_exc())
588 log.error(traceback.format_exc())
588 return False
589 return False
589
590
590 return True
591 return True
591
592
592 def reset_password(self, data):
593 def reset_password(self, data):
593 from rhodecode.lib.celerylib import tasks, run_task
594 from rhodecode.lib.celerylib import tasks, run_task
594 from rhodecode.model.notification import EmailNotificationModel
595 from rhodecode.model.notification import EmailNotificationModel
595 from rhodecode.lib import auth
596 from rhodecode.lib import auth
596 user_email = data['email']
597 user_email = data['email']
597 pre_db = True
598 pre_db = True
598 try:
599 try:
599 user = User.get_by_email(user_email)
600 user = User.get_by_email(user_email)
600 new_passwd = auth.PasswordGenerator().gen_password(
601 new_passwd = auth.PasswordGenerator().gen_password(
601 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
602 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
602 if user:
603 if user:
603 user.password = auth.get_crypt_password(new_passwd)
604 user.password = auth.get_crypt_password(new_passwd)
604 # also force this user to reset his password !
605 # also force this user to reset his password !
605 user.update_userdata(force_password_change=True)
606 user.update_userdata(force_password_change=True)
606
607
607 Session().add(user)
608 Session().add(user)
608
609
609 # now delete the token in question
610 # now delete the token in question
610 UserApiKeys = AuthTokenModel.cls
611 UserApiKeys = AuthTokenModel.cls
611 UserApiKeys().query().filter(
612 UserApiKeys().query().filter(
612 UserApiKeys.api_key == data['token']).delete()
613 UserApiKeys.api_key == data['token']).delete()
613
614
614 Session().commit()
615 Session().commit()
615 log.info('successfully reset password for `%s`', user_email)
616 log.info('successfully reset password for `%s`', user_email)
616
617
617 if new_passwd is None:
618 if new_passwd is None:
618 raise Exception('unable to generate new password')
619 raise Exception('unable to generate new password')
619
620
620 pre_db = False
621 pre_db = False
621
622
622 email_kwargs = {
623 email_kwargs = {
623 'new_password': new_passwd,
624 'new_password': new_passwd,
624 'user': user,
625 'user': user,
625 'email': user_email,
626 'email': user_email,
626 'date': datetime.datetime.now()
627 'date': datetime.datetime.now()
627 }
628 }
628
629
629 (subject, headers, email_body,
630 (subject, headers, email_body,
630 email_body_plaintext) = EmailNotificationModel().render_email(
631 email_body_plaintext) = EmailNotificationModel().render_email(
631 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
632 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
632 **email_kwargs)
633 **email_kwargs)
633
634
634 recipients = [user_email]
635 recipients = [user_email]
635
636
636 action_logger_generic(
637 action_logger_generic(
637 'sent new password to user: {} with email: {}'.format(
638 'sent new password to user: {} with email: {}'.format(
638 user, user_email), namespace='security.password_reset')
639 user, user_email), namespace='security.password_reset')
639
640
640 run_task(tasks.send_email, recipients, subject,
641 run_task(tasks.send_email, recipients, subject,
641 email_body_plaintext, email_body)
642 email_body_plaintext, email_body)
642
643
643 except Exception:
644 except Exception:
644 log.error('Failed to update user password')
645 log.error('Failed to update user password')
645 log.error(traceback.format_exc())
646 log.error(traceback.format_exc())
646 if pre_db:
647 if pre_db:
647 # we rollback only if local db stuff fails. If it goes into
648 # we rollback only if local db stuff fails. If it goes into
648 # run_task, we're pass rollback state this wouldn't work then
649 # run_task, we're pass rollback state this wouldn't work then
649 Session().rollback()
650 Session().rollback()
650
651
651 return True
652 return True
652
653
653 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
654 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
654 """
655 """
655 Fetches auth_user by user_id,or api_key if present.
656 Fetches auth_user by user_id,or api_key if present.
656 Fills auth_user attributes with those taken from database.
657 Fills auth_user attributes with those taken from database.
657 Additionally set's is_authenitated if lookup fails
658 Additionally set's is_authenitated if lookup fails
658 present in database
659 present in database
659
660
660 :param auth_user: instance of user to set attributes
661 :param auth_user: instance of user to set attributes
661 :param user_id: user id to fetch by
662 :param user_id: user id to fetch by
662 :param api_key: api key to fetch by
663 :param api_key: api key to fetch by
663 :param username: username to fetch by
664 :param username: username to fetch by
664 """
665 """
665 if user_id is None and api_key is None and username is None:
666 if user_id is None and api_key is None and username is None:
666 raise Exception('You need to pass user_id, api_key or username')
667 raise Exception('You need to pass user_id, api_key or username')
667
668
668 log.debug(
669 log.debug(
669 'AuthUser: fill data execution based on: '
670 'AuthUser: fill data execution based on: '
670 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
671 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
671 try:
672 try:
672 dbuser = None
673 dbuser = None
673 if user_id:
674 if user_id:
674 dbuser = self.get(user_id)
675 dbuser = self.get(user_id)
675 elif api_key:
676 elif api_key:
676 dbuser = self.get_by_auth_token(api_key)
677 dbuser = self.get_by_auth_token(api_key)
677 elif username:
678 elif username:
678 dbuser = self.get_by_username(username)
679 dbuser = self.get_by_username(username)
679
680
680 if not dbuser:
681 if not dbuser:
681 log.warning(
682 log.warning(
682 'Unable to lookup user by id:%s api_key:%s username:%s',
683 'Unable to lookup user by id:%s api_key:%s username:%s',
683 user_id, api_key, username)
684 user_id, api_key, username)
684 return False
685 return False
685 if not dbuser.active:
686 if not dbuser.active:
686 log.debug('User `%s:%s` is inactive, skipping fill data',
687 log.debug('User `%s:%s` is inactive, skipping fill data',
687 username, user_id)
688 username, user_id)
688 return False
689 return False
689
690
690 log.debug('AuthUser: filling found user:%s data', dbuser)
691 log.debug('AuthUser: filling found user:%s data', dbuser)
691 user_data = dbuser.get_dict()
692 user_data = dbuser.get_dict()
692
693
693 user_data.update({
694 user_data.update({
694 # set explicit the safe escaped values
695 # set explicit the safe escaped values
695 'first_name': dbuser.first_name,
696 'first_name': dbuser.first_name,
696 'last_name': dbuser.last_name,
697 'last_name': dbuser.last_name,
697 })
698 })
698
699
699 for k, v in user_data.items():
700 for k, v in user_data.items():
700 # properties of auth user we dont update
701 # properties of auth user we dont update
701 if k not in ['auth_tokens', 'permissions']:
702 if k not in ['auth_tokens', 'permissions']:
702 setattr(auth_user, k, v)
703 setattr(auth_user, k, v)
703
704
704 except Exception:
705 except Exception:
705 log.error(traceback.format_exc())
706 log.error(traceback.format_exc())
706 auth_user.is_authenticated = False
707 auth_user.is_authenticated = False
707 return False
708 return False
708
709
709 return True
710 return True
710
711
711 def has_perm(self, user, perm):
712 def has_perm(self, user, perm):
712 perm = self._get_perm(perm)
713 perm = self._get_perm(perm)
713 user = self._get_user(user)
714 user = self._get_user(user)
714
715
715 return UserToPerm.query().filter(UserToPerm.user == user)\
716 return UserToPerm.query().filter(UserToPerm.user == user)\
716 .filter(UserToPerm.permission == perm).scalar() is not None
717 .filter(UserToPerm.permission == perm).scalar() is not None
717
718
718 def grant_perm(self, user, perm):
719 def grant_perm(self, user, perm):
719 """
720 """
720 Grant user global permissions
721 Grant user global permissions
721
722
722 :param user:
723 :param user:
723 :param perm:
724 :param perm:
724 """
725 """
725 user = self._get_user(user)
726 user = self._get_user(user)
726 perm = self._get_perm(perm)
727 perm = self._get_perm(perm)
727 # if this permission is already granted skip it
728 # if this permission is already granted skip it
728 _perm = UserToPerm.query()\
729 _perm = UserToPerm.query()\
729 .filter(UserToPerm.user == user)\
730 .filter(UserToPerm.user == user)\
730 .filter(UserToPerm.permission == perm)\
731 .filter(UserToPerm.permission == perm)\
731 .scalar()
732 .scalar()
732 if _perm:
733 if _perm:
733 return
734 return
734 new = UserToPerm()
735 new = UserToPerm()
735 new.user = user
736 new.user = user
736 new.permission = perm
737 new.permission = perm
737 self.sa.add(new)
738 self.sa.add(new)
738 return new
739 return new
739
740
740 def revoke_perm(self, user, perm):
741 def revoke_perm(self, user, perm):
741 """
742 """
742 Revoke users global permissions
743 Revoke users global permissions
743
744
744 :param user:
745 :param user:
745 :param perm:
746 :param perm:
746 """
747 """
747 user = self._get_user(user)
748 user = self._get_user(user)
748 perm = self._get_perm(perm)
749 perm = self._get_perm(perm)
749
750
750 obj = UserToPerm.query()\
751 obj = UserToPerm.query()\
751 .filter(UserToPerm.user == user)\
752 .filter(UserToPerm.user == user)\
752 .filter(UserToPerm.permission == perm)\
753 .filter(UserToPerm.permission == perm)\
753 .scalar()
754 .scalar()
754 if obj:
755 if obj:
755 self.sa.delete(obj)
756 self.sa.delete(obj)
756
757
757 def add_extra_email(self, user, email):
758 def add_extra_email(self, user, email):
758 """
759 """
759 Adds email address to UserEmailMap
760 Adds email address to UserEmailMap
760
761
761 :param user:
762 :param user:
762 :param email:
763 :param email:
763 """
764 """
764
765
765 user = self._get_user(user)
766 user = self._get_user(user)
766
767
767 obj = UserEmailMap()
768 obj = UserEmailMap()
768 obj.user = user
769 obj.user = user
769 obj.email = email
770 obj.email = email
770 self.sa.add(obj)
771 self.sa.add(obj)
771 return obj
772 return obj
772
773
773 def delete_extra_email(self, user, email_id):
774 def delete_extra_email(self, user, email_id):
774 """
775 """
775 Removes email address from UserEmailMap
776 Removes email address from UserEmailMap
776
777
777 :param user:
778 :param user:
778 :param email_id:
779 :param email_id:
779 """
780 """
780 user = self._get_user(user)
781 user = self._get_user(user)
781 obj = UserEmailMap.query().get(email_id)
782 obj = UserEmailMap.query().get(email_id)
782 if obj and obj.user_id == user.user_id:
783 if obj and obj.user_id == user.user_id:
783 self.sa.delete(obj)
784 self.sa.delete(obj)
784
785
785 def parse_ip_range(self, ip_range):
786 def parse_ip_range(self, ip_range):
786 ip_list = []
787 ip_list = []
787
788
788 def make_unique(value):
789 def make_unique(value):
789 seen = []
790 seen = []
790 return [c for c in value if not (c in seen or seen.append(c))]
791 return [c for c in value if not (c in seen or seen.append(c))]
791
792
792 # firsts split by commas
793 # firsts split by commas
793 for ip_range in ip_range.split(','):
794 for ip_range in ip_range.split(','):
794 if not ip_range:
795 if not ip_range:
795 continue
796 continue
796 ip_range = ip_range.strip()
797 ip_range = ip_range.strip()
797 if '-' in ip_range:
798 if '-' in ip_range:
798 start_ip, end_ip = ip_range.split('-', 1)
799 start_ip, end_ip = ip_range.split('-', 1)
799 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
800 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
800 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
801 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
801 parsed_ip_range = []
802 parsed_ip_range = []
802
803
803 for index in xrange(int(start_ip), int(end_ip) + 1):
804 for index in xrange(int(start_ip), int(end_ip) + 1):
804 new_ip = ipaddress.ip_address(index)
805 new_ip = ipaddress.ip_address(index)
805 parsed_ip_range.append(str(new_ip))
806 parsed_ip_range.append(str(new_ip))
806 ip_list.extend(parsed_ip_range)
807 ip_list.extend(parsed_ip_range)
807 else:
808 else:
808 ip_list.append(ip_range)
809 ip_list.append(ip_range)
809
810
810 return make_unique(ip_list)
811 return make_unique(ip_list)
811
812
812 def add_extra_ip(self, user, ip, description=None):
813 def add_extra_ip(self, user, ip, description=None):
813 """
814 """
814 Adds ip address to UserIpMap
815 Adds ip address to UserIpMap
815
816
816 :param user:
817 :param user:
817 :param ip:
818 :param ip:
818 """
819 """
819
820
820 user = self._get_user(user)
821 user = self._get_user(user)
821 obj = UserIpMap()
822 obj = UserIpMap()
822 obj.user = user
823 obj.user = user
823 obj.ip_addr = ip
824 obj.ip_addr = ip
824 obj.description = description
825 obj.description = description
825 self.sa.add(obj)
826 self.sa.add(obj)
826 return obj
827 return obj
827
828
828 def delete_extra_ip(self, user, ip_id):
829 def delete_extra_ip(self, user, ip_id):
829 """
830 """
830 Removes ip address from UserIpMap
831 Removes ip address from UserIpMap
831
832
832 :param user:
833 :param user:
833 :param ip_id:
834 :param ip_id:
834 """
835 """
835 user = self._get_user(user)
836 user = self._get_user(user)
836 obj = UserIpMap.query().get(ip_id)
837 obj = UserIpMap.query().get(ip_id)
837 if obj and obj.user_id == user.user_id:
838 if obj and obj.user_id == user.user_id:
838 self.sa.delete(obj)
839 self.sa.delete(obj)
839
840
840 def get_accounts_in_creation_order(self, current_user=None):
841 def get_accounts_in_creation_order(self, current_user=None):
841 """
842 """
842 Get accounts in order of creation for deactivation for license limits
843 Get accounts in order of creation for deactivation for license limits
843
844
844 pick currently logged in user, and append to the list in position 0
845 pick currently logged in user, and append to the list in position 0
845 pick all super-admins in order of creation date and add it to the list
846 pick all super-admins in order of creation date and add it to the list
846 pick all other accounts in order of creation and add it to the list.
847 pick all other accounts in order of creation and add it to the list.
847
848
848 Based on that list, the last accounts can be disabled as they are
849 Based on that list, the last accounts can be disabled as they are
849 created at the end and don't include any of the super admins as well
850 created at the end and don't include any of the super admins as well
850 as the current user.
851 as the current user.
851
852
852 :param current_user: optionally current user running this operation
853 :param current_user: optionally current user running this operation
853 """
854 """
854
855
855 if not current_user:
856 if not current_user:
856 current_user = get_current_rhodecode_user()
857 current_user = get_current_rhodecode_user()
857 active_super_admins = [
858 active_super_admins = [
858 x.user_id for x in User.query()
859 x.user_id for x in User.query()
859 .filter(User.user_id != current_user.user_id)
860 .filter(User.user_id != current_user.user_id)
860 .filter(User.active == true())
861 .filter(User.active == true())
861 .filter(User.admin == true())
862 .filter(User.admin == true())
862 .order_by(User.created_on.asc())]
863 .order_by(User.created_on.asc())]
863
864
864 active_regular_users = [
865 active_regular_users = [
865 x.user_id for x in User.query()
866 x.user_id for x in User.query()
866 .filter(User.user_id != current_user.user_id)
867 .filter(User.user_id != current_user.user_id)
867 .filter(User.active == true())
868 .filter(User.active == true())
868 .filter(User.admin == false())
869 .filter(User.admin == false())
869 .order_by(User.created_on.asc())]
870 .order_by(User.created_on.asc())]
870
871
871 list_of_accounts = [current_user.user_id]
872 list_of_accounts = [current_user.user_id]
872 list_of_accounts += active_super_admins
873 list_of_accounts += active_super_admins
873 list_of_accounts += active_regular_users
874 list_of_accounts += active_regular_users
874
875
875 return list_of_accounts
876 return list_of_accounts
876
877
877 def deactivate_last_users(self, expected_users, current_user=None):
878 def deactivate_last_users(self, expected_users, current_user=None):
878 """
879 """
879 Deactivate accounts that are over the license limits.
880 Deactivate accounts that are over the license limits.
880 Algorithm of which accounts to disabled is based on the formula:
881 Algorithm of which accounts to disabled is based on the formula:
881
882
882 Get current user, then super admins in creation order, then regular
883 Get current user, then super admins in creation order, then regular
883 active users in creation order.
884 active users in creation order.
884
885
885 Using that list we mark all accounts from the end of it as inactive.
886 Using that list we mark all accounts from the end of it as inactive.
886 This way we block only latest created accounts.
887 This way we block only latest created accounts.
887
888
888 :param expected_users: list of users in special order, we deactivate
889 :param expected_users: list of users in special order, we deactivate
889 the end N ammoun of users from that list
890 the end N ammoun of users from that list
890 """
891 """
891
892
892 list_of_accounts = self.get_accounts_in_creation_order(
893 list_of_accounts = self.get_accounts_in_creation_order(
893 current_user=current_user)
894 current_user=current_user)
894
895
895 for acc_id in list_of_accounts[expected_users + 1:]:
896 for acc_id in list_of_accounts[expected_users + 1:]:
896 user = User.get(acc_id)
897 user = User.get(acc_id)
897 log.info('Deactivating account %s for license unlock', user)
898 log.info('Deactivating account %s for license unlock', user)
898 user.active = False
899 user.active = False
899 Session().add(user)
900 Session().add(user)
900 Session().commit()
901 Session().commit()
901
902
902 return
903 return
903
904
904 def get_user_log(self, user, filter_term):
905 def get_user_log(self, user, filter_term):
905 user_log = UserLog.query()\
906 user_log = UserLog.query()\
906 .filter(or_(UserLog.user_id == user.user_id,
907 .filter(or_(UserLog.user_id == user.user_id,
907 UserLog.username == user.username))\
908 UserLog.username == user.username))\
908 .options(joinedload(UserLog.user))\
909 .options(joinedload(UserLog.user))\
909 .options(joinedload(UserLog.repository))\
910 .options(joinedload(UserLog.repository))\
910 .order_by(UserLog.action_date.desc())
911 .order_by(UserLog.action_date.desc())
911
912
912 user_log = user_log_filter(user_log, filter_term)
913 user_log = user_log_filter(user_log, filter_term)
913 return user_log
914 return user_log
@@ -1,34 +1,35 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 colander
21 import colander
22 from rhodecode.model.validation_schema import validators, preparers, types
22 from rhodecode.model.validation_schema import validators, preparers, types
23
23
24
24
25 class ReviewerSchema(colander.MappingSchema):
25 class ReviewerSchema(colander.MappingSchema):
26 username = colander.SchemaNode(types.StrOrIntType())
26 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
27 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29 rules = colander.SchemaNode(colander.List(), missing=[])
29
30
30
31
31 class ReviewerListSchema(colander.SequenceSchema):
32 class ReviewerListSchema(colander.SequenceSchema):
32 reviewers = ReviewerSchema()
33 reviewers = ReviewerSchema()
33
34
34
35
@@ -1,2420 +1,2415 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'readme-box';
14 @import 'readme-box';
15 @import 'progress-bar';
15 @import 'progress-bar';
16
16
17 @import 'type';
17 @import 'type';
18 @import 'alerts';
18 @import 'alerts';
19 @import 'buttons';
19 @import 'buttons';
20 @import 'tags';
20 @import 'tags';
21 @import 'code-block';
21 @import 'code-block';
22 @import 'examples';
22 @import 'examples';
23 @import 'login';
23 @import 'login';
24 @import 'main-content';
24 @import 'main-content';
25 @import 'select2';
25 @import 'select2';
26 @import 'comments';
26 @import 'comments';
27 @import 'panels-bootstrap';
27 @import 'panels-bootstrap';
28 @import 'panels';
28 @import 'panels';
29 @import 'deform';
29 @import 'deform';
30
30
31 //--- BASE ------------------//
31 //--- BASE ------------------//
32 .noscript-error {
32 .noscript-error {
33 top: 0;
33 top: 0;
34 left: 0;
34 left: 0;
35 width: 100%;
35 width: 100%;
36 z-index: 101;
36 z-index: 101;
37 text-align: center;
37 text-align: center;
38 font-family: @text-semibold;
38 font-family: @text-semibold;
39 font-size: 120%;
39 font-size: 120%;
40 color: white;
40 color: white;
41 background-color: @alert2;
41 background-color: @alert2;
42 padding: 5px 0 5px 0;
42 padding: 5px 0 5px 0;
43 }
43 }
44
44
45 html {
45 html {
46 display: table;
46 display: table;
47 height: 100%;
47 height: 100%;
48 width: 100%;
48 width: 100%;
49 }
49 }
50
50
51 body {
51 body {
52 display: table-cell;
52 display: table-cell;
53 width: 100%;
53 width: 100%;
54 }
54 }
55
55
56 //--- LAYOUT ------------------//
56 //--- LAYOUT ------------------//
57
57
58 .hidden{
58 .hidden{
59 display: none !important;
59 display: none !important;
60 }
60 }
61
61
62 .box{
62 .box{
63 float: left;
63 float: left;
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .browser-header {
67 .browser-header {
68 clear: both;
68 clear: both;
69 }
69 }
70 .main {
70 .main {
71 clear: both;
71 clear: both;
72 padding:0 0 @pagepadding;
72 padding:0 0 @pagepadding;
73 height: auto;
73 height: auto;
74
74
75 &:after { //clearfix
75 &:after { //clearfix
76 content:"";
76 content:"";
77 clear:both;
77 clear:both;
78 width:100%;
78 width:100%;
79 display:block;
79 display:block;
80 }
80 }
81 }
81 }
82
82
83 .action-link{
83 .action-link{
84 margin-left: @padding;
84 margin-left: @padding;
85 padding-left: @padding;
85 padding-left: @padding;
86 border-left: @border-thickness solid @border-default-color;
86 border-left: @border-thickness solid @border-default-color;
87 }
87 }
88
88
89 input + .action-link, .action-link.first{
89 input + .action-link, .action-link.first{
90 border-left: none;
90 border-left: none;
91 }
91 }
92
92
93 .action-link.last{
93 .action-link.last{
94 margin-right: @padding;
94 margin-right: @padding;
95 padding-right: @padding;
95 padding-right: @padding;
96 }
96 }
97
97
98 .action-link.active,
98 .action-link.active,
99 .action-link.active a{
99 .action-link.active a{
100 color: @grey4;
100 color: @grey4;
101 }
101 }
102
102
103 .action-link.disabled {
103 .action-link.disabled {
104 color: @grey4;
104 color: @grey4;
105 cursor: inherit;
105 cursor: inherit;
106 }
106 }
107
107
108 .clipboard-action {
108 .clipboard-action {
109 cursor: pointer;
109 cursor: pointer;
110 }
110 }
111
111
112 ul.simple-list{
112 ul.simple-list{
113 list-style: none;
113 list-style: none;
114 margin: 0;
114 margin: 0;
115 padding: 0;
115 padding: 0;
116 }
116 }
117
117
118 .main-content {
118 .main-content {
119 padding-bottom: @pagepadding;
119 padding-bottom: @pagepadding;
120 }
120 }
121
121
122 .wide-mode-wrapper {
122 .wide-mode-wrapper {
123 max-width:4000px !important;
123 max-width:4000px !important;
124 }
124 }
125
125
126 .wrapper {
126 .wrapper {
127 position: relative;
127 position: relative;
128 max-width: @wrapper-maxwidth;
128 max-width: @wrapper-maxwidth;
129 margin: 0 auto;
129 margin: 0 auto;
130 }
130 }
131
131
132 #content {
132 #content {
133 clear: both;
133 clear: both;
134 padding: 0 @contentpadding;
134 padding: 0 @contentpadding;
135 }
135 }
136
136
137 .advanced-settings-fields{
137 .advanced-settings-fields{
138 input{
138 input{
139 margin-left: @textmargin;
139 margin-left: @textmargin;
140 margin-right: @padding/2;
140 margin-right: @padding/2;
141 }
141 }
142 }
142 }
143
143
144 .cs_files_title {
144 .cs_files_title {
145 margin: @pagepadding 0 0;
145 margin: @pagepadding 0 0;
146 }
146 }
147
147
148 input.inline[type="file"] {
148 input.inline[type="file"] {
149 display: inline;
149 display: inline;
150 }
150 }
151
151
152 .error_page {
152 .error_page {
153 margin: 10% auto;
153 margin: 10% auto;
154
154
155 h1 {
155 h1 {
156 color: @grey2;
156 color: @grey2;
157 }
157 }
158
158
159 .alert {
159 .alert {
160 margin: @padding 0;
160 margin: @padding 0;
161 }
161 }
162
162
163 .error-branding {
163 .error-branding {
164 font-family: @text-semibold;
164 font-family: @text-semibold;
165 color: @grey4;
165 color: @grey4;
166 }
166 }
167
167
168 .error_message {
168 .error_message {
169 font-family: @text-regular;
169 font-family: @text-regular;
170 }
170 }
171
171
172 .sidebar {
172 .sidebar {
173 min-height: 275px;
173 min-height: 275px;
174 margin: 0;
174 margin: 0;
175 padding: 0 0 @sidebarpadding @sidebarpadding;
175 padding: 0 0 @sidebarpadding @sidebarpadding;
176 border: none;
176 border: none;
177 }
177 }
178
178
179 .main-content {
179 .main-content {
180 position: relative;
180 position: relative;
181 margin: 0 @sidebarpadding @sidebarpadding;
181 margin: 0 @sidebarpadding @sidebarpadding;
182 padding: 0 0 0 @sidebarpadding;
182 padding: 0 0 0 @sidebarpadding;
183 border-left: @border-thickness solid @grey5;
183 border-left: @border-thickness solid @grey5;
184
184
185 @media (max-width:767px) {
185 @media (max-width:767px) {
186 clear: both;
186 clear: both;
187 width: 100%;
187 width: 100%;
188 margin: 0;
188 margin: 0;
189 border: none;
189 border: none;
190 }
190 }
191 }
191 }
192
192
193 .inner-column {
193 .inner-column {
194 float: left;
194 float: left;
195 width: 29.75%;
195 width: 29.75%;
196 min-height: 150px;
196 min-height: 150px;
197 margin: @sidebarpadding 2% 0 0;
197 margin: @sidebarpadding 2% 0 0;
198 padding: 0 2% 0 0;
198 padding: 0 2% 0 0;
199 border-right: @border-thickness solid @grey5;
199 border-right: @border-thickness solid @grey5;
200
200
201 @media (max-width:767px) {
201 @media (max-width:767px) {
202 clear: both;
202 clear: both;
203 width: 100%;
203 width: 100%;
204 border: none;
204 border: none;
205 }
205 }
206
206
207 ul {
207 ul {
208 padding-left: 1.25em;
208 padding-left: 1.25em;
209 }
209 }
210
210
211 &:last-child {
211 &:last-child {
212 margin: @sidebarpadding 0 0;
212 margin: @sidebarpadding 0 0;
213 border: none;
213 border: none;
214 }
214 }
215
215
216 h4 {
216 h4 {
217 margin: 0 0 @padding;
217 margin: 0 0 @padding;
218 font-family: @text-semibold;
218 font-family: @text-semibold;
219 }
219 }
220 }
220 }
221 }
221 }
222 .error-page-logo {
222 .error-page-logo {
223 width: 130px;
223 width: 130px;
224 height: 160px;
224 height: 160px;
225 }
225 }
226
226
227 // HEADER
227 // HEADER
228 .header {
228 .header {
229
229
230 // TODO: johbo: Fix login pages, so that they work without a min-height
230 // TODO: johbo: Fix login pages, so that they work without a min-height
231 // for the header and then remove the min-height. I chose a smaller value
231 // for the header and then remove the min-height. I chose a smaller value
232 // intentionally here to avoid rendering issues in the main navigation.
232 // intentionally here to avoid rendering issues in the main navigation.
233 min-height: 49px;
233 min-height: 49px;
234
234
235 position: relative;
235 position: relative;
236 vertical-align: bottom;
236 vertical-align: bottom;
237 padding: 0 @header-padding;
237 padding: 0 @header-padding;
238 background-color: @grey2;
238 background-color: @grey2;
239 color: @grey5;
239 color: @grey5;
240
240
241 .title {
241 .title {
242 overflow: visible;
242 overflow: visible;
243 }
243 }
244
244
245 &:before,
245 &:before,
246 &:after {
246 &:after {
247 content: "";
247 content: "";
248 clear: both;
248 clear: both;
249 width: 100%;
249 width: 100%;
250 }
250 }
251
251
252 // TODO: johbo: Avoids breaking "Repositories" chooser
252 // TODO: johbo: Avoids breaking "Repositories" chooser
253 .select2-container .select2-choice .select2-arrow {
253 .select2-container .select2-choice .select2-arrow {
254 display: none;
254 display: none;
255 }
255 }
256 }
256 }
257
257
258 #header-inner {
258 #header-inner {
259 &.title {
259 &.title {
260 margin: 0;
260 margin: 0;
261 }
261 }
262 &:before,
262 &:before,
263 &:after {
263 &:after {
264 content: "";
264 content: "";
265 clear: both;
265 clear: both;
266 }
266 }
267 }
267 }
268
268
269 // Gists
269 // Gists
270 #files_data {
270 #files_data {
271 clear: both; //for firefox
271 clear: both; //for firefox
272 }
272 }
273 #gistid {
273 #gistid {
274 margin-right: @padding;
274 margin-right: @padding;
275 }
275 }
276
276
277 // Global Settings Editor
277 // Global Settings Editor
278 .textarea.editor {
278 .textarea.editor {
279 float: left;
279 float: left;
280 position: relative;
280 position: relative;
281 max-width: @texteditor-width;
281 max-width: @texteditor-width;
282
282
283 select {
283 select {
284 position: absolute;
284 position: absolute;
285 top:10px;
285 top:10px;
286 right:0;
286 right:0;
287 }
287 }
288
288
289 .CodeMirror {
289 .CodeMirror {
290 margin: 0;
290 margin: 0;
291 }
291 }
292
292
293 .help-block {
293 .help-block {
294 margin: 0 0 @padding;
294 margin: 0 0 @padding;
295 padding:.5em;
295 padding:.5em;
296 background-color: @grey6;
296 background-color: @grey6;
297 &.pre-formatting {
297 &.pre-formatting {
298 white-space: pre;
298 white-space: pre;
299 }
299 }
300 }
300 }
301 }
301 }
302
302
303 ul.auth_plugins {
303 ul.auth_plugins {
304 margin: @padding 0 @padding @legend-width;
304 margin: @padding 0 @padding @legend-width;
305 padding: 0;
305 padding: 0;
306
306
307 li {
307 li {
308 margin-bottom: @padding;
308 margin-bottom: @padding;
309 line-height: 1em;
309 line-height: 1em;
310 list-style-type: none;
310 list-style-type: none;
311
311
312 .auth_buttons .btn {
312 .auth_buttons .btn {
313 margin-right: @padding;
313 margin-right: @padding;
314 }
314 }
315
315
316 &:before { content: none; }
316 &:before { content: none; }
317 }
317 }
318 }
318 }
319
319
320
320
321 // My Account PR list
321 // My Account PR list
322
322
323 #show_closed {
323 #show_closed {
324 margin: 0 1em 0 0;
324 margin: 0 1em 0 0;
325 }
325 }
326
326
327 .pullrequestlist {
327 .pullrequestlist {
328 .closed {
328 .closed {
329 background-color: @grey6;
329 background-color: @grey6;
330 }
330 }
331 .td-status {
331 .td-status {
332 padding-left: .5em;
332 padding-left: .5em;
333 }
333 }
334 .log-container .truncate {
334 .log-container .truncate {
335 height: 2.75em;
335 height: 2.75em;
336 white-space: pre-line;
336 white-space: pre-line;
337 }
337 }
338 table.rctable .user {
338 table.rctable .user {
339 padding-left: 0;
339 padding-left: 0;
340 }
340 }
341 table.rctable {
341 table.rctable {
342 td.td-description,
342 td.td-description,
343 .rc-user {
343 .rc-user {
344 min-width: auto;
344 min-width: auto;
345 }
345 }
346 }
346 }
347 }
347 }
348
348
349 // Pull Requests
349 // Pull Requests
350
350
351 .pullrequests_section_head {
351 .pullrequests_section_head {
352 display: block;
352 display: block;
353 clear: both;
353 clear: both;
354 margin: @padding 0;
354 margin: @padding 0;
355 font-family: @text-bold;
355 font-family: @text-bold;
356 }
356 }
357
357
358 .pr-origininfo, .pr-targetinfo {
358 .pr-origininfo, .pr-targetinfo {
359 position: relative;
359 position: relative;
360
360
361 .tag {
361 .tag {
362 display: inline-block;
362 display: inline-block;
363 margin: 0 1em .5em 0;
363 margin: 0 1em .5em 0;
364 }
364 }
365
365
366 .clone-url {
366 .clone-url {
367 display: inline-block;
367 display: inline-block;
368 margin: 0 0 .5em 0;
368 margin: 0 0 .5em 0;
369 padding: 0;
369 padding: 0;
370 line-height: 1.2em;
370 line-height: 1.2em;
371 }
371 }
372 }
372 }
373
373
374 .pr-mergeinfo {
374 .pr-mergeinfo {
375 min-width: 95% !important;
375 min-width: 95% !important;
376 padding: 0 !important;
376 padding: 0 !important;
377 border: 0;
377 border: 0;
378 }
378 }
379 .pr-mergeinfo-copy {
379 .pr-mergeinfo-copy {
380 padding: 0 0;
380 padding: 0 0;
381 }
381 }
382
382
383 .pr-pullinfo {
383 .pr-pullinfo {
384 min-width: 95% !important;
384 min-width: 95% !important;
385 padding: 0 !important;
385 padding: 0 !important;
386 border: 0;
386 border: 0;
387 }
387 }
388 .pr-pullinfo-copy {
388 .pr-pullinfo-copy {
389 padding: 0 0;
389 padding: 0 0;
390 }
390 }
391
391
392
392
393 #pr-title-input {
393 #pr-title-input {
394 width: 72%;
394 width: 72%;
395 font-size: 1em;
395 font-size: 1em;
396 font-family: @text-bold;
396 font-family: @text-bold;
397 margin: 0;
397 margin: 0;
398 padding: 0 0 0 @padding/4;
398 padding: 0 0 0 @padding/4;
399 line-height: 1.7em;
399 line-height: 1.7em;
400 color: @text-color;
400 color: @text-color;
401 letter-spacing: .02em;
401 letter-spacing: .02em;
402 }
402 }
403
403
404 #pullrequest_title {
404 #pullrequest_title {
405 width: 100%;
405 width: 100%;
406 box-sizing: border-box;
406 box-sizing: border-box;
407 }
407 }
408
408
409 #pr_open_message {
409 #pr_open_message {
410 border: @border-thickness solid #fff;
410 border: @border-thickness solid #fff;
411 border-radius: @border-radius;
411 border-radius: @border-radius;
412 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
412 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
413 text-align: left;
413 text-align: left;
414 overflow: hidden;
414 overflow: hidden;
415 }
415 }
416
416
417 .pr-submit-button {
417 .pr-submit-button {
418 float: right;
418 float: right;
419 margin: 0 0 0 5px;
419 margin: 0 0 0 5px;
420 }
420 }
421
421
422 .pr-spacing-container {
422 .pr-spacing-container {
423 padding: 20px;
423 padding: 20px;
424 clear: both
424 clear: both
425 }
425 }
426
426
427 #pr-description-input {
427 #pr-description-input {
428 margin-bottom: 0;
428 margin-bottom: 0;
429 }
429 }
430
430
431 .pr-description-label {
431 .pr-description-label {
432 vertical-align: top;
432 vertical-align: top;
433 }
433 }
434
434
435 .perms_section_head {
435 .perms_section_head {
436 min-width: 625px;
436 min-width: 625px;
437
437
438 h2 {
438 h2 {
439 margin-bottom: 0;
439 margin-bottom: 0;
440 }
440 }
441
441
442 .label-checkbox {
442 .label-checkbox {
443 float: left;
443 float: left;
444 }
444 }
445
445
446 &.field {
446 &.field {
447 margin: @space 0 @padding;
447 margin: @space 0 @padding;
448 }
448 }
449
449
450 &:first-child.field {
450 &:first-child.field {
451 margin-top: 0;
451 margin-top: 0;
452
452
453 .label {
453 .label {
454 margin-top: 0;
454 margin-top: 0;
455 padding-top: 0;
455 padding-top: 0;
456 }
456 }
457
457
458 .radios {
458 .radios {
459 padding-top: 0;
459 padding-top: 0;
460 }
460 }
461 }
461 }
462
462
463 .radios {
463 .radios {
464 position: relative;
464 position: relative;
465 width: 405px;
465 width: 405px;
466 }
466 }
467 }
467 }
468
468
469 //--- MODULES ------------------//
469 //--- MODULES ------------------//
470
470
471
471
472 // Server Announcement
472 // Server Announcement
473 #server-announcement {
473 #server-announcement {
474 width: 95%;
474 width: 95%;
475 margin: @padding auto;
475 margin: @padding auto;
476 padding: @padding;
476 padding: @padding;
477 border-width: 2px;
477 border-width: 2px;
478 border-style: solid;
478 border-style: solid;
479 .border-radius(2px);
479 .border-radius(2px);
480 font-family: @text-bold;
480 font-family: @text-bold;
481
481
482 &.info { border-color: @alert4; background-color: @alert4-inner; }
482 &.info { border-color: @alert4; background-color: @alert4-inner; }
483 &.warning { border-color: @alert3; background-color: @alert3-inner; }
483 &.warning { border-color: @alert3; background-color: @alert3-inner; }
484 &.error { border-color: @alert2; background-color: @alert2-inner; }
484 &.error { border-color: @alert2; background-color: @alert2-inner; }
485 &.success { border-color: @alert1; background-color: @alert1-inner; }
485 &.success { border-color: @alert1; background-color: @alert1-inner; }
486 &.neutral { border-color: @grey3; background-color: @grey6; }
486 &.neutral { border-color: @grey3; background-color: @grey6; }
487 }
487 }
488
488
489 // Fixed Sidebar Column
489 // Fixed Sidebar Column
490 .sidebar-col-wrapper {
490 .sidebar-col-wrapper {
491 padding-left: @sidebar-all-width;
491 padding-left: @sidebar-all-width;
492
492
493 .sidebar {
493 .sidebar {
494 width: @sidebar-width;
494 width: @sidebar-width;
495 margin-left: -@sidebar-all-width;
495 margin-left: -@sidebar-all-width;
496 }
496 }
497 }
497 }
498
498
499 .sidebar-col-wrapper.scw-small {
499 .sidebar-col-wrapper.scw-small {
500 padding-left: @sidebar-small-all-width;
500 padding-left: @sidebar-small-all-width;
501
501
502 .sidebar {
502 .sidebar {
503 width: @sidebar-small-width;
503 width: @sidebar-small-width;
504 margin-left: -@sidebar-small-all-width;
504 margin-left: -@sidebar-small-all-width;
505 }
505 }
506 }
506 }
507
507
508
508
509 // FOOTER
509 // FOOTER
510 #footer {
510 #footer {
511 padding: 0;
511 padding: 0;
512 text-align: center;
512 text-align: center;
513 vertical-align: middle;
513 vertical-align: middle;
514 color: @grey2;
514 color: @grey2;
515 background-color: @grey6;
515 background-color: @grey6;
516
516
517 p {
517 p {
518 margin: 0;
518 margin: 0;
519 padding: 1em;
519 padding: 1em;
520 line-height: 1em;
520 line-height: 1em;
521 }
521 }
522
522
523 .server-instance { //server instance
523 .server-instance { //server instance
524 display: none;
524 display: none;
525 }
525 }
526
526
527 .title {
527 .title {
528 float: none;
528 float: none;
529 margin: 0 auto;
529 margin: 0 auto;
530 }
530 }
531 }
531 }
532
532
533 button.close {
533 button.close {
534 padding: 0;
534 padding: 0;
535 cursor: pointer;
535 cursor: pointer;
536 background: transparent;
536 background: transparent;
537 border: 0;
537 border: 0;
538 .box-shadow(none);
538 .box-shadow(none);
539 -webkit-appearance: none;
539 -webkit-appearance: none;
540 }
540 }
541
541
542 .close {
542 .close {
543 float: right;
543 float: right;
544 font-size: 21px;
544 font-size: 21px;
545 font-family: @text-bootstrap;
545 font-family: @text-bootstrap;
546 line-height: 1em;
546 line-height: 1em;
547 font-weight: bold;
547 font-weight: bold;
548 color: @grey2;
548 color: @grey2;
549
549
550 &:hover,
550 &:hover,
551 &:focus {
551 &:focus {
552 color: @grey1;
552 color: @grey1;
553 text-decoration: none;
553 text-decoration: none;
554 cursor: pointer;
554 cursor: pointer;
555 }
555 }
556 }
556 }
557
557
558 // GRID
558 // GRID
559 .sorting,
559 .sorting,
560 .sorting_desc,
560 .sorting_desc,
561 .sorting_asc {
561 .sorting_asc {
562 cursor: pointer;
562 cursor: pointer;
563 }
563 }
564 .sorting_desc:after {
564 .sorting_desc:after {
565 content: "\00A0\25B2";
565 content: "\00A0\25B2";
566 font-size: .75em;
566 font-size: .75em;
567 }
567 }
568 .sorting_asc:after {
568 .sorting_asc:after {
569 content: "\00A0\25BC";
569 content: "\00A0\25BC";
570 font-size: .68em;
570 font-size: .68em;
571 }
571 }
572
572
573
573
574 .user_auth_tokens {
574 .user_auth_tokens {
575
575
576 &.truncate {
576 &.truncate {
577 white-space: nowrap;
577 white-space: nowrap;
578 overflow: hidden;
578 overflow: hidden;
579 text-overflow: ellipsis;
579 text-overflow: ellipsis;
580 }
580 }
581
581
582 .fields .field .input {
582 .fields .field .input {
583 margin: 0;
583 margin: 0;
584 }
584 }
585
585
586 input#description {
586 input#description {
587 width: 100px;
587 width: 100px;
588 margin: 0;
588 margin: 0;
589 }
589 }
590
590
591 .drop-menu {
591 .drop-menu {
592 // TODO: johbo: Remove this, should work out of the box when
592 // TODO: johbo: Remove this, should work out of the box when
593 // having multiple inputs inline
593 // having multiple inputs inline
594 margin: 0 0 0 5px;
594 margin: 0 0 0 5px;
595 }
595 }
596 }
596 }
597 #user_list_table {
597 #user_list_table {
598 .closed {
598 .closed {
599 background-color: @grey6;
599 background-color: @grey6;
600 }
600 }
601 }
601 }
602
602
603
603
604 input {
604 input {
605 &.disabled {
605 &.disabled {
606 opacity: .5;
606 opacity: .5;
607 }
607 }
608 }
608 }
609
609
610 // remove extra padding in firefox
610 // remove extra padding in firefox
611 input::-moz-focus-inner { border:0; padding:0 }
611 input::-moz-focus-inner { border:0; padding:0 }
612
612
613 .adjacent input {
613 .adjacent input {
614 margin-bottom: @padding;
614 margin-bottom: @padding;
615 }
615 }
616
616
617 .permissions_boxes {
617 .permissions_boxes {
618 display: block;
618 display: block;
619 }
619 }
620
620
621 //TODO: lisa: this should be in tables
621 //TODO: lisa: this should be in tables
622 .show_more_col {
622 .show_more_col {
623 width: 20px;
623 width: 20px;
624 }
624 }
625
625
626 //FORMS
626 //FORMS
627
627
628 .medium-inline,
628 .medium-inline,
629 input#description.medium-inline {
629 input#description.medium-inline {
630 display: inline;
630 display: inline;
631 width: @medium-inline-input-width;
631 width: @medium-inline-input-width;
632 min-width: 100px;
632 min-width: 100px;
633 }
633 }
634
634
635 select {
635 select {
636 //reset
636 //reset
637 -webkit-appearance: none;
637 -webkit-appearance: none;
638 -moz-appearance: none;
638 -moz-appearance: none;
639
639
640 display: inline-block;
640 display: inline-block;
641 height: 28px;
641 height: 28px;
642 width: auto;
642 width: auto;
643 margin: 0 @padding @padding 0;
643 margin: 0 @padding @padding 0;
644 padding: 0 18px 0 8px;
644 padding: 0 18px 0 8px;
645 line-height:1em;
645 line-height:1em;
646 font-size: @basefontsize;
646 font-size: @basefontsize;
647 border: @border-thickness solid @rcblue;
647 border: @border-thickness solid @rcblue;
648 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
648 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
649 color: @rcblue;
649 color: @rcblue;
650
650
651 &:after {
651 &:after {
652 content: "\00A0\25BE";
652 content: "\00A0\25BE";
653 }
653 }
654
654
655 &:focus {
655 &:focus {
656 outline: none;
656 outline: none;
657 }
657 }
658 }
658 }
659
659
660 option {
660 option {
661 &:focus {
661 &:focus {
662 outline: none;
662 outline: none;
663 }
663 }
664 }
664 }
665
665
666 input,
666 input,
667 textarea {
667 textarea {
668 padding: @input-padding;
668 padding: @input-padding;
669 border: @input-border-thickness solid @border-highlight-color;
669 border: @input-border-thickness solid @border-highlight-color;
670 .border-radius (@border-radius);
670 .border-radius (@border-radius);
671 font-family: @text-light;
671 font-family: @text-light;
672 font-size: @basefontsize;
672 font-size: @basefontsize;
673
673
674 &.input-sm {
674 &.input-sm {
675 padding: 5px;
675 padding: 5px;
676 }
676 }
677
677
678 &#description {
678 &#description {
679 min-width: @input-description-minwidth;
679 min-width: @input-description-minwidth;
680 min-height: 1em;
680 min-height: 1em;
681 padding: 10px;
681 padding: 10px;
682 }
682 }
683 }
683 }
684
684
685 .field-sm {
685 .field-sm {
686 input,
686 input,
687 textarea {
687 textarea {
688 padding: 5px;
688 padding: 5px;
689 }
689 }
690 }
690 }
691
691
692 textarea {
692 textarea {
693 display: block;
693 display: block;
694 clear: both;
694 clear: both;
695 width: 100%;
695 width: 100%;
696 min-height: 100px;
696 min-height: 100px;
697 margin-bottom: @padding;
697 margin-bottom: @padding;
698 .box-sizing(border-box);
698 .box-sizing(border-box);
699 overflow: auto;
699 overflow: auto;
700 }
700 }
701
701
702 label {
702 label {
703 font-family: @text-light;
703 font-family: @text-light;
704 }
704 }
705
705
706 // GRAVATARS
706 // GRAVATARS
707 // centers gravatar on username to the right
707 // centers gravatar on username to the right
708
708
709 .gravatar {
709 .gravatar {
710 display: inline;
710 display: inline;
711 min-width: 16px;
711 min-width: 16px;
712 min-height: 16px;
712 min-height: 16px;
713 margin: -5px 0;
713 margin: -5px 0;
714 padding: 0;
714 padding: 0;
715 line-height: 1em;
715 line-height: 1em;
716 border: 1px solid @grey4;
716 border: 1px solid @grey4;
717 box-sizing: content-box;
717 box-sizing: content-box;
718
718
719 &.gravatar-large {
719 &.gravatar-large {
720 margin: -0.5em .25em -0.5em 0;
720 margin: -0.5em .25em -0.5em 0;
721 }
721 }
722
722
723 & + .user {
723 & + .user {
724 display: inline;
724 display: inline;
725 margin: 0;
725 margin: 0;
726 padding: 0 0 0 .17em;
726 padding: 0 0 0 .17em;
727 line-height: 1em;
727 line-height: 1em;
728 }
728 }
729 }
729 }
730
730
731 .user-inline-data {
731 .user-inline-data {
732 display: inline-block;
732 display: inline-block;
733 float: left;
733 float: left;
734 padding-left: .5em;
734 padding-left: .5em;
735 line-height: 1.3em;
735 line-height: 1.3em;
736 }
736 }
737
737
738 .rc-user { // gravatar + user wrapper
738 .rc-user { // gravatar + user wrapper
739 float: left;
739 float: left;
740 position: relative;
740 position: relative;
741 min-width: 100px;
741 min-width: 100px;
742 max-width: 200px;
742 max-width: 200px;
743 min-height: (@gravatar-size + @border-thickness * 2); // account for border
743 min-height: (@gravatar-size + @border-thickness * 2); // account for border
744 display: block;
744 display: block;
745 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
745 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
746
746
747
747
748 .gravatar {
748 .gravatar {
749 display: block;
749 display: block;
750 position: absolute;
750 position: absolute;
751 top: 0;
751 top: 0;
752 left: 0;
752 left: 0;
753 min-width: @gravatar-size;
753 min-width: @gravatar-size;
754 min-height: @gravatar-size;
754 min-height: @gravatar-size;
755 margin: 0;
755 margin: 0;
756 }
756 }
757
757
758 .user {
758 .user {
759 display: block;
759 display: block;
760 max-width: 175px;
760 max-width: 175px;
761 padding-top: 2px;
761 padding-top: 2px;
762 overflow: hidden;
762 overflow: hidden;
763 text-overflow: ellipsis;
763 text-overflow: ellipsis;
764 }
764 }
765 }
765 }
766
766
767 .gist-gravatar,
767 .gist-gravatar,
768 .journal_container {
768 .journal_container {
769 .gravatar-large {
769 .gravatar-large {
770 margin: 0 .5em -10px 0;
770 margin: 0 .5em -10px 0;
771 }
771 }
772 }
772 }
773
773
774
774
775 // ADMIN SETTINGS
775 // ADMIN SETTINGS
776
776
777 // Tag Patterns
777 // Tag Patterns
778 .tag_patterns {
778 .tag_patterns {
779 .tag_input {
779 .tag_input {
780 margin-bottom: @padding;
780 margin-bottom: @padding;
781 }
781 }
782 }
782 }
783
783
784 .locked_input {
784 .locked_input {
785 position: relative;
785 position: relative;
786
786
787 input {
787 input {
788 display: inline;
788 display: inline;
789 margin: 3px 5px 0px 0px;
789 margin: 3px 5px 0px 0px;
790 }
790 }
791
791
792 br {
792 br {
793 display: none;
793 display: none;
794 }
794 }
795
795
796 .error-message {
796 .error-message {
797 float: left;
797 float: left;
798 width: 100%;
798 width: 100%;
799 }
799 }
800
800
801 .lock_input_button {
801 .lock_input_button {
802 display: inline;
802 display: inline;
803 }
803 }
804
804
805 .help-block {
805 .help-block {
806 clear: both;
806 clear: both;
807 }
807 }
808 }
808 }
809
809
810 // Notifications
810 // Notifications
811
811
812 .notifications_buttons {
812 .notifications_buttons {
813 margin: 0 0 @space 0;
813 margin: 0 0 @space 0;
814 padding: 0;
814 padding: 0;
815
815
816 .btn {
816 .btn {
817 display: inline-block;
817 display: inline-block;
818 }
818 }
819 }
819 }
820
820
821 .notification-list {
821 .notification-list {
822
822
823 div {
823 div {
824 display: inline-block;
824 display: inline-block;
825 vertical-align: middle;
825 vertical-align: middle;
826 }
826 }
827
827
828 .container {
828 .container {
829 display: block;
829 display: block;
830 margin: 0 0 @padding 0;
830 margin: 0 0 @padding 0;
831 }
831 }
832
832
833 .delete-notifications {
833 .delete-notifications {
834 margin-left: @padding;
834 margin-left: @padding;
835 text-align: right;
835 text-align: right;
836 cursor: pointer;
836 cursor: pointer;
837 }
837 }
838
838
839 .read-notifications {
839 .read-notifications {
840 margin-left: @padding/2;
840 margin-left: @padding/2;
841 text-align: right;
841 text-align: right;
842 width: 35px;
842 width: 35px;
843 cursor: pointer;
843 cursor: pointer;
844 }
844 }
845
845
846 .icon-minus-sign {
846 .icon-minus-sign {
847 color: @alert2;
847 color: @alert2;
848 }
848 }
849
849
850 .icon-ok-sign {
850 .icon-ok-sign {
851 color: @alert1;
851 color: @alert1;
852 }
852 }
853 }
853 }
854
854
855 .user_settings {
855 .user_settings {
856 float: left;
856 float: left;
857 clear: both;
857 clear: both;
858 display: block;
858 display: block;
859 width: 100%;
859 width: 100%;
860
860
861 .gravatar_box {
861 .gravatar_box {
862 margin-bottom: @padding;
862 margin-bottom: @padding;
863
863
864 &:after {
864 &:after {
865 content: " ";
865 content: " ";
866 clear: both;
866 clear: both;
867 width: 100%;
867 width: 100%;
868 }
868 }
869 }
869 }
870
870
871 .fields .field {
871 .fields .field {
872 clear: both;
872 clear: both;
873 }
873 }
874 }
874 }
875
875
876 .advanced_settings {
876 .advanced_settings {
877 margin-bottom: @space;
877 margin-bottom: @space;
878
878
879 .help-block {
879 .help-block {
880 margin-left: 0;
880 margin-left: 0;
881 }
881 }
882
882
883 button + .help-block {
883 button + .help-block {
884 margin-top: @padding;
884 margin-top: @padding;
885 }
885 }
886 }
886 }
887
887
888 // admin settings radio buttons and labels
888 // admin settings radio buttons and labels
889 .label-2 {
889 .label-2 {
890 float: left;
890 float: left;
891 width: @label2-width;
891 width: @label2-width;
892
892
893 label {
893 label {
894 color: @grey1;
894 color: @grey1;
895 }
895 }
896 }
896 }
897 .checkboxes {
897 .checkboxes {
898 float: left;
898 float: left;
899 width: @checkboxes-width;
899 width: @checkboxes-width;
900 margin-bottom: @padding;
900 margin-bottom: @padding;
901
901
902 .checkbox {
902 .checkbox {
903 width: 100%;
903 width: 100%;
904
904
905 label {
905 label {
906 margin: 0;
906 margin: 0;
907 padding: 0;
907 padding: 0;
908 }
908 }
909 }
909 }
910
910
911 .checkbox + .checkbox {
911 .checkbox + .checkbox {
912 display: inline-block;
912 display: inline-block;
913 }
913 }
914
914
915 label {
915 label {
916 margin-right: 1em;
916 margin-right: 1em;
917 }
917 }
918 }
918 }
919
919
920 // CHANGELOG
920 // CHANGELOG
921 .container_header {
921 .container_header {
922 float: left;
922 float: left;
923 display: block;
923 display: block;
924 width: 100%;
924 width: 100%;
925 margin: @padding 0 @padding;
925 margin: @padding 0 @padding;
926
926
927 #filter_changelog {
927 #filter_changelog {
928 float: left;
928 float: left;
929 margin-right: @padding;
929 margin-right: @padding;
930 }
930 }
931
931
932 .breadcrumbs_light {
932 .breadcrumbs_light {
933 display: inline-block;
933 display: inline-block;
934 }
934 }
935 }
935 }
936
936
937 .info_box {
937 .info_box {
938 float: right;
938 float: right;
939 }
939 }
940
940
941
941
942 #graph_nodes {
942 #graph_nodes {
943 padding-top: 43px;
943 padding-top: 43px;
944 }
944 }
945
945
946 #graph_content{
946 #graph_content{
947
947
948 // adjust for table headers so that graph renders properly
948 // adjust for table headers so that graph renders properly
949 // #graph_nodes padding - table cell padding
949 // #graph_nodes padding - table cell padding
950 padding-top: (@space - (@basefontsize * 2.4));
950 padding-top: (@space - (@basefontsize * 2.4));
951
951
952 &.graph_full_width {
952 &.graph_full_width {
953 width: 100%;
953 width: 100%;
954 max-width: 100%;
954 max-width: 100%;
955 }
955 }
956 }
956 }
957
957
958 #graph {
958 #graph {
959 .flag_status {
959 .flag_status {
960 margin: 0;
960 margin: 0;
961 }
961 }
962
962
963 .pagination-left {
963 .pagination-left {
964 float: left;
964 float: left;
965 clear: both;
965 clear: both;
966 }
966 }
967
967
968 .log-container {
968 .log-container {
969 max-width: 345px;
969 max-width: 345px;
970
970
971 .message{
971 .message{
972 max-width: 340px;
972 max-width: 340px;
973 }
973 }
974 }
974 }
975
975
976 .graph-col-wrapper {
976 .graph-col-wrapper {
977 padding-left: 110px;
977 padding-left: 110px;
978
978
979 #graph_nodes {
979 #graph_nodes {
980 width: 100px;
980 width: 100px;
981 margin-left: -110px;
981 margin-left: -110px;
982 float: left;
982 float: left;
983 clear: left;
983 clear: left;
984 }
984 }
985 }
985 }
986
986
987 .load-more-commits {
987 .load-more-commits {
988 text-align: center;
988 text-align: center;
989 }
989 }
990 .load-more-commits:hover {
990 .load-more-commits:hover {
991 background-color: @grey7;
991 background-color: @grey7;
992 }
992 }
993 .load-more-commits {
993 .load-more-commits {
994 a {
994 a {
995 display: block;
995 display: block;
996 }
996 }
997 }
997 }
998 }
998 }
999
999
1000 #filter_changelog {
1000 #filter_changelog {
1001 float: left;
1001 float: left;
1002 }
1002 }
1003
1003
1004
1004
1005 //--- THEME ------------------//
1005 //--- THEME ------------------//
1006
1006
1007 #logo {
1007 #logo {
1008 float: left;
1008 float: left;
1009 margin: 9px 0 0 0;
1009 margin: 9px 0 0 0;
1010
1010
1011 .header {
1011 .header {
1012 background-color: transparent;
1012 background-color: transparent;
1013 }
1013 }
1014
1014
1015 a {
1015 a {
1016 display: inline-block;
1016 display: inline-block;
1017 }
1017 }
1018
1018
1019 img {
1019 img {
1020 height:30px;
1020 height:30px;
1021 }
1021 }
1022 }
1022 }
1023
1023
1024 .logo-wrapper {
1024 .logo-wrapper {
1025 float:left;
1025 float:left;
1026 }
1026 }
1027
1027
1028 .branding{
1028 .branding{
1029 float: left;
1029 float: left;
1030 padding: 9px 2px;
1030 padding: 9px 2px;
1031 line-height: 1em;
1031 line-height: 1em;
1032 font-size: @navigation-fontsize;
1032 font-size: @navigation-fontsize;
1033 }
1033 }
1034
1034
1035 img {
1035 img {
1036 border: none;
1036 border: none;
1037 outline: none;
1037 outline: none;
1038 }
1038 }
1039 user-profile-header
1039 user-profile-header
1040 label {
1040 label {
1041
1041
1042 input[type="checkbox"] {
1042 input[type="checkbox"] {
1043 margin-right: 1em;
1043 margin-right: 1em;
1044 }
1044 }
1045 input[type="radio"] {
1045 input[type="radio"] {
1046 margin-right: 1em;
1046 margin-right: 1em;
1047 }
1047 }
1048 }
1048 }
1049
1049
1050 .flag_status {
1050 .flag_status {
1051 margin: 2px 8px 6px 2px;
1051 margin: 2px 8px 6px 2px;
1052 &.under_review {
1052 &.under_review {
1053 .circle(5px, @alert3);
1053 .circle(5px, @alert3);
1054 }
1054 }
1055 &.approved {
1055 &.approved {
1056 .circle(5px, @alert1);
1056 .circle(5px, @alert1);
1057 }
1057 }
1058 &.rejected,
1058 &.rejected,
1059 &.forced_closed{
1059 &.forced_closed{
1060 .circle(5px, @alert2);
1060 .circle(5px, @alert2);
1061 }
1061 }
1062 &.not_reviewed {
1062 &.not_reviewed {
1063 .circle(5px, @grey5);
1063 .circle(5px, @grey5);
1064 }
1064 }
1065 }
1065 }
1066
1066
1067 .flag_status_comment_box {
1067 .flag_status_comment_box {
1068 margin: 5px 6px 0px 2px;
1068 margin: 5px 6px 0px 2px;
1069 }
1069 }
1070 .test_pattern_preview {
1070 .test_pattern_preview {
1071 margin: @space 0;
1071 margin: @space 0;
1072
1072
1073 p {
1073 p {
1074 margin-bottom: 0;
1074 margin-bottom: 0;
1075 border-bottom: @border-thickness solid @border-default-color;
1075 border-bottom: @border-thickness solid @border-default-color;
1076 color: @grey3;
1076 color: @grey3;
1077 }
1077 }
1078
1078
1079 .btn {
1079 .btn {
1080 margin-bottom: @padding;
1080 margin-bottom: @padding;
1081 }
1081 }
1082 }
1082 }
1083 #test_pattern_result {
1083 #test_pattern_result {
1084 display: none;
1084 display: none;
1085 &:extend(pre);
1085 &:extend(pre);
1086 padding: .9em;
1086 padding: .9em;
1087 color: @grey3;
1087 color: @grey3;
1088 background-color: @grey7;
1088 background-color: @grey7;
1089 border-right: @border-thickness solid @border-default-color;
1089 border-right: @border-thickness solid @border-default-color;
1090 border-bottom: @border-thickness solid @border-default-color;
1090 border-bottom: @border-thickness solid @border-default-color;
1091 border-left: @border-thickness solid @border-default-color;
1091 border-left: @border-thickness solid @border-default-color;
1092 }
1092 }
1093
1093
1094 #repo_vcs_settings {
1094 #repo_vcs_settings {
1095 #inherit_overlay_vcs_default {
1095 #inherit_overlay_vcs_default {
1096 display: none;
1096 display: none;
1097 }
1097 }
1098 #inherit_overlay_vcs_custom {
1098 #inherit_overlay_vcs_custom {
1099 display: custom;
1099 display: custom;
1100 }
1100 }
1101 &.inherited {
1101 &.inherited {
1102 #inherit_overlay_vcs_default {
1102 #inherit_overlay_vcs_default {
1103 display: block;
1103 display: block;
1104 }
1104 }
1105 #inherit_overlay_vcs_custom {
1105 #inherit_overlay_vcs_custom {
1106 display: none;
1106 display: none;
1107 }
1107 }
1108 }
1108 }
1109 }
1109 }
1110
1110
1111 .issue-tracker-link {
1111 .issue-tracker-link {
1112 color: @rcblue;
1112 color: @rcblue;
1113 }
1113 }
1114
1114
1115 // Issue Tracker Table Show/Hide
1115 // Issue Tracker Table Show/Hide
1116 #repo_issue_tracker {
1116 #repo_issue_tracker {
1117 #inherit_overlay {
1117 #inherit_overlay {
1118 display: none;
1118 display: none;
1119 }
1119 }
1120 #custom_overlay {
1120 #custom_overlay {
1121 display: custom;
1121 display: custom;
1122 }
1122 }
1123 &.inherited {
1123 &.inherited {
1124 #inherit_overlay {
1124 #inherit_overlay {
1125 display: block;
1125 display: block;
1126 }
1126 }
1127 #custom_overlay {
1127 #custom_overlay {
1128 display: none;
1128 display: none;
1129 }
1129 }
1130 }
1130 }
1131 }
1131 }
1132 table.issuetracker {
1132 table.issuetracker {
1133 &.readonly {
1133 &.readonly {
1134 tr, td {
1134 tr, td {
1135 color: @grey3;
1135 color: @grey3;
1136 }
1136 }
1137 }
1137 }
1138 .edit {
1138 .edit {
1139 display: none;
1139 display: none;
1140 }
1140 }
1141 .editopen {
1141 .editopen {
1142 .edit {
1142 .edit {
1143 display: inline;
1143 display: inline;
1144 }
1144 }
1145 .entry {
1145 .entry {
1146 display: none;
1146 display: none;
1147 }
1147 }
1148 }
1148 }
1149 tr td.td-action {
1149 tr td.td-action {
1150 min-width: 117px;
1150 min-width: 117px;
1151 }
1151 }
1152 td input {
1152 td input {
1153 max-width: none;
1153 max-width: none;
1154 min-width: 30px;
1154 min-width: 30px;
1155 width: 80%;
1155 width: 80%;
1156 }
1156 }
1157 .issuetracker_pref input {
1157 .issuetracker_pref input {
1158 width: 40%;
1158 width: 40%;
1159 }
1159 }
1160 input.edit_issuetracker_update {
1160 input.edit_issuetracker_update {
1161 margin-right: 0;
1161 margin-right: 0;
1162 width: auto;
1162 width: auto;
1163 }
1163 }
1164 }
1164 }
1165
1165
1166 table.integrations {
1166 table.integrations {
1167 .td-icon {
1167 .td-icon {
1168 width: 20px;
1168 width: 20px;
1169 .integration-icon {
1169 .integration-icon {
1170 height: 20px;
1170 height: 20px;
1171 width: 20px;
1171 width: 20px;
1172 }
1172 }
1173 }
1173 }
1174 }
1174 }
1175
1175
1176 .integrations {
1176 .integrations {
1177 a.integration-box {
1177 a.integration-box {
1178 color: @text-color;
1178 color: @text-color;
1179 &:hover {
1179 &:hover {
1180 .panel {
1180 .panel {
1181 background: #fbfbfb;
1181 background: #fbfbfb;
1182 }
1182 }
1183 }
1183 }
1184 .integration-icon {
1184 .integration-icon {
1185 width: 30px;
1185 width: 30px;
1186 height: 30px;
1186 height: 30px;
1187 margin-right: 20px;
1187 margin-right: 20px;
1188 float: left;
1188 float: left;
1189 }
1189 }
1190
1190
1191 .panel-body {
1191 .panel-body {
1192 padding: 10px;
1192 padding: 10px;
1193 }
1193 }
1194 .panel {
1194 .panel {
1195 margin-bottom: 10px;
1195 margin-bottom: 10px;
1196 }
1196 }
1197 h2 {
1197 h2 {
1198 display: inline-block;
1198 display: inline-block;
1199 margin: 0;
1199 margin: 0;
1200 min-width: 140px;
1200 min-width: 140px;
1201 }
1201 }
1202 }
1202 }
1203 a.integration-box.dummy-integration {
1203 a.integration-box.dummy-integration {
1204 color: @grey4
1204 color: @grey4
1205 }
1205 }
1206 }
1206 }
1207
1207
1208 //Permissions Settings
1208 //Permissions Settings
1209 #add_perm {
1209 #add_perm {
1210 margin: 0 0 @padding;
1210 margin: 0 0 @padding;
1211 cursor: pointer;
1211 cursor: pointer;
1212 }
1212 }
1213
1213
1214 .perm_ac {
1214 .perm_ac {
1215 input {
1215 input {
1216 width: 95%;
1216 width: 95%;
1217 }
1217 }
1218 }
1218 }
1219
1219
1220 .autocomplete-suggestions {
1220 .autocomplete-suggestions {
1221 width: auto !important; // overrides autocomplete.js
1221 width: auto !important; // overrides autocomplete.js
1222 margin: 0;
1222 margin: 0;
1223 border: @border-thickness solid @rcblue;
1223 border: @border-thickness solid @rcblue;
1224 border-radius: @border-radius;
1224 border-radius: @border-radius;
1225 color: @rcblue;
1225 color: @rcblue;
1226 background-color: white;
1226 background-color: white;
1227 }
1227 }
1228 .autocomplete-selected {
1228 .autocomplete-selected {
1229 background: #F0F0F0;
1229 background: #F0F0F0;
1230 }
1230 }
1231 .ac-container-wrap {
1231 .ac-container-wrap {
1232 margin: 0;
1232 margin: 0;
1233 padding: 8px;
1233 padding: 8px;
1234 border-bottom: @border-thickness solid @rclightblue;
1234 border-bottom: @border-thickness solid @rclightblue;
1235 list-style-type: none;
1235 list-style-type: none;
1236 cursor: pointer;
1236 cursor: pointer;
1237
1237
1238 &:hover {
1238 &:hover {
1239 background-color: @rclightblue;
1239 background-color: @rclightblue;
1240 }
1240 }
1241
1241
1242 img {
1242 img {
1243 height: @gravatar-size;
1243 height: @gravatar-size;
1244 width: @gravatar-size;
1244 width: @gravatar-size;
1245 margin-right: 1em;
1245 margin-right: 1em;
1246 }
1246 }
1247
1247
1248 strong {
1248 strong {
1249 font-weight: normal;
1249 font-weight: normal;
1250 }
1250 }
1251 }
1251 }
1252
1252
1253 // Settings Dropdown
1253 // Settings Dropdown
1254 .user-menu .container {
1254 .user-menu .container {
1255 padding: 0 4px;
1255 padding: 0 4px;
1256 margin: 0;
1256 margin: 0;
1257 }
1257 }
1258
1258
1259 .user-menu .gravatar {
1259 .user-menu .gravatar {
1260 cursor: pointer;
1260 cursor: pointer;
1261 }
1261 }
1262
1262
1263 .codeblock {
1263 .codeblock {
1264 margin-bottom: @padding;
1264 margin-bottom: @padding;
1265 clear: both;
1265 clear: both;
1266
1266
1267 .stats{
1267 .stats{
1268 overflow: hidden;
1268 overflow: hidden;
1269 }
1269 }
1270
1270
1271 .message{
1271 .message{
1272 textarea{
1272 textarea{
1273 margin: 0;
1273 margin: 0;
1274 }
1274 }
1275 }
1275 }
1276
1276
1277 .code-header {
1277 .code-header {
1278 .stats {
1278 .stats {
1279 line-height: 2em;
1279 line-height: 2em;
1280
1280
1281 .revision_id {
1281 .revision_id {
1282 margin-left: 0;
1282 margin-left: 0;
1283 }
1283 }
1284 .buttons {
1284 .buttons {
1285 padding-right: 0;
1285 padding-right: 0;
1286 }
1286 }
1287 }
1287 }
1288
1288
1289 .item{
1289 .item{
1290 margin-right: 0.5em;
1290 margin-right: 0.5em;
1291 }
1291 }
1292 }
1292 }
1293
1293
1294 #editor_container{
1294 #editor_container{
1295 position: relative;
1295 position: relative;
1296 margin: @padding;
1296 margin: @padding;
1297 }
1297 }
1298 }
1298 }
1299
1299
1300 #file_history_container {
1300 #file_history_container {
1301 display: none;
1301 display: none;
1302 }
1302 }
1303
1303
1304 .file-history-inner {
1304 .file-history-inner {
1305 margin-bottom: 10px;
1305 margin-bottom: 10px;
1306 }
1306 }
1307
1307
1308 // Pull Requests
1308 // Pull Requests
1309 .summary-details {
1309 .summary-details {
1310 width: 72%;
1310 width: 72%;
1311 }
1311 }
1312 .pr-summary {
1312 .pr-summary {
1313 border-bottom: @border-thickness solid @grey5;
1313 border-bottom: @border-thickness solid @grey5;
1314 margin-bottom: @space;
1314 margin-bottom: @space;
1315 }
1315 }
1316 .reviewers-title {
1316 .reviewers-title {
1317 width: 25%;
1317 width: 25%;
1318 min-width: 200px;
1318 min-width: 200px;
1319 }
1319 }
1320 .reviewers {
1320 .reviewers {
1321 width: 25%;
1321 width: 25%;
1322 min-width: 200px;
1322 min-width: 200px;
1323 }
1323 }
1324 .reviewers ul li {
1324 .reviewers ul li {
1325 position: relative;
1325 position: relative;
1326 width: 100%;
1326 width: 100%;
1327 margin-bottom: 8px;
1327 padding-bottom: 8px;
1328 }
1328 }
1329
1329
1330 .reviewer_entry {
1330 .reviewer_entry {
1331 min-height: 55px;
1331 min-height: 55px;
1332 }
1332 }
1333
1333
1334 .reviewers_member {
1334 .reviewers_member {
1335 width: 100%;
1335 width: 100%;
1336 overflow: auto;
1336 overflow: auto;
1337 }
1337 }
1338
1338 .reviewer_reason {
1339 .reviewer_reason_container {
1340 padding-left: 20px;
1339 padding-left: 20px;
1341 }
1340 line-height: 1.5em;
1342
1341 }
1343 .reviewer_reason {
1344 }
1345
1346 .reviewer_status {
1342 .reviewer_status {
1347 display: inline-block;
1343 display: inline-block;
1348 vertical-align: top;
1344 vertical-align: top;
1349 width: 7%;
1345 width: 25px;
1350 min-width: 20px;
1346 min-width: 25px;
1351 height: 1.2em;
1347 height: 1.2em;
1352 margin-top: 3px;
1348 margin-top: 3px;
1353 line-height: 1em;
1349 line-height: 1em;
1354 }
1350 }
1355
1351
1356 .reviewer_name {
1352 .reviewer_name {
1357 display: inline-block;
1353 display: inline-block;
1358 max-width: 83%;
1354 max-width: 83%;
1359 padding-right: 20px;
1355 padding-right: 20px;
1360 vertical-align: middle;
1356 vertical-align: middle;
1361 line-height: 1;
1357 line-height: 1;
1362
1358
1363 .rc-user {
1359 .rc-user {
1364 min-width: 0;
1360 min-width: 0;
1365 margin: -2px 1em 0 0;
1361 margin: -2px 1em 0 0;
1366 }
1362 }
1367
1363
1368 .reviewer {
1364 .reviewer {
1369 float: left;
1365 float: left;
1370 }
1366 }
1371 }
1367 }
1372
1368
1373 .reviewer_member_mandatory,
1369 .reviewer_member_mandatory {
1370 position: absolute;
1371 left: 15px;
1372 top: 8px;
1373 width: 16px;
1374 font-size: 11px;
1375 margin: 0;
1376 padding: 0;
1377 color: black;
1378 }
1379
1374 .reviewer_member_mandatory_remove,
1380 .reviewer_member_mandatory_remove,
1375 .reviewer_member_remove {
1381 .reviewer_member_remove {
1376 position: absolute;
1382 position: absolute;
1377 right: 0;
1383 right: 0;
1378 top: 0;
1384 top: 0;
1379 width: 16px;
1385 width: 16px;
1380 margin-bottom: 10px;
1386 margin-bottom: 10px;
1381 padding: 0;
1387 padding: 0;
1382 color: black;
1388 color: black;
1383 }
1389 }
1384
1390
1385 .reviewer_member_mandatory_remove {
1391 .reviewer_member_mandatory_remove {
1386 color: @grey4;
1392 color: @grey4;
1387 }
1393 }
1388
1394
1389 .reviewer_member_mandatory {
1390 padding-top:20px;
1391 }
1392
1393 .reviewer_member_status {
1395 .reviewer_member_status {
1394 margin-top: 5px;
1396 margin-top: 5px;
1395 }
1397 }
1396 .pr-summary #summary{
1398 .pr-summary #summary{
1397 width: 100%;
1399 width: 100%;
1398 }
1400 }
1399 .pr-summary .action_button:hover {
1401 .pr-summary .action_button:hover {
1400 border: 0;
1402 border: 0;
1401 cursor: pointer;
1403 cursor: pointer;
1402 }
1404 }
1403 .pr-details-title {
1405 .pr-details-title {
1404 padding-bottom: 8px;
1406 padding-bottom: 8px;
1405 border-bottom: @border-thickness solid @grey5;
1407 border-bottom: @border-thickness solid @grey5;
1406
1408
1407 .action_button.disabled {
1409 .action_button.disabled {
1408 color: @grey4;
1410 color: @grey4;
1409 cursor: inherit;
1411 cursor: inherit;
1410 }
1412 }
1411 .action_button {
1413 .action_button {
1412 color: @rcblue;
1414 color: @rcblue;
1413 }
1415 }
1414 }
1416 }
1415 .pr-details-content {
1417 .pr-details-content {
1416 margin-top: @textmargin;
1418 margin-top: @textmargin;
1417 margin-bottom: @textmargin;
1419 margin-bottom: @textmargin;
1418 }
1420 }
1419 .pr-description {
1421 .pr-description {
1420 white-space:pre-wrap;
1422 white-space:pre-wrap;
1421 }
1423 }
1422
1424
1423 .pr-reviewer-rules {
1425 .pr-reviewer-rules {
1424 padding: 10px 0px 20px 0px;
1426 padding: 10px 0px 20px 0px;
1425 }
1427 }
1426
1428
1427 .group_members {
1429 .group_members {
1428 margin-top: 0;
1430 margin-top: 0;
1429 padding: 0;
1431 padding: 0;
1430 list-style: outside none none;
1432 list-style: outside none none;
1431
1433
1432 img {
1434 img {
1433 height: @gravatar-size;
1435 height: @gravatar-size;
1434 width: @gravatar-size;
1436 width: @gravatar-size;
1435 margin-right: .5em;
1437 margin-right: .5em;
1436 margin-left: 3px;
1438 margin-left: 3px;
1437 }
1439 }
1438
1440
1439 .to-delete {
1441 .to-delete {
1440 .user {
1442 .user {
1441 text-decoration: line-through;
1443 text-decoration: line-through;
1442 }
1444 }
1443 }
1445 }
1444 }
1446 }
1445
1447
1446 .compare_view_commits_title {
1448 .compare_view_commits_title {
1447 .disabled {
1449 .disabled {
1448 cursor: inherit;
1450 cursor: inherit;
1449 &:hover{
1451 &:hover{
1450 background-color: inherit;
1452 background-color: inherit;
1451 color: inherit;
1453 color: inherit;
1452 }
1454 }
1453 }
1455 }
1454 }
1456 }
1455
1457
1456 .subtitle-compare {
1458 .subtitle-compare {
1457 margin: -15px 0px 0px 0px;
1459 margin: -15px 0px 0px 0px;
1458 }
1460 }
1459
1461
1460 .comments-summary-td {
1462 .comments-summary-td {
1461 border-top: 1px dashed @grey5;
1463 border-top: 1px dashed @grey5;
1462 }
1464 }
1463
1465
1464 // new entry in group_members
1466 // new entry in group_members
1465 .td-author-new-entry {
1467 .td-author-new-entry {
1466 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1468 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1467 }
1469 }
1468
1470
1469 .usergroup_member_remove {
1471 .usergroup_member_remove {
1470 width: 16px;
1472 width: 16px;
1471 margin-bottom: 10px;
1473 margin-bottom: 10px;
1472 padding: 0;
1474 padding: 0;
1473 color: black !important;
1475 color: black !important;
1474 cursor: pointer;
1476 cursor: pointer;
1475 }
1477 }
1476
1478
1477 .reviewer_ac .ac-input {
1479 .reviewer_ac .ac-input {
1478 width: 92%;
1480 width: 92%;
1479 margin-bottom: 1em;
1481 margin-bottom: 1em;
1480 }
1482 }
1481
1483
1482 .compare_view_commits tr{
1484 .compare_view_commits tr{
1483 height: 20px;
1485 height: 20px;
1484 }
1486 }
1485 .compare_view_commits td {
1487 .compare_view_commits td {
1486 vertical-align: top;
1488 vertical-align: top;
1487 padding-top: 10px;
1489 padding-top: 10px;
1488 }
1490 }
1489 .compare_view_commits .author {
1491 .compare_view_commits .author {
1490 margin-left: 5px;
1492 margin-left: 5px;
1491 }
1493 }
1492
1494
1493 .compare_view_commits {
1495 .compare_view_commits {
1494 .color-a {
1496 .color-a {
1495 color: @alert1;
1497 color: @alert1;
1496 }
1498 }
1497
1499
1498 .color-c {
1500 .color-c {
1499 color: @color3;
1501 color: @color3;
1500 }
1502 }
1501
1503
1502 .color-r {
1504 .color-r {
1503 color: @color5;
1505 color: @color5;
1504 }
1506 }
1505
1507
1506 .color-a-bg {
1508 .color-a-bg {
1507 background-color: @alert1;
1509 background-color: @alert1;
1508 }
1510 }
1509
1511
1510 .color-c-bg {
1512 .color-c-bg {
1511 background-color: @alert3;
1513 background-color: @alert3;
1512 }
1514 }
1513
1515
1514 .color-r-bg {
1516 .color-r-bg {
1515 background-color: @alert2;
1517 background-color: @alert2;
1516 }
1518 }
1517
1519
1518 .color-a-border {
1520 .color-a-border {
1519 border: 1px solid @alert1;
1521 border: 1px solid @alert1;
1520 }
1522 }
1521
1523
1522 .color-c-border {
1524 .color-c-border {
1523 border: 1px solid @alert3;
1525 border: 1px solid @alert3;
1524 }
1526 }
1525
1527
1526 .color-r-border {
1528 .color-r-border {
1527 border: 1px solid @alert2;
1529 border: 1px solid @alert2;
1528 }
1530 }
1529
1531
1530 .commit-change-indicator {
1532 .commit-change-indicator {
1531 width: 15px;
1533 width: 15px;
1532 height: 15px;
1534 height: 15px;
1533 position: relative;
1535 position: relative;
1534 left: 15px;
1536 left: 15px;
1535 }
1537 }
1536
1538
1537 .commit-change-content {
1539 .commit-change-content {
1538 text-align: center;
1540 text-align: center;
1539 vertical-align: middle;
1541 vertical-align: middle;
1540 line-height: 15px;
1542 line-height: 15px;
1541 }
1543 }
1542 }
1544 }
1543
1545
1544 .compare_view_files {
1546 .compare_view_files {
1545 width: 100%;
1547 width: 100%;
1546
1548
1547 td {
1549 td {
1548 vertical-align: middle;
1550 vertical-align: middle;
1549 }
1551 }
1550 }
1552 }
1551
1553
1552 .compare_view_filepath {
1554 .compare_view_filepath {
1553 color: @grey1;
1555 color: @grey1;
1554 }
1556 }
1555
1557
1556 .show_more {
1558 .show_more {
1557 display: inline-block;
1559 display: inline-block;
1558 position: relative;
1560 position: relative;
1559 vertical-align: middle;
1561 vertical-align: middle;
1560 width: 4px;
1562 width: 4px;
1561 height: @basefontsize;
1563 height: @basefontsize;
1562
1564
1563 &:after {
1565 &:after {
1564 content: "\00A0\25BE";
1566 content: "\00A0\25BE";
1565 display: inline-block;
1567 display: inline-block;
1566 width:10px;
1568 width:10px;
1567 line-height: 5px;
1569 line-height: 5px;
1568 font-size: 12px;
1570 font-size: 12px;
1569 cursor: pointer;
1571 cursor: pointer;
1570 }
1572 }
1571 }
1573 }
1572
1574
1573 .journal_more .show_more {
1575 .journal_more .show_more {
1574 display: inline;
1576 display: inline;
1575
1577
1576 &:after {
1578 &:after {
1577 content: none;
1579 content: none;
1578 }
1580 }
1579 }
1581 }
1580
1582
1581 .open .show_more:after,
1583 .open .show_more:after,
1582 .select2-dropdown-open .show_more:after {
1584 .select2-dropdown-open .show_more:after {
1583 .rotate(180deg);
1585 .rotate(180deg);
1584 margin-left: 4px;
1586 margin-left: 4px;
1585 }
1587 }
1586
1588
1587
1589
1588 .compare_view_commits .collapse_commit:after {
1590 .compare_view_commits .collapse_commit:after {
1589 cursor: pointer;
1591 cursor: pointer;
1590 content: "\00A0\25B4";
1592 content: "\00A0\25B4";
1591 margin-left: -3px;
1593 margin-left: -3px;
1592 font-size: 17px;
1594 font-size: 17px;
1593 color: @grey4;
1595 color: @grey4;
1594 }
1596 }
1595
1597
1596 .diff_links {
1598 .diff_links {
1597 margin-left: 8px;
1599 margin-left: 8px;
1598 }
1600 }
1599
1601
1600 div.ancestor {
1602 div.ancestor {
1601 margin: -30px 0px;
1603 margin: -30px 0px;
1602 }
1604 }
1603
1605
1604 .cs_icon_td input[type="checkbox"] {
1606 .cs_icon_td input[type="checkbox"] {
1605 display: none;
1607 display: none;
1606 }
1608 }
1607
1609
1608 .cs_icon_td .expand_file_icon:after {
1610 .cs_icon_td .expand_file_icon:after {
1609 cursor: pointer;
1611 cursor: pointer;
1610 content: "\00A0\25B6";
1612 content: "\00A0\25B6";
1611 font-size: 12px;
1613 font-size: 12px;
1612 color: @grey4;
1614 color: @grey4;
1613 }
1615 }
1614
1616
1615 .cs_icon_td .collapse_file_icon:after {
1617 .cs_icon_td .collapse_file_icon:after {
1616 cursor: pointer;
1618 cursor: pointer;
1617 content: "\00A0\25BC";
1619 content: "\00A0\25BC";
1618 font-size: 12px;
1620 font-size: 12px;
1619 color: @grey4;
1621 color: @grey4;
1620 }
1622 }
1621
1623
1622 /*new binary
1624 /*new binary
1623 NEW_FILENODE = 1
1625 NEW_FILENODE = 1
1624 DEL_FILENODE = 2
1626 DEL_FILENODE = 2
1625 MOD_FILENODE = 3
1627 MOD_FILENODE = 3
1626 RENAMED_FILENODE = 4
1628 RENAMED_FILENODE = 4
1627 COPIED_FILENODE = 5
1629 COPIED_FILENODE = 5
1628 CHMOD_FILENODE = 6
1630 CHMOD_FILENODE = 6
1629 BIN_FILENODE = 7
1631 BIN_FILENODE = 7
1630 */
1632 */
1631 .cs_files_expand {
1633 .cs_files_expand {
1632 font-size: @basefontsize + 5px;
1634 font-size: @basefontsize + 5px;
1633 line-height: 1.8em;
1635 line-height: 1.8em;
1634 float: right;
1636 float: right;
1635 }
1637 }
1636
1638
1637 .cs_files_expand span{
1639 .cs_files_expand span{
1638 color: @rcblue;
1640 color: @rcblue;
1639 cursor: pointer;
1641 cursor: pointer;
1640 }
1642 }
1641 .cs_files {
1643 .cs_files {
1642 clear: both;
1644 clear: both;
1643 padding-bottom: @padding;
1645 padding-bottom: @padding;
1644
1646
1645 .cur_cs {
1647 .cur_cs {
1646 margin: 10px 2px;
1648 margin: 10px 2px;
1647 font-weight: bold;
1649 font-weight: bold;
1648 }
1650 }
1649
1651
1650 .node {
1652 .node {
1651 float: left;
1653 float: left;
1652 }
1654 }
1653
1655
1654 .changes {
1656 .changes {
1655 float: right;
1657 float: right;
1656 color: white;
1658 color: white;
1657 font-size: @basefontsize - 4px;
1659 font-size: @basefontsize - 4px;
1658 margin-top: 4px;
1660 margin-top: 4px;
1659 opacity: 0.6;
1661 opacity: 0.6;
1660 filter: Alpha(opacity=60); /* IE8 and earlier */
1662 filter: Alpha(opacity=60); /* IE8 and earlier */
1661
1663
1662 .added {
1664 .added {
1663 background-color: @alert1;
1665 background-color: @alert1;
1664 float: left;
1666 float: left;
1665 text-align: center;
1667 text-align: center;
1666 }
1668 }
1667
1669
1668 .deleted {
1670 .deleted {
1669 background-color: @alert2;
1671 background-color: @alert2;
1670 float: left;
1672 float: left;
1671 text-align: center;
1673 text-align: center;
1672 }
1674 }
1673
1675
1674 .bin {
1676 .bin {
1675 background-color: @alert1;
1677 background-color: @alert1;
1676 text-align: center;
1678 text-align: center;
1677 }
1679 }
1678
1680
1679 /*new binary*/
1681 /*new binary*/
1680 .bin.bin1 {
1682 .bin.bin1 {
1681 background-color: @alert1;
1683 background-color: @alert1;
1682 text-align: center;
1684 text-align: center;
1683 }
1685 }
1684
1686
1685 /*deleted binary*/
1687 /*deleted binary*/
1686 .bin.bin2 {
1688 .bin.bin2 {
1687 background-color: @alert2;
1689 background-color: @alert2;
1688 text-align: center;
1690 text-align: center;
1689 }
1691 }
1690
1692
1691 /*mod binary*/
1693 /*mod binary*/
1692 .bin.bin3 {
1694 .bin.bin3 {
1693 background-color: @grey2;
1695 background-color: @grey2;
1694 text-align: center;
1696 text-align: center;
1695 }
1697 }
1696
1698
1697 /*rename file*/
1699 /*rename file*/
1698 .bin.bin4 {
1700 .bin.bin4 {
1699 background-color: @alert4;
1701 background-color: @alert4;
1700 text-align: center;
1702 text-align: center;
1701 }
1703 }
1702
1704
1703 /*copied file*/
1705 /*copied file*/
1704 .bin.bin5 {
1706 .bin.bin5 {
1705 background-color: @alert4;
1707 background-color: @alert4;
1706 text-align: center;
1708 text-align: center;
1707 }
1709 }
1708
1710
1709 /*chmod file*/
1711 /*chmod file*/
1710 .bin.bin6 {
1712 .bin.bin6 {
1711 background-color: @grey2;
1713 background-color: @grey2;
1712 text-align: center;
1714 text-align: center;
1713 }
1715 }
1714 }
1716 }
1715 }
1717 }
1716
1718
1717 .cs_files .cs_added, .cs_files .cs_A,
1719 .cs_files .cs_added, .cs_files .cs_A,
1718 .cs_files .cs_added, .cs_files .cs_M,
1720 .cs_files .cs_added, .cs_files .cs_M,
1719 .cs_files .cs_added, .cs_files .cs_D {
1721 .cs_files .cs_added, .cs_files .cs_D {
1720 height: 16px;
1722 height: 16px;
1721 padding-right: 10px;
1723 padding-right: 10px;
1722 margin-top: 7px;
1724 margin-top: 7px;
1723 text-align: left;
1725 text-align: left;
1724 }
1726 }
1725
1727
1726 .cs_icon_td {
1728 .cs_icon_td {
1727 min-width: 16px;
1729 min-width: 16px;
1728 width: 16px;
1730 width: 16px;
1729 }
1731 }
1730
1732
1731 .pull-request-merge {
1733 .pull-request-merge {
1732 border: 1px solid @grey5;
1734 border: 1px solid @grey5;
1733 padding: 10px 0px 20px;
1735 padding: 10px 0px 20px;
1734 margin-top: 10px;
1736 margin-top: 10px;
1735 margin-bottom: 20px;
1737 margin-bottom: 20px;
1736 }
1738 }
1737
1739
1738 .pull-request-merge ul {
1740 .pull-request-merge ul {
1739 padding: 0px 0px;
1741 padding: 0px 0px;
1740 }
1742 }
1741
1743
1742 .pull-request-merge li:before{
1744 .pull-request-merge li:before{
1743 content:none;
1745 content:none;
1744 }
1746 }
1745
1747
1746 .pull-request-merge .pull-request-wrap {
1748 .pull-request-merge .pull-request-wrap {
1747 height: auto;
1749 height: auto;
1748 padding: 0px 0px;
1750 padding: 0px 0px;
1749 text-align: right;
1751 text-align: right;
1750 }
1752 }
1751
1753
1752 .pull-request-merge span {
1754 .pull-request-merge span {
1753 margin-right: 5px;
1755 margin-right: 5px;
1754 }
1756 }
1755
1757
1756 .pull-request-merge-actions {
1758 .pull-request-merge-actions {
1757 min-height: 30px;
1759 min-height: 30px;
1758 padding: 0px 0px;
1760 padding: 0px 0px;
1759 }
1761 }
1760
1762
1761 .pull-request-merge-info {
1763 .pull-request-merge-info {
1762 padding: 0px 5px 5px 0px;
1764 padding: 0px 5px 5px 0px;
1763 }
1765 }
1764
1766
1765 .merge-status {
1767 .merge-status {
1766 margin-right: 5px;
1768 margin-right: 5px;
1767 }
1769 }
1768
1770
1769 .merge-message {
1771 .merge-message {
1770 font-size: 1.2em
1772 font-size: 1.2em
1771 }
1773 }
1772
1774
1773 .merge-message.success i,
1775 .merge-message.success i,
1774 .merge-icon.success i {
1776 .merge-icon.success i {
1775 color:@alert1;
1777 color:@alert1;
1776 }
1778 }
1777
1779
1778 .merge-message.warning i,
1780 .merge-message.warning i,
1779 .merge-icon.warning i {
1781 .merge-icon.warning i {
1780 color: @alert3;
1782 color: @alert3;
1781 }
1783 }
1782
1784
1783 .merge-message.error i,
1785 .merge-message.error i,
1784 .merge-icon.error i {
1786 .merge-icon.error i {
1785 color:@alert2;
1787 color:@alert2;
1786 }
1788 }
1787
1789
1788 .pr-versions {
1790 .pr-versions {
1789 font-size: 1.1em;
1791 font-size: 1.1em;
1790
1792
1791 table {
1793 table {
1792 padding: 0px 5px;
1794 padding: 0px 5px;
1793 }
1795 }
1794
1796
1795 td {
1797 td {
1796 line-height: 15px;
1798 line-height: 15px;
1797 }
1799 }
1798
1800
1799 .flag_status {
1801 .flag_status {
1800 margin: 0;
1802 margin: 0;
1801 }
1803 }
1802
1804
1803 .compare-radio-button {
1805 .compare-radio-button {
1804 position: relative;
1806 position: relative;
1805 top: -3px;
1807 top: -3px;
1806 }
1808 }
1807 }
1809 }
1808
1810
1809
1811
1810 #close_pull_request {
1812 #close_pull_request {
1811 margin-right: 0px;
1813 margin-right: 0px;
1812 }
1814 }
1813
1815
1814 .empty_data {
1816 .empty_data {
1815 color: @grey4;
1817 color: @grey4;
1816 }
1818 }
1817
1819
1818 #changeset_compare_view_content {
1820 #changeset_compare_view_content {
1819 margin-bottom: @space;
1821 margin-bottom: @space;
1820 clear: both;
1822 clear: both;
1821 width: 100%;
1823 width: 100%;
1822 box-sizing: border-box;
1824 box-sizing: border-box;
1823 .border-radius(@border-radius);
1825 .border-radius(@border-radius);
1824
1826
1825 .help-block {
1827 .help-block {
1826 margin: @padding 0;
1828 margin: @padding 0;
1827 color: @text-color;
1829 color: @text-color;
1828 &.pre-formatting {
1830 &.pre-formatting {
1829 white-space: pre;
1831 white-space: pre;
1830 }
1832 }
1831 }
1833 }
1832
1834
1833 .empty_data {
1835 .empty_data {
1834 margin: @padding 0;
1836 margin: @padding 0;
1835 }
1837 }
1836
1838
1837 .alert {
1839 .alert {
1838 margin-bottom: @space;
1840 margin-bottom: @space;
1839 }
1841 }
1840 }
1842 }
1841
1843
1842 .table_disp {
1844 .table_disp {
1843 .status {
1845 .status {
1844 width: auto;
1846 width: auto;
1845
1847
1846 .flag_status {
1848 .flag_status {
1847 float: left;
1849 float: left;
1848 }
1850 }
1849 }
1851 }
1850 }
1852 }
1851
1853
1852 .no-object-border {
1853 text-align: center;
1854 padding: 20px;
1855 border-radius: @border-radius-base;
1856 border: 1px solid @grey4;
1857 color: @grey4;
1858 }
1859
1854
1860 .creation_in_progress {
1855 .creation_in_progress {
1861 color: @grey4
1856 color: @grey4
1862 }
1857 }
1863
1858
1864 .status_box_menu {
1859 .status_box_menu {
1865 margin: 0;
1860 margin: 0;
1866 }
1861 }
1867
1862
1868 .notification-table{
1863 .notification-table{
1869 margin-bottom: @space;
1864 margin-bottom: @space;
1870 display: table;
1865 display: table;
1871 width: 100%;
1866 width: 100%;
1872
1867
1873 .container{
1868 .container{
1874 display: table-row;
1869 display: table-row;
1875
1870
1876 .notification-header{
1871 .notification-header{
1877 border-bottom: @border-thickness solid @border-default-color;
1872 border-bottom: @border-thickness solid @border-default-color;
1878 }
1873 }
1879
1874
1880 .notification-subject{
1875 .notification-subject{
1881 display: table-cell;
1876 display: table-cell;
1882 }
1877 }
1883 }
1878 }
1884 }
1879 }
1885
1880
1886 // Notifications
1881 // Notifications
1887 .notification-header{
1882 .notification-header{
1888 display: table;
1883 display: table;
1889 width: 100%;
1884 width: 100%;
1890 padding: floor(@basefontsize/2) 0;
1885 padding: floor(@basefontsize/2) 0;
1891 line-height: 1em;
1886 line-height: 1em;
1892
1887
1893 .desc, .delete-notifications, .read-notifications{
1888 .desc, .delete-notifications, .read-notifications{
1894 display: table-cell;
1889 display: table-cell;
1895 text-align: left;
1890 text-align: left;
1896 }
1891 }
1897
1892
1898 .desc{
1893 .desc{
1899 width: 1163px;
1894 width: 1163px;
1900 }
1895 }
1901
1896
1902 .delete-notifications, .read-notifications{
1897 .delete-notifications, .read-notifications{
1903 width: 35px;
1898 width: 35px;
1904 min-width: 35px; //fixes when only one button is displayed
1899 min-width: 35px; //fixes when only one button is displayed
1905 }
1900 }
1906 }
1901 }
1907
1902
1908 .notification-body {
1903 .notification-body {
1909 .markdown-block,
1904 .markdown-block,
1910 .rst-block {
1905 .rst-block {
1911 padding: @padding 0;
1906 padding: @padding 0;
1912 }
1907 }
1913
1908
1914 .notification-subject {
1909 .notification-subject {
1915 padding: @textmargin 0;
1910 padding: @textmargin 0;
1916 border-bottom: @border-thickness solid @border-default-color;
1911 border-bottom: @border-thickness solid @border-default-color;
1917 }
1912 }
1918 }
1913 }
1919
1914
1920
1915
1921 .notifications_buttons{
1916 .notifications_buttons{
1922 float: right;
1917 float: right;
1923 }
1918 }
1924
1919
1925 #notification-status{
1920 #notification-status{
1926 display: inline;
1921 display: inline;
1927 }
1922 }
1928
1923
1929 // Repositories
1924 // Repositories
1930
1925
1931 #summary.fields{
1926 #summary.fields{
1932 display: table;
1927 display: table;
1933
1928
1934 .field{
1929 .field{
1935 display: table-row;
1930 display: table-row;
1936
1931
1937 .label-summary{
1932 .label-summary{
1938 display: table-cell;
1933 display: table-cell;
1939 min-width: @label-summary-minwidth;
1934 min-width: @label-summary-minwidth;
1940 padding-top: @padding/2;
1935 padding-top: @padding/2;
1941 padding-bottom: @padding/2;
1936 padding-bottom: @padding/2;
1942 padding-right: @padding/2;
1937 padding-right: @padding/2;
1943 }
1938 }
1944
1939
1945 .input{
1940 .input{
1946 display: table-cell;
1941 display: table-cell;
1947 padding: @padding/2;
1942 padding: @padding/2;
1948
1943
1949 input{
1944 input{
1950 min-width: 29em;
1945 min-width: 29em;
1951 padding: @padding/4;
1946 padding: @padding/4;
1952 }
1947 }
1953 }
1948 }
1954 .statistics, .downloads{
1949 .statistics, .downloads{
1955 .disabled{
1950 .disabled{
1956 color: @grey4;
1951 color: @grey4;
1957 }
1952 }
1958 }
1953 }
1959 }
1954 }
1960 }
1955 }
1961
1956
1962 #summary{
1957 #summary{
1963 width: 70%;
1958 width: 70%;
1964 }
1959 }
1965
1960
1966
1961
1967 // Journal
1962 // Journal
1968 .journal.title {
1963 .journal.title {
1969 h5 {
1964 h5 {
1970 float: left;
1965 float: left;
1971 margin: 0;
1966 margin: 0;
1972 width: 70%;
1967 width: 70%;
1973 }
1968 }
1974
1969
1975 ul {
1970 ul {
1976 float: right;
1971 float: right;
1977 display: inline-block;
1972 display: inline-block;
1978 margin: 0;
1973 margin: 0;
1979 width: 30%;
1974 width: 30%;
1980 text-align: right;
1975 text-align: right;
1981
1976
1982 li {
1977 li {
1983 display: inline;
1978 display: inline;
1984 font-size: @journal-fontsize;
1979 font-size: @journal-fontsize;
1985 line-height: 1em;
1980 line-height: 1em;
1986
1981
1987 &:before { content: none; }
1982 &:before { content: none; }
1988 }
1983 }
1989 }
1984 }
1990 }
1985 }
1991
1986
1992 .filterexample {
1987 .filterexample {
1993 position: absolute;
1988 position: absolute;
1994 top: 95px;
1989 top: 95px;
1995 left: @contentpadding;
1990 left: @contentpadding;
1996 color: @rcblue;
1991 color: @rcblue;
1997 font-size: 11px;
1992 font-size: 11px;
1998 font-family: @text-regular;
1993 font-family: @text-regular;
1999 cursor: help;
1994 cursor: help;
2000
1995
2001 &:hover {
1996 &:hover {
2002 color: @rcdarkblue;
1997 color: @rcdarkblue;
2003 }
1998 }
2004
1999
2005 @media (max-width:768px) {
2000 @media (max-width:768px) {
2006 position: relative;
2001 position: relative;
2007 top: auto;
2002 top: auto;
2008 left: auto;
2003 left: auto;
2009 display: block;
2004 display: block;
2010 }
2005 }
2011 }
2006 }
2012
2007
2013
2008
2014 #journal{
2009 #journal{
2015 margin-bottom: @space;
2010 margin-bottom: @space;
2016
2011
2017 .journal_day{
2012 .journal_day{
2018 margin-bottom: @textmargin/2;
2013 margin-bottom: @textmargin/2;
2019 padding-bottom: @textmargin/2;
2014 padding-bottom: @textmargin/2;
2020 font-size: @journal-fontsize;
2015 font-size: @journal-fontsize;
2021 border-bottom: @border-thickness solid @border-default-color;
2016 border-bottom: @border-thickness solid @border-default-color;
2022 }
2017 }
2023
2018
2024 .journal_container{
2019 .journal_container{
2025 margin-bottom: @space;
2020 margin-bottom: @space;
2026
2021
2027 .journal_user{
2022 .journal_user{
2028 display: inline-block;
2023 display: inline-block;
2029 }
2024 }
2030 .journal_action_container{
2025 .journal_action_container{
2031 display: block;
2026 display: block;
2032 margin-top: @textmargin;
2027 margin-top: @textmargin;
2033
2028
2034 div{
2029 div{
2035 display: inline;
2030 display: inline;
2036 }
2031 }
2037
2032
2038 div.journal_action_params{
2033 div.journal_action_params{
2039 display: block;
2034 display: block;
2040 }
2035 }
2041
2036
2042 div.journal_repo:after{
2037 div.journal_repo:after{
2043 content: "\A";
2038 content: "\A";
2044 white-space: pre;
2039 white-space: pre;
2045 }
2040 }
2046
2041
2047 div.date{
2042 div.date{
2048 display: block;
2043 display: block;
2049 margin-bottom: @textmargin;
2044 margin-bottom: @textmargin;
2050 }
2045 }
2051 }
2046 }
2052 }
2047 }
2053 }
2048 }
2054
2049
2055 // Files
2050 // Files
2056 .edit-file-title {
2051 .edit-file-title {
2057 border-bottom: @border-thickness solid @border-default-color;
2052 border-bottom: @border-thickness solid @border-default-color;
2058
2053
2059 .breadcrumbs {
2054 .breadcrumbs {
2060 margin-bottom: 0;
2055 margin-bottom: 0;
2061 }
2056 }
2062 }
2057 }
2063
2058
2064 .edit-file-fieldset {
2059 .edit-file-fieldset {
2065 margin-top: @sidebarpadding;
2060 margin-top: @sidebarpadding;
2066
2061
2067 .fieldset {
2062 .fieldset {
2068 .left-label {
2063 .left-label {
2069 width: 13%;
2064 width: 13%;
2070 }
2065 }
2071 .right-content {
2066 .right-content {
2072 width: 87%;
2067 width: 87%;
2073 max-width: 100%;
2068 max-width: 100%;
2074 }
2069 }
2075 .filename-label {
2070 .filename-label {
2076 margin-top: 13px;
2071 margin-top: 13px;
2077 }
2072 }
2078 .commit-message-label {
2073 .commit-message-label {
2079 margin-top: 4px;
2074 margin-top: 4px;
2080 }
2075 }
2081 .file-upload-input {
2076 .file-upload-input {
2082 input {
2077 input {
2083 display: none;
2078 display: none;
2084 }
2079 }
2085 margin-top: 10px;
2080 margin-top: 10px;
2086 }
2081 }
2087 .file-upload-label {
2082 .file-upload-label {
2088 margin-top: 10px;
2083 margin-top: 10px;
2089 }
2084 }
2090 p {
2085 p {
2091 margin-top: 5px;
2086 margin-top: 5px;
2092 }
2087 }
2093
2088
2094 }
2089 }
2095 .custom-path-link {
2090 .custom-path-link {
2096 margin-left: 5px;
2091 margin-left: 5px;
2097 }
2092 }
2098 #commit {
2093 #commit {
2099 resize: vertical;
2094 resize: vertical;
2100 }
2095 }
2101 }
2096 }
2102
2097
2103 .delete-file-preview {
2098 .delete-file-preview {
2104 max-height: 250px;
2099 max-height: 250px;
2105 }
2100 }
2106
2101
2107 .new-file,
2102 .new-file,
2108 #filter_activate,
2103 #filter_activate,
2109 #filter_deactivate {
2104 #filter_deactivate {
2110 float: left;
2105 float: left;
2111 margin: 0 0 0 15px;
2106 margin: 0 0 0 15px;
2112 }
2107 }
2113
2108
2114 h3.files_location{
2109 h3.files_location{
2115 line-height: 2.4em;
2110 line-height: 2.4em;
2116 }
2111 }
2117
2112
2118 .browser-nav {
2113 .browser-nav {
2119 display: table;
2114 display: table;
2120 margin-bottom: @space;
2115 margin-bottom: @space;
2121
2116
2122
2117
2123 .info_box {
2118 .info_box {
2124 display: inline-table;
2119 display: inline-table;
2125 height: 2.5em;
2120 height: 2.5em;
2126
2121
2127 .browser-cur-rev, .info_box_elem {
2122 .browser-cur-rev, .info_box_elem {
2128 display: table-cell;
2123 display: table-cell;
2129 vertical-align: middle;
2124 vertical-align: middle;
2130 }
2125 }
2131
2126
2132 .info_box_elem {
2127 .info_box_elem {
2133 border-top: @border-thickness solid @rcblue;
2128 border-top: @border-thickness solid @rcblue;
2134 border-bottom: @border-thickness solid @rcblue;
2129 border-bottom: @border-thickness solid @rcblue;
2135
2130
2136 #at_rev, a {
2131 #at_rev, a {
2137 padding: 0.6em 0.9em;
2132 padding: 0.6em 0.9em;
2138 margin: 0;
2133 margin: 0;
2139 .box-shadow(none);
2134 .box-shadow(none);
2140 border: 0;
2135 border: 0;
2141 height: 12px;
2136 height: 12px;
2142 }
2137 }
2143
2138
2144 input#at_rev {
2139 input#at_rev {
2145 max-width: 50px;
2140 max-width: 50px;
2146 text-align: right;
2141 text-align: right;
2147 }
2142 }
2148
2143
2149 &.previous {
2144 &.previous {
2150 border: @border-thickness solid @rcblue;
2145 border: @border-thickness solid @rcblue;
2151 .disabled {
2146 .disabled {
2152 color: @grey4;
2147 color: @grey4;
2153 cursor: not-allowed;
2148 cursor: not-allowed;
2154 }
2149 }
2155 }
2150 }
2156
2151
2157 &.next {
2152 &.next {
2158 border: @border-thickness solid @rcblue;
2153 border: @border-thickness solid @rcblue;
2159 .disabled {
2154 .disabled {
2160 color: @grey4;
2155 color: @grey4;
2161 cursor: not-allowed;
2156 cursor: not-allowed;
2162 }
2157 }
2163 }
2158 }
2164 }
2159 }
2165
2160
2166 .browser-cur-rev {
2161 .browser-cur-rev {
2167
2162
2168 span{
2163 span{
2169 margin: 0;
2164 margin: 0;
2170 color: @rcblue;
2165 color: @rcblue;
2171 height: 12px;
2166 height: 12px;
2172 display: inline-block;
2167 display: inline-block;
2173 padding: 0.7em 1em ;
2168 padding: 0.7em 1em ;
2174 border: @border-thickness solid @rcblue;
2169 border: @border-thickness solid @rcblue;
2175 margin-right: @padding;
2170 margin-right: @padding;
2176 }
2171 }
2177 }
2172 }
2178 }
2173 }
2179
2174
2180 .search_activate {
2175 .search_activate {
2181 display: table-cell;
2176 display: table-cell;
2182 vertical-align: middle;
2177 vertical-align: middle;
2183
2178
2184 input, label{
2179 input, label{
2185 margin: 0;
2180 margin: 0;
2186 padding: 0;
2181 padding: 0;
2187 }
2182 }
2188
2183
2189 input{
2184 input{
2190 margin-left: @textmargin;
2185 margin-left: @textmargin;
2191 }
2186 }
2192
2187
2193 }
2188 }
2194 }
2189 }
2195
2190
2196 .browser-cur-rev{
2191 .browser-cur-rev{
2197 margin-bottom: @textmargin;
2192 margin-bottom: @textmargin;
2198 }
2193 }
2199
2194
2200 #node_filter_box_loading{
2195 #node_filter_box_loading{
2201 .info_text;
2196 .info_text;
2202 }
2197 }
2203
2198
2204 .browser-search {
2199 .browser-search {
2205 margin: -25px 0px 5px 0px;
2200 margin: -25px 0px 5px 0px;
2206 }
2201 }
2207
2202
2208 .node-filter {
2203 .node-filter {
2209 font-size: @repo-title-fontsize;
2204 font-size: @repo-title-fontsize;
2210 padding: 4px 0px 0px 0px;
2205 padding: 4px 0px 0px 0px;
2211
2206
2212 .node-filter-path {
2207 .node-filter-path {
2213 float: left;
2208 float: left;
2214 color: @grey4;
2209 color: @grey4;
2215 }
2210 }
2216 .node-filter-input {
2211 .node-filter-input {
2217 float: left;
2212 float: left;
2218 margin: -2px 0px 0px 2px;
2213 margin: -2px 0px 0px 2px;
2219 input {
2214 input {
2220 padding: 2px;
2215 padding: 2px;
2221 border: none;
2216 border: none;
2222 font-size: @repo-title-fontsize;
2217 font-size: @repo-title-fontsize;
2223 }
2218 }
2224 }
2219 }
2225 }
2220 }
2226
2221
2227
2222
2228 .browser-result{
2223 .browser-result{
2229 td a{
2224 td a{
2230 margin-left: 0.5em;
2225 margin-left: 0.5em;
2231 display: inline-block;
2226 display: inline-block;
2232
2227
2233 em{
2228 em{
2234 font-family: @text-bold;
2229 font-family: @text-bold;
2235 }
2230 }
2236 }
2231 }
2237 }
2232 }
2238
2233
2239 .browser-highlight{
2234 .browser-highlight{
2240 background-color: @grey5-alpha;
2235 background-color: @grey5-alpha;
2241 }
2236 }
2242
2237
2243
2238
2244 // Search
2239 // Search
2245
2240
2246 .search-form{
2241 .search-form{
2247 #q {
2242 #q {
2248 width: @search-form-width;
2243 width: @search-form-width;
2249 }
2244 }
2250 .fields{
2245 .fields{
2251 margin: 0 0 @space;
2246 margin: 0 0 @space;
2252 }
2247 }
2253
2248
2254 label{
2249 label{
2255 display: inline-block;
2250 display: inline-block;
2256 margin-right: @textmargin;
2251 margin-right: @textmargin;
2257 padding-top: 0.25em;
2252 padding-top: 0.25em;
2258 }
2253 }
2259
2254
2260
2255
2261 .results{
2256 .results{
2262 clear: both;
2257 clear: both;
2263 margin: 0 0 @padding;
2258 margin: 0 0 @padding;
2264 }
2259 }
2265 }
2260 }
2266
2261
2267 div.search-feedback-items {
2262 div.search-feedback-items {
2268 display: inline-block;
2263 display: inline-block;
2269 padding:0px 0px 0px 96px;
2264 padding:0px 0px 0px 96px;
2270 }
2265 }
2271
2266
2272 div.search-code-body {
2267 div.search-code-body {
2273 background-color: #ffffff; padding: 5px 0 5px 10px;
2268 background-color: #ffffff; padding: 5px 0 5px 10px;
2274 pre {
2269 pre {
2275 .match { background-color: #faffa6;}
2270 .match { background-color: #faffa6;}
2276 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2271 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2277 }
2272 }
2278 }
2273 }
2279
2274
2280 .expand_commit.search {
2275 .expand_commit.search {
2281 .show_more.open {
2276 .show_more.open {
2282 height: auto;
2277 height: auto;
2283 max-height: none;
2278 max-height: none;
2284 }
2279 }
2285 }
2280 }
2286
2281
2287 .search-results {
2282 .search-results {
2288
2283
2289 h2 {
2284 h2 {
2290 margin-bottom: 0;
2285 margin-bottom: 0;
2291 }
2286 }
2292 .codeblock {
2287 .codeblock {
2293 border: none;
2288 border: none;
2294 background: transparent;
2289 background: transparent;
2295 }
2290 }
2296
2291
2297 .codeblock-header {
2292 .codeblock-header {
2298 border: none;
2293 border: none;
2299 background: transparent;
2294 background: transparent;
2300 }
2295 }
2301
2296
2302 .code-body {
2297 .code-body {
2303 border: @border-thickness solid @border-default-color;
2298 border: @border-thickness solid @border-default-color;
2304 .border-radius(@border-radius);
2299 .border-radius(@border-radius);
2305 }
2300 }
2306
2301
2307 .td-commit {
2302 .td-commit {
2308 &:extend(pre);
2303 &:extend(pre);
2309 border-bottom: @border-thickness solid @border-default-color;
2304 border-bottom: @border-thickness solid @border-default-color;
2310 }
2305 }
2311
2306
2312 .message {
2307 .message {
2313 height: auto;
2308 height: auto;
2314 max-width: 350px;
2309 max-width: 350px;
2315 white-space: normal;
2310 white-space: normal;
2316 text-overflow: initial;
2311 text-overflow: initial;
2317 overflow: visible;
2312 overflow: visible;
2318
2313
2319 .match { background-color: #faffa6;}
2314 .match { background-color: #faffa6;}
2320 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2315 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2321 }
2316 }
2322
2317
2323 }
2318 }
2324
2319
2325 table.rctable td.td-search-results div {
2320 table.rctable td.td-search-results div {
2326 max-width: 100%;
2321 max-width: 100%;
2327 }
2322 }
2328
2323
2329 #tip-box, .tip-box{
2324 #tip-box, .tip-box{
2330 padding: @menupadding/2;
2325 padding: @menupadding/2;
2331 display: block;
2326 display: block;
2332 border: @border-thickness solid @border-highlight-color;
2327 border: @border-thickness solid @border-highlight-color;
2333 .border-radius(@border-radius);
2328 .border-radius(@border-radius);
2334 background-color: white;
2329 background-color: white;
2335 z-index: 99;
2330 z-index: 99;
2336 white-space: pre-wrap;
2331 white-space: pre-wrap;
2337 }
2332 }
2338
2333
2339 #linktt {
2334 #linktt {
2340 width: 79px;
2335 width: 79px;
2341 }
2336 }
2342
2337
2343 #help_kb .modal-content{
2338 #help_kb .modal-content{
2344 max-width: 750px;
2339 max-width: 750px;
2345 margin: 10% auto;
2340 margin: 10% auto;
2346
2341
2347 table{
2342 table{
2348 td,th{
2343 td,th{
2349 border-bottom: none;
2344 border-bottom: none;
2350 line-height: 2.5em;
2345 line-height: 2.5em;
2351 }
2346 }
2352 th{
2347 th{
2353 padding-bottom: @textmargin/2;
2348 padding-bottom: @textmargin/2;
2354 }
2349 }
2355 td.keys{
2350 td.keys{
2356 text-align: center;
2351 text-align: center;
2357 }
2352 }
2358 }
2353 }
2359
2354
2360 .block-left{
2355 .block-left{
2361 width: 45%;
2356 width: 45%;
2362 margin-right: 5%;
2357 margin-right: 5%;
2363 }
2358 }
2364 .modal-footer{
2359 .modal-footer{
2365 clear: both;
2360 clear: both;
2366 }
2361 }
2367 .key.tag{
2362 .key.tag{
2368 padding: 0.5em;
2363 padding: 0.5em;
2369 background-color: @rcblue;
2364 background-color: @rcblue;
2370 color: white;
2365 color: white;
2371 border-color: @rcblue;
2366 border-color: @rcblue;
2372 .box-shadow(none);
2367 .box-shadow(none);
2373 }
2368 }
2374 }
2369 }
2375
2370
2376
2371
2377
2372
2378 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2373 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2379
2374
2380 @import 'statistics-graph';
2375 @import 'statistics-graph';
2381 @import 'tables';
2376 @import 'tables';
2382 @import 'forms';
2377 @import 'forms';
2383 @import 'diff';
2378 @import 'diff';
2384 @import 'summary';
2379 @import 'summary';
2385 @import 'navigation';
2380 @import 'navigation';
2386
2381
2387 //--- SHOW/HIDE SECTIONS --//
2382 //--- SHOW/HIDE SECTIONS --//
2388
2383
2389 .btn-collapse {
2384 .btn-collapse {
2390 float: right;
2385 float: right;
2391 text-align: right;
2386 text-align: right;
2392 font-family: @text-light;
2387 font-family: @text-light;
2393 font-size: @basefontsize;
2388 font-size: @basefontsize;
2394 cursor: pointer;
2389 cursor: pointer;
2395 border: none;
2390 border: none;
2396 color: @rcblue;
2391 color: @rcblue;
2397 }
2392 }
2398
2393
2399 table.rctable,
2394 table.rctable,
2400 table.dataTable {
2395 table.dataTable {
2401 .btn-collapse {
2396 .btn-collapse {
2402 float: right;
2397 float: right;
2403 text-align: right;
2398 text-align: right;
2404 }
2399 }
2405 }
2400 }
2406
2401
2407
2402
2408 // TODO: johbo: Fix for IE10, this avoids that we see a border
2403 // TODO: johbo: Fix for IE10, this avoids that we see a border
2409 // and padding around checkboxes and radio boxes. Move to the right place,
2404 // and padding around checkboxes and radio boxes. Move to the right place,
2410 // or better: Remove this once we did the form refactoring.
2405 // or better: Remove this once we did the form refactoring.
2411 input[type=checkbox],
2406 input[type=checkbox],
2412 input[type=radio] {
2407 input[type=radio] {
2413 padding: 0;
2408 padding: 0;
2414 border: none;
2409 border: none;
2415 }
2410 }
2416
2411
2417 .toggle-ajax-spinner{
2412 .toggle-ajax-spinner{
2418 height: 16px;
2413 height: 16px;
2419 width: 16px;
2414 width: 16px;
2420 }
2415 }
@@ -1,603 +1,586 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
44 $('#save').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
47 $('#save').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0) {
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All individual reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158
159 if (data.rules.voting_groups !== undefined) {
160 $.each(data.rules.voting_groups, function(index, rule_data) {
161 self.$rulesList.append(
162 self.addRule(rule_data.text)
163 )
164 });
165 }
166
158 if (data.rules.use_code_authors_for_review) {
167 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
168 self.$rulesList.append(
160 self.addRule(
169 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
170 _gettext('Reviewers picked from source code changes.'))
162 )
171 )
163 }
172 }
164 if (data.rules.forbid_adding_reviewers) {
173 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
174 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
175 self.$rulesList.append(
167 self.addRule(
176 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
177 _gettext('Adding new reviewers is forbidden.'))
169 )
178 )
170 }
179 }
171 if (data.rules.forbid_author_to_review) {
180 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
181 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
182 self.$rulesList.append(
174 self.addRule(
183 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
184 _gettext('Author is not allowed to be a reviewer.'))
176 )
185 )
177 }
186 }
178 if (data.rules.forbid_commit_author_to_review) {
187 if (data.rules.forbid_commit_author_to_review) {
179
188
180 if (data.rules_data.forbidden_users) {
189 if (data.rules_data.forbidden_users) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 self.forbidReviewUsers.push(member_data)
191 self.forbidReviewUsers.push(member_data)
183 });
192 });
184
193
185 }
194 }
186
195
187 self.$rulesList.append(
196 self.$rulesList.append(
188 self.addRule(
197 self.addRule(
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
198 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 )
199 )
191 }
200 }
192
201
193 return self.forbidReviewUsers
202 return self.forbidReviewUsers
194 };
203 };
195
204
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
197
206
198 if (self.currentRequest) {
207 if (self.currentRequest) {
199 // make sure we cleanup old running requests before triggering this
208 // make sure we cleanup old running requests before triggering this
200 // again
209 // again
201 self.currentRequest.abort();
210 self.currentRequest.abort();
202 }
211 }
203
212
204 $('.calculate-reviewers').show();
213 $('.calculate-reviewers').show();
205 // reset reviewer members
214 // reset reviewer members
206 self.$reviewMembers.empty();
215 self.$reviewMembers.empty();
207
216
208 prButtonLock(true, null, 'reviewers');
217 prButtonLock(true, null, 'reviewers');
209 $('#user').hide(); // hide user autocomplete before load
218 $('#user').hide(); // hide user autocomplete before load
210
219
211 var url = pyroutes.url('repo_default_reviewers_data',
220 var url = pyroutes.url('repo_default_reviewers_data',
212 {
221 {
213 'repo_name': templateContext.repo_name,
222 'repo_name': templateContext.repo_name,
214 'source_repo': sourceRepo,
223 'source_repo': sourceRepo,
215 'source_ref': sourceRef[2],
224 'source_ref': sourceRef[2],
216 'target_repo': targetRepo,
225 'target_repo': targetRepo,
217 'target_ref': targetRef[2]
226 'target_ref': targetRef[2]
218 });
227 });
219
228
220 self.currentRequest = $.get(url)
229 self.currentRequest = $.get(url)
221 .done(function(data) {
230 .done(function(data) {
222 self.currentRequest = null;
231 self.currentRequest = null;
223
232
224 // review rules
233 // review rules
225 self.loadReviewRules(data);
234 self.loadReviewRules(data);
226
235
227 for (var i = 0; i < data.reviewers.length; i++) {
236 for (var i = 0; i < data.reviewers.length; i++) {
228 var reviewer = data.reviewers[i];
237 var reviewer = data.reviewers[i];
229 self.addReviewMember(
238 self.addReviewMember(
230 reviewer.user_id, reviewer.first_name,
239 reviewer, reviewer.reasons, reviewer.mandatory);
231 reviewer.last_name, reviewer.username,
232 reviewer.gravatar_link, reviewer.reasons,
233 reviewer.mandatory);
234 }
240 }
235 $('.calculate-reviewers').hide();
241 $('.calculate-reviewers').hide();
236 prButtonLock(false, null, 'reviewers');
242 prButtonLock(false, null, 'reviewers');
237 $('#user').show(); // show user autocomplete after load
243 $('#user').show(); // show user autocomplete after load
238 });
244 });
239 };
245 };
240
246
241 // check those, refactor
247 // check those, refactor
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
248 this.removeReviewMember = function(reviewer_id, mark_delete) {
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
249 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
244
250
245 if(typeof(mark_delete) === undefined){
251 if(typeof(mark_delete) === undefined){
246 mark_delete = false;
252 mark_delete = false;
247 }
253 }
248
254
249 if(mark_delete === true){
255 if(mark_delete === true){
250 if (reviewer){
256 if (reviewer){
251 // now delete the input
257 // now delete the input
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
258 $('#reviewer_{0} input'.format(reviewer_id)).remove();
253 // mark as to-delete
259 // mark as to-delete
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
260 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
255 obj.addClass('to-delete');
261 obj.addClass('to-delete');
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
262 obj.css({"text-decoration":"line-through", "opacity": 0.5});
257 }
263 }
258 }
264 }
259 else{
265 else{
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
266 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 }
267 }
262 };
268 };
269 this.reviewMemberEntry = function() {
263
270
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
271 };
272 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
265 var members = self.$reviewMembers.get(0);
273 var members = self.$reviewMembers.get(0);
266 var reasons_html = '';
274 var id = reviewer_obj.user_id;
267 var reasons_inputs = '';
275 var username = reviewer_obj.username;
276
268 var reasons = reasons || [];
277 var reasons = reasons || [];
269 var mandatory = mandatory || false;
278 var mandatory = mandatory || false;
270
279
271 if (reasons) {
280 // register IDS to check if we don't have this ID already in
272 for (var i = 0; i < reasons.length; i++) {
281 var currentIds = [];
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 }
276 }
277 var tmpl = '' +
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 '<div class="reviewer_status">'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 '</div>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 '<span class="reviewer_name user">{1}</span>'+
285 reasons_html +
286 '<input type="hidden" name="user_id" value="{2}">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 '{3}'+
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290
291 if (mandatory) {
292 tmpl += ''+
293 '<div class="reviewer_member_mandatory_remove">' +
294 '<i class="icon-remove-sign"></i>'+
295 '</div>' +
296 '<input type="hidden" name="mandatory" value="true">'+
297 '<div class="reviewer_member_mandatory">' +
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 '</div>';
300
301 } else {
302 tmpl += ''+
303 '<input type="hidden" name="mandatory" value="false">'+
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 '<i class="icon-remove-sign"></i>'+
306 '</div>';
307 }
308 // continue template
309 tmpl += ''+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 '</li>' ;
312
313 var displayname = "{0} ({1} {2})".format(
314 nname, escapeHtml(fname), escapeHtml(lname));
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 // check if we don't have this ID already in
317 var ids = [];
318 var _els = self.$reviewMembers.find('li').toArray();
282 var _els = self.$reviewMembers.find('li').toArray();
319 for (el in _els){
283 for (el in _els){
320 ids.push(_els[el].id)
284 currentIds.push(_els[el].id)
321 }
285 }
322
286
323 var userAllowedReview = function(userId) {
287 var userAllowedReview = function(userId) {
324 var allowed = true;
288 var allowed = true;
325 $.each(self.forbidReviewUsers, function(index, member_data) {
289 $.each(self.forbidReviewUsers, function(index, member_data) {
326 if (parseInt(userId) === member_data['user_id']) {
290 if (parseInt(userId) === member_data['user_id']) {
327 allowed = false;
291 allowed = false;
328 return false // breaks the loop
292 return false // breaks the loop
329 }
293 }
330 });
294 });
331 return allowed
295 return allowed
332 };
296 };
333
297
334 var userAllowed = userAllowedReview(id);
298 var userAllowed = userAllowedReview(id);
335 if (!userAllowed){
299 if (!userAllowed){
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
300 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
337 }
301 } else {
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
302 // only add if it's not there
303 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
339
304
340 if(shouldAdd) {
305 if (alreadyReviewer) {
341 // only add if it's not there
306 alert(_gettext('User `{0}` already in reviewers').format(username));
342 members.innerHTML += element;
307 } else {
308 members.innerHTML += renderTemplate('reviewMemberEntry', {
309 'member': reviewer_obj,
310 'mandatory': mandatory,
311 'allowed_to_update': true,
312 'review_status': 'not_reviewed',
313 'review_status_label': _gettext('Not Reviewed'),
314 'reasons': reasons
315 });
316 }
343 }
317 }
344
318
345 };
319 };
346
320
347 this.updateReviewers = function(repo_name, pull_request_id){
321 this.updateReviewers = function(repo_name, pull_request_id){
348 var postData = '_method=put&' + $('#reviewers input').serialize();
322 var postData = $('#reviewers input').serialize();
349 _updatePullRequest(repo_name, pull_request_id, postData);
323 _updatePullRequest(repo_name, pull_request_id, postData);
350 };
324 };
351
325
352 };
326 };
353
327
354
328
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
329 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
356 var url = pyroutes.url(
330 var url = pyroutes.url(
357 'pullrequest_update',
331 'pullrequest_update',
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
332 {"repo_name": repo_name, "pull_request_id": pull_request_id});
359 if (typeof postData === 'string' ) {
333 if (typeof postData === 'string' ) {
360 postData += '&csrf_token=' + CSRF_TOKEN;
334 postData += '&csrf_token=' + CSRF_TOKEN;
361 } else {
335 } else {
362 postData.csrf_token = CSRF_TOKEN;
336 postData.csrf_token = CSRF_TOKEN;
363 }
337 }
364 var success = function(o) {
338 var success = function(o) {
365 window.location.reload();
339 window.location.reload();
366 };
340 };
367 ajaxPOST(url, postData, success);
341 ajaxPOST(url, postData, success);
368 };
342 };
369
343
370 /**
344 /**
371 * PULL REQUEST update commits
345 * PULL REQUEST update commits
372 */
346 */
373 var updateCommits = function(repo_name, pull_request_id) {
347 var updateCommits = function(repo_name, pull_request_id) {
374 var postData = {
348 var postData = {
375 'update_commits': true};
349 'update_commits': true};
376 _updatePullRequest(repo_name, pull_request_id, postData);
350 _updatePullRequest(repo_name, pull_request_id, postData);
377 };
351 };
378
352
379
353
380 /**
354 /**
381 * PULL REQUEST edit info
355 * PULL REQUEST edit info
382 */
356 */
383 var editPullRequest = function(repo_name, pull_request_id, title, description) {
357 var editPullRequest = function(repo_name, pull_request_id, title, description) {
384 var url = pyroutes.url(
358 var url = pyroutes.url(
385 'pullrequest_update',
359 'pullrequest_update',
386 {"repo_name": repo_name, "pull_request_id": pull_request_id});
360 {"repo_name": repo_name, "pull_request_id": pull_request_id});
387
361
388 var postData = {
362 var postData = {
389 'title': title,
363 'title': title,
390 'description': description,
364 'description': description,
391 'edit_pull_request': true,
365 'edit_pull_request': true,
392 'csrf_token': CSRF_TOKEN
366 'csrf_token': CSRF_TOKEN
393 };
367 };
394 var success = function(o) {
368 var success = function(o) {
395 window.location.reload();
369 window.location.reload();
396 };
370 };
397 ajaxPOST(url, postData, success);
371 ajaxPOST(url, postData, success);
398 };
372 };
399
373
400 var initPullRequestsCodeMirror = function (textAreaId) {
374 var initPullRequestsCodeMirror = function (textAreaId) {
401 var ta = $(textAreaId).get(0);
375 var ta = $(textAreaId).get(0);
402 var initialHeight = '100px';
376 var initialHeight = '100px';
403
377
404 // default options
378 // default options
405 var codeMirrorOptions = {
379 var codeMirrorOptions = {
406 mode: "text",
380 mode: "text",
407 lineNumbers: false,
381 lineNumbers: false,
408 indentUnit: 4,
382 indentUnit: 4,
409 theme: 'rc-input'
383 theme: 'rc-input'
410 };
384 };
411
385
412 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
386 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
413 // marker for manually set description
387 // marker for manually set description
414 codeMirrorInstance._userDefinedDesc = false;
388 codeMirrorInstance._userDefinedDesc = false;
415 codeMirrorInstance.setSize(null, initialHeight);
389 codeMirrorInstance.setSize(null, initialHeight);
416 codeMirrorInstance.on("change", function(instance, changeObj) {
390 codeMirrorInstance.on("change", function(instance, changeObj) {
417 var height = initialHeight;
391 var height = initialHeight;
418 var lines = instance.lineCount();
392 var lines = instance.lineCount();
419 if (lines > 6 && lines < 20) {
393 if (lines > 6 && lines < 20) {
420 height = "auto"
394 height = "auto"
421 }
395 }
422 else if (lines >= 20) {
396 else if (lines >= 20) {
423 height = 20 * 15;
397 height = 20 * 15;
424 }
398 }
425 instance.setSize(null, height);
399 instance.setSize(null, height);
426
400
427 // detect if the change was trigger by auto desc, or user input
401 // detect if the change was trigger by auto desc, or user input
428 changeOrigin = changeObj.origin;
402 changeOrigin = changeObj.origin;
429
403
430 if (changeOrigin === "setValue") {
404 if (changeOrigin === "setValue") {
431 cmLog.debug('Change triggered by setValue');
405 cmLog.debug('Change triggered by setValue');
432 }
406 }
433 else {
407 else {
434 cmLog.debug('user triggered change !');
408 cmLog.debug('user triggered change !');
435 // set special marker to indicate user has created an input.
409 // set special marker to indicate user has created an input.
436 instance._userDefinedDesc = true;
410 instance._userDefinedDesc = true;
437 }
411 }
438
412
439 });
413 });
440
414
441 return codeMirrorInstance
415 return codeMirrorInstance
442 };
416 };
443
417
444 /**
418 /**
445 * Reviewer autocomplete
419 * Reviewer autocomplete
446 */
420 */
447 var ReviewerAutoComplete = function(inputId) {
421 var ReviewerAutoComplete = function(inputId) {
448 $(inputId).autocomplete({
422 $(inputId).autocomplete({
449 serviceUrl: pyroutes.url('user_autocomplete_data'),
423 serviceUrl: pyroutes.url('user_autocomplete_data'),
450 minChars:2,
424 minChars:2,
451 maxHeight:400,
425 maxHeight:400,
452 deferRequestBy: 300, //miliseconds
426 deferRequestBy: 300, //miliseconds
453 showNoSuggestionNotice: true,
427 showNoSuggestionNotice: true,
454 tabDisabled: true,
428 tabDisabled: true,
455 autoSelectFirst: true,
429 autoSelectFirst: true,
456 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
430 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
457 formatResult: autocompleteFormatResult,
431 formatResult: autocompleteFormatResult,
458 lookupFilter: autocompleteFilterResult,
432 lookupFilter: autocompleteFilterResult,
459 onSelect: function(element, data) {
433 onSelect: function(element, data) {
434 var mandatory = false;
435 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
460
436
461 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
437 // add whole user groups
462 if (data.value_type == 'user_group') {
438 if (data.value_type == 'user_group') {
463 reasons.push(_gettext('member of "{0}"').format(data.value_display));
439 reasons.push(_gettext('member of "{0}"').format(data.value_display));
464
440
465 $.each(data.members, function(index, member_data) {
441 $.each(data.members, function(index, member_data) {
466 reviewersController.addReviewMember(
442 var reviewer = member_data;
467 member_data.id, member_data.first_name, member_data.last_name,
443 reviewer['user_id'] = member_data['id'];
468 member_data.username, member_data.icon_link, reasons);
444 reviewer['gravatar_link'] = member_data['icon_link'];
445 reviewer['user_link'] = member_data['profile_link'];
446 reviewer['rules'] = [];
447 reviewersController.addReviewMember(reviewer, reasons, mandatory);
469 })
448 })
470
449 }
471 } else {
450 // add single user
472 reviewersController.addReviewMember(
451 else {
473 data.id, data.first_name, data.last_name,
452 var reviewer = data;
474 data.username, data.icon_link, reasons);
453 reviewer['user_id'] = data['id'];
454 reviewer['gravatar_link'] = data['icon_link'];
455 reviewer['user_link'] = data['profile_link'];
456 reviewer['rules'] = [];
457 reviewersController.addReviewMember(reviewer, reasons, mandatory);
475 }
458 }
476
459
477 $(inputId).val('');
460 $(inputId).val('');
478 }
461 }
479 });
462 });
480 };
463 };
481
464
482
465
483 VersionController = function () {
466 VersionController = function () {
484 var self = this;
467 var self = this;
485 this.$verSource = $('input[name=ver_source]');
468 this.$verSource = $('input[name=ver_source]');
486 this.$verTarget = $('input[name=ver_target]');
469 this.$verTarget = $('input[name=ver_target]');
487 this.$showVersionDiff = $('#show-version-diff');
470 this.$showVersionDiff = $('#show-version-diff');
488
471
489 this.adjustRadioSelectors = function (curNode) {
472 this.adjustRadioSelectors = function (curNode) {
490 var getVal = function (item) {
473 var getVal = function (item) {
491 if (item == 'latest') {
474 if (item == 'latest') {
492 return Number.MAX_SAFE_INTEGER
475 return Number.MAX_SAFE_INTEGER
493 }
476 }
494 else {
477 else {
495 return parseInt(item)
478 return parseInt(item)
496 }
479 }
497 };
480 };
498
481
499 var curVal = getVal($(curNode).val());
482 var curVal = getVal($(curNode).val());
500 var cleared = false;
483 var cleared = false;
501
484
502 $.each(self.$verSource, function (index, value) {
485 $.each(self.$verSource, function (index, value) {
503 var elVal = getVal($(value).val());
486 var elVal = getVal($(value).val());
504
487
505 if (elVal > curVal) {
488 if (elVal > curVal) {
506 if ($(value).is(':checked')) {
489 if ($(value).is(':checked')) {
507 cleared = true;
490 cleared = true;
508 }
491 }
509 $(value).attr('disabled', 'disabled');
492 $(value).attr('disabled', 'disabled');
510 $(value).removeAttr('checked');
493 $(value).removeAttr('checked');
511 $(value).css({'opacity': 0.1});
494 $(value).css({'opacity': 0.1});
512 }
495 }
513 else {
496 else {
514 $(value).css({'opacity': 1});
497 $(value).css({'opacity': 1});
515 $(value).removeAttr('disabled');
498 $(value).removeAttr('disabled');
516 }
499 }
517 });
500 });
518
501
519 if (cleared) {
502 if (cleared) {
520 // if we unchecked an active, set the next one to same loc.
503 // if we unchecked an active, set the next one to same loc.
521 $(this.$verSource).filter('[value={0}]'.format(
504 $(this.$verSource).filter('[value={0}]'.format(
522 curVal)).attr('checked', 'checked');
505 curVal)).attr('checked', 'checked');
523 }
506 }
524
507
525 self.setLockAction(false,
508 self.setLockAction(false,
526 $(curNode).data('verPos'),
509 $(curNode).data('verPos'),
527 $(this.$verSource).filter(':checked').data('verPos')
510 $(this.$verSource).filter(':checked').data('verPos')
528 );
511 );
529 };
512 };
530
513
531
514
532 this.attachVersionListener = function () {
515 this.attachVersionListener = function () {
533 self.$verTarget.change(function (e) {
516 self.$verTarget.change(function (e) {
534 self.adjustRadioSelectors(this)
517 self.adjustRadioSelectors(this)
535 });
518 });
536 self.$verSource.change(function (e) {
519 self.$verSource.change(function (e) {
537 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
520 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
538 });
521 });
539 };
522 };
540
523
541 this.init = function () {
524 this.init = function () {
542
525
543 var curNode = self.$verTarget.filter(':checked');
526 var curNode = self.$verTarget.filter(':checked');
544 self.adjustRadioSelectors(curNode);
527 self.adjustRadioSelectors(curNode);
545 self.setLockAction(true);
528 self.setLockAction(true);
546 self.attachVersionListener();
529 self.attachVersionListener();
547
530
548 };
531 };
549
532
550 this.setLockAction = function (state, selectedVersion, otherVersion) {
533 this.setLockAction = function (state, selectedVersion, otherVersion) {
551 var $showVersionDiff = this.$showVersionDiff;
534 var $showVersionDiff = this.$showVersionDiff;
552
535
553 if (state) {
536 if (state) {
554 $showVersionDiff.attr('disabled', 'disabled');
537 $showVersionDiff.attr('disabled', 'disabled');
555 $showVersionDiff.addClass('disabled');
538 $showVersionDiff.addClass('disabled');
556 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
539 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
557 }
540 }
558 else {
541 else {
559 $showVersionDiff.removeAttr('disabled');
542 $showVersionDiff.removeAttr('disabled');
560 $showVersionDiff.removeClass('disabled');
543 $showVersionDiff.removeClass('disabled');
561
544
562 if (selectedVersion == otherVersion) {
545 if (selectedVersion == otherVersion) {
563 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
546 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
564 } else {
547 } else {
565 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
548 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
566 }
549 }
567 }
550 }
568
551
569 };
552 };
570
553
571 this.showVersionDiff = function () {
554 this.showVersionDiff = function () {
572 var target = self.$verTarget.filter(':checked');
555 var target = self.$verTarget.filter(':checked');
573 var source = self.$verSource.filter(':checked');
556 var source = self.$verSource.filter(':checked');
574
557
575 if (target.val() && source.val()) {
558 if (target.val() && source.val()) {
576 var params = {
559 var params = {
577 'pull_request_id': templateContext.pull_request_data.pull_request_id,
560 'pull_request_id': templateContext.pull_request_data.pull_request_id,
578 'repo_name': templateContext.repo_name,
561 'repo_name': templateContext.repo_name,
579 'version': target.val(),
562 'version': target.val(),
580 'from_version': source.val()
563 'from_version': source.val()
581 };
564 };
582 window.location = pyroutes.url('pullrequest_show', params)
565 window.location = pyroutes.url('pullrequest_show', params)
583 }
566 }
584
567
585 return false;
568 return false;
586 };
569 };
587
570
588 this.toggleVersionView = function (elem) {
571 this.toggleVersionView = function (elem) {
589
572
590 if (this.$showVersionDiff.is(':visible')) {
573 if (this.$showVersionDiff.is(':visible')) {
591 $('.version-pr').hide();
574 $('.version-pr').hide();
592 this.$showVersionDiff.hide();
575 this.$showVersionDiff.hide();
593 $(elem).html($(elem).data('toggleOn'))
576 $(elem).html($(elem).data('toggleOn'))
594 } else {
577 } else {
595 $('.version-pr').show();
578 $('.version-pr').show();
596 this.$showVersionDiff.show();
579 this.$showVersionDiff.show();
597 $(elem).html($(elem).data('toggleOff'))
580 $(elem).html($(elem).data('toggleOff'))
598 }
581 }
599
582
600 return false
583 return false
601 }
584 }
602
585
603 }; No newline at end of file
586 };
@@ -1,609 +1,611 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="root.mako"/>
2 <%inherit file="root.mako"/>
3
3
4 <%include file="/ejs_templates/templates.html"/>
5
4 <div class="outerwrapper">
6 <div class="outerwrapper">
5 <!-- HEADER -->
7 <!-- HEADER -->
6 <div class="header">
8 <div class="header">
7 <div id="header-inner" class="wrapper">
9 <div id="header-inner" class="wrapper">
8 <div id="logo">
10 <div id="logo">
9 <div class="logo-wrapper">
11 <div class="logo-wrapper">
10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 </div>
13 </div>
12 %if c.rhodecode_name:
14 %if c.rhodecode_name:
13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
15 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 %endif
16 %endif
15 </div>
17 </div>
16 <!-- MENU BAR NAV -->
18 <!-- MENU BAR NAV -->
17 ${self.menu_bar_nav()}
19 ${self.menu_bar_nav()}
18 <!-- END MENU BAR NAV -->
20 <!-- END MENU BAR NAV -->
19 </div>
21 </div>
20 </div>
22 </div>
21 ${self.menu_bar_subnav()}
23 ${self.menu_bar_subnav()}
22 <!-- END HEADER -->
24 <!-- END HEADER -->
23
25
24 <!-- CONTENT -->
26 <!-- CONTENT -->
25 <div id="content" class="wrapper">
27 <div id="content" class="wrapper">
26
28
27 <rhodecode-toast id="notifications"></rhodecode-toast>
29 <rhodecode-toast id="notifications"></rhodecode-toast>
28
30
29 <div class="main">
31 <div class="main">
30 ${next.main()}
32 ${next.main()}
31 </div>
33 </div>
32 </div>
34 </div>
33 <!-- END CONTENT -->
35 <!-- END CONTENT -->
34
36
35 </div>
37 </div>
36 <!-- FOOTER -->
38 <!-- FOOTER -->
37 <div id="footer">
39 <div id="footer">
38 <div id="footer-inner" class="title wrapper">
40 <div id="footer-inner" class="title wrapper">
39 <div>
41 <div>
40 <p class="footer-link-right">
42 <p class="footer-link-right">
41 % if c.visual.show_version:
43 % if c.visual.show_version:
42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
44 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 % endif
45 % endif
44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
46 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 % if c.visual.rhodecode_support_url:
47 % if c.visual.rhodecode_support_url:
46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
48 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 % endif
49 % endif
48 </p>
50 </p>
49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 <p class="server-instance" style="display:${sid}">
52 <p class="server-instance" style="display:${sid}">
51 ## display hidden instance ID if specially defined
53 ## display hidden instance ID if specially defined
52 % if c.rhodecode_instanceid:
54 % if c.rhodecode_instanceid:
53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
55 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 % endif
56 % endif
55 </p>
57 </p>
56 </div>
58 </div>
57 </div>
59 </div>
58 </div>
60 </div>
59
61
60 <!-- END FOOTER -->
62 <!-- END FOOTER -->
61
63
62 ### MAKO DEFS ###
64 ### MAKO DEFS ###
63
65
64 <%def name="menu_bar_subnav()">
66 <%def name="menu_bar_subnav()">
65 </%def>
67 </%def>
66
68
67 <%def name="breadcrumbs(class_='breadcrumbs')">
69 <%def name="breadcrumbs(class_='breadcrumbs')">
68 <div class="${class_}">
70 <div class="${class_}">
69 ${self.breadcrumbs_links()}
71 ${self.breadcrumbs_links()}
70 </div>
72 </div>
71 </%def>
73 </%def>
72
74
73 <%def name="admin_menu()">
75 <%def name="admin_menu()">
74 <ul class="admin_menu submenu">
76 <ul class="admin_menu submenu">
75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
77 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
78 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
77 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
79 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
80 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
81 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
80 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
82 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
83 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
84 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
85 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
86 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
85 </ul>
87 </ul>
86 </%def>
88 </%def>
87
89
88
90
89 <%def name="dt_info_panel(elements)">
91 <%def name="dt_info_panel(elements)">
90 <dl class="dl-horizontal">
92 <dl class="dl-horizontal">
91 %for dt, dd, title, show_items in elements:
93 %for dt, dd, title, show_items in elements:
92 <dt>${dt}:</dt>
94 <dt>${dt}:</dt>
93 <dd title="${h.tooltip(title)}">
95 <dd title="${h.tooltip(title)}">
94 %if callable(dd):
96 %if callable(dd):
95 ## allow lazy evaluation of elements
97 ## allow lazy evaluation of elements
96 ${dd()}
98 ${dd()}
97 %else:
99 %else:
98 ${dd}
100 ${dd}
99 %endif
101 %endif
100 %if show_items:
102 %if show_items:
101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
103 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 %endif
104 %endif
103 </dd>
105 </dd>
104
106
105 %if show_items:
107 %if show_items:
106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
108 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 %for item in show_items:
109 %for item in show_items:
108 <dt></dt>
110 <dt></dt>
109 <dd>${item}</dd>
111 <dd>${item}</dd>
110 %endfor
112 %endfor
111 </div>
113 </div>
112 %endif
114 %endif
113
115
114 %endfor
116 %endfor
115 </dl>
117 </dl>
116 </%def>
118 </%def>
117
119
118
120
119 <%def name="gravatar(email, size=16)">
121 <%def name="gravatar(email, size=16)">
120 <%
122 <%
121 if (size > 16):
123 if (size > 16):
122 gravatar_class = 'gravatar gravatar-large'
124 gravatar_class = 'gravatar gravatar-large'
123 else:
125 else:
124 gravatar_class = 'gravatar'
126 gravatar_class = 'gravatar'
125 %>
127 %>
126 <%doc>
128 <%doc>
127 TODO: johbo: For now we serve double size images to make it smooth
129 TODO: johbo: For now we serve double size images to make it smooth
128 for retina. This is how it worked until now. Should be replaced
130 for retina. This is how it worked until now. Should be replaced
129 with a better solution at some point.
131 with a better solution at some point.
130 </%doc>
132 </%doc>
131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
133 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 </%def>
134 </%def>
133
135
134
136
135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
137 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 <% email = h.email_or_none(contact) %>
138 <% email = h.email_or_none(contact) %>
137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
139 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 ${self.gravatar(email, size)}
140 ${self.gravatar(email, size)}
139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
141 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 </div>
142 </div>
141 </%def>
143 </%def>
142
144
143
145
144 ## admin menu used for people that have some admin resources
146 ## admin menu used for people that have some admin resources
145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
147 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 <ul class="submenu">
148 <ul class="submenu">
147 %if repositories:
149 %if repositories:
148 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
150 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
149 %endif
151 %endif
150 %if repository_groups:
152 %if repository_groups:
151 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
153 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
152 %endif
154 %endif
153 %if user_groups:
155 %if user_groups:
154 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
156 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
155 %endif
157 %endif
156 </ul>
158 </ul>
157 </%def>
159 </%def>
158
160
159 <%def name="repo_page_title(repo_instance)">
161 <%def name="repo_page_title(repo_instance)">
160 <div class="title-content">
162 <div class="title-content">
161 <div class="title-main">
163 <div class="title-main">
162 ## SVN/HG/GIT icons
164 ## SVN/HG/GIT icons
163 %if h.is_hg(repo_instance):
165 %if h.is_hg(repo_instance):
164 <i class="icon-hg"></i>
166 <i class="icon-hg"></i>
165 %endif
167 %endif
166 %if h.is_git(repo_instance):
168 %if h.is_git(repo_instance):
167 <i class="icon-git"></i>
169 <i class="icon-git"></i>
168 %endif
170 %endif
169 %if h.is_svn(repo_instance):
171 %if h.is_svn(repo_instance):
170 <i class="icon-svn"></i>
172 <i class="icon-svn"></i>
171 %endif
173 %endif
172
174
173 ## public/private
175 ## public/private
174 %if repo_instance.private:
176 %if repo_instance.private:
175 <i class="icon-repo-private"></i>
177 <i class="icon-repo-private"></i>
176 %else:
178 %else:
177 <i class="icon-repo-public"></i>
179 <i class="icon-repo-public"></i>
178 %endif
180 %endif
179
181
180 ## repo name with group name
182 ## repo name with group name
181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
183 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182
184
183 </div>
185 </div>
184
186
185 ## FORKED
187 ## FORKED
186 %if repo_instance.fork:
188 %if repo_instance.fork:
187 <p>
189 <p>
188 <i class="icon-code-fork"></i> ${_('Fork of')}
190 <i class="icon-code-fork"></i> ${_('Fork of')}
189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
191 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 </p>
192 </p>
191 %endif
193 %endif
192
194
193 ## IMPORTED FROM REMOTE
195 ## IMPORTED FROM REMOTE
194 %if repo_instance.clone_uri:
196 %if repo_instance.clone_uri:
195 <p>
197 <p>
196 <i class="icon-code-fork"></i> ${_('Clone from')}
198 <i class="icon-code-fork"></i> ${_('Clone from')}
197 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
199 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 </p>
200 </p>
199 %endif
201 %endif
200
202
201 ## LOCKING STATUS
203 ## LOCKING STATUS
202 %if repo_instance.locked[0]:
204 %if repo_instance.locked[0]:
203 <p class="locking_locked">
205 <p class="locking_locked">
204 <i class="icon-repo-lock"></i>
206 <i class="icon-repo-lock"></i>
205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
207 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 </p>
208 </p>
207 %elif repo_instance.enable_locking:
209 %elif repo_instance.enable_locking:
208 <p class="locking_unlocked">
210 <p class="locking_unlocked">
209 <i class="icon-repo-unlock"></i>
211 <i class="icon-repo-unlock"></i>
210 ${_('Repository not locked. Pull repository to lock it.')}
212 ${_('Repository not locked. Pull repository to lock it.')}
211 </p>
213 </p>
212 %endif
214 %endif
213
215
214 </div>
216 </div>
215 </%def>
217 </%def>
216
218
217 <%def name="repo_menu(active=None)">
219 <%def name="repo_menu(active=None)">
218 <%
220 <%
219 def is_active(selected):
221 def is_active(selected):
220 if selected == active:
222 if selected == active:
221 return "active"
223 return "active"
222 %>
224 %>
223
225
224 <!--- CONTEXT BAR -->
226 <!--- CONTEXT BAR -->
225 <div id="context-bar">
227 <div id="context-bar">
226 <div class="wrapper">
228 <div class="wrapper">
227 <ul id="context-pages" class="horizontal-list navigation">
229 <ul id="context-pages" class="horizontal-list navigation">
228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
230 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
231 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
232 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
231 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
233 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
232 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
233 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
234 <li class="${is_active('showpullrequest')}">
236 <li class="${is_active('showpullrequest')}">
235 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
236 %if c.repository_pull_requests:
238 %if c.repository_pull_requests:
237 <span class="pr_notifications">${c.repository_pull_requests}</span>
239 <span class="pr_notifications">${c.repository_pull_requests}</span>
238 %endif
240 %endif
239 <div class="menulabel">${_('Pull Requests')}</div>
241 <div class="menulabel">${_('Pull Requests')}</div>
240 </a>
242 </a>
241 </li>
243 </li>
242 %endif
244 %endif
243 <li class="${is_active('options')}">
245 <li class="${is_active('options')}">
244 <a class="menulink dropdown">
246 <a class="menulink dropdown">
245 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
246 </a>
248 </a>
247 <ul class="submenu">
249 <ul class="submenu">
248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
250 %endif
252 %endif
251 %if c.rhodecode_db_repo.fork:
253 %if c.rhodecode_db_repo.fork:
252 <li>
254 <li>
253 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
255 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
254 href="${h.route_path('repo_compare',
256 href="${h.route_path('repo_compare',
255 repo_name=c.rhodecode_db_repo.fork.repo_name,
257 repo_name=c.rhodecode_db_repo.fork.repo_name,
256 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
258 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
257 source_ref=c.rhodecode_db_repo.landing_rev[1],
259 source_ref=c.rhodecode_db_repo.landing_rev[1],
258 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
260 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
259 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
261 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
260 _query=dict(merge=1))}"
262 _query=dict(merge=1))}"
261 >
263 >
262 ${_('Compare fork')}
264 ${_('Compare fork')}
263 </a>
265 </a>
264 </li>
266 </li>
265 %endif
267 %endif
266
268
267 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
269 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
268
270
269 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
271 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
270 %if c.rhodecode_db_repo.locked[0]:
272 %if c.rhodecode_db_repo.locked[0]:
271 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
273 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
272 %else:
274 %else:
273 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
275 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
274 %endif
276 %endif
275 %endif
277 %endif
276 %if c.rhodecode_user.username != h.DEFAULT_USER:
278 %if c.rhodecode_user.username != h.DEFAULT_USER:
277 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
279 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
278 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
280 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
279 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
281 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
280 %endif
282 %endif
281 %endif
283 %endif
282 </ul>
284 </ul>
283 </li>
285 </li>
284 </ul>
286 </ul>
285 </div>
287 </div>
286 <div class="clear"></div>
288 <div class="clear"></div>
287 </div>
289 </div>
288 <!--- END CONTEXT BAR -->
290 <!--- END CONTEXT BAR -->
289
291
290 </%def>
292 </%def>
291
293
292 <%def name="usermenu(active=False)">
294 <%def name="usermenu(active=False)">
293 ## USER MENU
295 ## USER MENU
294 <li id="quick_login_li" class="${'active' if active else ''}">
296 <li id="quick_login_li" class="${'active' if active else ''}">
295 <a id="quick_login_link" class="menulink childs">
297 <a id="quick_login_link" class="menulink childs">
296 ${gravatar(c.rhodecode_user.email, 20)}
298 ${gravatar(c.rhodecode_user.email, 20)}
297 <span class="user">
299 <span class="user">
298 %if c.rhodecode_user.username != h.DEFAULT_USER:
300 %if c.rhodecode_user.username != h.DEFAULT_USER:
299 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
301 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
300 %else:
302 %else:
301 <span>${_('Sign in')}</span>
303 <span>${_('Sign in')}</span>
302 %endif
304 %endif
303 </span>
305 </span>
304 </a>
306 </a>
305
307
306 <div class="user-menu submenu">
308 <div class="user-menu submenu">
307 <div id="quick_login">
309 <div id="quick_login">
308 %if c.rhodecode_user.username == h.DEFAULT_USER:
310 %if c.rhodecode_user.username == h.DEFAULT_USER:
309 <h4>${_('Sign in to your account')}</h4>
311 <h4>${_('Sign in to your account')}</h4>
310 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
312 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
311 <div class="form form-vertical">
313 <div class="form form-vertical">
312 <div class="fields">
314 <div class="fields">
313 <div class="field">
315 <div class="field">
314 <div class="label">
316 <div class="label">
315 <label for="username">${_('Username')}:</label>
317 <label for="username">${_('Username')}:</label>
316 </div>
318 </div>
317 <div class="input">
319 <div class="input">
318 ${h.text('username',class_='focus',tabindex=1)}
320 ${h.text('username',class_='focus',tabindex=1)}
319 </div>
321 </div>
320
322
321 </div>
323 </div>
322 <div class="field">
324 <div class="field">
323 <div class="label">
325 <div class="label">
324 <label for="password">${_('Password')}:</label>
326 <label for="password">${_('Password')}:</label>
325 %if h.HasPermissionAny('hg.password_reset.enabled')():
327 %if h.HasPermissionAny('hg.password_reset.enabled')():
326 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
328 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
327 %endif
329 %endif
328 </div>
330 </div>
329 <div class="input">
331 <div class="input">
330 ${h.password('password',class_='focus',tabindex=2)}
332 ${h.password('password',class_='focus',tabindex=2)}
331 </div>
333 </div>
332 </div>
334 </div>
333 <div class="buttons">
335 <div class="buttons">
334 <div class="register">
336 <div class="register">
335 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
337 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
336 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
338 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
337 %endif
339 %endif
338 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
340 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
339 </div>
341 </div>
340 <div class="submit">
342 <div class="submit">
341 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
343 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
342 </div>
344 </div>
343 </div>
345 </div>
344 </div>
346 </div>
345 </div>
347 </div>
346 ${h.end_form()}
348 ${h.end_form()}
347 %else:
349 %else:
348 <div class="">
350 <div class="">
349 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
351 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
350 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
352 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
351 <div class="email">${c.rhodecode_user.email}</div>
353 <div class="email">${c.rhodecode_user.email}</div>
352 </div>
354 </div>
353 <div class="">
355 <div class="">
354 <ol class="links">
356 <ol class="links">
355 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
357 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
356 % if c.rhodecode_user.personal_repo_group:
358 % if c.rhodecode_user.personal_repo_group:
357 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
359 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
358 % endif
360 % endif
359 <li class="logout">
361 <li class="logout">
360 ${h.secure_form(h.route_path('logout'), request=request)}
362 ${h.secure_form(h.route_path('logout'), request=request)}
361 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
363 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
362 ${h.end_form()}
364 ${h.end_form()}
363 </li>
365 </li>
364 </ol>
366 </ol>
365 </div>
367 </div>
366 %endif
368 %endif
367 </div>
369 </div>
368 </div>
370 </div>
369 %if c.rhodecode_user.username != h.DEFAULT_USER:
371 %if c.rhodecode_user.username != h.DEFAULT_USER:
370 <div class="pill_container">
372 <div class="pill_container">
371 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
373 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
372 </div>
374 </div>
373 % endif
375 % endif
374 </li>
376 </li>
375 </%def>
377 </%def>
376
378
377 <%def name="menu_items(active=None)">
379 <%def name="menu_items(active=None)">
378 <%
380 <%
379 def is_active(selected):
381 def is_active(selected):
380 if selected == active:
382 if selected == active:
381 return "active"
383 return "active"
382 return ""
384 return ""
383 %>
385 %>
384 <ul id="quick" class="main_nav navigation horizontal-list">
386 <ul id="quick" class="main_nav navigation horizontal-list">
385 <!-- repo switcher -->
387 <!-- repo switcher -->
386 <li class="${is_active('repositories')} repo_switcher_li has_select2">
388 <li class="${is_active('repositories')} repo_switcher_li has_select2">
387 <input id="repo_switcher" name="repo_switcher" type="hidden">
389 <input id="repo_switcher" name="repo_switcher" type="hidden">
388 </li>
390 </li>
389
391
390 ## ROOT MENU
392 ## ROOT MENU
391 %if c.rhodecode_user.username != h.DEFAULT_USER:
393 %if c.rhodecode_user.username != h.DEFAULT_USER:
392 <li class="${is_active('journal')}">
394 <li class="${is_active('journal')}">
393 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
395 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
394 <div class="menulabel">${_('Journal')}</div>
396 <div class="menulabel">${_('Journal')}</div>
395 </a>
397 </a>
396 </li>
398 </li>
397 %else:
399 %else:
398 <li class="${is_active('journal')}">
400 <li class="${is_active('journal')}">
399 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
401 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
400 <div class="menulabel">${_('Public journal')}</div>
402 <div class="menulabel">${_('Public journal')}</div>
401 </a>
403 </a>
402 </li>
404 </li>
403 %endif
405 %endif
404 <li class="${is_active('gists')}">
406 <li class="${is_active('gists')}">
405 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
407 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
406 <div class="menulabel">${_('Gists')}</div>
408 <div class="menulabel">${_('Gists')}</div>
407 </a>
409 </a>
408 </li>
410 </li>
409 <li class="${is_active('search')}">
411 <li class="${is_active('search')}">
410 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
412 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
411 <div class="menulabel">${_('Search')}</div>
413 <div class="menulabel">${_('Search')}</div>
412 </a>
414 </a>
413 </li>
415 </li>
414 % if h.HasPermissionAll('hg.admin')('access admin main page'):
416 % if h.HasPermissionAll('hg.admin')('access admin main page'):
415 <li class="${is_active('admin')}">
417 <li class="${is_active('admin')}">
416 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
418 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
419 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
418 </a>
420 </a>
419 ${admin_menu()}
421 ${admin_menu()}
420 </li>
422 </li>
421 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
423 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
422 <li class="${is_active('admin')}">
424 <li class="${is_active('admin')}">
423 <a class="menulink childs" title="${_('Delegated Admin settings')}">
425 <a class="menulink childs" title="${_('Delegated Admin settings')}">
424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
426 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
425 </a>
427 </a>
426 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
428 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
427 c.rhodecode_user.repository_groups_admin,
429 c.rhodecode_user.repository_groups_admin,
428 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
430 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
429 </li>
431 </li>
430 % endif
432 % endif
431 % if c.debug_style:
433 % if c.debug_style:
432 <li class="${is_active('debug_style')}">
434 <li class="${is_active('debug_style')}">
433 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
435 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
434 <div class="menulabel">${_('Style')}</div>
436 <div class="menulabel">${_('Style')}</div>
435 </a>
437 </a>
436 </li>
438 </li>
437 % endif
439 % endif
438 ## render extra user menu
440 ## render extra user menu
439 ${usermenu(active=(active=='my_account'))}
441 ${usermenu(active=(active=='my_account'))}
440 </ul>
442 </ul>
441
443
442 <script type="text/javascript">
444 <script type="text/javascript">
443 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
445 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
444
446
445 /*format the look of items in the list*/
447 /*format the look of items in the list*/
446 var format = function(state, escapeMarkup){
448 var format = function(state, escapeMarkup){
447 if (!state.id){
449 if (!state.id){
448 return state.text; // optgroup
450 return state.text; // optgroup
449 }
451 }
450 var obj_dict = state.obj;
452 var obj_dict = state.obj;
451 var tmpl = '';
453 var tmpl = '';
452
454
453 if(obj_dict && state.type == 'repo'){
455 if(obj_dict && state.type == 'repo'){
454 if(obj_dict['repo_type'] === 'hg'){
456 if(obj_dict['repo_type'] === 'hg'){
455 tmpl += '<i class="icon-hg"></i> ';
457 tmpl += '<i class="icon-hg"></i> ';
456 }
458 }
457 else if(obj_dict['repo_type'] === 'git'){
459 else if(obj_dict['repo_type'] === 'git'){
458 tmpl += '<i class="icon-git"></i> ';
460 tmpl += '<i class="icon-git"></i> ';
459 }
461 }
460 else if(obj_dict['repo_type'] === 'svn'){
462 else if(obj_dict['repo_type'] === 'svn'){
461 tmpl += '<i class="icon-svn"></i> ';
463 tmpl += '<i class="icon-svn"></i> ';
462 }
464 }
463 if(obj_dict['private']){
465 if(obj_dict['private']){
464 tmpl += '<i class="icon-lock" ></i> ';
466 tmpl += '<i class="icon-lock" ></i> ';
465 }
467 }
466 else if(visual_show_public_icon){
468 else if(visual_show_public_icon){
467 tmpl += '<i class="icon-unlock-alt"></i> ';
469 tmpl += '<i class="icon-unlock-alt"></i> ';
468 }
470 }
469 }
471 }
470 if(obj_dict && state.type == 'commit') {
472 if(obj_dict && state.type == 'commit') {
471 tmpl += '<i class="icon-tag"></i>';
473 tmpl += '<i class="icon-tag"></i>';
472 }
474 }
473 if(obj_dict && state.type == 'group'){
475 if(obj_dict && state.type == 'group'){
474 tmpl += '<i class="icon-folder-close"></i> ';
476 tmpl += '<i class="icon-folder-close"></i> ';
475 }
477 }
476 tmpl += escapeMarkup(state.text);
478 tmpl += escapeMarkup(state.text);
477 return tmpl;
479 return tmpl;
478 };
480 };
479
481
480 var formatResult = function(result, container, query, escapeMarkup) {
482 var formatResult = function(result, container, query, escapeMarkup) {
481 return format(result, escapeMarkup);
483 return format(result, escapeMarkup);
482 };
484 };
483
485
484 var formatSelection = function(data, container, escapeMarkup) {
486 var formatSelection = function(data, container, escapeMarkup) {
485 return format(data, escapeMarkup);
487 return format(data, escapeMarkup);
486 };
488 };
487
489
488 $("#repo_switcher").select2({
490 $("#repo_switcher").select2({
489 cachedDataSource: {},
491 cachedDataSource: {},
490 minimumInputLength: 2,
492 minimumInputLength: 2,
491 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
493 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
492 dropdownAutoWidth: true,
494 dropdownAutoWidth: true,
493 formatResult: formatResult,
495 formatResult: formatResult,
494 formatSelection: formatSelection,
496 formatSelection: formatSelection,
495 containerCssClass: "repo-switcher",
497 containerCssClass: "repo-switcher",
496 dropdownCssClass: "repo-switcher-dropdown",
498 dropdownCssClass: "repo-switcher-dropdown",
497 escapeMarkup: function(m){
499 escapeMarkup: function(m){
498 // don't escape our custom placeholder
500 // don't escape our custom placeholder
499 if(m.substr(0,23) == '<div class="menulabel">'){
501 if(m.substr(0,23) == '<div class="menulabel">'){
500 return m;
502 return m;
501 }
503 }
502
504
503 return Select2.util.escapeMarkup(m);
505 return Select2.util.escapeMarkup(m);
504 },
506 },
505 query: $.debounce(250, function(query){
507 query: $.debounce(250, function(query){
506 self = this;
508 self = this;
507 var cacheKey = query.term;
509 var cacheKey = query.term;
508 var cachedData = self.cachedDataSource[cacheKey];
510 var cachedData = self.cachedDataSource[cacheKey];
509
511
510 if (cachedData) {
512 if (cachedData) {
511 query.callback({results: cachedData.results});
513 query.callback({results: cachedData.results});
512 } else {
514 } else {
513 $.ajax({
515 $.ajax({
514 url: pyroutes.url('goto_switcher_data'),
516 url: pyroutes.url('goto_switcher_data'),
515 data: {'query': query.term},
517 data: {'query': query.term},
516 dataType: 'json',
518 dataType: 'json',
517 type: 'GET',
519 type: 'GET',
518 success: function(data) {
520 success: function(data) {
519 self.cachedDataSource[cacheKey] = data;
521 self.cachedDataSource[cacheKey] = data;
520 query.callback({results: data.results});
522 query.callback({results: data.results});
521 },
523 },
522 error: function(data, textStatus, errorThrown) {
524 error: function(data, textStatus, errorThrown) {
523 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
525 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
524 }
526 }
525 })
527 })
526 }
528 }
527 })
529 })
528 });
530 });
529
531
530 $("#repo_switcher").on('select2-selecting', function(e){
532 $("#repo_switcher").on('select2-selecting', function(e){
531 e.preventDefault();
533 e.preventDefault();
532 window.location = e.choice.url;
534 window.location = e.choice.url;
533 });
535 });
534
536
535 </script>
537 </script>
536 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
538 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
537 </%def>
539 </%def>
538
540
539 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
541 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
540 <div class="modal-dialog">
542 <div class="modal-dialog">
541 <div class="modal-content">
543 <div class="modal-content">
542 <div class="modal-header">
544 <div class="modal-header">
543 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
545 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
544 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
546 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
545 </div>
547 </div>
546 <div class="modal-body">
548 <div class="modal-body">
547 <div class="block-left">
549 <div class="block-left">
548 <table class="keyboard-mappings">
550 <table class="keyboard-mappings">
549 <tbody>
551 <tbody>
550 <tr>
552 <tr>
551 <th></th>
553 <th></th>
552 <th>${_('Site-wide shortcuts')}</th>
554 <th>${_('Site-wide shortcuts')}</th>
553 </tr>
555 </tr>
554 <%
556 <%
555 elems = [
557 elems = [
556 ('/', 'Open quick search box'),
558 ('/', 'Open quick search box'),
557 ('g h', 'Goto home page'),
559 ('g h', 'Goto home page'),
558 ('g g', 'Goto my private gists page'),
560 ('g g', 'Goto my private gists page'),
559 ('g G', 'Goto my public gists page'),
561 ('g G', 'Goto my public gists page'),
560 ('n r', 'New repository page'),
562 ('n r', 'New repository page'),
561 ('n g', 'New gist page'),
563 ('n g', 'New gist page'),
562 ]
564 ]
563 %>
565 %>
564 %for key, desc in elems:
566 %for key, desc in elems:
565 <tr>
567 <tr>
566 <td class="keys">
568 <td class="keys">
567 <span class="key tag">${key}</span>
569 <span class="key tag">${key}</span>
568 </td>
570 </td>
569 <td>${desc}</td>
571 <td>${desc}</td>
570 </tr>
572 </tr>
571 %endfor
573 %endfor
572 </tbody>
574 </tbody>
573 </table>
575 </table>
574 </div>
576 </div>
575 <div class="block-left">
577 <div class="block-left">
576 <table class="keyboard-mappings">
578 <table class="keyboard-mappings">
577 <tbody>
579 <tbody>
578 <tr>
580 <tr>
579 <th></th>
581 <th></th>
580 <th>${_('Repositories')}</th>
582 <th>${_('Repositories')}</th>
581 </tr>
583 </tr>
582 <%
584 <%
583 elems = [
585 elems = [
584 ('g s', 'Goto summary page'),
586 ('g s', 'Goto summary page'),
585 ('g c', 'Goto changelog page'),
587 ('g c', 'Goto changelog page'),
586 ('g f', 'Goto files page'),
588 ('g f', 'Goto files page'),
587 ('g F', 'Goto files page with file search activated'),
589 ('g F', 'Goto files page with file search activated'),
588 ('g p', 'Goto pull requests page'),
590 ('g p', 'Goto pull requests page'),
589 ('g o', 'Goto repository settings'),
591 ('g o', 'Goto repository settings'),
590 ('g O', 'Goto repository permissions settings'),
592 ('g O', 'Goto repository permissions settings'),
591 ]
593 ]
592 %>
594 %>
593 %for key, desc in elems:
595 %for key, desc in elems:
594 <tr>
596 <tr>
595 <td class="keys">
597 <td class="keys">
596 <span class="key tag">${key}</span>
598 <span class="key tag">${key}</span>
597 </td>
599 </td>
598 <td>${desc}</td>
600 <td>${desc}</td>
599 </tr>
601 </tr>
600 %endfor
602 %endfor
601 </tbody>
603 </tbody>
602 </table>
604 </table>
603 </div>
605 </div>
604 </div>
606 </div>
605 <div class="modal-footer">
607 <div class="modal-footer">
606 </div>
608 </div>
607 </div><!-- /.modal-content -->
609 </div><!-- /.modal-content -->
608 </div><!-- /.modal-dialog -->
610 </div><!-- /.modal-dialog -->
609 </div><!-- /.modal -->
611 </div><!-- /.modal -->
@@ -1,530 +1,530 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('New pull request')}
4 ${c.repo_name} ${_('New pull request')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('New pull request')}
8 ${_('New pull request')}
9 </%def>
9 </%def>
10
10
11 <%def name="menu_bar_nav()">
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='repositories')}
12 ${self.menu_items(active='repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_subnav()">
15 <%def name="menu_bar_subnav()">
16 ${self.repo_menu(active='showpullrequest')}
16 ${self.repo_menu(active='showpullrequest')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <div class="title">
21 <div class="title">
22 ${self.repo_page_title(c.rhodecode_db_repo)}
22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 </div>
23 </div>
24
24
25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
26
26
27 ${self.breadcrumbs()}
27 ${self.breadcrumbs()}
28
28
29 <div class="box pr-summary">
29 <div class="box pr-summary">
30
30
31 <div class="summary-details block-left">
31 <div class="summary-details block-left">
32
32
33
33
34 <div class="pr-details-title">
34 <div class="pr-details-title">
35 ${_('Pull request summary')}
35 ${_('Pull request summary')}
36 </div>
36 </div>
37
37
38 <div class="form" style="padding-top: 10px">
38 <div class="form" style="padding-top: 10px">
39 <!-- fields -->
39 <!-- fields -->
40
40
41 <div class="fields" >
41 <div class="fields" >
42
42
43 <div class="field">
43 <div class="field">
44 <div class="label">
44 <div class="label">
45 <label for="pullrequest_title">${_('Title')}:</label>
45 <label for="pullrequest_title">${_('Title')}:</label>
46 </div>
46 </div>
47 <div class="input">
47 <div class="input">
48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 </div>
49 </div>
50 </div>
50 </div>
51
51
52 <div class="field">
52 <div class="field">
53 <div class="label label-textarea">
53 <div class="label label-textarea">
54 <label for="pullrequest_desc">${_('Description')}:</label>
54 <label for="pullrequest_desc">${_('Description')}:</label>
55 </div>
55 </div>
56 <div class="textarea text-area editor">
56 <div class="textarea text-area editor">
57 ${h.textarea('pullrequest_desc',size=30, )}
57 ${h.textarea('pullrequest_desc',size=30, )}
58 <span class="help-block">${_('Write a short description on this pull request')}</span>
58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 </div>
59 </div>
60 </div>
60 </div>
61
61
62 <div class="field">
62 <div class="field">
63 <div class="label label-textarea">
63 <div class="label label-textarea">
64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 </div>
65 </div>
66
66
67 ## TODO: johbo: Abusing the "content" class here to get the
67 ## TODO: johbo: Abusing the "content" class here to get the
68 ## desired effect. Should be replaced by a proper solution.
68 ## desired effect. Should be replaced by a proper solution.
69
69
70 ##ORG
70 ##ORG
71 <div class="content">
71 <div class="content">
72 <strong>${_('Source repository')}:</strong>
72 <strong>${_('Source repository')}:</strong>
73 ${c.rhodecode_db_repo.description}
73 ${c.rhodecode_db_repo.description}
74 </div>
74 </div>
75 <div class="content">
75 <div class="content">
76 ${h.hidden('source_repo')}
76 ${h.hidden('source_repo')}
77 ${h.hidden('source_ref')}
77 ${h.hidden('source_ref')}
78 </div>
78 </div>
79
79
80 ##OTHER, most Probably the PARENT OF THIS FORK
80 ##OTHER, most Probably the PARENT OF THIS FORK
81 <div class="content">
81 <div class="content">
82 ## filled with JS
82 ## filled with JS
83 <div id="target_repo_desc"></div>
83 <div id="target_repo_desc"></div>
84 </div>
84 </div>
85
85
86 <div class="content">
86 <div class="content">
87 ${h.hidden('target_repo')}
87 ${h.hidden('target_repo')}
88 ${h.hidden('target_ref')}
88 ${h.hidden('target_ref')}
89 <span id="target_ref_loading" style="display: none">
89 <span id="target_ref_loading" style="display: none">
90 ${_('Loading refs...')}
90 ${_('Loading refs...')}
91 </span>
91 </span>
92 </div>
92 </div>
93 </div>
93 </div>
94
94
95 <div class="field">
95 <div class="field">
96 <div class="label label-textarea">
96 <div class="label label-textarea">
97 <label for="pullrequest_submit"></label>
97 <label for="pullrequest_submit"></label>
98 </div>
98 </div>
99 <div class="input">
99 <div class="input">
100 <div class="pr-submit-button">
100 <div class="pr-submit-button">
101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 </div>
102 </div>
103 <div id="pr_open_message"></div>
103 <div id="pr_open_message"></div>
104 </div>
104 </div>
105 </div>
105 </div>
106
106
107 <div class="pr-spacing-container"></div>
107 <div class="pr-spacing-container"></div>
108 </div>
108 </div>
109 </div>
109 </div>
110 </div>
110 </div>
111 <div>
111 <div>
112 ## AUTHOR
112 ## AUTHOR
113 <div class="reviewers-title block-right">
113 <div class="reviewers-title block-right">
114 <div class="pr-details-title">
114 <div class="pr-details-title">
115 ${_('Author of this pull request')}
115 ${_('Author of this pull request')}
116 </div>
116 </div>
117 </div>
117 </div>
118 <div class="block-right pr-details-content reviewers">
118 <div class="block-right pr-details-content reviewers">
119 <ul class="group_members">
119 <ul class="group_members">
120 <li>
120 <li>
121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 </li>
122 </li>
123 </ul>
123 </ul>
124 </div>
124 </div>
125
125
126 ## REVIEW RULES
126 ## REVIEW RULES
127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 <div class="pr-details-title">
128 <div class="pr-details-title">
129 ${_('Reviewer rules')}
129 ${_('Reviewer rules')}
130 </div>
130 </div>
131 <div class="pr-reviewer-rules">
131 <div class="pr-reviewer-rules">
132 ## review rules will be appended here, by default reviewers logic
132 ## review rules will be appended here, by default reviewers logic
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 ## REVIEWERS
136 ## REVIEWERS
137 <div class="reviewers-title block-right">
137 <div class="reviewers-title block-right">
138 <div class="pr-details-title">
138 <div class="pr-details-title">
139 ${_('Pull request reviewers')}
139 ${_('Pull request reviewers')}
140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 </div>
141 </div>
142 </div>
142 </div>
143 <div id="reviewers" class="block-right pr-details-content reviewers">
143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 ## members goes here, filled via JS based on initial selection !
144 ## members goes here, filled via JS based on initial selection !
145 <input type="hidden" name="__start__" value="review_members:sequence">
145 <input type="hidden" name="__start__" value="review_members:sequence">
146 <ul id="review_members" class="group_members"></ul>
146 <ul id="review_members" class="group_members"></ul>
147 <input type="hidden" name="__end__" value="review_members:sequence">
147 <input type="hidden" name="__end__" value="review_members:sequence">
148 <div id="add_reviewer_input" class='ac'>
148 <div id="add_reviewer_input" class='ac'>
149 <div class="reviewer_ac">
149 <div class="reviewer_ac">
150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 <div id="reviewers_container"></div>
151 <div id="reviewers_container"></div>
152 </div>
152 </div>
153 </div>
153 </div>
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157 <div class="box">
157 <div class="box">
158 <div>
158 <div>
159 ## overview pulled by ajax
159 ## overview pulled by ajax
160 <div id="pull_request_overview"></div>
160 <div id="pull_request_overview"></div>
161 </div>
161 </div>
162 </div>
162 </div>
163 ${h.end_form()}
163 ${h.end_form()}
164 </div>
164 </div>
165
165
166 <script type="text/javascript">
166 <script type="text/javascript">
167 $(function(){
167 $(function(){
168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172
172
173 var $pullRequestForm = $('#pull_request_form');
173 var $pullRequestForm = $('#pull_request_form');
174 var $sourceRepo = $('#source_repo', $pullRequestForm);
174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 var $targetRepo = $('#target_repo', $pullRequestForm);
175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 var $sourceRef = $('#source_ref', $pullRequestForm);
176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 var $targetRef = $('#target_ref', $pullRequestForm);
177 var $targetRef = $('#target_ref', $pullRequestForm);
178
178
179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181
181
182 var targetRepo = function() { return $targetRepo.eq(0).val() };
182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184
184
185 var calculateContainerWidth = function() {
185 var calculateContainerWidth = function() {
186 var maxWidth = 0;
186 var maxWidth = 0;
187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 $.each(repoSelect2Containers, function(idx, value) {
188 $.each(repoSelect2Containers, function(idx, value) {
189 $(value).select2('container').width('auto');
189 $(value).select2('container').width('auto');
190 var curWidth = $(value).select2('container').width();
190 var curWidth = $(value).select2('container').width();
191 if (maxWidth <= curWidth) {
191 if (maxWidth <= curWidth) {
192 maxWidth = curWidth;
192 maxWidth = curWidth;
193 }
193 }
194 $.each(repoSelect2Containers, function(idx, value) {
194 $.each(repoSelect2Containers, function(idx, value) {
195 $(value).select2('container').width(maxWidth + 10);
195 $(value).select2('container').width(maxWidth + 10);
196 });
196 });
197 });
197 });
198 };
198 };
199
199
200 var initRefSelection = function(selectedRef) {
200 var initRefSelection = function(selectedRef) {
201 return function(element, callback) {
201 return function(element, callback) {
202 // translate our select2 id into a text, it's a mapping to show
202 // translate our select2 id into a text, it's a mapping to show
203 // simple label when selecting by internal ID.
203 // simple label when selecting by internal ID.
204 var id, refData;
204 var id, refData;
205 if (selectedRef === undefined) {
205 if (selectedRef === undefined) {
206 id = element.val();
206 id = element.val();
207 refData = element.val().split(':');
207 refData = element.val().split(':');
208 } else {
208 } else {
209 id = selectedRef;
209 id = selectedRef;
210 refData = selectedRef.split(':');
210 refData = selectedRef.split(':');
211 }
211 }
212
212
213 var text = refData[1];
213 var text = refData[1];
214 if (refData[0] === 'rev') {
214 if (refData[0] === 'rev') {
215 text = text.substring(0, 12);
215 text = text.substring(0, 12);
216 }
216 }
217
217
218 var data = {id: id, text: text};
218 var data = {id: id, text: text};
219
219
220 callback(data);
220 callback(data);
221 };
221 };
222 };
222 };
223
223
224 var formatRefSelection = function(item) {
224 var formatRefSelection = function(item) {
225 var prefix = '';
225 var prefix = '';
226 var refData = item.id.split(':');
226 var refData = item.id.split(':');
227 if (refData[0] === 'branch') {
227 if (refData[0] === 'branch') {
228 prefix = '<i class="icon-branch"></i>';
228 prefix = '<i class="icon-branch"></i>';
229 }
229 }
230 else if (refData[0] === 'book') {
230 else if (refData[0] === 'book') {
231 prefix = '<i class="icon-bookmark"></i>';
231 prefix = '<i class="icon-bookmark"></i>';
232 }
232 }
233 else if (refData[0] === 'tag') {
233 else if (refData[0] === 'tag') {
234 prefix = '<i class="icon-tag"></i>';
234 prefix = '<i class="icon-tag"></i>';
235 }
235 }
236
236
237 var originalOption = item.element;
237 var originalOption = item.element;
238 return prefix + item.text;
238 return prefix + item.text;
239 };
239 };
240
240
241 // custom code mirror
241 // custom code mirror
242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243
243
244 reviewersController = new ReviewersController();
244 reviewersController = new ReviewersController();
245
245
246 var queryTargetRepo = function(self, query) {
246 var queryTargetRepo = function(self, query) {
247 // cache ALL results if query is empty
247 // cache ALL results if query is empty
248 var cacheKey = query.term || '__';
248 var cacheKey = query.term || '__';
249 var cachedData = self.cachedDataSource[cacheKey];
249 var cachedData = self.cachedDataSource[cacheKey];
250
250
251 if (cachedData) {
251 if (cachedData) {
252 query.callback({results: cachedData.results});
252 query.callback({results: cachedData.results});
253 } else {
253 } else {
254 $.ajax({
254 $.ajax({
255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 data: {query: query.term},
256 data: {query: query.term},
257 dataType: 'json',
257 dataType: 'json',
258 type: 'GET',
258 type: 'GET',
259 success: function(data) {
259 success: function(data) {
260 self.cachedDataSource[cacheKey] = data;
260 self.cachedDataSource[cacheKey] = data;
261 query.callback({results: data.results});
261 query.callback({results: data.results});
262 },
262 },
263 error: function(data, textStatus, errorThrown) {
263 error: function(data, textStatus, errorThrown) {
264 alert(
264 alert(
265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 }
266 }
267 });
267 });
268 }
268 }
269 };
269 };
270
270
271 var queryTargetRefs = function(initialData, query) {
271 var queryTargetRefs = function(initialData, query) {
272 var data = {results: []};
272 var data = {results: []};
273 // filter initialData
273 // filter initialData
274 $.each(initialData, function() {
274 $.each(initialData, function() {
275 var section = this.text;
275 var section = this.text;
276 var children = [];
276 var children = [];
277 $.each(this.children, function() {
277 $.each(this.children, function() {
278 if (query.term.length === 0 ||
278 if (query.term.length === 0 ||
279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 children.push({'id': this.id, 'text': this.text})
280 children.push({'id': this.id, 'text': this.text})
281 }
281 }
282 });
282 });
283 data.results.push({'text': section, 'children': children})
283 data.results.push({'text': section, 'children': children})
284 });
284 });
285 query.callback({results: data.results});
285 query.callback({results: data.results});
286 };
286 };
287
287
288 var loadRepoRefDiffPreview = function() {
288 var loadRepoRefDiffPreview = function() {
289
289
290 var url_data = {
290 var url_data = {
291 'repo_name': targetRepo(),
291 'repo_name': targetRepo(),
292 'target_repo': sourceRepo(),
292 'target_repo': sourceRepo(),
293 'source_ref': targetRef()[2],
293 'source_ref': targetRef()[2],
294 'source_ref_type': 'rev',
294 'source_ref_type': 'rev',
295 'target_ref': sourceRef()[2],
295 'target_ref': sourceRef()[2],
296 'target_ref_type': 'rev',
296 'target_ref_type': 'rev',
297 'merge': true,
297 'merge': true,
298 '_': Date.now() // bypass browser caching
298 '_': Date.now() // bypass browser caching
299 }; // gather the source/target ref and repo here
299 }; // gather the source/target ref and repo here
300
300
301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 prButtonLock(true, "${_('Please select source and target')}");
302 prButtonLock(true, "${_('Please select source and target')}");
303 return;
303 return;
304 }
304 }
305 var url = pyroutes.url('repo_compare', url_data);
305 var url = pyroutes.url('repo_compare', url_data);
306
306
307 // lock PR button, so we cannot send PR before it's calculated
307 // lock PR button, so we cannot send PR before it's calculated
308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309
309
310 if (loadRepoRefDiffPreview._currentRequest) {
310 if (loadRepoRefDiffPreview._currentRequest) {
311 loadRepoRefDiffPreview._currentRequest.abort();
311 loadRepoRefDiffPreview._currentRequest.abort();
312 }
312 }
313
313
314 loadRepoRefDiffPreview._currentRequest = $.get(url)
314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 .error(function(data, textStatus, errorThrown) {
315 .error(function(data, textStatus, errorThrown) {
316 if (textStatus !== 'abort') {
316 if (textStatus !== 'abort') {
317 alert(
317 alert(
318 "Error while processing request.\nError code {0} ({1}).".format(
318 "Error while processing request.\nError code {0} ({1}).".format(
319 data.status, data.statusText));
319 data.status, data.statusText));
320 }
320 }
321
321
322 })
322 })
323 .done(function(data) {
323 .done(function(data) {
324 loadRepoRefDiffPreview._currentRequest = null;
324 loadRepoRefDiffPreview._currentRequest = null;
325 $('#pull_request_overview').html(data);
325 $('#pull_request_overview').html(data);
326
326
327 var commitElements = $(data).find('tr[commit_id]');
327 var commitElements = $(data).find('tr[commit_id]');
328
328
329 var prTitleAndDesc = getTitleAndDescription(
329 var prTitleAndDesc = getTitleAndDescription(
330 sourceRef()[1], commitElements, 5);
330 sourceRef()[1], commitElements, 5);
331
331
332 var title = prTitleAndDesc[0];
332 var title = prTitleAndDesc[0];
333 var proposedDescription = prTitleAndDesc[1];
333 var proposedDescription = prTitleAndDesc[1];
334
334
335 var useGeneratedTitle = (
335 var useGeneratedTitle = (
336 $('#pullrequest_title').hasClass('autogenerated-title') ||
336 $('#pullrequest_title').hasClass('autogenerated-title') ||
337 $('#pullrequest_title').val() === "");
337 $('#pullrequest_title').val() === "");
338
338
339 if (title && useGeneratedTitle) {
339 if (title && useGeneratedTitle) {
340 // use generated title if we haven't specified our own
340 // use generated title if we haven't specified our own
341 $('#pullrequest_title').val(title);
341 $('#pullrequest_title').val(title);
342 $('#pullrequest_title').addClass('autogenerated-title');
342 $('#pullrequest_title').addClass('autogenerated-title');
343
343
344 }
344 }
345
345
346 var useGeneratedDescription = (
346 var useGeneratedDescription = (
347 !codeMirrorInstance._userDefinedDesc ||
347 !codeMirrorInstance._userDefinedDesc ||
348 codeMirrorInstance.getValue() === "");
348 codeMirrorInstance.getValue() === "");
349
349
350 if (proposedDescription && useGeneratedDescription) {
350 if (proposedDescription && useGeneratedDescription) {
351 // set proposed content, if we haven't defined our own,
351 // set proposed content, if we haven't defined our own,
352 // or we don't have description written
352 // or we don't have description written
353 codeMirrorInstance._userDefinedDesc = false; // reset state
353 codeMirrorInstance._userDefinedDesc = false; // reset state
354 codeMirrorInstance.setValue(proposedDescription);
354 codeMirrorInstance.setValue(proposedDescription);
355 }
355 }
356
356
357 var msg = '';
357 var msg = '';
358 if (commitElements.length === 1) {
358 if (commitElements.length === 1) {
359 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
359 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
360 } else {
360 } else {
361 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
361 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
362 }
362 }
363
363
364 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
364 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
365
365
366 if (commitElements.length) {
366 if (commitElements.length) {
367 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
367 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
368 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
368 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
369 }
369 }
370 else {
370 else {
371 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
371 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
372 }
372 }
373
373
374
374
375 });
375 });
376 };
376 };
377
377
378 var Select2Box = function(element, overrides) {
378 var Select2Box = function(element, overrides) {
379 var globalDefaults = {
379 var globalDefaults = {
380 dropdownAutoWidth: true,
380 dropdownAutoWidth: true,
381 containerCssClass: "drop-menu",
381 containerCssClass: "drop-menu",
382 dropdownCssClass: "drop-menu-dropdown"
382 dropdownCssClass: "drop-menu-dropdown"
383 };
383 };
384
384
385 var initSelect2 = function(defaultOptions) {
385 var initSelect2 = function(defaultOptions) {
386 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
386 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
387 element.select2(options);
387 element.select2(options);
388 };
388 };
389
389
390 return {
390 return {
391 initRef: function() {
391 initRef: function() {
392 var defaultOptions = {
392 var defaultOptions = {
393 minimumResultsForSearch: 5,
393 minimumResultsForSearch: 5,
394 formatSelection: formatRefSelection
394 formatSelection: formatRefSelection
395 };
395 };
396
396
397 initSelect2(defaultOptions);
397 initSelect2(defaultOptions);
398 },
398 },
399
399
400 initRepo: function(defaultValue, readOnly) {
400 initRepo: function(defaultValue, readOnly) {
401 var defaultOptions = {
401 var defaultOptions = {
402 initSelection : function (element, callback) {
402 initSelection : function (element, callback) {
403 var data = {id: defaultValue, text: defaultValue};
403 var data = {id: defaultValue, text: defaultValue};
404 callback(data);
404 callback(data);
405 }
405 }
406 };
406 };
407
407
408 initSelect2(defaultOptions);
408 initSelect2(defaultOptions);
409
409
410 element.select2('val', defaultSourceRepo);
410 element.select2('val', defaultSourceRepo);
411 if (readOnly === true) {
411 if (readOnly === true) {
412 element.select2('readonly', true);
412 element.select2('readonly', true);
413 }
413 }
414 }
414 }
415 };
415 };
416 };
416 };
417
417
418 var initTargetRefs = function(refsData, selectedRef){
418 var initTargetRefs = function(refsData, selectedRef){
419 Select2Box($targetRef, {
419 Select2Box($targetRef, {
420 query: function(query) {
420 query: function(query) {
421 queryTargetRefs(refsData, query);
421 queryTargetRefs(refsData, query);
422 },
422 },
423 initSelection : initRefSelection(selectedRef)
423 initSelection : initRefSelection(selectedRef)
424 }).initRef();
424 }).initRef();
425
425
426 if (!(selectedRef === undefined)) {
426 if (!(selectedRef === undefined)) {
427 $targetRef.select2('val', selectedRef);
427 $targetRef.select2('val', selectedRef);
428 }
428 }
429 };
429 };
430
430
431 var targetRepoChanged = function(repoData) {
431 var targetRepoChanged = function(repoData) {
432 // generate new DESC of target repo displayed next to select
432 // generate new DESC of target repo displayed next to select
433 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
433 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
434 $('#target_repo_desc').html(
434 $('#target_repo_desc').html(
435 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Use as source</a>".format(repoData['description'], prLink)
435 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
436 );
436 );
437
437
438 // generate dynamic select2 for refs.
438 // generate dynamic select2 for refs.
439 initTargetRefs(repoData['refs']['select2_refs'],
439 initTargetRefs(repoData['refs']['select2_refs'],
440 repoData['refs']['selected_ref']);
440 repoData['refs']['selected_ref']);
441
441
442 };
442 };
443
443
444 var sourceRefSelect2 = Select2Box($sourceRef, {
444 var sourceRefSelect2 = Select2Box($sourceRef, {
445 placeholder: "${_('Select commit reference')}",
445 placeholder: "${_('Select commit reference')}",
446 query: function(query) {
446 query: function(query) {
447 var initialData = defaultSourceRepoData['refs']['select2_refs'];
447 var initialData = defaultSourceRepoData['refs']['select2_refs'];
448 queryTargetRefs(initialData, query)
448 queryTargetRefs(initialData, query)
449 },
449 },
450 initSelection: initRefSelection()
450 initSelection: initRefSelection()
451 }
451 }
452 );
452 );
453
453
454 var sourceRepoSelect2 = Select2Box($sourceRepo, {
454 var sourceRepoSelect2 = Select2Box($sourceRepo, {
455 query: function(query) {}
455 query: function(query) {}
456 });
456 });
457
457
458 var targetRepoSelect2 = Select2Box($targetRepo, {
458 var targetRepoSelect2 = Select2Box($targetRepo, {
459 cachedDataSource: {},
459 cachedDataSource: {},
460 query: $.debounce(250, function(query) {
460 query: $.debounce(250, function(query) {
461 queryTargetRepo(this, query);
461 queryTargetRepo(this, query);
462 }),
462 }),
463 formatResult: formatResult
463 formatResult: formatResult
464 });
464 });
465
465
466 sourceRefSelect2.initRef();
466 sourceRefSelect2.initRef();
467
467
468 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
468 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
469
469
470 targetRepoSelect2.initRepo(defaultTargetRepo, false);
470 targetRepoSelect2.initRepo(defaultTargetRepo, false);
471
471
472 $sourceRef.on('change', function(e){
472 $sourceRef.on('change', function(e){
473 loadRepoRefDiffPreview();
473 loadRepoRefDiffPreview();
474 reviewersController.loadDefaultReviewers(
474 reviewersController.loadDefaultReviewers(
475 sourceRepo(), sourceRef(), targetRepo(), targetRef());
475 sourceRepo(), sourceRef(), targetRepo(), targetRef());
476 });
476 });
477
477
478 $targetRef.on('change', function(e){
478 $targetRef.on('change', function(e){
479 loadRepoRefDiffPreview();
479 loadRepoRefDiffPreview();
480 reviewersController.loadDefaultReviewers(
480 reviewersController.loadDefaultReviewers(
481 sourceRepo(), sourceRef(), targetRepo(), targetRef());
481 sourceRepo(), sourceRef(), targetRepo(), targetRef());
482 });
482 });
483
483
484 $targetRepo.on('change', function(e){
484 $targetRepo.on('change', function(e){
485 var repoName = $(this).val();
485 var repoName = $(this).val();
486 calculateContainerWidth();
486 calculateContainerWidth();
487 $targetRef.select2('destroy');
487 $targetRef.select2('destroy');
488 $('#target_ref_loading').show();
488 $('#target_ref_loading').show();
489
489
490 $.ajax({
490 $.ajax({
491 url: pyroutes.url('pullrequest_repo_refs',
491 url: pyroutes.url('pullrequest_repo_refs',
492 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
492 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
493 data: {},
493 data: {},
494 dataType: 'json',
494 dataType: 'json',
495 type: 'GET',
495 type: 'GET',
496 success: function(data) {
496 success: function(data) {
497 $('#target_ref_loading').hide();
497 $('#target_ref_loading').hide();
498 targetRepoChanged(data);
498 targetRepoChanged(data);
499 loadRepoRefDiffPreview();
499 loadRepoRefDiffPreview();
500 },
500 },
501 error: function(data, textStatus, errorThrown) {
501 error: function(data, textStatus, errorThrown) {
502 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
502 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
503 }
503 }
504 })
504 })
505
505
506 });
506 });
507
507
508 prButtonLock(true, "${_('Please select source and target')}", 'all');
508 prButtonLock(true, "${_('Please select source and target')}", 'all');
509
509
510 // auto-load on init, the target refs select2
510 // auto-load on init, the target refs select2
511 calculateContainerWidth();
511 calculateContainerWidth();
512 targetRepoChanged(defaultTargetRepoData);
512 targetRepoChanged(defaultTargetRepoData);
513
513
514 $('#pullrequest_title').on('keyup', function(e){
514 $('#pullrequest_title').on('keyup', function(e){
515 $(this).removeClass('autogenerated-title');
515 $(this).removeClass('autogenerated-title');
516 });
516 });
517
517
518 % if c.default_source_ref:
518 % if c.default_source_ref:
519 // in case we have a pre-selected value, use it now
519 // in case we have a pre-selected value, use it now
520 $sourceRef.select2('val', '${c.default_source_ref}');
520 $sourceRef.select2('val', '${c.default_source_ref}');
521 loadRepoRefDiffPreview();
521 loadRepoRefDiffPreview();
522 reviewersController.loadDefaultReviewers(
522 reviewersController.loadDefaultReviewers(
523 sourceRepo(), sourceRef(), targetRepo(), targetRef());
523 sourceRepo(), sourceRef(), targetRepo(), targetRef());
524 % endif
524 % endif
525
525
526 ReviewerAutoComplete('#user');
526 ReviewerAutoComplete('#user');
527 });
527 });
528 </script>
528 </script>
529
529
530 </%def>
530 </%def>
@@ -1,869 +1,853 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Source')}:</label>
71 <label>${_('Source')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
90 % endif
91 </div>
91 </div>
92 %if h.is_hg(c.pull_request.source_repo):
92 %if h.is_hg(c.pull_request.source_repo):
93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
94 %elif h.is_git(c.pull_request.source_repo):
94 %elif h.is_git(c.pull_request.source_repo):
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
96 %endif
96 %endif
97
97
98 <div class="">
98 <div class="">
99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
101 </div>
101 </div>
102
102
103 </div>
103 </div>
104 </div>
104 </div>
105 <div class="field">
105 <div class="field">
106 <div class="label-summary">
106 <div class="label-summary">
107 <label>${_('Target')}:</label>
107 <label>${_('Target')}:</label>
108 </div>
108 </div>
109 <div class="input">
109 <div class="input">
110 <div class="pr-targetinfo">
110 <div class="pr-targetinfo">
111 ## branch link is only valid if it is a branch
111 ## branch link is only valid if it is a branch
112 <span class="tag">
112 <span class="tag">
113 %if c.pull_request.target_ref_parts.type == 'branch':
113 %if c.pull_request.target_ref_parts.type == 'branch':
114 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
114 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
115 %else:
115 %else:
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
117 %endif
117 %endif
118 </span>
118 </span>
119 <span class="clone-url">
119 <span class="clone-url">
120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
121 </span>
121 </span>
122 </div>
122 </div>
123 </div>
123 </div>
124 </div>
124 </div>
125
125
126 ## Link to the shadow repository.
126 ## Link to the shadow repository.
127 <div class="field">
127 <div class="field">
128 <div class="label-summary">
128 <div class="label-summary">
129 <label>${_('Merge')}:</label>
129 <label>${_('Merge')}:</label>
130 </div>
130 </div>
131 <div class="input">
131 <div class="input">
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
133 %if h.is_hg(c.pull_request.target_repo):
133 %if h.is_hg(c.pull_request.target_repo):
134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
135 %elif h.is_git(c.pull_request.target_repo):
135 %elif h.is_git(c.pull_request.target_repo):
136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
137 %endif
137 %endif
138 <div class="">
138 <div class="">
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
140 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
140 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
141 </div>
141 </div>
142 % else:
142 % else:
143 <div class="">
143 <div class="">
144 ${_('Shadow repository data not available')}.
144 ${_('Shadow repository data not available')}.
145 </div>
145 </div>
146 % endif
146 % endif
147 </div>
147 </div>
148 </div>
148 </div>
149
149
150 <div class="field">
150 <div class="field">
151 <div class="label-summary">
151 <div class="label-summary">
152 <label>${_('Review')}:</label>
152 <label>${_('Review')}:</label>
153 </div>
153 </div>
154 <div class="input">
154 <div class="input">
155 %if c.pull_request_review_status:
155 %if c.pull_request_review_status:
156 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
156 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
157 <span class="changeset-status-lbl tooltip">
157 <span class="changeset-status-lbl tooltip">
158 %if c.pull_request.is_closed():
158 %if c.pull_request.is_closed():
159 ${_('Closed')},
159 ${_('Closed')},
160 %endif
160 %endif
161 ${h.commit_status_lbl(c.pull_request_review_status)}
161 ${h.commit_status_lbl(c.pull_request_review_status)}
162 </span>
162 </span>
163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
164 %endif
164 %endif
165 </div>
165 </div>
166 </div>
166 </div>
167 <div class="field">
167 <div class="field">
168 <div class="pr-description-label label-summary">
168 <div class="pr-description-label label-summary">
169 <label>${_('Description')}:</label>
169 <label>${_('Description')}:</label>
170 </div>
170 </div>
171 <div id="pr-desc" class="input">
171 <div id="pr-desc" class="input">
172 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
172 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
173 </div>
173 </div>
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
175 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
175 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
176 </div>
176 </div>
177 </div>
177 </div>
178
178
179 <div class="field">
179 <div class="field">
180 <div class="label-summary">
180 <div class="label-summary">
181 <label>${_('Versions')}:</label>
181 <label>${_('Versions')}:</label>
182 </div>
182 </div>
183
183
184 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
184 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
185 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
185 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
186
186
187 <div class="pr-versions">
187 <div class="pr-versions">
188 % if c.show_version_changes:
188 % if c.show_version_changes:
189 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
189 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
190 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
190 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
191 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
191 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
192 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
192 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
193 data-toggle-off="${_('Hide all versions of this pull request')}">
193 data-toggle-off="${_('Hide all versions of this pull request')}">
194 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
194 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
195 </a>
195 </a>
196 <table>
196 <table>
197 ## SHOW ALL VERSIONS OF PR
197 ## SHOW ALL VERSIONS OF PR
198 <% ver_pr = None %>
198 <% ver_pr = None %>
199
199
200 % for data in reversed(list(enumerate(c.versions, 1))):
200 % for data in reversed(list(enumerate(c.versions, 1))):
201 <% ver_pos = data[0] %>
201 <% ver_pos = data[0] %>
202 <% ver = data[1] %>
202 <% ver = data[1] %>
203 <% ver_pr = ver.pull_request_version_id %>
203 <% ver_pr = ver.pull_request_version_id %>
204 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
204 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
205
205
206 <tr class="version-pr" style="display: ${display_row}">
206 <tr class="version-pr" style="display: ${display_row}">
207 <td>
207 <td>
208 <code>
208 <code>
209 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
209 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
210 </code>
210 </code>
211 </td>
211 </td>
212 <td>
212 <td>
213 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
213 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
214 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
214 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
215 </td>
215 </td>
216 <td>
216 <td>
217 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
217 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
218 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
218 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
219 </div>
219 </div>
220 </td>
220 </td>
221 <td>
221 <td>
222 % if c.at_version_num != ver_pr:
222 % if c.at_version_num != ver_pr:
223 <i class="icon-comment"></i>
223 <i class="icon-comment"></i>
224 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
224 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
225 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
225 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
226 </code>
226 </code>
227 % endif
227 % endif
228 </td>
228 </td>
229 <td>
229 <td>
230 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
230 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
231 </td>
231 </td>
232 <td>
232 <td>
233 ${h.age_component(ver.updated_on, time_is_local=True)}
233 ${h.age_component(ver.updated_on, time_is_local=True)}
234 </td>
234 </td>
235 </tr>
235 </tr>
236 % endfor
236 % endfor
237
237
238 <tr>
238 <tr>
239 <td colspan="6">
239 <td colspan="6">
240 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
240 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
241 data-label-text-locked="${_('select versions to show changes')}"
241 data-label-text-locked="${_('select versions to show changes')}"
242 data-label-text-diff="${_('show changes between versions')}"
242 data-label-text-diff="${_('show changes between versions')}"
243 data-label-text-show="${_('show pull request for this version')}"
243 data-label-text-show="${_('show pull request for this version')}"
244 >
244 >
245 ${_('select versions to show changes')}
245 ${_('select versions to show changes')}
246 </button>
246 </button>
247 </td>
247 </td>
248 </tr>
248 </tr>
249
249
250 ## show comment/inline comments summary
250 ## show comment/inline comments summary
251 <%def name="comments_summary()">
251 <%def name="comments_summary()">
252 <tr>
252 <tr>
253 <td colspan="6" class="comments-summary-td">
253 <td colspan="6" class="comments-summary-td">
254
254
255 % if c.at_version:
255 % if c.at_version:
256 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
256 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
257 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
257 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
258 ${_('Comments at this version')}:
258 ${_('Comments at this version')}:
259 % else:
259 % else:
260 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
260 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
261 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
261 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
262 ${_('Comments for this pull request')}:
262 ${_('Comments for this pull request')}:
263 % endif
263 % endif
264
264
265
265
266 %if general_comm_count_ver:
266 %if general_comm_count_ver:
267 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
267 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
268 %else:
268 %else:
269 ${_("%d General ") % general_comm_count_ver}
269 ${_("%d General ") % general_comm_count_ver}
270 %endif
270 %endif
271
271
272 %if inline_comm_count_ver:
272 %if inline_comm_count_ver:
273 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
273 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
274 %else:
274 %else:
275 , ${_("%d Inline") % inline_comm_count_ver}
275 , ${_("%d Inline") % inline_comm_count_ver}
276 %endif
276 %endif
277
277
278 %if outdated_comm_count_ver:
278 %if outdated_comm_count_ver:
279 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
279 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
280 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
280 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
281 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
281 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
282 %else:
282 %else:
283 , ${_("%d Outdated") % outdated_comm_count_ver}
283 , ${_("%d Outdated") % outdated_comm_count_ver}
284 %endif
284 %endif
285 </td>
285 </td>
286 </tr>
286 </tr>
287 </%def>
287 </%def>
288 ${comments_summary()}
288 ${comments_summary()}
289 </table>
289 </table>
290 % else:
290 % else:
291 <div class="input">
291 <div class="input">
292 ${_('Pull request versions not available')}.
292 ${_('Pull request versions not available')}.
293 </div>
293 </div>
294 <div>
294 <div>
295 <table>
295 <table>
296 ${comments_summary()}
296 ${comments_summary()}
297 </table>
297 </table>
298 </div>
298 </div>
299 % endif
299 % endif
300 </div>
300 </div>
301 </div>
301 </div>
302
302
303 <div id="pr-save" class="field" style="display: none;">
303 <div id="pr-save" class="field" style="display: none;">
304 <div class="label-summary"></div>
304 <div class="label-summary"></div>
305 <div class="input">
305 <div class="input">
306 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
306 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
307 </div>
307 </div>
308 </div>
308 </div>
309 </div>
309 </div>
310 </div>
310 </div>
311 <div>
311 <div>
312 ## AUTHOR
312 ## AUTHOR
313 <div class="reviewers-title block-right">
313 <div class="reviewers-title block-right">
314 <div class="pr-details-title">
314 <div class="pr-details-title">
315 ${_('Author of this pull request')}
315 ${_('Author of this pull request')}
316 </div>
316 </div>
317 </div>
317 </div>
318 <div class="block-right pr-details-content reviewers">
318 <div class="block-right pr-details-content reviewers">
319 <ul class="group_members">
319 <ul class="group_members">
320 <li>
320 <li>
321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
322 </li>
322 </li>
323 </ul>
323 </ul>
324 </div>
324 </div>
325
325
326 ## REVIEW RULES
326 ## REVIEW RULES
327 <div id="review_rules" style="display: none" class="reviewers-title block-right">
327 <div id="review_rules" style="display: none" class="reviewers-title block-right">
328 <div class="pr-details-title">
328 <div class="pr-details-title">
329 ${_('Reviewer rules')}
329 ${_('Reviewer rules')}
330 %if c.allowed_to_update:
330 %if c.allowed_to_update:
331 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
331 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
332 %endif
332 %endif
333 </div>
333 </div>
334 <div class="pr-reviewer-rules">
334 <div class="pr-reviewer-rules">
335 ## review rules will be appended here, by default reviewers logic
335 ## review rules will be appended here, by default reviewers logic
336 </div>
336 </div>
337 <input id="review_data" type="hidden" name="review_data" value="">
337 <input id="review_data" type="hidden" name="review_data" value="">
338 </div>
338 </div>
339
339
340 ## REVIEWERS
340 ## REVIEWERS
341 <div class="reviewers-title block-right">
341 <div class="reviewers-title block-right">
342 <div class="pr-details-title">
342 <div class="pr-details-title">
343 ${_('Pull request reviewers')} / <a href="#toggleReasons" onclick="$('.reviewer_reason').toggle(); return false">${_('show reasons')}</a>
343 ${_('Pull request reviewers')}
344 %if c.allowed_to_update:
344 %if c.allowed_to_update:
345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
346 %endif
346 %endif
347 </div>
347 </div>
348 </div>
348 </div>
349 <div id="reviewers" class="block-right pr-details-content reviewers">
349 <div id="reviewers" class="block-right pr-details-content reviewers">
350 ## members goes here !
350
351 ## members redering block
351 <input type="hidden" name="__start__" value="review_members:sequence">
352 <input type="hidden" name="__start__" value="review_members:sequence">
352 <ul id="review_members" class="group_members">
353 <ul id="review_members" class="group_members">
353 %for member,reasons,mandatory,status in c.pull_request_reviewers:
354
354 <li id="reviewer_${member.user_id}">
355 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
355 <div class="reviewers_member">
356 <script>
356 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
357 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
357 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
358 var status = "${(status[0][1].status if status else 'not_reviewed')}";
358 </div>
359 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
359 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
360 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
360 ${self.gravatar_with_user(member.email, 16)}
361
361 </div>
362 var entry = renderTemplate('reviewMemberEntry', {
362 <input type="hidden" name="__start__" value="reviewer:mapping">
363 'member': member,
363 <input type="hidden" name="__start__" value="reasons:sequence">
364 'mandatory': member.mandatory,
364 % if reasons:
365 'reasons': member.reasons,
365 <div class="reviewer_reason_container">
366 'allowed_to_update': allowed_to_update,
366 %for reason in reasons:
367 'review_status': status,
367 <div class="reviewer_reason" style="display: none">- ${reason}</div>
368 'review_status_label': status_lbl,
368 <input type="hidden" name="reason" value="${reason}">
369 'user_group': member.user_group
370 });
371 $('#review_members').append(entry)
372 </script>
373
369 %endfor
374 % endfor
370 </div>
375
371 % endif
372 <input type="hidden" name="__end__" value="reasons:sequence">
373 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
374 <input type="hidden" name="mandatory" value="${mandatory}"/>
375 <input type="hidden" name="__end__" value="reviewer:mapping">
376 % if mandatory:
377 <div class="reviewer_member_mandatory_remove">
378 <i class="icon-remove-sign"></i>
379 </div>
380 <div class="reviewer_member_mandatory">
381 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
382 </div>
383 % else:
384 %if c.allowed_to_update:
385 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
386 <i class="icon-remove-sign" ></i>
387 </div>
388 %endif
389 % endif
390 </div>
391 </li>
392 %endfor
393 </ul>
376 </ul>
394 <input type="hidden" name="__end__" value="review_members:sequence">
377 <input type="hidden" name="__end__" value="review_members:sequence">
378 ## end members redering block
395
379
396 %if not c.pull_request.is_closed():
380 %if not c.pull_request.is_closed():
397 <div id="add_reviewer" class="ac" style="display: none;">
381 <div id="add_reviewer" class="ac" style="display: none;">
398 %if c.allowed_to_update:
382 %if c.allowed_to_update:
399 % if not c.forbid_adding_reviewers:
383 % if not c.forbid_adding_reviewers:
400 <div id="add_reviewer_input" class="reviewer_ac">
384 <div id="add_reviewer_input" class="reviewer_ac">
401 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
385 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
402 <div id="reviewers_container"></div>
386 <div id="reviewers_container"></div>
403 </div>
387 </div>
404 % endif
388 % endif
405 <div class="pull-right">
389 <div class="pull-right">
406 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
390 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
407 </div>
391 </div>
408 %endif
392 %endif
409 </div>
393 </div>
410 %endif
394 %endif
411 </div>
395 </div>
412 </div>
396 </div>
413 </div>
397 </div>
414 <div class="box">
398 <div class="box">
415 ##DIFF
399 ##DIFF
416 <div class="table" >
400 <div class="table" >
417 <div id="changeset_compare_view_content">
401 <div id="changeset_compare_view_content">
418 ##CS
402 ##CS
419 % if c.missing_requirements:
403 % if c.missing_requirements:
420 <div class="box">
404 <div class="box">
421 <div class="alert alert-warning">
405 <div class="alert alert-warning">
422 <div>
406 <div>
423 <strong>${_('Missing requirements:')}</strong>
407 <strong>${_('Missing requirements:')}</strong>
424 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
408 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
425 </div>
409 </div>
426 </div>
410 </div>
427 </div>
411 </div>
428 % elif c.missing_commits:
412 % elif c.missing_commits:
429 <div class="box">
413 <div class="box">
430 <div class="alert alert-warning">
414 <div class="alert alert-warning">
431 <div>
415 <div>
432 <strong>${_('Missing commits')}:</strong>
416 <strong>${_('Missing commits')}:</strong>
433 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
417 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
434 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
418 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
435 </div>
419 </div>
436 </div>
420 </div>
437 </div>
421 </div>
438 % endif
422 % endif
439
423
440 <div class="compare_view_commits_title">
424 <div class="compare_view_commits_title">
441 % if not c.compare_mode:
425 % if not c.compare_mode:
442
426
443 % if c.at_version_pos:
427 % if c.at_version_pos:
444 <h4>
428 <h4>
445 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
429 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
446 </h4>
430 </h4>
447 % endif
431 % endif
448
432
449 <div class="pull-left">
433 <div class="pull-left">
450 <div class="btn-group">
434 <div class="btn-group">
451 <a
435 <a
452 class="btn"
436 class="btn"
453 href="#"
437 href="#"
454 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
438 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
455 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
439 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
456 </a>
440 </a>
457 <a
441 <a
458 class="btn"
442 class="btn"
459 href="#"
443 href="#"
460 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
444 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
461 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
445 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
462 </a>
446 </a>
463 </div>
447 </div>
464 </div>
448 </div>
465
449
466 <div class="pull-right">
450 <div class="pull-right">
467 % if c.allowed_to_update and not c.pull_request.is_closed():
451 % if c.allowed_to_update and not c.pull_request.is_closed():
468 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
452 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
469 % else:
453 % else:
470 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
454 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
471 % endif
455 % endif
472
456
473 </div>
457 </div>
474 % endif
458 % endif
475 </div>
459 </div>
476
460
477 % if not c.missing_commits:
461 % if not c.missing_commits:
478 % if c.compare_mode:
462 % if c.compare_mode:
479 % if c.at_version:
463 % if c.at_version:
480 <h4>
464 <h4>
481 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
465 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
482 </h4>
466 </h4>
483
467
484 <div class="subtitle-compare">
468 <div class="subtitle-compare">
485 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
469 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
486 </div>
470 </div>
487
471
488 <div class="container">
472 <div class="container">
489 <table class="rctable compare_view_commits">
473 <table class="rctable compare_view_commits">
490 <tr>
474 <tr>
491 <th></th>
475 <th></th>
492 <th>${_('Time')}</th>
476 <th>${_('Time')}</th>
493 <th>${_('Author')}</th>
477 <th>${_('Author')}</th>
494 <th>${_('Commit')}</th>
478 <th>${_('Commit')}</th>
495 <th></th>
479 <th></th>
496 <th>${_('Description')}</th>
480 <th>${_('Description')}</th>
497 </tr>
481 </tr>
498
482
499 % for c_type, commit in c.commit_changes:
483 % for c_type, commit in c.commit_changes:
500 % if c_type in ['a', 'r']:
484 % if c_type in ['a', 'r']:
501 <%
485 <%
502 if c_type == 'a':
486 if c_type == 'a':
503 cc_title = _('Commit added in displayed changes')
487 cc_title = _('Commit added in displayed changes')
504 elif c_type == 'r':
488 elif c_type == 'r':
505 cc_title = _('Commit removed in displayed changes')
489 cc_title = _('Commit removed in displayed changes')
506 else:
490 else:
507 cc_title = ''
491 cc_title = ''
508 %>
492 %>
509 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
493 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
510 <td>
494 <td>
511 <div class="commit-change-indicator color-${c_type}-border">
495 <div class="commit-change-indicator color-${c_type}-border">
512 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
496 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
513 ${c_type.upper()}
497 ${c_type.upper()}
514 </div>
498 </div>
515 </div>
499 </div>
516 </td>
500 </td>
517 <td class="td-time">
501 <td class="td-time">
518 ${h.age_component(commit.date)}
502 ${h.age_component(commit.date)}
519 </td>
503 </td>
520 <td class="td-user">
504 <td class="td-user">
521 ${base.gravatar_with_user(commit.author, 16)}
505 ${base.gravatar_with_user(commit.author, 16)}
522 </td>
506 </td>
523 <td class="td-hash">
507 <td class="td-hash">
524 <code>
508 <code>
525 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
509 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
526 r${commit.revision}:${h.short_id(commit.raw_id)}
510 r${commit.revision}:${h.short_id(commit.raw_id)}
527 </a>
511 </a>
528 ${h.hidden('revisions', commit.raw_id)}
512 ${h.hidden('revisions', commit.raw_id)}
529 </code>
513 </code>
530 </td>
514 </td>
531 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
515 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
532 <div class="show_more_col">
516 <div class="show_more_col">
533 <i class="show_more"></i>
517 <i class="show_more"></i>
534 </div>
518 </div>
535 </td>
519 </td>
536 <td class="mid td-description">
520 <td class="mid td-description">
537 <div class="log-container truncate-wrap">
521 <div class="log-container truncate-wrap">
538 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
522 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
539 ${h.urlify_commit_message(commit.message, c.repo_name)}
523 ${h.urlify_commit_message(commit.message, c.repo_name)}
540 </div>
524 </div>
541 </div>
525 </div>
542 </td>
526 </td>
543 </tr>
527 </tr>
544 % endif
528 % endif
545 % endfor
529 % endfor
546 </table>
530 </table>
547 </div>
531 </div>
548
532
549 <script>
533 <script>
550 $('.expand_commit').on('click',function(e){
534 $('.expand_commit').on('click',function(e){
551 var target_expand = $(this);
535 var target_expand = $(this);
552 var cid = target_expand.data('commitId');
536 var cid = target_expand.data('commitId');
553
537
554 if (target_expand.hasClass('open')){
538 if (target_expand.hasClass('open')){
555 $('#c-'+cid).css({
539 $('#c-'+cid).css({
556 'height': '1.5em',
540 'height': '1.5em',
557 'white-space': 'nowrap',
541 'white-space': 'nowrap',
558 'text-overflow': 'ellipsis',
542 'text-overflow': 'ellipsis',
559 'overflow':'hidden'
543 'overflow':'hidden'
560 });
544 });
561 target_expand.removeClass('open');
545 target_expand.removeClass('open');
562 }
546 }
563 else {
547 else {
564 $('#c-'+cid).css({
548 $('#c-'+cid).css({
565 'height': 'auto',
549 'height': 'auto',
566 'white-space': 'pre-line',
550 'white-space': 'pre-line',
567 'text-overflow': 'initial',
551 'text-overflow': 'initial',
568 'overflow':'visible'
552 'overflow':'visible'
569 });
553 });
570 target_expand.addClass('open');
554 target_expand.addClass('open');
571 }
555 }
572 });
556 });
573 </script>
557 </script>
574
558
575 % endif
559 % endif
576
560
577 % else:
561 % else:
578 <%include file="/compare/compare_commits.mako" />
562 <%include file="/compare/compare_commits.mako" />
579 % endif
563 % endif
580
564
581 <div class="cs_files">
565 <div class="cs_files">
582 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
566 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
583 ${cbdiffs.render_diffset_menu()}
567 ${cbdiffs.render_diffset_menu()}
584 ${cbdiffs.render_diffset(
568 ${cbdiffs.render_diffset(
585 c.diffset, use_comments=True,
569 c.diffset, use_comments=True,
586 collapse_when_files_over=30,
570 collapse_when_files_over=30,
587 disable_new_comments=not c.allowed_to_comment,
571 disable_new_comments=not c.allowed_to_comment,
588 deleted_files_comments=c.deleted_files_comments)}
572 deleted_files_comments=c.deleted_files_comments)}
589 </div>
573 </div>
590 % else:
574 % else:
591 ## skipping commits we need to clear the view for missing commits
575 ## skipping commits we need to clear the view for missing commits
592 <div style="clear:both;"></div>
576 <div style="clear:both;"></div>
593 % endif
577 % endif
594
578
595 </div>
579 </div>
596 </div>
580 </div>
597
581
598 ## template for inline comment form
582 ## template for inline comment form
599 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
583 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
600
584
601 ## render general comments
585 ## render general comments
602
586
603 <div id="comment-tr-show">
587 <div id="comment-tr-show">
604 <div class="comment">
588 <div class="comment">
605 % if general_outdated_comm_count_ver:
589 % if general_outdated_comm_count_ver:
606 <div class="meta">
590 <div class="meta">
607 % if general_outdated_comm_count_ver == 1:
591 % if general_outdated_comm_count_ver == 1:
608 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
592 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
609 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
593 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
610 % else:
594 % else:
611 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
595 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
612 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
596 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
613 % endif
597 % endif
614 </div>
598 </div>
615 % endif
599 % endif
616 </div>
600 </div>
617 </div>
601 </div>
618
602
619 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
603 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
620
604
621 % if not c.pull_request.is_closed():
605 % if not c.pull_request.is_closed():
622 ## merge status, and merge action
606 ## merge status, and merge action
623 <div class="pull-request-merge">
607 <div class="pull-request-merge">
624 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
608 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
625 </div>
609 </div>
626
610
627 ## main comment form and it status
611 ## main comment form and it status
628 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
612 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
629 pull_request_id=c.pull_request.pull_request_id),
613 pull_request_id=c.pull_request.pull_request_id),
630 c.pull_request_review_status,
614 c.pull_request_review_status,
631 is_pull_request=True, change_status=c.allowed_to_change_status)}
615 is_pull_request=True, change_status=c.allowed_to_change_status)}
632 %endif
616 %endif
633
617
634 <script type="text/javascript">
618 <script type="text/javascript">
635 if (location.hash) {
619 if (location.hash) {
636 var result = splitDelimitedHash(location.hash);
620 var result = splitDelimitedHash(location.hash);
637 var line = $('html').find(result.loc);
621 var line = $('html').find(result.loc);
638 // show hidden comments if we use location.hash
622 // show hidden comments if we use location.hash
639 if (line.hasClass('comment-general')) {
623 if (line.hasClass('comment-general')) {
640 $(line).show();
624 $(line).show();
641 } else if (line.hasClass('comment-inline')) {
625 } else if (line.hasClass('comment-inline')) {
642 $(line).show();
626 $(line).show();
643 var $cb = $(line).closest('.cb');
627 var $cb = $(line).closest('.cb');
644 $cb.removeClass('cb-collapsed')
628 $cb.removeClass('cb-collapsed')
645 }
629 }
646 if (line.length > 0){
630 if (line.length > 0){
647 offsetScroll(line, 70);
631 offsetScroll(line, 70);
648 }
632 }
649 }
633 }
650
634
651 versionController = new VersionController();
635 versionController = new VersionController();
652 versionController.init();
636 versionController.init();
653
637
654 reviewersController = new ReviewersController();
638 reviewersController = new ReviewersController();
655
639
656 $(function(){
640 $(function(){
657
641
658 // custom code mirror
642 // custom code mirror
659 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
643 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
660
644
661 var PRDetails = {
645 var PRDetails = {
662 editButton: $('#open_edit_pullrequest'),
646 editButton: $('#open_edit_pullrequest'),
663 closeButton: $('#close_edit_pullrequest'),
647 closeButton: $('#close_edit_pullrequest'),
664 deleteButton: $('#delete_pullrequest'),
648 deleteButton: $('#delete_pullrequest'),
665 viewFields: $('#pr-desc, #pr-title'),
649 viewFields: $('#pr-desc, #pr-title'),
666 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
650 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
667
651
668 init: function() {
652 init: function() {
669 var that = this;
653 var that = this;
670 this.editButton.on('click', function(e) { that.edit(); });
654 this.editButton.on('click', function(e) { that.edit(); });
671 this.closeButton.on('click', function(e) { that.view(); });
655 this.closeButton.on('click', function(e) { that.view(); });
672 },
656 },
673
657
674 edit: function(event) {
658 edit: function(event) {
675 this.viewFields.hide();
659 this.viewFields.hide();
676 this.editButton.hide();
660 this.editButton.hide();
677 this.deleteButton.hide();
661 this.deleteButton.hide();
678 this.closeButton.show();
662 this.closeButton.show();
679 this.editFields.show();
663 this.editFields.show();
680 codeMirrorInstance.refresh();
664 codeMirrorInstance.refresh();
681 },
665 },
682
666
683 view: function(event) {
667 view: function(event) {
684 this.editButton.show();
668 this.editButton.show();
685 this.deleteButton.show();
669 this.deleteButton.show();
686 this.editFields.hide();
670 this.editFields.hide();
687 this.closeButton.hide();
671 this.closeButton.hide();
688 this.viewFields.show();
672 this.viewFields.show();
689 }
673 }
690 };
674 };
691
675
692 var ReviewersPanel = {
676 var ReviewersPanel = {
693 editButton: $('#open_edit_reviewers'),
677 editButton: $('#open_edit_reviewers'),
694 closeButton: $('#close_edit_reviewers'),
678 closeButton: $('#close_edit_reviewers'),
695 addButton: $('#add_reviewer'),
679 addButton: $('#add_reviewer'),
696 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
680 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
697
681
698 init: function() {
682 init: function() {
699 var self = this;
683 var self = this;
700 this.editButton.on('click', function(e) { self.edit(); });
684 this.editButton.on('click', function(e) { self.edit(); });
701 this.closeButton.on('click', function(e) { self.close(); });
685 this.closeButton.on('click', function(e) { self.close(); });
702 },
686 },
703
687
704 edit: function(event) {
688 edit: function(event) {
705 this.editButton.hide();
689 this.editButton.hide();
706 this.closeButton.show();
690 this.closeButton.show();
707 this.addButton.show();
691 this.addButton.show();
708 this.removeButtons.css('visibility', 'visible');
692 this.removeButtons.css('visibility', 'visible');
709 // review rules
693 // review rules
710 reviewersController.loadReviewRules(
694 reviewersController.loadReviewRules(
711 ${c.pull_request.reviewer_data_json | n});
695 ${c.pull_request.reviewer_data_json | n});
712 },
696 },
713
697
714 close: function(event) {
698 close: function(event) {
715 this.editButton.show();
699 this.editButton.show();
716 this.closeButton.hide();
700 this.closeButton.hide();
717 this.addButton.hide();
701 this.addButton.hide();
718 this.removeButtons.css('visibility', 'hidden');
702 this.removeButtons.css('visibility', 'hidden');
719 // hide review rules
703 // hide review rules
720 reviewersController.hideReviewRules()
704 reviewersController.hideReviewRules()
721 }
705 }
722 };
706 };
723
707
724 PRDetails.init();
708 PRDetails.init();
725 ReviewersPanel.init();
709 ReviewersPanel.init();
726
710
727 showOutdated = function(self){
711 showOutdated = function(self){
728 $('.comment-inline.comment-outdated').show();
712 $('.comment-inline.comment-outdated').show();
729 $('.filediff-outdated').show();
713 $('.filediff-outdated').show();
730 $('.showOutdatedComments').hide();
714 $('.showOutdatedComments').hide();
731 $('.hideOutdatedComments').show();
715 $('.hideOutdatedComments').show();
732 };
716 };
733
717
734 hideOutdated = function(self){
718 hideOutdated = function(self){
735 $('.comment-inline.comment-outdated').hide();
719 $('.comment-inline.comment-outdated').hide();
736 $('.filediff-outdated').hide();
720 $('.filediff-outdated').hide();
737 $('.hideOutdatedComments').hide();
721 $('.hideOutdatedComments').hide();
738 $('.showOutdatedComments').show();
722 $('.showOutdatedComments').show();
739 };
723 };
740
724
741 refreshMergeChecks = function(){
725 refreshMergeChecks = function(){
742 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
726 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
743 $('.pull-request-merge').css('opacity', 0.3);
727 $('.pull-request-merge').css('opacity', 0.3);
744 $('.action-buttons-extra').css('opacity', 0.3);
728 $('.action-buttons-extra').css('opacity', 0.3);
745
729
746 $('.pull-request-merge').load(
730 $('.pull-request-merge').load(
747 loadUrl, function() {
731 loadUrl, function() {
748 $('.pull-request-merge').css('opacity', 1);
732 $('.pull-request-merge').css('opacity', 1);
749
733
750 $('.action-buttons-extra').css('opacity', 1);
734 $('.action-buttons-extra').css('opacity', 1);
751 injectCloseAction();
735 injectCloseAction();
752 }
736 }
753 );
737 );
754 };
738 };
755
739
756 injectCloseAction = function() {
740 injectCloseAction = function() {
757 var closeAction = $('#close-pull-request-action').html();
741 var closeAction = $('#close-pull-request-action').html();
758 var $actionButtons = $('.action-buttons-extra');
742 var $actionButtons = $('.action-buttons-extra');
759 // clear the action before
743 // clear the action before
760 $actionButtons.html("");
744 $actionButtons.html("");
761 $actionButtons.html(closeAction);
745 $actionButtons.html(closeAction);
762 };
746 };
763
747
764 closePullRequest = function (status) {
748 closePullRequest = function (status) {
765 // inject closing flag
749 // inject closing flag
766 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
750 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
767 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
751 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
768 $(generalCommentForm.submitForm).submit();
752 $(generalCommentForm.submitForm).submit();
769 };
753 };
770
754
771 $('#show-outdated-comments').on('click', function(e){
755 $('#show-outdated-comments').on('click', function(e){
772 var button = $(this);
756 var button = $(this);
773 var outdated = $('.comment-outdated');
757 var outdated = $('.comment-outdated');
774
758
775 if (button.html() === "(Show)") {
759 if (button.html() === "(Show)") {
776 button.html("(Hide)");
760 button.html("(Hide)");
777 outdated.show();
761 outdated.show();
778 } else {
762 } else {
779 button.html("(Show)");
763 button.html("(Show)");
780 outdated.hide();
764 outdated.hide();
781 }
765 }
782 });
766 });
783
767
784 $('.show-inline-comments').on('change', function(e){
768 $('.show-inline-comments').on('change', function(e){
785 var show = 'none';
769 var show = 'none';
786 var target = e.currentTarget;
770 var target = e.currentTarget;
787 if(target.checked){
771 if(target.checked){
788 show = ''
772 show = ''
789 }
773 }
790 var boxid = $(target).attr('id_for');
774 var boxid = $(target).attr('id_for');
791 var comments = $('#{0} .inline-comments'.format(boxid));
775 var comments = $('#{0} .inline-comments'.format(boxid));
792 var fn_display = function(idx){
776 var fn_display = function(idx){
793 $(this).css('display', show);
777 $(this).css('display', show);
794 };
778 };
795 $(comments).each(fn_display);
779 $(comments).each(fn_display);
796 var btns = $('#{0} .inline-comments-button'.format(boxid));
780 var btns = $('#{0} .inline-comments-button'.format(boxid));
797 $(btns).each(fn_display);
781 $(btns).each(fn_display);
798 });
782 });
799
783
800 $('#merge_pull_request_form').submit(function() {
784 $('#merge_pull_request_form').submit(function() {
801 if (!$('#merge_pull_request').attr('disabled')) {
785 if (!$('#merge_pull_request').attr('disabled')) {
802 $('#merge_pull_request').attr('disabled', 'disabled');
786 $('#merge_pull_request').attr('disabled', 'disabled');
803 }
787 }
804 return true;
788 return true;
805 });
789 });
806
790
807 $('#edit_pull_request').on('click', function(e){
791 $('#edit_pull_request').on('click', function(e){
808 var title = $('#pr-title-input').val();
792 var title = $('#pr-title-input').val();
809 var description = codeMirrorInstance.getValue();
793 var description = codeMirrorInstance.getValue();
810 editPullRequest(
794 editPullRequest(
811 "${c.repo_name}", "${c.pull_request.pull_request_id}",
795 "${c.repo_name}", "${c.pull_request.pull_request_id}",
812 title, description);
796 title, description);
813 });
797 });
814
798
815 $('#update_pull_request').on('click', function(e){
799 $('#update_pull_request').on('click', function(e){
816 $(this).attr('disabled', 'disabled');
800 $(this).attr('disabled', 'disabled');
817 $(this).addClass('disabled');
801 $(this).addClass('disabled');
818 $(this).html(_gettext('Saving...'));
802 $(this).html(_gettext('Saving...'));
819 reviewersController.updateReviewers(
803 reviewersController.updateReviewers(
820 "${c.repo_name}", "${c.pull_request.pull_request_id}");
804 "${c.repo_name}", "${c.pull_request.pull_request_id}");
821 });
805 });
822
806
823 $('#update_commits').on('click', function(e){
807 $('#update_commits').on('click', function(e){
824 var isDisabled = !$(e.currentTarget).attr('disabled');
808 var isDisabled = !$(e.currentTarget).attr('disabled');
825 $(e.currentTarget).attr('disabled', 'disabled');
809 $(e.currentTarget).attr('disabled', 'disabled');
826 $(e.currentTarget).addClass('disabled');
810 $(e.currentTarget).addClass('disabled');
827 $(e.currentTarget).removeClass('btn-primary');
811 $(e.currentTarget).removeClass('btn-primary');
828 $(e.currentTarget).text(_gettext('Updating...'));
812 $(e.currentTarget).text(_gettext('Updating...'));
829 if(isDisabled){
813 if(isDisabled){
830 updateCommits(
814 updateCommits(
831 "${c.repo_name}", "${c.pull_request.pull_request_id}");
815 "${c.repo_name}", "${c.pull_request.pull_request_id}");
832 }
816 }
833 });
817 });
834 // fixing issue with caches on firefox
818 // fixing issue with caches on firefox
835 $('#update_commits').removeAttr("disabled");
819 $('#update_commits').removeAttr("disabled");
836
820
837 $('.show-inline-comments').on('click', function(e){
821 $('.show-inline-comments').on('click', function(e){
838 var boxid = $(this).attr('data-comment-id');
822 var boxid = $(this).attr('data-comment-id');
839 var button = $(this);
823 var button = $(this);
840
824
841 if(button.hasClass("comments-visible")) {
825 if(button.hasClass("comments-visible")) {
842 $('#{0} .inline-comments'.format(boxid)).each(function(index){
826 $('#{0} .inline-comments'.format(boxid)).each(function(index){
843 $(this).hide();
827 $(this).hide();
844 });
828 });
845 button.removeClass("comments-visible");
829 button.removeClass("comments-visible");
846 } else {
830 } else {
847 $('#{0} .inline-comments'.format(boxid)).each(function(index){
831 $('#{0} .inline-comments'.format(boxid)).each(function(index){
848 $(this).show();
832 $(this).show();
849 });
833 });
850 button.addClass("comments-visible");
834 button.addClass("comments-visible");
851 }
835 }
852 });
836 });
853
837
854 // register submit callback on commentForm form to track TODOs
838 // register submit callback on commentForm form to track TODOs
855 window.commentFormGlobalSubmitSuccessCallback = function(){
839 window.commentFormGlobalSubmitSuccessCallback = function(){
856 refreshMergeChecks();
840 refreshMergeChecks();
857 };
841 };
858 // initial injection
842 // initial injection
859 injectCloseAction();
843 injectCloseAction();
860
844
861 ReviewerAutoComplete('#user');
845 ReviewerAutoComplete('#user');
862
846
863 })
847 })
864 </script>
848 </script>
865
849
866 </div>
850 </div>
867 </div>
851 </div>
868
852
869 </%def>
853 </%def>
@@ -1,860 +1,860 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.route_path')
68 'rhodecode.lib.helpers.route_path')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'ąć')
80 mergeable=True, name_suffix=u'ąć')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def cleanup_pull_request():
86 def cleanup_pull_request():
87 calls = [mock.call(
87 calls = [mock.call(
88 self.pull_request, self.pull_request.author, 'create')]
88 self.pull_request, self.pull_request.author, 'create')]
89 self.hook_mock.assert_has_calls(calls)
89 self.hook_mock.assert_has_calls(calls)
90
90
91 self.workspace_remove_patcher.stop()
91 self.workspace_remove_patcher.stop()
92 self.merge_patcher.stop()
92 self.merge_patcher.stop()
93 self.comment_patcher.stop()
93 self.comment_patcher.stop()
94 self.notification_patcher.stop()
94 self.notification_patcher.stop()
95 self.helper_patcher.stop()
95 self.helper_patcher.stop()
96 self.hook_patcher.stop()
96 self.hook_patcher.stop()
97 self.invalidation_patcher.stop()
97 self.invalidation_patcher.stop()
98
98
99 return self.pull_request
99 return self.pull_request
100
100
101 def test_get_all(self, pull_request):
101 def test_get_all(self, pull_request):
102 prs = PullRequestModel().get_all(pull_request.target_repo)
102 prs = PullRequestModel().get_all(pull_request.target_repo)
103 assert isinstance(prs, list)
103 assert isinstance(prs, list)
104 assert len(prs) == 1
104 assert len(prs) == 1
105
105
106 def test_count_all(self, pull_request):
106 def test_count_all(self, pull_request):
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 assert pr_count == 1
108 assert pr_count == 1
109
109
110 def test_get_awaiting_review(self, pull_request):
110 def test_get_awaiting_review(self, pull_request):
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 assert isinstance(prs, list)
112 assert isinstance(prs, list)
113 assert len(prs) == 1
113 assert len(prs) == 1
114
114
115 def test_count_awaiting_review(self, pull_request):
115 def test_count_awaiting_review(self, pull_request):
116 pr_count = PullRequestModel().count_awaiting_review(
116 pr_count = PullRequestModel().count_awaiting_review(
117 pull_request.target_repo)
117 pull_request.target_repo)
118 assert pr_count == 1
118 assert pr_count == 1
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False, [])],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
126 assert isinstance(prs, list)
126 assert isinstance(prs, list)
127 assert len(prs) == 1
127 assert len(prs) == 1
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False, [])],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
135 assert pr_count == 1
135 assert pr_count == 1
136
136
137 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
138 PullRequestModel().delete(pull_request, pull_request.author)
138 PullRequestModel().delete(pull_request, pull_request.author)
139
139
140 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
141 self.workspace_id)
141 self.workspace_id)
142
142
143 def test_close_calls_cleanup_and_hook(self, pull_request):
143 def test_close_calls_cleanup_and_hook(self, pull_request):
144 PullRequestModel().close_pull_request(
144 PullRequestModel().close_pull_request(
145 pull_request, pull_request.author)
145 pull_request, pull_request.author)
146
146
147 self.workspace_remove_mock.assert_called_once_with(
147 self.workspace_remove_mock.assert_called_once_with(
148 self.workspace_id)
148 self.workspace_id)
149 self.hook_mock.assert_called_with(
149 self.hook_mock.assert_called_with(
150 self.pull_request, self.pull_request.author, 'close')
150 self.pull_request, self.pull_request.author, 'close')
151
151
152 def test_merge_status(self, pull_request):
152 def test_merge_status(self, pull_request):
153 self.merge_mock.return_value = MergeResponse(
153 self.merge_mock.return_value = MergeResponse(
154 True, False, None, MergeFailureReason.NONE)
154 True, False, None, MergeFailureReason.NONE)
155
155
156 assert pull_request._last_merge_source_rev is None
156 assert pull_request._last_merge_source_rev is None
157 assert pull_request._last_merge_target_rev is None
157 assert pull_request._last_merge_target_rev is None
158 assert pull_request.last_merge_status is None
158 assert pull_request.last_merge_status is None
159
159
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_with(
163 self.merge_mock.assert_called_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
167 use_rebase=False, close_branch=False)
167 use_rebase=False, close_branch=False)
168
168
169 assert pull_request._last_merge_source_rev == self.source_commit
169 assert pull_request._last_merge_source_rev == self.source_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
172
172
173 self.merge_mock.reset_mock()
173 self.merge_mock.reset_mock()
174 status, msg = PullRequestModel().merge_status(pull_request)
174 status, msg = PullRequestModel().merge_status(pull_request)
175 assert status is True
175 assert status is True
176 assert msg.eval() == 'This pull request can be automatically merged.'
176 assert msg.eval() == 'This pull request can be automatically merged.'
177 assert self.merge_mock.called is False
177 assert self.merge_mock.called is False
178
178
179 def test_merge_status_known_failure(self, pull_request):
179 def test_merge_status_known_failure(self, pull_request):
180 self.merge_mock.return_value = MergeResponse(
180 self.merge_mock.return_value = MergeResponse(
181 False, False, None, MergeFailureReason.MERGE_FAILED)
181 False, False, None, MergeFailureReason.MERGE_FAILED)
182
182
183 assert pull_request._last_merge_source_rev is None
183 assert pull_request._last_merge_source_rev is None
184 assert pull_request._last_merge_target_rev is None
184 assert pull_request._last_merge_target_rev is None
185 assert pull_request.last_merge_status is None
185 assert pull_request.last_merge_status is None
186
186
187 status, msg = PullRequestModel().merge_status(pull_request)
187 status, msg = PullRequestModel().merge_status(pull_request)
188 assert status is False
188 assert status is False
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_with(
192 self.merge_mock.assert_called_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
196 use_rebase=False, close_branch=False)
196 use_rebase=False, close_branch=False)
197
197
198 assert pull_request._last_merge_source_rev == self.source_commit
198 assert pull_request._last_merge_source_rev == self.source_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
200 assert (
200 assert (
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
202
202
203 self.merge_mock.reset_mock()
203 self.merge_mock.reset_mock()
204 status, msg = PullRequestModel().merge_status(pull_request)
204 status, msg = PullRequestModel().merge_status(pull_request)
205 assert status is False
205 assert status is False
206 assert (
206 assert (
207 msg.eval() ==
207 msg.eval() ==
208 'This pull request cannot be merged because of merge conflicts.')
208 'This pull request cannot be merged because of merge conflicts.')
209 assert self.merge_mock.called is False
209 assert self.merge_mock.called is False
210
210
211 def test_merge_status_unknown_failure(self, pull_request):
211 def test_merge_status_unknown_failure(self, pull_request):
212 self.merge_mock.return_value = MergeResponse(
212 self.merge_mock.return_value = MergeResponse(
213 False, False, None, MergeFailureReason.UNKNOWN)
213 False, False, None, MergeFailureReason.UNKNOWN)
214
214
215 assert pull_request._last_merge_source_rev is None
215 assert pull_request._last_merge_source_rev is None
216 assert pull_request._last_merge_target_rev is None
216 assert pull_request._last_merge_target_rev is None
217 assert pull_request.last_merge_status is None
217 assert pull_request.last_merge_status is None
218
218
219 status, msg = PullRequestModel().merge_status(pull_request)
219 status, msg = PullRequestModel().merge_status(pull_request)
220 assert status is False
220 assert status is False
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_with(
224 self.merge_mock.assert_called_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
228 use_rebase=False, close_branch=False)
228 use_rebase=False, close_branch=False)
229
229
230 assert pull_request._last_merge_source_rev is None
230 assert pull_request._last_merge_source_rev is None
231 assert pull_request._last_merge_target_rev is None
231 assert pull_request._last_merge_target_rev is None
232 assert pull_request.last_merge_status is None
232 assert pull_request.last_merge_status is None
233
233
234 self.merge_mock.reset_mock()
234 self.merge_mock.reset_mock()
235 status, msg = PullRequestModel().merge_status(pull_request)
235 status, msg = PullRequestModel().merge_status(pull_request)
236 assert status is False
236 assert status is False
237 assert msg.eval() == (
237 assert msg.eval() == (
238 'This pull request cannot be merged because of an unhandled'
238 'This pull request cannot be merged because of an unhandled'
239 ' exception.')
239 ' exception.')
240 assert self.merge_mock.called is True
240 assert self.merge_mock.called is True
241
241
242 def test_merge_status_when_target_is_locked(self, pull_request):
242 def test_merge_status_when_target_is_locked(self, pull_request):
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
244 status, msg = PullRequestModel().merge_status(pull_request)
244 status, msg = PullRequestModel().merge_status(pull_request)
245 assert status is False
245 assert status is False
246 assert msg.eval() == (
246 assert msg.eval() == (
247 'This pull request cannot be merged because the target repository'
247 'This pull request cannot be merged because the target repository'
248 ' is locked.')
248 ' is locked.')
249
249
250 def test_merge_status_requirements_check_target(self, pull_request):
250 def test_merge_status_requirements_check_target(self, pull_request):
251
251
252 def has_largefiles(self, repo):
252 def has_largefiles(self, repo):
253 return repo == pull_request.source_repo
253 return repo == pull_request.source_repo
254
254
255 patcher = mock.patch.object(
255 patcher = mock.patch.object(
256 PullRequestModel, '_has_largefiles', has_largefiles)
256 PullRequestModel, '_has_largefiles', has_largefiles)
257 with patcher:
257 with patcher:
258 status, msg = PullRequestModel().merge_status(pull_request)
258 status, msg = PullRequestModel().merge_status(pull_request)
259
259
260 assert status is False
260 assert status is False
261 assert msg == 'Target repository large files support is disabled.'
261 assert msg == 'Target repository large files support is disabled.'
262
262
263 def test_merge_status_requirements_check_source(self, pull_request):
263 def test_merge_status_requirements_check_source(self, pull_request):
264
264
265 def has_largefiles(self, repo):
265 def has_largefiles(self, repo):
266 return repo == pull_request.target_repo
266 return repo == pull_request.target_repo
267
267
268 patcher = mock.patch.object(
268 patcher = mock.patch.object(
269 PullRequestModel, '_has_largefiles', has_largefiles)
269 PullRequestModel, '_has_largefiles', has_largefiles)
270 with patcher:
270 with patcher:
271 status, msg = PullRequestModel().merge_status(pull_request)
271 status, msg = PullRequestModel().merge_status(pull_request)
272
272
273 assert status is False
273 assert status is False
274 assert msg == 'Source repository large files support is disabled.'
274 assert msg == 'Source repository large files support is disabled.'
275
275
276 def test_merge(self, pull_request, merge_extras):
276 def test_merge(self, pull_request, merge_extras):
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
278 merge_ref = Reference(
278 merge_ref = Reference(
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
280 self.merge_mock.return_value = MergeResponse(
280 self.merge_mock.return_value = MergeResponse(
281 True, True, merge_ref, MergeFailureReason.NONE)
281 True, True, merge_ref, MergeFailureReason.NONE)
282
282
283 merge_extras['repository'] = pull_request.target_repo.repo_name
283 merge_extras['repository'] = pull_request.target_repo.repo_name
284 PullRequestModel().merge(
284 PullRequestModel().merge(
285 pull_request, pull_request.author, extras=merge_extras)
285 pull_request, pull_request.author, extras=merge_extras)
286
286
287 message = (
287 message = (
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
289 u'\n\n {pr_title}'.format(
289 u'\n\n {pr_title}'.format(
290 pr_id=pull_request.pull_request_id,
290 pr_id=pull_request.pull_request_id,
291 source_repo=safe_unicode(
291 source_repo=safe_unicode(
292 pull_request.source_repo.scm_instance().name),
292 pull_request.source_repo.scm_instance().name),
293 source_ref_name=pull_request.source_ref_parts.name,
293 source_ref_name=pull_request.source_ref_parts.name,
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_with(
297 self.merge_mock.assert_called_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
301 user_name=user.username, user_email=user.email, message=message,
301 user_name=user.username, user_email=user.email, message=message,
302 use_rebase=False, close_branch=False
302 use_rebase=False, close_branch=False
303 )
303 )
304 self.invalidation_mock.assert_called_once_with(
304 self.invalidation_mock.assert_called_once_with(
305 pull_request.target_repo.repo_name)
305 pull_request.target_repo.repo_name)
306
306
307 self.hook_mock.assert_called_with(
307 self.hook_mock.assert_called_with(
308 self.pull_request, self.pull_request.author, 'merge')
308 self.pull_request, self.pull_request.author, 'merge')
309
309
310 pull_request = PullRequest.get(pull_request.pull_request_id)
310 pull_request = PullRequest.get(pull_request.pull_request_id)
311 assert (
311 assert (
312 pull_request.merge_rev ==
312 pull_request.merge_rev ==
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314
314
315 def test_merge_failed(self, pull_request, merge_extras):
315 def test_merge_failed(self, pull_request, merge_extras):
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
317 merge_ref = Reference(
317 merge_ref = Reference(
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
319 self.merge_mock.return_value = MergeResponse(
319 self.merge_mock.return_value = MergeResponse(
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
321
321
322 merge_extras['repository'] = pull_request.target_repo.repo_name
322 merge_extras['repository'] = pull_request.target_repo.repo_name
323 PullRequestModel().merge(
323 PullRequestModel().merge(
324 pull_request, pull_request.author, extras=merge_extras)
324 pull_request, pull_request.author, extras=merge_extras)
325
325
326 message = (
326 message = (
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
328 u'\n\n {pr_title}'.format(
328 u'\n\n {pr_title}'.format(
329 pr_id=pull_request.pull_request_id,
329 pr_id=pull_request.pull_request_id,
330 source_repo=safe_unicode(
330 source_repo=safe_unicode(
331 pull_request.source_repo.scm_instance().name),
331 pull_request.source_repo.scm_instance().name),
332 source_ref_name=pull_request.source_ref_parts.name,
332 source_ref_name=pull_request.source_ref_parts.name,
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_with(
336 self.merge_mock.assert_called_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
340 user_name=user.username, user_email=user.email, message=message,
340 user_name=user.username, user_email=user.email, message=message,
341 use_rebase=False, close_branch=False
341 use_rebase=False, close_branch=False
342 )
342 )
343
343
344 pull_request = PullRequest.get(pull_request.pull_request_id)
344 pull_request = PullRequest.get(pull_request.pull_request_id)
345 assert self.invalidation_mock.called is False
345 assert self.invalidation_mock.called is False
346 assert pull_request.merge_rev is None
346 assert pull_request.merge_rev is None
347
347
348 def test_get_commit_ids(self, pull_request):
348 def test_get_commit_ids(self, pull_request):
349 # The PR has been not merget yet, so expect an exception
349 # The PR has been not merget yet, so expect an exception
350 with pytest.raises(ValueError):
350 with pytest.raises(ValueError):
351 PullRequestModel()._get_commit_ids(pull_request)
351 PullRequestModel()._get_commit_ids(pull_request)
352
352
353 # Merge revision is in the revisions list
353 # Merge revision is in the revisions list
354 pull_request.merge_rev = pull_request.revisions[0]
354 pull_request.merge_rev = pull_request.revisions[0]
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions
356 assert commit_ids == pull_request.revisions
357
357
358 # Merge revision is not in the revisions list
358 # Merge revision is not in the revisions list
359 pull_request.merge_rev = 'f000' * 10
359 pull_request.merge_rev = 'f000' * 10
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
362
362
363 def test_get_diff_from_pr_version(self, pull_request):
363 def test_get_diff_from_pr_version(self, pull_request):
364 source_repo = pull_request.source_repo
364 source_repo = pull_request.source_repo
365 source_ref_id = pull_request.source_ref_parts.commit_id
365 source_ref_id = pull_request.source_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
368 source_repo, source_ref_id, target_ref_id, context=6)
368 source_repo, source_ref_id, target_ref_id, context=6)
369 assert 'file_1' in diff.raw
369 assert 'file_1' in diff.raw
370
370
371 def test_generate_title_returns_unicode(self):
371 def test_generate_title_returns_unicode(self):
372 title = PullRequestModel().generate_pullrequest_title(
372 title = PullRequestModel().generate_pullrequest_title(
373 source='source-dummy',
373 source='source-dummy',
374 source_ref='source-ref-dummy',
374 source_ref='source-ref-dummy',
375 target='target-dummy',
375 target='target-dummy',
376 )
376 )
377 assert type(title) == unicode
377 assert type(title) == unicode
378
378
379
379
380 @pytest.mark.usefixtures('config_stub')
380 @pytest.mark.usefixtures('config_stub')
381 class TestIntegrationMerge(object):
381 class TestIntegrationMerge(object):
382 @pytest.mark.parametrize('extra_config', (
382 @pytest.mark.parametrize('extra_config', (
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
384 ))
384 ))
385 def test_merge_triggers_push_hooks(
385 def test_merge_triggers_push_hooks(
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
387 extra_config):
387 extra_config):
388 pull_request = pr_util.create_pull_request(
388 pull_request = pr_util.create_pull_request(
389 approved=True, mergeable=True)
389 approved=True, mergeable=True)
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
391 merge_extras['repository'] = pull_request.target_repo.repo_name
391 merge_extras['repository'] = pull_request.target_repo.repo_name
392 Session().commit()
392 Session().commit()
393
393
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
395 merge_state = PullRequestModel().merge(
395 merge_state = PullRequestModel().merge(
396 pull_request, user_admin, extras=merge_extras)
396 pull_request, user_admin, extras=merge_extras)
397
397
398 assert merge_state.executed
398 assert merge_state.executed
399 assert 'pre_push' in capture_rcextensions
399 assert 'pre_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
401
401
402 def test_merge_can_be_rejected_by_pre_push_hook(
402 def test_merge_can_be_rejected_by_pre_push_hook(
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
404 pull_request = pr_util.create_pull_request(
404 pull_request = pr_util.create_pull_request(
405 approved=True, mergeable=True)
405 approved=True, mergeable=True)
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
407 merge_extras['repository'] = pull_request.target_repo.repo_name
407 merge_extras['repository'] = pull_request.target_repo.repo_name
408 Session().commit()
408 Session().commit()
409
409
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
411 pre_pull.side_effect = RepositoryError("Disallow push!")
411 pre_pull.side_effect = RepositoryError("Disallow push!")
412 merge_status = PullRequestModel().merge(
412 merge_status = PullRequestModel().merge(
413 pull_request, user_admin, extras=merge_extras)
413 pull_request, user_admin, extras=merge_extras)
414
414
415 assert not merge_status.executed
415 assert not merge_status.executed
416 assert 'pre_push' not in capture_rcextensions
416 assert 'pre_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
418
418
419 def test_merge_fails_if_target_is_locked(
419 def test_merge_fails_if_target_is_locked(
420 self, pr_util, user_regular, merge_extras):
420 self, pr_util, user_regular, merge_extras):
421 pull_request = pr_util.create_pull_request(
421 pull_request = pr_util.create_pull_request(
422 approved=True, mergeable=True)
422 approved=True, mergeable=True)
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
424 pull_request.target_repo.locked = locked_by
424 pull_request.target_repo.locked = locked_by
425 # TODO: johbo: Check if this can work based on the database, currently
425 # TODO: johbo: Check if this can work based on the database, currently
426 # all data is pre-computed, that's why just updating the DB is not
426 # all data is pre-computed, that's why just updating the DB is not
427 # enough.
427 # enough.
428 merge_extras['locked_by'] = locked_by
428 merge_extras['locked_by'] = locked_by
429 merge_extras['repository'] = pull_request.target_repo.repo_name
429 merge_extras['repository'] = pull_request.target_repo.repo_name
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
431 Session().commit()
431 Session().commit()
432 merge_status = PullRequestModel().merge(
432 merge_status = PullRequestModel().merge(
433 pull_request, user_regular, extras=merge_extras)
433 pull_request, user_regular, extras=merge_extras)
434 assert not merge_status.executed
434 assert not merge_status.executed
435
435
436
436
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
438 (False, 1, 0),
438 (False, 1, 0),
439 (True, 0, 1),
439 (True, 0, 1),
440 ])
440 ])
441 def test_outdated_comments(
441 def test_outdated_comments(
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
443 pull_request = pr_util.create_pull_request()
443 pull_request = pr_util.create_pull_request()
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
445
445
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
447 pr_util.add_one_commit()
447 pr_util.add_one_commit()
448 assert_inline_comments(
448 assert_inline_comments(
449 pull_request, visible=inlines_count, outdated=outdated_count)
449 pull_request, visible=inlines_count, outdated=outdated_count)
450 outdated_comment_mock.assert_called_with(pull_request)
450 outdated_comment_mock.assert_called_with(pull_request)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def merge_extras(user_regular):
454 def merge_extras(user_regular):
455 """
455 """
456 Context for the vcs operation when running a merge.
456 Context for the vcs operation when running a merge.
457 """
457 """
458 extras = {
458 extras = {
459 'ip': '127.0.0.1',
459 'ip': '127.0.0.1',
460 'username': user_regular.username,
460 'username': user_regular.username,
461 'user_id': user_regular.user_id,
461 'user_id': user_regular.user_id,
462 'action': 'push',
462 'action': 'push',
463 'repository': 'fake_target_repo_name',
463 'repository': 'fake_target_repo_name',
464 'scm': 'git',
464 'scm': 'git',
465 'config': 'fake_config_ini_path',
465 'config': 'fake_config_ini_path',
466 'make_lock': None,
466 'make_lock': None,
467 'locked_by': [None, None, None],
467 'locked_by': [None, None, None],
468 'server_url': 'http://test.example.com:5000',
468 'server_url': 'http://test.example.com:5000',
469 'hooks': ['push', 'pull'],
469 'hooks': ['push', 'pull'],
470 'is_shadow_repo': False,
470 'is_shadow_repo': False,
471 }
471 }
472 return extras
472 return extras
473
473
474
474
475 @pytest.mark.usefixtures('config_stub')
475 @pytest.mark.usefixtures('config_stub')
476 class TestUpdateCommentHandling(object):
476 class TestUpdateCommentHandling(object):
477
477
478 @pytest.fixture(autouse=True, scope='class')
478 @pytest.fixture(autouse=True, scope='class')
479 def enable_outdated_comments(self, request, baseapp):
479 def enable_outdated_comments(self, request, baseapp):
480 config_patch = mock.patch.dict(
480 config_patch = mock.patch.dict(
481 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
482 config_patch.start()
482 config_patch.start()
483
483
484 @request.addfinalizer
484 @request.addfinalizer
485 def cleanup():
485 def cleanup():
486 config_patch.stop()
486 config_patch.stop()
487
487
488 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
489 commits = [
489 commits = [
490 {'message': 'a'},
490 {'message': 'a'},
491 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
492 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
493 ]
493 ]
494 pull_request = pr_util.create_pull_request(
494 pull_request = pr_util.create_pull_request(
495 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 commits=commits, target_head='a', source_head='b', revisions=['b'])
496 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.create_inline_comment(file_path='file_b')
497 pr_util.add_one_commit(head='c')
497 pr_util.add_one_commit(head='c')
498
498
499 assert_inline_comments(pull_request, visible=1, outdated=0)
499 assert_inline_comments(pull_request, visible=1, outdated=0)
500
500
501 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 def test_comment_stays_unflagged_on_change_above(self, pr_util):
502 original_content = ''.join(
502 original_content = ''.join(
503 ['line {}\n'.format(x) for x in range(1, 11)])
503 ['line {}\n'.format(x) for x in range(1, 11)])
504 updated_content = 'new_line_at_top\n' + original_content
504 updated_content = 'new_line_at_top\n' + original_content
505 commits = [
505 commits = [
506 {'message': 'a'},
506 {'message': 'a'},
507 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
508 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
509 ]
509 ]
510 pull_request = pr_util.create_pull_request(
510 pull_request = pr_util.create_pull_request(
511 commits=commits, target_head='a', source_head='b', revisions=['b'])
511 commits=commits, target_head='a', source_head='b', revisions=['b'])
512
512
513 with outdated_comments_patcher():
513 with outdated_comments_patcher():
514 comment = pr_util.create_inline_comment(
514 comment = pr_util.create_inline_comment(
515 line_no=u'n8', file_path='file_b')
515 line_no=u'n8', file_path='file_b')
516 pr_util.add_one_commit(head='c')
516 pr_util.add_one_commit(head='c')
517
517
518 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert_inline_comments(pull_request, visible=1, outdated=0)
519 assert comment.line_no == u'n9'
519 assert comment.line_no == u'n9'
520
520
521 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 def test_comment_stays_unflagged_on_change_below(self, pr_util):
522 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
523 updated_content = original_content + 'new_line_at_end\n'
523 updated_content = original_content + 'new_line_at_end\n'
524 commits = [
524 commits = [
525 {'message': 'a'},
525 {'message': 'a'},
526 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
527 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
528 ]
528 ]
529 pull_request = pr_util.create_pull_request(
529 pull_request = pr_util.create_pull_request(
530 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 commits=commits, target_head='a', source_head='b', revisions=['b'])
531 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.create_inline_comment(file_path='file_b')
532 pr_util.add_one_commit(head='c')
532 pr_util.add_one_commit(head='c')
533
533
534 assert_inline_comments(pull_request, visible=1, outdated=0)
534 assert_inline_comments(pull_request, visible=1, outdated=0)
535
535
536 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
537 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
538 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
539 change_lines = list(base_lines)
539 change_lines = list(base_lines)
540 change_lines.insert(6, 'line 6a added\n')
540 change_lines.insert(6, 'line 6a added\n')
541
541
542 # Changes on the last line of sight
542 # Changes on the last line of sight
543 update_lines = list(change_lines)
543 update_lines = list(change_lines)
544 update_lines[0] = 'line 1 changed\n'
544 update_lines[0] = 'line 1 changed\n'
545 update_lines[-1] = 'line 12 changed\n'
545 update_lines[-1] = 'line 12 changed\n'
546
546
547 def file_b(lines):
547 def file_b(lines):
548 return FileNode('file_b', ''.join(lines))
548 return FileNode('file_b', ''.join(lines))
549
549
550 commits = [
550 commits = [
551 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'a', 'added': [file_b(base_lines)]},
552 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'b', 'changed': [file_b(change_lines)]},
553 {'message': 'c', 'changed': [file_b(update_lines)]},
553 {'message': 'c', 'changed': [file_b(update_lines)]},
554 ]
554 ]
555
555
556 pull_request = pr_util.create_pull_request(
556 pull_request = pr_util.create_pull_request(
557 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 commits=commits, target_head='a', source_head='b', revisions=['b'])
558 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
559
559
560 with outdated_comments_patcher():
560 with outdated_comments_patcher():
561 pr_util.add_one_commit(head='c')
561 pr_util.add_one_commit(head='c')
562 assert_inline_comments(pull_request, visible=0, outdated=1)
562 assert_inline_comments(pull_request, visible=0, outdated=1)
563
563
564 @pytest.mark.parametrize("change, content", [
564 @pytest.mark.parametrize("change, content", [
565 ('changed', 'changed\n'),
565 ('changed', 'changed\n'),
566 ('removed', ''),
566 ('removed', ''),
567 ], ids=['changed', 'removed'])
567 ], ids=['changed', 'removed'])
568 def test_comment_flagged_on_change(self, pr_util, change, content):
568 def test_comment_flagged_on_change(self, pr_util, change, content):
569 commits = [
569 commits = [
570 {'message': 'a'},
570 {'message': 'a'},
571 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
572 {'message': 'c', change: [FileNode('file_b', content)]},
572 {'message': 'c', change: [FileNode('file_b', content)]},
573 ]
573 ]
574 pull_request = pr_util.create_pull_request(
574 pull_request = pr_util.create_pull_request(
575 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 commits=commits, target_head='a', source_head='b', revisions=['b'])
576 pr_util.create_inline_comment(file_path='file_b')
576 pr_util.create_inline_comment(file_path='file_b')
577
577
578 with outdated_comments_patcher():
578 with outdated_comments_patcher():
579 pr_util.add_one_commit(head='c')
579 pr_util.add_one_commit(head='c')
580 assert_inline_comments(pull_request, visible=0, outdated=1)
580 assert_inline_comments(pull_request, visible=0, outdated=1)
581
581
582
582
583 @pytest.mark.usefixtures('config_stub')
583 @pytest.mark.usefixtures('config_stub')
584 class TestUpdateChangedFiles(object):
584 class TestUpdateChangedFiles(object):
585
585
586 def test_no_changes_on_unchanged_diff(self, pr_util):
586 def test_no_changes_on_unchanged_diff(self, pr_util):
587 commits = [
587 commits = [
588 {'message': 'a'},
588 {'message': 'a'},
589 {'message': 'b',
589 {'message': 'b',
590 'added': [FileNode('file_b', 'test_content b\n')]},
590 'added': [FileNode('file_b', 'test_content b\n')]},
591 {'message': 'c',
591 {'message': 'c',
592 'added': [FileNode('file_c', 'test_content c\n')]},
592 'added': [FileNode('file_c', 'test_content c\n')]},
593 ]
593 ]
594 # open a PR from a to b, adding file_b
594 # open a PR from a to b, adding file_b
595 pull_request = pr_util.create_pull_request(
595 pull_request = pr_util.create_pull_request(
596 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 commits=commits, target_head='a', source_head='b', revisions=['b'],
597 name_suffix='per-file-review')
597 name_suffix='per-file-review')
598
598
599 # modify PR adding new file file_c
599 # modify PR adding new file file_c
600 pr_util.add_one_commit(head='c')
600 pr_util.add_one_commit(head='c')
601
601
602 assert_pr_file_changes(
602 assert_pr_file_changes(
603 pull_request,
603 pull_request,
604 added=['file_c'],
604 added=['file_c'],
605 modified=[],
605 modified=[],
606 removed=[])
606 removed=[])
607
607
608 def test_modify_and_undo_modification_diff(self, pr_util):
608 def test_modify_and_undo_modification_diff(self, pr_util):
609 commits = [
609 commits = [
610 {'message': 'a'},
610 {'message': 'a'},
611 {'message': 'b',
611 {'message': 'b',
612 'added': [FileNode('file_b', 'test_content b\n')]},
612 'added': [FileNode('file_b', 'test_content b\n')]},
613 {'message': 'c',
613 {'message': 'c',
614 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 'changed': [FileNode('file_b', 'test_content b modified\n')]},
615 {'message': 'd',
615 {'message': 'd',
616 'changed': [FileNode('file_b', 'test_content b\n')]},
616 'changed': [FileNode('file_b', 'test_content b\n')]},
617 ]
617 ]
618 # open a PR from a to b, adding file_b
618 # open a PR from a to b, adding file_b
619 pull_request = pr_util.create_pull_request(
619 pull_request = pr_util.create_pull_request(
620 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 commits=commits, target_head='a', source_head='b', revisions=['b'],
621 name_suffix='per-file-review')
621 name_suffix='per-file-review')
622
622
623 # modify PR modifying file file_b
623 # modify PR modifying file file_b
624 pr_util.add_one_commit(head='c')
624 pr_util.add_one_commit(head='c')
625
625
626 assert_pr_file_changes(
626 assert_pr_file_changes(
627 pull_request,
627 pull_request,
628 added=[],
628 added=[],
629 modified=['file_b'],
629 modified=['file_b'],
630 removed=[])
630 removed=[])
631
631
632 # move the head again to d, which rollbacks change,
632 # move the head again to d, which rollbacks change,
633 # meaning we should indicate no changes
633 # meaning we should indicate no changes
634 pr_util.add_one_commit(head='d')
634 pr_util.add_one_commit(head='d')
635
635
636 assert_pr_file_changes(
636 assert_pr_file_changes(
637 pull_request,
637 pull_request,
638 added=[],
638 added=[],
639 modified=[],
639 modified=[],
640 removed=[])
640 removed=[])
641
641
642 def test_updated_all_files_in_pr(self, pr_util):
642 def test_updated_all_files_in_pr(self, pr_util):
643 commits = [
643 commits = [
644 {'message': 'a'},
644 {'message': 'a'},
645 {'message': 'b', 'added': [
645 {'message': 'b', 'added': [
646 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_a', 'test_content a\n'),
647 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_b', 'test_content b\n'),
648 FileNode('file_c', 'test_content c\n')]},
648 FileNode('file_c', 'test_content c\n')]},
649 {'message': 'c', 'changed': [
649 {'message': 'c', 'changed': [
650 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_a', 'test_content a changed\n'),
651 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_b', 'test_content b changed\n'),
652 FileNode('file_c', 'test_content c changed\n')]},
652 FileNode('file_c', 'test_content c changed\n')]},
653 ]
653 ]
654 # open a PR from a to b, changing 3 files
654 # open a PR from a to b, changing 3 files
655 pull_request = pr_util.create_pull_request(
655 pull_request = pr_util.create_pull_request(
656 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 commits=commits, target_head='a', source_head='b', revisions=['b'],
657 name_suffix='per-file-review')
657 name_suffix='per-file-review')
658
658
659 pr_util.add_one_commit(head='c')
659 pr_util.add_one_commit(head='c')
660
660
661 assert_pr_file_changes(
661 assert_pr_file_changes(
662 pull_request,
662 pull_request,
663 added=[],
663 added=[],
664 modified=['file_a', 'file_b', 'file_c'],
664 modified=['file_a', 'file_b', 'file_c'],
665 removed=[])
665 removed=[])
666
666
667 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 def test_updated_and_removed_all_files_in_pr(self, pr_util):
668 commits = [
668 commits = [
669 {'message': 'a'},
669 {'message': 'a'},
670 {'message': 'b', 'added': [
670 {'message': 'b', 'added': [
671 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_a', 'test_content a\n'),
672 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_b', 'test_content b\n'),
673 FileNode('file_c', 'test_content c\n')]},
673 FileNode('file_c', 'test_content c\n')]},
674 {'message': 'c', 'removed': [
674 {'message': 'c', 'removed': [
675 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_a', 'test_content a changed\n'),
676 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_b', 'test_content b changed\n'),
677 FileNode('file_c', 'test_content c changed\n')]},
677 FileNode('file_c', 'test_content c changed\n')]},
678 ]
678 ]
679 # open a PR from a to b, removing 3 files
679 # open a PR from a to b, removing 3 files
680 pull_request = pr_util.create_pull_request(
680 pull_request = pr_util.create_pull_request(
681 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 commits=commits, target_head='a', source_head='b', revisions=['b'],
682 name_suffix='per-file-review')
682 name_suffix='per-file-review')
683
683
684 pr_util.add_one_commit(head='c')
684 pr_util.add_one_commit(head='c')
685
685
686 assert_pr_file_changes(
686 assert_pr_file_changes(
687 pull_request,
687 pull_request,
688 added=[],
688 added=[],
689 modified=[],
689 modified=[],
690 removed=['file_a', 'file_b', 'file_c'])
690 removed=['file_a', 'file_b', 'file_c'])
691
691
692
692
693 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
694 model = PullRequestModel()
694 model = PullRequestModel()
695 pull_request = pr_util.create_pull_request()
695 pull_request = pr_util.create_pull_request()
696 pr_util.update_source_repository()
696 pr_util.update_source_repository()
697
697
698 model.update_commits(pull_request)
698 model.update_commits(pull_request)
699
699
700 # Expect that it has a version entry now
700 # Expect that it has a version entry now
701 assert len(model.get_versions(pull_request)) == 1
701 assert len(model.get_versions(pull_request)) == 1
702
702
703
703
704 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
705 pull_request = pr_util.create_pull_request()
705 pull_request = pr_util.create_pull_request()
706 model = PullRequestModel()
706 model = PullRequestModel()
707 model.update_commits(pull_request)
707 model.update_commits(pull_request)
708
708
709 # Expect that it still has no versions
709 # Expect that it still has no versions
710 assert len(model.get_versions(pull_request)) == 0
710 assert len(model.get_versions(pull_request)) == 0
711
711
712
712
713 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
714 model = PullRequestModel()
714 model = PullRequestModel()
715 pull_request = pr_util.create_pull_request()
715 pull_request = pr_util.create_pull_request()
716 comment = pr_util.create_comment()
716 comment = pr_util.create_comment()
717 pr_util.update_source_repository()
717 pr_util.update_source_repository()
718
718
719 model.update_commits(pull_request)
719 model.update_commits(pull_request)
720
720
721 # Expect that the comment is linked to the pr version now
721 # Expect that the comment is linked to the pr version now
722 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722 assert comment.pull_request_version == model.get_versions(pull_request)[0]
723
723
724
724
725 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
726 model = PullRequestModel()
726 model = PullRequestModel()
727 pull_request = pr_util.create_pull_request()
727 pull_request = pr_util.create_pull_request()
728 pr_util.update_source_repository()
728 pr_util.update_source_repository()
729 pr_util.update_source_repository()
729 pr_util.update_source_repository()
730
730
731 model.update_commits(pull_request)
731 model.update_commits(pull_request)
732
732
733 # Expect to find a new comment about the change
733 # Expect to find a new comment about the change
734 expected_message = textwrap.dedent(
734 expected_message = textwrap.dedent(
735 """\
735 """\
736 Pull request updated. Auto status change to |under_review|
736 Pull request updated. Auto status change to |under_review|
737
737
738 .. role:: added
738 .. role:: added
739 .. role:: removed
739 .. role:: removed
740 .. parsed-literal::
740 .. parsed-literal::
741
741
742 Changed commits:
742 Changed commits:
743 * :added:`1 added`
743 * :added:`1 added`
744 * :removed:`0 removed`
744 * :removed:`0 removed`
745
745
746 Changed files:
746 Changed files:
747 * `A file_2 <#a_c--92ed3b5f07b4>`_
747 * `A file_2 <#a_c--92ed3b5f07b4>`_
748
748
749 .. |under_review| replace:: *"Under Review"*"""
749 .. |under_review| replace:: *"Under Review"*"""
750 )
750 )
751 pull_request_comments = sorted(
751 pull_request_comments = sorted(
752 pull_request.comments, key=lambda c: c.modified_at)
752 pull_request.comments, key=lambda c: c.modified_at)
753 update_comment = pull_request_comments[-1]
753 update_comment = pull_request_comments[-1]
754 assert update_comment.text == expected_message
754 assert update_comment.text == expected_message
755
755
756
756
757 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
758 pull_request = pr_util.create_pull_request()
758 pull_request = pr_util.create_pull_request()
759
759
760 # Avoiding default values
760 # Avoiding default values
761 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request.status = PullRequest.STATUS_CLOSED
762 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_source_rev = "0" * 40
763 pull_request._last_merge_target_rev = "1" * 40
763 pull_request._last_merge_target_rev = "1" * 40
764 pull_request.last_merge_status = 1
764 pull_request.last_merge_status = 1
765 pull_request.merge_rev = "2" * 40
765 pull_request.merge_rev = "2" * 40
766
766
767 # Remember automatic values
767 # Remember automatic values
768 created_on = pull_request.created_on
768 created_on = pull_request.created_on
769 updated_on = pull_request.updated_on
769 updated_on = pull_request.updated_on
770
770
771 # Create a new version of the pull request
771 # Create a new version of the pull request
772 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772 version = PullRequestModel()._create_version_from_snapshot(pull_request)
773
773
774 # Check attributes
774 # Check attributes
775 assert version.title == pr_util.create_parameters['title']
775 assert version.title == pr_util.create_parameters['title']
776 assert version.description == pr_util.create_parameters['description']
776 assert version.description == pr_util.create_parameters['description']
777 assert version.status == PullRequest.STATUS_CLOSED
777 assert version.status == PullRequest.STATUS_CLOSED
778
778
779 # versions get updated created_on
779 # versions get updated created_on
780 assert version.created_on != created_on
780 assert version.created_on != created_on
781
781
782 assert version.updated_on == updated_on
782 assert version.updated_on == updated_on
783 assert version.user_id == pull_request.user_id
783 assert version.user_id == pull_request.user_id
784 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.revisions == pr_util.create_parameters['revisions']
785 assert version.source_repo == pr_util.source_repository
785 assert version.source_repo == pr_util.source_repository
786 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.source_ref == pr_util.create_parameters['source_ref']
787 assert version.target_repo == pr_util.target_repository
787 assert version.target_repo == pr_util.target_repository
788 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version.target_ref == pr_util.create_parameters['target_ref']
789 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
790 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
791 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.last_merge_status == pull_request.last_merge_status
792 assert version.merge_rev == pull_request.merge_rev
792 assert version.merge_rev == pull_request.merge_rev
793 assert version.pull_request == pull_request
793 assert version.pull_request == pull_request
794
794
795
795
796 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
797 version1 = pr_util.create_version_of_pull_request()
797 version1 = pr_util.create_version_of_pull_request()
798 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_linked = pr_util.create_comment(linked_to=version1)
799 comment_unlinked = pr_util.create_comment()
799 comment_unlinked = pr_util.create_comment()
800 version2 = pr_util.create_version_of_pull_request()
800 version2 = pr_util.create_version_of_pull_request()
801
801
802 PullRequestModel()._link_comments_to_version(version2)
802 PullRequestModel()._link_comments_to_version(version2)
803
803
804 # Expect that only the new comment is linked to version2
804 # Expect that only the new comment is linked to version2
805 assert (
805 assert (
806 comment_unlinked.pull_request_version_id ==
806 comment_unlinked.pull_request_version_id ==
807 version2.pull_request_version_id)
807 version2.pull_request_version_id)
808 assert (
808 assert (
809 comment_linked.pull_request_version_id ==
809 comment_linked.pull_request_version_id ==
810 version1.pull_request_version_id)
810 version1.pull_request_version_id)
811 assert (
811 assert (
812 comment_unlinked.pull_request_version_id !=
812 comment_unlinked.pull_request_version_id !=
813 comment_linked.pull_request_version_id)
813 comment_linked.pull_request_version_id)
814
814
815
815
816 def test_calculate_commits():
816 def test_calculate_commits():
817 old_ids = [1, 2, 3]
817 old_ids = [1, 2, 3]
818 new_ids = [1, 3, 4, 5]
818 new_ids = [1, 3, 4, 5]
819 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
820 assert change.added == [4, 5]
820 assert change.added == [4, 5]
821 assert change.common == [1, 3]
821 assert change.common == [1, 3]
822 assert change.removed == [2]
822 assert change.removed == [2]
823 assert change.total == [1, 3, 4, 5]
823 assert change.total == [1, 3, 4, 5]
824
824
825
825
826 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 def assert_inline_comments(pull_request, visible=None, outdated=None):
827 if visible is not None:
827 if visible is not None:
828 inline_comments = CommentsModel().get_inline_comments(
828 inline_comments = CommentsModel().get_inline_comments(
829 pull_request.target_repo.repo_id, pull_request=pull_request)
829 pull_request.target_repo.repo_id, pull_request=pull_request)
830 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_cnt = CommentsModel().get_inline_comments_count(
831 inline_comments)
831 inline_comments)
832 assert inline_cnt == visible
832 assert inline_cnt == visible
833 if outdated is not None:
833 if outdated is not None:
834 outdated_comments = CommentsModel().get_outdated_comments(
834 outdated_comments = CommentsModel().get_outdated_comments(
835 pull_request.target_repo.repo_id, pull_request)
835 pull_request.target_repo.repo_id, pull_request)
836 assert len(outdated_comments) == outdated
836 assert len(outdated_comments) == outdated
837
837
838
838
839 def assert_pr_file_changes(
839 def assert_pr_file_changes(
840 pull_request, added=None, modified=None, removed=None):
840 pull_request, added=None, modified=None, removed=None):
841 pr_versions = PullRequestModel().get_versions(pull_request)
841 pr_versions = PullRequestModel().get_versions(pull_request)
842 # always use first version, ie original PR to calculate changes
842 # always use first version, ie original PR to calculate changes
843 pull_request_version = pr_versions[0]
843 pull_request_version = pr_versions[0]
844 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
845 pull_request, pull_request_version)
845 pull_request, pull_request_version)
846 file_changes = PullRequestModel()._calculate_file_changes(
846 file_changes = PullRequestModel()._calculate_file_changes(
847 old_diff_data, new_diff_data)
847 old_diff_data, new_diff_data)
848
848
849 assert added == file_changes.added, \
849 assert added == file_changes.added, \
850 'expected added:%s vs value:%s' % (added, file_changes.added)
850 'expected added:%s vs value:%s' % (added, file_changes.added)
851 assert modified == file_changes.modified, \
851 assert modified == file_changes.modified, \
852 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
853 assert removed == file_changes.removed, \
853 assert removed == file_changes.removed, \
854 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
855
855
856
856
857 def outdated_comments_patcher(use_outdated=True):
857 def outdated_comments_patcher(use_outdated=True):
858 return mock.patch.object(
858 return mock.patch.object(
859 CommentsModel, 'use_outdated_comments',
859 CommentsModel, 'use_outdated_comments',
860 return_value=use_outdated)
860 return_value=use_outdated)
@@ -1,323 +1,328 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 pytest
21 import pytest
22 import mock
22 import mock
23
23
24 from rhodecode.lib.utils2 import safe_unicode
24 from rhodecode.lib.utils2 import safe_unicode
25 from rhodecode.model.db import (
25 from rhodecode.model.db import (
26 true, User, UserGroup, UserGroupMember, UserEmailMap, Permission, UserIpMap)
26 true, User, UserGroup, UserGroupMember, UserEmailMap, Permission, UserIpMap)
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.user import UserModel
28 from rhodecode.model.user import UserModel
29 from rhodecode.model.user_group import UserGroupModel
29 from rhodecode.model.user_group import UserGroupModel
30 from rhodecode.model.repo import RepoModel
30 from rhodecode.model.repo import RepoModel
31 from rhodecode.model.repo_group import RepoGroupModel
31 from rhodecode.model.repo_group import RepoGroupModel
32 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.fixture import Fixture
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 class TestGetUsers(object):
37 class TestGetUsers(object):
38 def test_returns_active_users(self, backend, user_util):
38 def test_returns_active_users(self, backend, user_util):
39 for i in range(4):
39 for i in range(4):
40 is_active = i % 2 == 0
40 is_active = i % 2 == 0
41 user_util.create_user(active=is_active, lastname='Fake user')
41 user_util.create_user(active=is_active, lastname='Fake user')
42
42
43 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
43 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
44 with mock.patch('rhodecode.lib.helpers.link_to_user'):
44 users = UserModel().get_users()
45 users = UserModel().get_users()
45 fake_users = [u for u in users if u['last_name'] == 'Fake user']
46 fake_users = [u for u in users if u['last_name'] == 'Fake user']
46 assert len(fake_users) == 2
47 assert len(fake_users) == 2
47
48
48 expected_keys = (
49 expected_keys = (
49 'id', 'first_name', 'last_name', 'username', 'icon_link',
50 'id', 'first_name', 'last_name', 'username', 'icon_link',
50 'value_display', 'value', 'value_type')
51 'value_display', 'value', 'value_type')
51 for user in users:
52 for user in users:
52 assert user['value_type'] is 'user'
53 assert user['value_type'] is 'user'
53 for key in expected_keys:
54 for key in expected_keys:
54 assert key in user
55 assert key in user
55
56
56 def test_returns_user_filtered_by_last_name(self, backend, user_util):
57 def test_returns_user_filtered_by_last_name(self, backend, user_util):
57 keywords = ('aBc', u'ünicode')
58 keywords = ('aBc', u'ünicode')
58 for keyword in keywords:
59 for keyword in keywords:
59 for i in range(2):
60 for i in range(2):
60 user_util.create_user(
61 user_util.create_user(
61 active=True, lastname=u'Fake {} user'.format(keyword))
62 active=True, lastname=u'Fake {} user'.format(keyword))
62
63
63 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
64 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
65 with mock.patch('rhodecode.lib.helpers.link_to_user'):
64 keyword = keywords[1].lower()
66 keyword = keywords[1].lower()
65 users = UserModel().get_users(name_contains=keyword)
67 users = UserModel().get_users(name_contains=keyword)
66
68
67 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
69 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
68 assert len(fake_users) == 2
70 assert len(fake_users) == 2
69 for user in fake_users:
71 for user in fake_users:
70 assert user['last_name'] == safe_unicode('Fake ünicode user')
72 assert user['last_name'] == safe_unicode('Fake ünicode user')
71
73
72 def test_returns_user_filtered_by_first_name(self, backend, user_util):
74 def test_returns_user_filtered_by_first_name(self, backend, user_util):
73 created_users = []
75 created_users = []
74 keywords = ('aBc', u'ünicode')
76 keywords = ('aBc', u'ünicode')
75 for keyword in keywords:
77 for keyword in keywords:
76 for i in range(2):
78 for i in range(2):
77 created_users.append(user_util.create_user(
79 created_users.append(user_util.create_user(
78 active=True, lastname='Fake user',
80 active=True, lastname='Fake user',
79 firstname=u'Fake {} user'.format(keyword)))
81 firstname=u'Fake {} user'.format(keyword)))
80
82
81 keyword = keywords[1].lower()
83 keyword = keywords[1].lower()
82 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
84 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
85 with mock.patch('rhodecode.lib.helpers.link_to_user'):
83 users = UserModel().get_users(name_contains=keyword)
86 users = UserModel().get_users(name_contains=keyword)
84
87
85 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
88 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
86 assert len(fake_users) == 2
89 assert len(fake_users) == 2
87 for user in fake_users:
90 for user in fake_users:
88 assert user['first_name'] == safe_unicode('Fake ünicode user')
91 assert user['first_name'] == safe_unicode('Fake ünicode user')
89
92
90 def test_returns_user_filtered_by_username(self, backend, user_util):
93 def test_returns_user_filtered_by_username(self, backend, user_util):
91 created_users = []
94 created_users = []
92 for i in range(5):
95 for i in range(5):
93 created_users.append(user_util.create_user(
96 created_users.append(user_util.create_user(
94 active=True, lastname='Fake user'))
97 active=True, lastname='Fake user'))
95
98
96 user_filter = created_users[-1].username[-2:]
99 user_filter = created_users[-1].username[-2:]
97 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
100 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
101 with mock.patch('rhodecode.lib.helpers.link_to_user'):
98 users = UserModel().get_users(name_contains=user_filter)
102 users = UserModel().get_users(name_contains=user_filter)
99
103
100 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
104 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
101 assert len(fake_users) == 1
105 assert len(fake_users) == 1
102 assert fake_users[0]['username'] == created_users[-1].username
106 assert fake_users[0]['username'] == created_users[-1].username
103
107
104 def test_returns_limited_user_list(self, backend, user_util):
108 def test_returns_limited_user_list(self, backend, user_util):
105 created_users = []
109 created_users = []
106 for i in range(5):
110 for i in range(5):
107 created_users.append(user_util.create_user(
111 created_users.append(user_util.create_user(
108 active=True, lastname='Fake user'))
112 active=True, lastname='Fake user'))
109
113
110 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
114 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
115 with mock.patch('rhodecode.lib.helpers.link_to_user'):
111 users = UserModel().get_users(name_contains='Fake', limit=3)
116 users = UserModel().get_users(name_contains='Fake', limit=3)
112
117
113 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
118 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
114 assert len(fake_users) == 3
119 assert len(fake_users) == 3
115
120
116
121
117 @pytest.fixture
122 @pytest.fixture
118 def test_user(request, baseapp):
123 def test_user(request, baseapp):
119 usr = UserModel().create_or_update(
124 usr = UserModel().create_or_update(
120 username=u'test_user',
125 username=u'test_user',
121 password=u'qweqwe',
126 password=u'qweqwe',
122 email=u'main_email@rhodecode.org',
127 email=u'main_email@rhodecode.org',
123 firstname=u'u1', lastname=u'u1')
128 firstname=u'u1', lastname=u'u1')
124 Session().commit()
129 Session().commit()
125 assert User.get_by_username(u'test_user') == usr
130 assert User.get_by_username(u'test_user') == usr
126
131
127 @request.addfinalizer
132 @request.addfinalizer
128 def cleanup():
133 def cleanup():
129 if UserModel().get_user(usr.user_id) is None:
134 if UserModel().get_user(usr.user_id) is None:
130 return
135 return
131
136
132 perm = Permission.query().all()
137 perm = Permission.query().all()
133 for p in perm:
138 for p in perm:
134 UserModel().revoke_perm(usr, p)
139 UserModel().revoke_perm(usr, p)
135
140
136 UserModel().delete(usr.user_id)
141 UserModel().delete(usr.user_id)
137 Session().commit()
142 Session().commit()
138
143
139 return usr
144 return usr
140
145
141
146
142 def test_create_and_remove(test_user):
147 def test_create_and_remove(test_user):
143 usr = test_user
148 usr = test_user
144
149
145 # make user group
150 # make user group
146 user_group = fixture.create_user_group('some_example_group')
151 user_group = fixture.create_user_group('some_example_group')
147 Session().commit()
152 Session().commit()
148
153
149 UserGroupModel().add_user_to_group(user_group, usr)
154 UserGroupModel().add_user_to_group(user_group, usr)
150 Session().commit()
155 Session().commit()
151
156
152 assert UserGroup.get(user_group.users_group_id) == user_group
157 assert UserGroup.get(user_group.users_group_id) == user_group
153 assert UserGroupMember.query().count() == 1
158 assert UserGroupMember.query().count() == 1
154 UserModel().delete(usr.user_id)
159 UserModel().delete(usr.user_id)
155 Session().commit()
160 Session().commit()
156
161
157 assert UserGroupMember.query().all() == []
162 assert UserGroupMember.query().all() == []
158
163
159
164
160 def test_additonal_email_as_main(test_user):
165 def test_additonal_email_as_main(test_user):
161 with pytest.raises(AttributeError):
166 with pytest.raises(AttributeError):
162 m = UserEmailMap()
167 m = UserEmailMap()
163 m.email = test_user.email
168 m.email = test_user.email
164 m.user = test_user
169 m.user = test_user
165 Session().add(m)
170 Session().add(m)
166 Session().commit()
171 Session().commit()
167
172
168
173
169 def test_extra_email_map(test_user):
174 def test_extra_email_map(test_user):
170
175
171 m = UserEmailMap()
176 m = UserEmailMap()
172 m.email = u'main_email2@rhodecode.org'
177 m.email = u'main_email2@rhodecode.org'
173 m.user = test_user
178 m.user = test_user
174 Session().add(m)
179 Session().add(m)
175 Session().commit()
180 Session().commit()
176
181
177 u = User.get_by_email(email='main_email@rhodecode.org')
182 u = User.get_by_email(email='main_email@rhodecode.org')
178 assert test_user.user_id == u.user_id
183 assert test_user.user_id == u.user_id
179 assert test_user.username == u.username
184 assert test_user.username == u.username
180
185
181 u = User.get_by_email(email='main_email2@rhodecode.org')
186 u = User.get_by_email(email='main_email2@rhodecode.org')
182 assert test_user.user_id == u.user_id
187 assert test_user.user_id == u.user_id
183 assert test_user.username == u.username
188 assert test_user.username == u.username
184 u = User.get_by_email(email='main_email3@rhodecode.org')
189 u = User.get_by_email(email='main_email3@rhodecode.org')
185 assert u is None
190 assert u is None
186
191
187
192
188 def test_get_api_data_replaces_secret_data_by_default(test_user):
193 def test_get_api_data_replaces_secret_data_by_default(test_user):
189 api_data = test_user.get_api_data()
194 api_data = test_user.get_api_data()
190 api_key_length = 40
195 api_key_length = 40
191 expected_replacement = '*' * api_key_length
196 expected_replacement = '*' * api_key_length
192
197
193 for key in api_data['auth_tokens']:
198 for key in api_data['auth_tokens']:
194 assert key == expected_replacement
199 assert key == expected_replacement
195
200
196
201
197 def test_get_api_data_includes_secret_data_if_activated(test_user):
202 def test_get_api_data_includes_secret_data_if_activated(test_user):
198 api_data = test_user.get_api_data(include_secrets=True)
203 api_data = test_user.get_api_data(include_secrets=True)
199 assert api_data['auth_tokens'] == test_user.auth_tokens
204 assert api_data['auth_tokens'] == test_user.auth_tokens
200
205
201
206
202 def test_add_perm(test_user):
207 def test_add_perm(test_user):
203 perm = Permission.query().all()[0]
208 perm = Permission.query().all()[0]
204 UserModel().grant_perm(test_user, perm)
209 UserModel().grant_perm(test_user, perm)
205 Session().commit()
210 Session().commit()
206 assert UserModel().has_perm(test_user, perm)
211 assert UserModel().has_perm(test_user, perm)
207
212
208
213
209 def test_has_perm(test_user):
214 def test_has_perm(test_user):
210 perm = Permission.query().all()
215 perm = Permission.query().all()
211 for p in perm:
216 for p in perm:
212 assert not UserModel().has_perm(test_user, p)
217 assert not UserModel().has_perm(test_user, p)
213
218
214
219
215 def test_revoke_perm(test_user):
220 def test_revoke_perm(test_user):
216 perm = Permission.query().all()[0]
221 perm = Permission.query().all()[0]
217 UserModel().grant_perm(test_user, perm)
222 UserModel().grant_perm(test_user, perm)
218 Session().commit()
223 Session().commit()
219 assert UserModel().has_perm(test_user, perm)
224 assert UserModel().has_perm(test_user, perm)
220
225
221 # revoke
226 # revoke
222 UserModel().revoke_perm(test_user, perm)
227 UserModel().revoke_perm(test_user, perm)
223 Session().commit()
228 Session().commit()
224 assert not UserModel().has_perm(test_user, perm)
229 assert not UserModel().has_perm(test_user, perm)
225
230
226
231
227 @pytest.mark.parametrize("ip_range, expected, expect_errors", [
232 @pytest.mark.parametrize("ip_range, expected, expect_errors", [
228 ('', [], False),
233 ('', [], False),
229 ('127.0.0.1', ['127.0.0.1'], False),
234 ('127.0.0.1', ['127.0.0.1'], False),
230 ('127.0.0.1,127.0.0.2', ['127.0.0.1', '127.0.0.2'], False),
235 ('127.0.0.1,127.0.0.2', ['127.0.0.1', '127.0.0.2'], False),
231 ('127.0.0.1 , 127.0.0.2', ['127.0.0.1', '127.0.0.2'], False),
236 ('127.0.0.1 , 127.0.0.2', ['127.0.0.1', '127.0.0.2'], False),
232 (
237 (
233 '127.0.0.1,172.172.172.0,127.0.0.2',
238 '127.0.0.1,172.172.172.0,127.0.0.2',
234 ['127.0.0.1', '172.172.172.0', '127.0.0.2'], False),
239 ['127.0.0.1', '172.172.172.0', '127.0.0.2'], False),
235 (
240 (
236 '127.0.0.1-127.0.0.5',
241 '127.0.0.1-127.0.0.5',
237 ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5'],
242 ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5'],
238 False),
243 False),
239 (
244 (
240 '127.0.0.1 - 127.0.0.5',
245 '127.0.0.1 - 127.0.0.5',
241 ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5'],
246 ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5'],
242 False
247 False
243 ),
248 ),
244 ('-', [], True),
249 ('-', [], True),
245 ('127.0.0.1-32', [], True),
250 ('127.0.0.1-32', [], True),
246 (
251 (
247 '127.0.0.1,127.0.0.1,127.0.0.1,127.0.0.1-127.0.0.2,127.0.0.2',
252 '127.0.0.1,127.0.0.1,127.0.0.1,127.0.0.1-127.0.0.2,127.0.0.2',
248 ['127.0.0.1', '127.0.0.2'], False),
253 ['127.0.0.1', '127.0.0.2'], False),
249 (
254 (
250 '127.0.0.1-127.0.0.2,127.0.0.4-127.0.0.6,',
255 '127.0.0.1-127.0.0.2,127.0.0.4-127.0.0.6,',
251 ['127.0.0.1', '127.0.0.2', '127.0.0.4', '127.0.0.5', '127.0.0.6'],
256 ['127.0.0.1', '127.0.0.2', '127.0.0.4', '127.0.0.5', '127.0.0.6'],
252 False
257 False
253 ),
258 ),
254 (
259 (
255 '127.0.0.1-127.0.0.2,127.0.0.1-127.0.0.6,',
260 '127.0.0.1-127.0.0.2,127.0.0.1-127.0.0.6,',
256 ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5',
261 ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5',
257 '127.0.0.6'],
262 '127.0.0.6'],
258 False
263 False
259 ),
264 ),
260 ])
265 ])
261 def test_ip_range_generator(ip_range, expected, expect_errors):
266 def test_ip_range_generator(ip_range, expected, expect_errors):
262 func = UserModel().parse_ip_range
267 func = UserModel().parse_ip_range
263 if expect_errors:
268 if expect_errors:
264 pytest.raises(ValueError, func, ip_range)
269 pytest.raises(ValueError, func, ip_range)
265 else:
270 else:
266 parsed_list = func(ip_range)
271 parsed_list = func(ip_range)
267 assert parsed_list == expected
272 assert parsed_list == expected
268
273
269
274
270 def test_user_delete_cascades_ip_whitelist(test_user):
275 def test_user_delete_cascades_ip_whitelist(test_user):
271 sample_ip = '1.1.1.1'
276 sample_ip = '1.1.1.1'
272 uid_map = UserIpMap(user_id=test_user.user_id, ip_addr=sample_ip)
277 uid_map = UserIpMap(user_id=test_user.user_id, ip_addr=sample_ip)
273 Session().add(uid_map)
278 Session().add(uid_map)
274 Session().delete(test_user)
279 Session().delete(test_user)
275 try:
280 try:
276 Session().flush()
281 Session().flush()
277 finally:
282 finally:
278 Session().rollback()
283 Session().rollback()
279
284
280
285
281 def test_account_for_deactivation_generation(test_user):
286 def test_account_for_deactivation_generation(test_user):
282 accounts = UserModel().get_accounts_in_creation_order(
287 accounts = UserModel().get_accounts_in_creation_order(
283 current_user=test_user)
288 current_user=test_user)
284 # current user should be #1 in the list
289 # current user should be #1 in the list
285 assert accounts[0] == test_user.user_id
290 assert accounts[0] == test_user.user_id
286 active_users = User.query().filter(User.active == true()).count()
291 active_users = User.query().filter(User.active == true()).count()
287 assert active_users == len(accounts)
292 assert active_users == len(accounts)
288
293
289
294
290 def test_user_delete_cascades_permissions_on_repo(backend, test_user):
295 def test_user_delete_cascades_permissions_on_repo(backend, test_user):
291 test_repo = backend.create_repo()
296 test_repo = backend.create_repo()
292 RepoModel().grant_user_permission(
297 RepoModel().grant_user_permission(
293 test_repo, test_user, 'repository.write')
298 test_repo, test_user, 'repository.write')
294 Session().commit()
299 Session().commit()
295
300
296 assert test_user.repo_to_perm
301 assert test_user.repo_to_perm
297
302
298 UserModel().delete(test_user)
303 UserModel().delete(test_user)
299 Session().commit()
304 Session().commit()
300
305
301
306
302 def test_user_delete_cascades_permissions_on_repo_group(
307 def test_user_delete_cascades_permissions_on_repo_group(
303 test_repo_group, test_user):
308 test_repo_group, test_user):
304 RepoGroupModel().grant_user_permission(
309 RepoGroupModel().grant_user_permission(
305 test_repo_group, test_user, 'group.write')
310 test_repo_group, test_user, 'group.write')
306 Session().commit()
311 Session().commit()
307
312
308 assert test_user.repo_group_to_perm
313 assert test_user.repo_group_to_perm
309
314
310 Session().delete(test_user)
315 Session().delete(test_user)
311 Session().commit()
316 Session().commit()
312
317
313
318
314 def test_user_delete_cascades_permissions_on_user_group(
319 def test_user_delete_cascades_permissions_on_user_group(
315 test_user_group, test_user):
320 test_user_group, test_user):
316 UserGroupModel().grant_user_permission(
321 UserGroupModel().grant_user_permission(
317 test_user_group, test_user, 'usergroup.write')
322 test_user_group, test_user, 'usergroup.write')
318 Session().commit()
323 Session().commit()
319
324
320 assert test_user.user_group_to_perm
325 assert test_user.user_group_to_perm
321
326
322 Session().delete(test_user)
327 Session().delete(test_user)
323 Session().commit()
328 Session().commit()
@@ -1,1858 +1,1858 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 collections
21 import collections
22 import datetime
22 import datetime
23 import hashlib
23 import hashlib
24 import os
24 import os
25 import re
25 import re
26 import pprint
26 import pprint
27 import shutil
27 import shutil
28 import socket
28 import socket
29 import subprocess32
29 import subprocess32
30 import time
30 import time
31 import uuid
31 import uuid
32 import dateutil.tz
32 import dateutil.tz
33 import functools
33 import functools
34
34
35 import mock
35 import mock
36 import pyramid.testing
36 import pyramid.testing
37 import pytest
37 import pytest
38 import colander
38 import colander
39 import requests
39 import requests
40 import pyramid.paster
40 import pyramid.paster
41
41
42 import rhodecode
42 import rhodecode
43 from rhodecode.lib.utils2 import AttributeDict
43 from rhodecode.lib.utils2 import AttributeDict
44 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.db import (
46 from rhodecode.model.db import (
47 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
47 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
48 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
48 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.pull_request import PullRequestModel
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.repo_group import RepoGroupModel
52 from rhodecode.model.repo_group import RepoGroupModel
53 from rhodecode.model.user import UserModel
53 from rhodecode.model.user import UserModel
54 from rhodecode.model.settings import VcsSettingsModel
54 from rhodecode.model.settings import VcsSettingsModel
55 from rhodecode.model.user_group import UserGroupModel
55 from rhodecode.model.user_group import UserGroupModel
56 from rhodecode.model.integration import IntegrationModel
56 from rhodecode.model.integration import IntegrationModel
57 from rhodecode.integrations import integration_type_registry
57 from rhodecode.integrations import integration_type_registry
58 from rhodecode.integrations.types.base import IntegrationTypeBase
58 from rhodecode.integrations.types.base import IntegrationTypeBase
59 from rhodecode.lib.utils import repo2db_mapper
59 from rhodecode.lib.utils import repo2db_mapper
60 from rhodecode.lib.vcs import create_vcsserver_proxy
60 from rhodecode.lib.vcs import create_vcsserver_proxy
61 from rhodecode.lib.vcs.backends import get_backend
61 from rhodecode.lib.vcs.backends import get_backend
62 from rhodecode.lib.vcs.nodes import FileNode
62 from rhodecode.lib.vcs.nodes import FileNode
63 from rhodecode.tests import (
63 from rhodecode.tests import (
64 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
64 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
65 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
65 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
66 TEST_USER_REGULAR_PASS)
66 TEST_USER_REGULAR_PASS)
67 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access
67 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access
68 from rhodecode.tests.fixture import Fixture
68 from rhodecode.tests.fixture import Fixture
69 from rhodecode.config import utils as config_utils
69 from rhodecode.config import utils as config_utils
70
70
71 def _split_comma(value):
71 def _split_comma(value):
72 return value.split(',')
72 return value.split(',')
73
73
74
74
75 def pytest_addoption(parser):
75 def pytest_addoption(parser):
76 parser.addoption(
76 parser.addoption(
77 '--keep-tmp-path', action='store_true',
77 '--keep-tmp-path', action='store_true',
78 help="Keep the test temporary directories")
78 help="Keep the test temporary directories")
79 parser.addoption(
79 parser.addoption(
80 '--backends', action='store', type=_split_comma,
80 '--backends', action='store', type=_split_comma,
81 default=['git', 'hg', 'svn'],
81 default=['git', 'hg', 'svn'],
82 help="Select which backends to test for backend specific tests.")
82 help="Select which backends to test for backend specific tests.")
83 parser.addoption(
83 parser.addoption(
84 '--dbs', action='store', type=_split_comma,
84 '--dbs', action='store', type=_split_comma,
85 default=['sqlite'],
85 default=['sqlite'],
86 help="Select which database to test for database specific tests. "
86 help="Select which database to test for database specific tests. "
87 "Possible options are sqlite,postgres,mysql")
87 "Possible options are sqlite,postgres,mysql")
88 parser.addoption(
88 parser.addoption(
89 '--appenlight', '--ae', action='store_true',
89 '--appenlight', '--ae', action='store_true',
90 help="Track statistics in appenlight.")
90 help="Track statistics in appenlight.")
91 parser.addoption(
91 parser.addoption(
92 '--appenlight-api-key', '--ae-key',
92 '--appenlight-api-key', '--ae-key',
93 help="API key for Appenlight.")
93 help="API key for Appenlight.")
94 parser.addoption(
94 parser.addoption(
95 '--appenlight-url', '--ae-url',
95 '--appenlight-url', '--ae-url',
96 default="https://ae.rhodecode.com",
96 default="https://ae.rhodecode.com",
97 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
97 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
98 parser.addoption(
98 parser.addoption(
99 '--sqlite-connection-string', action='store',
99 '--sqlite-connection-string', action='store',
100 default='', help="Connection string for the dbs tests with SQLite")
100 default='', help="Connection string for the dbs tests with SQLite")
101 parser.addoption(
101 parser.addoption(
102 '--postgres-connection-string', action='store',
102 '--postgres-connection-string', action='store',
103 default='', help="Connection string for the dbs tests with Postgres")
103 default='', help="Connection string for the dbs tests with Postgres")
104 parser.addoption(
104 parser.addoption(
105 '--mysql-connection-string', action='store',
105 '--mysql-connection-string', action='store',
106 default='', help="Connection string for the dbs tests with MySQL")
106 default='', help="Connection string for the dbs tests with MySQL")
107 parser.addoption(
107 parser.addoption(
108 '--repeat', type=int, default=100,
108 '--repeat', type=int, default=100,
109 help="Number of repetitions in performance tests.")
109 help="Number of repetitions in performance tests.")
110
110
111
111
112 def pytest_configure(config):
112 def pytest_configure(config):
113 from rhodecode.config import patches
113 from rhodecode.config import patches
114
114
115
115
116 def pytest_collection_modifyitems(session, config, items):
116 def pytest_collection_modifyitems(session, config, items):
117 # nottest marked, compare nose, used for transition from nose to pytest
117 # nottest marked, compare nose, used for transition from nose to pytest
118 remaining = [
118 remaining = [
119 i for i in items if getattr(i.obj, '__test__', True)]
119 i for i in items if getattr(i.obj, '__test__', True)]
120 items[:] = remaining
120 items[:] = remaining
121
121
122
122
123 def pytest_generate_tests(metafunc):
123 def pytest_generate_tests(metafunc):
124 # Support test generation based on --backend parameter
124 # Support test generation based on --backend parameter
125 if 'backend_alias' in metafunc.fixturenames:
125 if 'backend_alias' in metafunc.fixturenames:
126 backends = get_backends_from_metafunc(metafunc)
126 backends = get_backends_from_metafunc(metafunc)
127 scope = None
127 scope = None
128 if not backends:
128 if not backends:
129 pytest.skip("Not enabled for any of selected backends")
129 pytest.skip("Not enabled for any of selected backends")
130 metafunc.parametrize('backend_alias', backends, scope=scope)
130 metafunc.parametrize('backend_alias', backends, scope=scope)
131 elif hasattr(metafunc.function, 'backends'):
131 elif hasattr(metafunc.function, 'backends'):
132 backends = get_backends_from_metafunc(metafunc)
132 backends = get_backends_from_metafunc(metafunc)
133 if not backends:
133 if not backends:
134 pytest.skip("Not enabled for any of selected backends")
134 pytest.skip("Not enabled for any of selected backends")
135
135
136
136
137 def get_backends_from_metafunc(metafunc):
137 def get_backends_from_metafunc(metafunc):
138 requested_backends = set(metafunc.config.getoption('--backends'))
138 requested_backends = set(metafunc.config.getoption('--backends'))
139 if hasattr(metafunc.function, 'backends'):
139 if hasattr(metafunc.function, 'backends'):
140 # Supported backends by this test function, created from
140 # Supported backends by this test function, created from
141 # pytest.mark.backends
141 # pytest.mark.backends
142 backends = metafunc.function.backends.args
142 backends = metafunc.function.backends.args
143 elif hasattr(metafunc.cls, 'backend_alias'):
143 elif hasattr(metafunc.cls, 'backend_alias'):
144 # Support class attribute "backend_alias", this is mainly
144 # Support class attribute "backend_alias", this is mainly
145 # for legacy reasons for tests not yet using pytest.mark.backends
145 # for legacy reasons for tests not yet using pytest.mark.backends
146 backends = [metafunc.cls.backend_alias]
146 backends = [metafunc.cls.backend_alias]
147 else:
147 else:
148 backends = metafunc.config.getoption('--backends')
148 backends = metafunc.config.getoption('--backends')
149 return requested_backends.intersection(backends)
149 return requested_backends.intersection(backends)
150
150
151
151
152 @pytest.fixture(scope='session', autouse=True)
152 @pytest.fixture(scope='session', autouse=True)
153 def activate_example_rcextensions(request):
153 def activate_example_rcextensions(request):
154 """
154 """
155 Patch in an example rcextensions module which verifies passed in kwargs.
155 Patch in an example rcextensions module which verifies passed in kwargs.
156 """
156 """
157 from rhodecode.tests.other import example_rcextensions
157 from rhodecode.tests.other import example_rcextensions
158
158
159 old_extensions = rhodecode.EXTENSIONS
159 old_extensions = rhodecode.EXTENSIONS
160 rhodecode.EXTENSIONS = example_rcextensions
160 rhodecode.EXTENSIONS = example_rcextensions
161
161
162 @request.addfinalizer
162 @request.addfinalizer
163 def cleanup():
163 def cleanup():
164 rhodecode.EXTENSIONS = old_extensions
164 rhodecode.EXTENSIONS = old_extensions
165
165
166
166
167 @pytest.fixture
167 @pytest.fixture
168 def capture_rcextensions():
168 def capture_rcextensions():
169 """
169 """
170 Returns the recorded calls to entry points in rcextensions.
170 Returns the recorded calls to entry points in rcextensions.
171 """
171 """
172 calls = rhodecode.EXTENSIONS.calls
172 calls = rhodecode.EXTENSIONS.calls
173 calls.clear()
173 calls.clear()
174 # Note: At this moment, it is still the empty dict, but that will
174 # Note: At this moment, it is still the empty dict, but that will
175 # be filled during the test run and since it is a reference this
175 # be filled during the test run and since it is a reference this
176 # is enough to make it work.
176 # is enough to make it work.
177 return calls
177 return calls
178
178
179
179
180 @pytest.fixture(scope='session')
180 @pytest.fixture(scope='session')
181 def http_environ_session():
181 def http_environ_session():
182 """
182 """
183 Allow to use "http_environ" in session scope.
183 Allow to use "http_environ" in session scope.
184 """
184 """
185 return http_environ(
185 return http_environ(
186 http_host_stub=http_host_stub())
186 http_host_stub=http_host_stub())
187
187
188
188
189 @pytest.fixture
189 @pytest.fixture
190 def http_host_stub():
190 def http_host_stub():
191 """
191 """
192 Value of HTTP_HOST in the test run.
192 Value of HTTP_HOST in the test run.
193 """
193 """
194 return 'example.com:80'
194 return 'example.com:80'
195
195
196
196
197 @pytest.fixture
197 @pytest.fixture
198 def http_host_only_stub():
198 def http_host_only_stub():
199 """
199 """
200 Value of HTTP_HOST in the test run.
200 Value of HTTP_HOST in the test run.
201 """
201 """
202 return http_host_stub().split(':')[0]
202 return http_host_stub().split(':')[0]
203
203
204
204
205 @pytest.fixture
205 @pytest.fixture
206 def http_environ(http_host_stub):
206 def http_environ(http_host_stub):
207 """
207 """
208 HTTP extra environ keys.
208 HTTP extra environ keys.
209
209
210 User by the test application and as well for setting up the pylons
210 User by the test application and as well for setting up the pylons
211 environment. In the case of the fixture "app" it should be possible
211 environment. In the case of the fixture "app" it should be possible
212 to override this for a specific test case.
212 to override this for a specific test case.
213 """
213 """
214 return {
214 return {
215 'SERVER_NAME': http_host_only_stub(),
215 'SERVER_NAME': http_host_only_stub(),
216 'SERVER_PORT': http_host_stub.split(':')[1],
216 'SERVER_PORT': http_host_stub.split(':')[1],
217 'HTTP_HOST': http_host_stub,
217 'HTTP_HOST': http_host_stub,
218 'HTTP_USER_AGENT': 'rc-test-agent',
218 'HTTP_USER_AGENT': 'rc-test-agent',
219 'REQUEST_METHOD': 'GET'
219 'REQUEST_METHOD': 'GET'
220 }
220 }
221
221
222
222
223 @pytest.fixture(scope='session')
223 @pytest.fixture(scope='session')
224 def baseapp(ini_config, vcsserver, http_environ_session):
224 def baseapp(ini_config, vcsserver, http_environ_session):
225 from rhodecode.lib.pyramid_utils import get_app_config
225 from rhodecode.lib.pyramid_utils import get_app_config
226 from rhodecode.config.middleware import make_pyramid_app
226 from rhodecode.config.middleware import make_pyramid_app
227
227
228 print("Using the RhodeCode configuration:{}".format(ini_config))
228 print("Using the RhodeCode configuration:{}".format(ini_config))
229 pyramid.paster.setup_logging(ini_config)
229 pyramid.paster.setup_logging(ini_config)
230
230
231 settings = get_app_config(ini_config)
231 settings = get_app_config(ini_config)
232 app = make_pyramid_app({'__file__': ini_config}, **settings)
232 app = make_pyramid_app({'__file__': ini_config}, **settings)
233
233
234 return app
234 return app
235
235
236
236
237 @pytest.fixture(scope='function')
237 @pytest.fixture(scope='function')
238 def app(request, config_stub, baseapp, http_environ):
238 def app(request, config_stub, baseapp, http_environ):
239 app = CustomTestApp(
239 app = CustomTestApp(
240 baseapp,
240 baseapp,
241 extra_environ=http_environ)
241 extra_environ=http_environ)
242 if request.cls:
242 if request.cls:
243 request.cls.app = app
243 request.cls.app = app
244 return app
244 return app
245
245
246
246
247 @pytest.fixture(scope='session')
247 @pytest.fixture(scope='session')
248 def app_settings(baseapp, ini_config):
248 def app_settings(baseapp, ini_config):
249 """
249 """
250 Settings dictionary used to create the app.
250 Settings dictionary used to create the app.
251
251
252 Parses the ini file and passes the result through the sanitize and apply
252 Parses the ini file and passes the result through the sanitize and apply
253 defaults mechanism in `rhodecode.config.middleware`.
253 defaults mechanism in `rhodecode.config.middleware`.
254 """
254 """
255 return baseapp.config.get_settings()
255 return baseapp.config.get_settings()
256
256
257
257
258 @pytest.fixture(scope='session')
258 @pytest.fixture(scope='session')
259 def db_connection(ini_settings):
259 def db_connection(ini_settings):
260 # Initialize the database connection.
260 # Initialize the database connection.
261 config_utils.initialize_database(ini_settings)
261 config_utils.initialize_database(ini_settings)
262
262
263
263
264 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
264 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
265
265
266
266
267 def _autologin_user(app, *args):
267 def _autologin_user(app, *args):
268 session = login_user_session(app, *args)
268 session = login_user_session(app, *args)
269 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
269 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
270 return LoginData(csrf_token, session['rhodecode_user'])
270 return LoginData(csrf_token, session['rhodecode_user'])
271
271
272
272
273 @pytest.fixture
273 @pytest.fixture
274 def autologin_user(app):
274 def autologin_user(app):
275 """
275 """
276 Utility fixture which makes sure that the admin user is logged in
276 Utility fixture which makes sure that the admin user is logged in
277 """
277 """
278 return _autologin_user(app)
278 return _autologin_user(app)
279
279
280
280
281 @pytest.fixture
281 @pytest.fixture
282 def autologin_regular_user(app):
282 def autologin_regular_user(app):
283 """
283 """
284 Utility fixture which makes sure that the regular user is logged in
284 Utility fixture which makes sure that the regular user is logged in
285 """
285 """
286 return _autologin_user(
286 return _autologin_user(
287 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
287 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
288
288
289
289
290 @pytest.fixture(scope='function')
290 @pytest.fixture(scope='function')
291 def csrf_token(request, autologin_user):
291 def csrf_token(request, autologin_user):
292 return autologin_user.csrf_token
292 return autologin_user.csrf_token
293
293
294
294
295 @pytest.fixture(scope='function')
295 @pytest.fixture(scope='function')
296 def xhr_header(request):
296 def xhr_header(request):
297 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
297 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
298
298
299
299
300 @pytest.fixture
300 @pytest.fixture
301 def real_crypto_backend(monkeypatch):
301 def real_crypto_backend(monkeypatch):
302 """
302 """
303 Switch the production crypto backend on for this test.
303 Switch the production crypto backend on for this test.
304
304
305 During the test run the crypto backend is replaced with a faster
305 During the test run the crypto backend is replaced with a faster
306 implementation based on the MD5 algorithm.
306 implementation based on the MD5 algorithm.
307 """
307 """
308 monkeypatch.setattr(rhodecode, 'is_test', False)
308 monkeypatch.setattr(rhodecode, 'is_test', False)
309
309
310
310
311 @pytest.fixture(scope='class')
311 @pytest.fixture(scope='class')
312 def index_location(request, baseapp):
312 def index_location(request, baseapp):
313 index_location = baseapp.config.get_settings()['search.location']
313 index_location = baseapp.config.get_settings()['search.location']
314 if request.cls:
314 if request.cls:
315 request.cls.index_location = index_location
315 request.cls.index_location = index_location
316 return index_location
316 return index_location
317
317
318
318
319 @pytest.fixture(scope='session', autouse=True)
319 @pytest.fixture(scope='session', autouse=True)
320 def tests_tmp_path(request):
320 def tests_tmp_path(request):
321 """
321 """
322 Create temporary directory to be used during the test session.
322 Create temporary directory to be used during the test session.
323 """
323 """
324 if not os.path.exists(TESTS_TMP_PATH):
324 if not os.path.exists(TESTS_TMP_PATH):
325 os.makedirs(TESTS_TMP_PATH)
325 os.makedirs(TESTS_TMP_PATH)
326
326
327 if not request.config.getoption('--keep-tmp-path'):
327 if not request.config.getoption('--keep-tmp-path'):
328 @request.addfinalizer
328 @request.addfinalizer
329 def remove_tmp_path():
329 def remove_tmp_path():
330 shutil.rmtree(TESTS_TMP_PATH)
330 shutil.rmtree(TESTS_TMP_PATH)
331
331
332 return TESTS_TMP_PATH
332 return TESTS_TMP_PATH
333
333
334
334
335 @pytest.fixture
335 @pytest.fixture
336 def test_repo_group(request):
336 def test_repo_group(request):
337 """
337 """
338 Create a temporary repository group, and destroy it after
338 Create a temporary repository group, and destroy it after
339 usage automatically
339 usage automatically
340 """
340 """
341 fixture = Fixture()
341 fixture = Fixture()
342 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
342 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
343 repo_group = fixture.create_repo_group(repogroupid)
343 repo_group = fixture.create_repo_group(repogroupid)
344
344
345 def _cleanup():
345 def _cleanup():
346 fixture.destroy_repo_group(repogroupid)
346 fixture.destroy_repo_group(repogroupid)
347
347
348 request.addfinalizer(_cleanup)
348 request.addfinalizer(_cleanup)
349 return repo_group
349 return repo_group
350
350
351
351
352 @pytest.fixture
352 @pytest.fixture
353 def test_user_group(request):
353 def test_user_group(request):
354 """
354 """
355 Create a temporary user group, and destroy it after
355 Create a temporary user group, and destroy it after
356 usage automatically
356 usage automatically
357 """
357 """
358 fixture = Fixture()
358 fixture = Fixture()
359 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
359 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
360 user_group = fixture.create_user_group(usergroupid)
360 user_group = fixture.create_user_group(usergroupid)
361
361
362 def _cleanup():
362 def _cleanup():
363 fixture.destroy_user_group(user_group)
363 fixture.destroy_user_group(user_group)
364
364
365 request.addfinalizer(_cleanup)
365 request.addfinalizer(_cleanup)
366 return user_group
366 return user_group
367
367
368
368
369 @pytest.fixture(scope='session')
369 @pytest.fixture(scope='session')
370 def test_repo(request):
370 def test_repo(request):
371 container = TestRepoContainer()
371 container = TestRepoContainer()
372 request.addfinalizer(container._cleanup)
372 request.addfinalizer(container._cleanup)
373 return container
373 return container
374
374
375
375
376 class TestRepoContainer(object):
376 class TestRepoContainer(object):
377 """
377 """
378 Container for test repositories which are used read only.
378 Container for test repositories which are used read only.
379
379
380 Repositories will be created on demand and re-used during the lifetime
380 Repositories will be created on demand and re-used during the lifetime
381 of this object.
381 of this object.
382
382
383 Usage to get the svn test repository "minimal"::
383 Usage to get the svn test repository "minimal"::
384
384
385 test_repo = TestContainer()
385 test_repo = TestContainer()
386 repo = test_repo('minimal', 'svn')
386 repo = test_repo('minimal', 'svn')
387
387
388 """
388 """
389
389
390 dump_extractors = {
390 dump_extractors = {
391 'git': utils.extract_git_repo_from_dump,
391 'git': utils.extract_git_repo_from_dump,
392 'hg': utils.extract_hg_repo_from_dump,
392 'hg': utils.extract_hg_repo_from_dump,
393 'svn': utils.extract_svn_repo_from_dump,
393 'svn': utils.extract_svn_repo_from_dump,
394 }
394 }
395
395
396 def __init__(self):
396 def __init__(self):
397 self._cleanup_repos = []
397 self._cleanup_repos = []
398 self._fixture = Fixture()
398 self._fixture = Fixture()
399 self._repos = {}
399 self._repos = {}
400
400
401 def __call__(self, dump_name, backend_alias, config=None):
401 def __call__(self, dump_name, backend_alias, config=None):
402 key = (dump_name, backend_alias)
402 key = (dump_name, backend_alias)
403 if key not in self._repos:
403 if key not in self._repos:
404 repo = self._create_repo(dump_name, backend_alias, config)
404 repo = self._create_repo(dump_name, backend_alias, config)
405 self._repos[key] = repo.repo_id
405 self._repos[key] = repo.repo_id
406 return Repository.get(self._repos[key])
406 return Repository.get(self._repos[key])
407
407
408 def _create_repo(self, dump_name, backend_alias, config):
408 def _create_repo(self, dump_name, backend_alias, config):
409 repo_name = '%s-%s' % (backend_alias, dump_name)
409 repo_name = '%s-%s' % (backend_alias, dump_name)
410 backend_class = get_backend(backend_alias)
410 backend_class = get_backend(backend_alias)
411 dump_extractor = self.dump_extractors[backend_alias]
411 dump_extractor = self.dump_extractors[backend_alias]
412 repo_path = dump_extractor(dump_name, repo_name)
412 repo_path = dump_extractor(dump_name, repo_name)
413
413
414 vcs_repo = backend_class(repo_path, config=config)
414 vcs_repo = backend_class(repo_path, config=config)
415 repo2db_mapper({repo_name: vcs_repo})
415 repo2db_mapper({repo_name: vcs_repo})
416
416
417 repo = RepoModel().get_by_repo_name(repo_name)
417 repo = RepoModel().get_by_repo_name(repo_name)
418 self._cleanup_repos.append(repo_name)
418 self._cleanup_repos.append(repo_name)
419 return repo
419 return repo
420
420
421 def _cleanup(self):
421 def _cleanup(self):
422 for repo_name in reversed(self._cleanup_repos):
422 for repo_name in reversed(self._cleanup_repos):
423 self._fixture.destroy_repo(repo_name)
423 self._fixture.destroy_repo(repo_name)
424
424
425
425
426 @pytest.fixture
426 @pytest.fixture
427 def backend(request, backend_alias, baseapp, test_repo):
427 def backend(request, backend_alias, baseapp, test_repo):
428 """
428 """
429 Parametrized fixture which represents a single backend implementation.
429 Parametrized fixture which represents a single backend implementation.
430
430
431 It respects the option `--backends` to focus the test run on specific
431 It respects the option `--backends` to focus the test run on specific
432 backend implementations.
432 backend implementations.
433
433
434 It also supports `pytest.mark.xfail_backends` to mark tests as failing
434 It also supports `pytest.mark.xfail_backends` to mark tests as failing
435 for specific backends. This is intended as a utility for incremental
435 for specific backends. This is intended as a utility for incremental
436 development of a new backend implementation.
436 development of a new backend implementation.
437 """
437 """
438 if backend_alias not in request.config.getoption('--backends'):
438 if backend_alias not in request.config.getoption('--backends'):
439 pytest.skip("Backend %s not selected." % (backend_alias, ))
439 pytest.skip("Backend %s not selected." % (backend_alias, ))
440
440
441 utils.check_xfail_backends(request.node, backend_alias)
441 utils.check_xfail_backends(request.node, backend_alias)
442 utils.check_skip_backends(request.node, backend_alias)
442 utils.check_skip_backends(request.node, backend_alias)
443
443
444 repo_name = 'vcs_test_%s' % (backend_alias, )
444 repo_name = 'vcs_test_%s' % (backend_alias, )
445 backend = Backend(
445 backend = Backend(
446 alias=backend_alias,
446 alias=backend_alias,
447 repo_name=repo_name,
447 repo_name=repo_name,
448 test_name=request.node.name,
448 test_name=request.node.name,
449 test_repo_container=test_repo)
449 test_repo_container=test_repo)
450 request.addfinalizer(backend.cleanup)
450 request.addfinalizer(backend.cleanup)
451 return backend
451 return backend
452
452
453
453
454 @pytest.fixture
454 @pytest.fixture
455 def backend_git(request, baseapp, test_repo):
455 def backend_git(request, baseapp, test_repo):
456 return backend(request, 'git', baseapp, test_repo)
456 return backend(request, 'git', baseapp, test_repo)
457
457
458
458
459 @pytest.fixture
459 @pytest.fixture
460 def backend_hg(request, baseapp, test_repo):
460 def backend_hg(request, baseapp, test_repo):
461 return backend(request, 'hg', baseapp, test_repo)
461 return backend(request, 'hg', baseapp, test_repo)
462
462
463
463
464 @pytest.fixture
464 @pytest.fixture
465 def backend_svn(request, baseapp, test_repo):
465 def backend_svn(request, baseapp, test_repo):
466 return backend(request, 'svn', baseapp, test_repo)
466 return backend(request, 'svn', baseapp, test_repo)
467
467
468
468
469 @pytest.fixture
469 @pytest.fixture
470 def backend_random(backend_git):
470 def backend_random(backend_git):
471 """
471 """
472 Use this to express that your tests need "a backend.
472 Use this to express that your tests need "a backend.
473
473
474 A few of our tests need a backend, so that we can run the code. This
474 A few of our tests need a backend, so that we can run the code. This
475 fixture is intended to be used for such cases. It will pick one of the
475 fixture is intended to be used for such cases. It will pick one of the
476 backends and run the tests.
476 backends and run the tests.
477
477
478 The fixture `backend` would run the test multiple times for each
478 The fixture `backend` would run the test multiple times for each
479 available backend which is a pure waste of time if the test is
479 available backend which is a pure waste of time if the test is
480 independent of the backend type.
480 independent of the backend type.
481 """
481 """
482 # TODO: johbo: Change this to pick a random backend
482 # TODO: johbo: Change this to pick a random backend
483 return backend_git
483 return backend_git
484
484
485
485
486 @pytest.fixture
486 @pytest.fixture
487 def backend_stub(backend_git):
487 def backend_stub(backend_git):
488 """
488 """
489 Use this to express that your tests need a backend stub
489 Use this to express that your tests need a backend stub
490
490
491 TODO: mikhail: Implement a real stub logic instead of returning
491 TODO: mikhail: Implement a real stub logic instead of returning
492 a git backend
492 a git backend
493 """
493 """
494 return backend_git
494 return backend_git
495
495
496
496
497 @pytest.fixture
497 @pytest.fixture
498 def repo_stub(backend_stub):
498 def repo_stub(backend_stub):
499 """
499 """
500 Use this to express that your tests need a repository stub
500 Use this to express that your tests need a repository stub
501 """
501 """
502 return backend_stub.create_repo()
502 return backend_stub.create_repo()
503
503
504
504
505 class Backend(object):
505 class Backend(object):
506 """
506 """
507 Represents the test configuration for one supported backend
507 Represents the test configuration for one supported backend
508
508
509 Provides easy access to different test repositories based on
509 Provides easy access to different test repositories based on
510 `__getitem__`. Such repositories will only be created once per test
510 `__getitem__`. Such repositories will only be created once per test
511 session.
511 session.
512 """
512 """
513
513
514 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
514 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
515 _master_repo = None
515 _master_repo = None
516 _commit_ids = {}
516 _commit_ids = {}
517
517
518 def __init__(self, alias, repo_name, test_name, test_repo_container):
518 def __init__(self, alias, repo_name, test_name, test_repo_container):
519 self.alias = alias
519 self.alias = alias
520 self.repo_name = repo_name
520 self.repo_name = repo_name
521 self._cleanup_repos = []
521 self._cleanup_repos = []
522 self._test_name = test_name
522 self._test_name = test_name
523 self._test_repo_container = test_repo_container
523 self._test_repo_container = test_repo_container
524 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
524 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
525 # Fixture will survive in the end.
525 # Fixture will survive in the end.
526 self._fixture = Fixture()
526 self._fixture = Fixture()
527
527
528 def __getitem__(self, key):
528 def __getitem__(self, key):
529 return self._test_repo_container(key, self.alias)
529 return self._test_repo_container(key, self.alias)
530
530
531 def create_test_repo(self, key, config=None):
531 def create_test_repo(self, key, config=None):
532 return self._test_repo_container(key, self.alias, config)
532 return self._test_repo_container(key, self.alias, config)
533
533
534 @property
534 @property
535 def repo(self):
535 def repo(self):
536 """
536 """
537 Returns the "current" repository. This is the vcs_test repo or the
537 Returns the "current" repository. This is the vcs_test repo or the
538 last repo which has been created with `create_repo`.
538 last repo which has been created with `create_repo`.
539 """
539 """
540 from rhodecode.model.db import Repository
540 from rhodecode.model.db import Repository
541 return Repository.get_by_repo_name(self.repo_name)
541 return Repository.get_by_repo_name(self.repo_name)
542
542
543 @property
543 @property
544 def default_branch_name(self):
544 def default_branch_name(self):
545 VcsRepository = get_backend(self.alias)
545 VcsRepository = get_backend(self.alias)
546 return VcsRepository.DEFAULT_BRANCH_NAME
546 return VcsRepository.DEFAULT_BRANCH_NAME
547
547
548 @property
548 @property
549 def default_head_id(self):
549 def default_head_id(self):
550 """
550 """
551 Returns the default head id of the underlying backend.
551 Returns the default head id of the underlying backend.
552
552
553 This will be the default branch name in case the backend does have a
553 This will be the default branch name in case the backend does have a
554 default branch. In the other cases it will point to a valid head
554 default branch. In the other cases it will point to a valid head
555 which can serve as the base to create a new commit on top of it.
555 which can serve as the base to create a new commit on top of it.
556 """
556 """
557 vcsrepo = self.repo.scm_instance()
557 vcsrepo = self.repo.scm_instance()
558 head_id = (
558 head_id = (
559 vcsrepo.DEFAULT_BRANCH_NAME or
559 vcsrepo.DEFAULT_BRANCH_NAME or
560 vcsrepo.commit_ids[-1])
560 vcsrepo.commit_ids[-1])
561 return head_id
561 return head_id
562
562
563 @property
563 @property
564 def commit_ids(self):
564 def commit_ids(self):
565 """
565 """
566 Returns the list of commits for the last created repository
566 Returns the list of commits for the last created repository
567 """
567 """
568 return self._commit_ids
568 return self._commit_ids
569
569
570 def create_master_repo(self, commits):
570 def create_master_repo(self, commits):
571 """
571 """
572 Create a repository and remember it as a template.
572 Create a repository and remember it as a template.
573
573
574 This allows to easily create derived repositories to construct
574 This allows to easily create derived repositories to construct
575 more complex scenarios for diff, compare and pull requests.
575 more complex scenarios for diff, compare and pull requests.
576
576
577 Returns a commit map which maps from commit message to raw_id.
577 Returns a commit map which maps from commit message to raw_id.
578 """
578 """
579 self._master_repo = self.create_repo(commits=commits)
579 self._master_repo = self.create_repo(commits=commits)
580 return self._commit_ids
580 return self._commit_ids
581
581
582 def create_repo(
582 def create_repo(
583 self, commits=None, number_of_commits=0, heads=None,
583 self, commits=None, number_of_commits=0, heads=None,
584 name_suffix=u'', **kwargs):
584 name_suffix=u'', **kwargs):
585 """
585 """
586 Create a repository and record it for later cleanup.
586 Create a repository and record it for later cleanup.
587
587
588 :param commits: Optional. A sequence of dict instances.
588 :param commits: Optional. A sequence of dict instances.
589 Will add a commit per entry to the new repository.
589 Will add a commit per entry to the new repository.
590 :param number_of_commits: Optional. If set to a number, this number of
590 :param number_of_commits: Optional. If set to a number, this number of
591 commits will be added to the new repository.
591 commits will be added to the new repository.
592 :param heads: Optional. Can be set to a sequence of of commit
592 :param heads: Optional. Can be set to a sequence of of commit
593 names which shall be pulled in from the master repository.
593 names which shall be pulled in from the master repository.
594
594
595 """
595 """
596 self.repo_name = self._next_repo_name() + name_suffix
596 self.repo_name = self._next_repo_name() + name_suffix
597 repo = self._fixture.create_repo(
597 repo = self._fixture.create_repo(
598 self.repo_name, repo_type=self.alias, **kwargs)
598 self.repo_name, repo_type=self.alias, **kwargs)
599 self._cleanup_repos.append(repo.repo_name)
599 self._cleanup_repos.append(repo.repo_name)
600
600
601 commits = commits or [
601 commits = commits or [
602 {'message': 'Commit %s of %s' % (x, self.repo_name)}
602 {'message': 'Commit %s of %s' % (x, self.repo_name)}
603 for x in xrange(number_of_commits)]
603 for x in xrange(number_of_commits)]
604 self._add_commits_to_repo(repo.scm_instance(), commits)
604 self._add_commits_to_repo(repo.scm_instance(), commits)
605 if heads:
605 if heads:
606 self.pull_heads(repo, heads)
606 self.pull_heads(repo, heads)
607
607
608 return repo
608 return repo
609
609
610 def pull_heads(self, repo, heads):
610 def pull_heads(self, repo, heads):
611 """
611 """
612 Make sure that repo contains all commits mentioned in `heads`
612 Make sure that repo contains all commits mentioned in `heads`
613 """
613 """
614 vcsmaster = self._master_repo.scm_instance()
614 vcsmaster = self._master_repo.scm_instance()
615 vcsrepo = repo.scm_instance()
615 vcsrepo = repo.scm_instance()
616 vcsrepo.config.clear_section('hooks')
616 vcsrepo.config.clear_section('hooks')
617 commit_ids = [self._commit_ids[h] for h in heads]
617 commit_ids = [self._commit_ids[h] for h in heads]
618 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
618 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
619
619
620 def create_fork(self):
620 def create_fork(self):
621 repo_to_fork = self.repo_name
621 repo_to_fork = self.repo_name
622 self.repo_name = self._next_repo_name()
622 self.repo_name = self._next_repo_name()
623 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
623 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
624 self._cleanup_repos.append(self.repo_name)
624 self._cleanup_repos.append(self.repo_name)
625 return repo
625 return repo
626
626
627 def new_repo_name(self, suffix=u''):
627 def new_repo_name(self, suffix=u''):
628 self.repo_name = self._next_repo_name() + suffix
628 self.repo_name = self._next_repo_name() + suffix
629 self._cleanup_repos.append(self.repo_name)
629 self._cleanup_repos.append(self.repo_name)
630 return self.repo_name
630 return self.repo_name
631
631
632 def _next_repo_name(self):
632 def _next_repo_name(self):
633 return u"%s_%s" % (
633 return u"%s_%s" % (
634 self.invalid_repo_name.sub(u'_', self._test_name),
634 self.invalid_repo_name.sub(u'_', self._test_name),
635 len(self._cleanup_repos))
635 len(self._cleanup_repos))
636
636
637 def ensure_file(self, filename, content='Test content\n'):
637 def ensure_file(self, filename, content='Test content\n'):
638 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
638 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
639 commits = [
639 commits = [
640 {'added': [
640 {'added': [
641 FileNode(filename, content=content),
641 FileNode(filename, content=content),
642 ]},
642 ]},
643 ]
643 ]
644 self._add_commits_to_repo(self.repo.scm_instance(), commits)
644 self._add_commits_to_repo(self.repo.scm_instance(), commits)
645
645
646 def enable_downloads(self):
646 def enable_downloads(self):
647 repo = self.repo
647 repo = self.repo
648 repo.enable_downloads = True
648 repo.enable_downloads = True
649 Session().add(repo)
649 Session().add(repo)
650 Session().commit()
650 Session().commit()
651
651
652 def cleanup(self):
652 def cleanup(self):
653 for repo_name in reversed(self._cleanup_repos):
653 for repo_name in reversed(self._cleanup_repos):
654 self._fixture.destroy_repo(repo_name)
654 self._fixture.destroy_repo(repo_name)
655
655
656 def _add_commits_to_repo(self, repo, commits):
656 def _add_commits_to_repo(self, repo, commits):
657 commit_ids = _add_commits_to_repo(repo, commits)
657 commit_ids = _add_commits_to_repo(repo, commits)
658 if not commit_ids:
658 if not commit_ids:
659 return
659 return
660 self._commit_ids = commit_ids
660 self._commit_ids = commit_ids
661
661
662 # Creating refs for Git to allow fetching them from remote repository
662 # Creating refs for Git to allow fetching them from remote repository
663 if self.alias == 'git':
663 if self.alias == 'git':
664 refs = {}
664 refs = {}
665 for message in self._commit_ids:
665 for message in self._commit_ids:
666 # TODO: mikhail: do more special chars replacements
666 # TODO: mikhail: do more special chars replacements
667 ref_name = 'refs/test-refs/{}'.format(
667 ref_name = 'refs/test-refs/{}'.format(
668 message.replace(' ', ''))
668 message.replace(' ', ''))
669 refs[ref_name] = self._commit_ids[message]
669 refs[ref_name] = self._commit_ids[message]
670 self._create_refs(repo, refs)
670 self._create_refs(repo, refs)
671
671
672 def _create_refs(self, repo, refs):
672 def _create_refs(self, repo, refs):
673 for ref_name in refs:
673 for ref_name in refs:
674 repo.set_refs(ref_name, refs[ref_name])
674 repo.set_refs(ref_name, refs[ref_name])
675
675
676
676
677 @pytest.fixture
677 @pytest.fixture
678 def vcsbackend(request, backend_alias, tests_tmp_path, baseapp, test_repo):
678 def vcsbackend(request, backend_alias, tests_tmp_path, baseapp, test_repo):
679 """
679 """
680 Parametrized fixture which represents a single vcs backend implementation.
680 Parametrized fixture which represents a single vcs backend implementation.
681
681
682 See the fixture `backend` for more details. This one implements the same
682 See the fixture `backend` for more details. This one implements the same
683 concept, but on vcs level. So it does not provide model instances etc.
683 concept, but on vcs level. So it does not provide model instances etc.
684
684
685 Parameters are generated dynamically, see :func:`pytest_generate_tests`
685 Parameters are generated dynamically, see :func:`pytest_generate_tests`
686 for how this works.
686 for how this works.
687 """
687 """
688 if backend_alias not in request.config.getoption('--backends'):
688 if backend_alias not in request.config.getoption('--backends'):
689 pytest.skip("Backend %s not selected." % (backend_alias, ))
689 pytest.skip("Backend %s not selected." % (backend_alias, ))
690
690
691 utils.check_xfail_backends(request.node, backend_alias)
691 utils.check_xfail_backends(request.node, backend_alias)
692 utils.check_skip_backends(request.node, backend_alias)
692 utils.check_skip_backends(request.node, backend_alias)
693
693
694 repo_name = 'vcs_test_%s' % (backend_alias, )
694 repo_name = 'vcs_test_%s' % (backend_alias, )
695 repo_path = os.path.join(tests_tmp_path, repo_name)
695 repo_path = os.path.join(tests_tmp_path, repo_name)
696 backend = VcsBackend(
696 backend = VcsBackend(
697 alias=backend_alias,
697 alias=backend_alias,
698 repo_path=repo_path,
698 repo_path=repo_path,
699 test_name=request.node.name,
699 test_name=request.node.name,
700 test_repo_container=test_repo)
700 test_repo_container=test_repo)
701 request.addfinalizer(backend.cleanup)
701 request.addfinalizer(backend.cleanup)
702 return backend
702 return backend
703
703
704
704
705 @pytest.fixture
705 @pytest.fixture
706 def vcsbackend_git(request, tests_tmp_path, baseapp, test_repo):
706 def vcsbackend_git(request, tests_tmp_path, baseapp, test_repo):
707 return vcsbackend(request, 'git', tests_tmp_path, baseapp, test_repo)
707 return vcsbackend(request, 'git', tests_tmp_path, baseapp, test_repo)
708
708
709
709
710 @pytest.fixture
710 @pytest.fixture
711 def vcsbackend_hg(request, tests_tmp_path, baseapp, test_repo):
711 def vcsbackend_hg(request, tests_tmp_path, baseapp, test_repo):
712 return vcsbackend(request, 'hg', tests_tmp_path, baseapp, test_repo)
712 return vcsbackend(request, 'hg', tests_tmp_path, baseapp, test_repo)
713
713
714
714
715 @pytest.fixture
715 @pytest.fixture
716 def vcsbackend_svn(request, tests_tmp_path, baseapp, test_repo):
716 def vcsbackend_svn(request, tests_tmp_path, baseapp, test_repo):
717 return vcsbackend(request, 'svn', tests_tmp_path, baseapp, test_repo)
717 return vcsbackend(request, 'svn', tests_tmp_path, baseapp, test_repo)
718
718
719
719
720 @pytest.fixture
720 @pytest.fixture
721 def vcsbackend_random(vcsbackend_git):
721 def vcsbackend_random(vcsbackend_git):
722 """
722 """
723 Use this to express that your tests need "a vcsbackend".
723 Use this to express that your tests need "a vcsbackend".
724
724
725 The fixture `vcsbackend` would run the test multiple times for each
725 The fixture `vcsbackend` would run the test multiple times for each
726 available vcs backend which is a pure waste of time if the test is
726 available vcs backend which is a pure waste of time if the test is
727 independent of the vcs backend type.
727 independent of the vcs backend type.
728 """
728 """
729 # TODO: johbo: Change this to pick a random backend
729 # TODO: johbo: Change this to pick a random backend
730 return vcsbackend_git
730 return vcsbackend_git
731
731
732
732
733 @pytest.fixture
733 @pytest.fixture
734 def vcsbackend_stub(vcsbackend_git):
734 def vcsbackend_stub(vcsbackend_git):
735 """
735 """
736 Use this to express that your test just needs a stub of a vcsbackend.
736 Use this to express that your test just needs a stub of a vcsbackend.
737
737
738 Plan is to eventually implement an in-memory stub to speed tests up.
738 Plan is to eventually implement an in-memory stub to speed tests up.
739 """
739 """
740 return vcsbackend_git
740 return vcsbackend_git
741
741
742
742
743 class VcsBackend(object):
743 class VcsBackend(object):
744 """
744 """
745 Represents the test configuration for one supported vcs backend.
745 Represents the test configuration for one supported vcs backend.
746 """
746 """
747
747
748 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
748 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
749
749
750 def __init__(self, alias, repo_path, test_name, test_repo_container):
750 def __init__(self, alias, repo_path, test_name, test_repo_container):
751 self.alias = alias
751 self.alias = alias
752 self._repo_path = repo_path
752 self._repo_path = repo_path
753 self._cleanup_repos = []
753 self._cleanup_repos = []
754 self._test_name = test_name
754 self._test_name = test_name
755 self._test_repo_container = test_repo_container
755 self._test_repo_container = test_repo_container
756
756
757 def __getitem__(self, key):
757 def __getitem__(self, key):
758 return self._test_repo_container(key, self.alias).scm_instance()
758 return self._test_repo_container(key, self.alias).scm_instance()
759
759
760 @property
760 @property
761 def repo(self):
761 def repo(self):
762 """
762 """
763 Returns the "current" repository. This is the vcs_test repo of the last
763 Returns the "current" repository. This is the vcs_test repo of the last
764 repo which has been created.
764 repo which has been created.
765 """
765 """
766 Repository = get_backend(self.alias)
766 Repository = get_backend(self.alias)
767 return Repository(self._repo_path)
767 return Repository(self._repo_path)
768
768
769 @property
769 @property
770 def backend(self):
770 def backend(self):
771 """
771 """
772 Returns the backend implementation class.
772 Returns the backend implementation class.
773 """
773 """
774 return get_backend(self.alias)
774 return get_backend(self.alias)
775
775
776 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
776 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
777 repo_name = self._next_repo_name()
777 repo_name = self._next_repo_name()
778 self._repo_path = get_new_dir(repo_name)
778 self._repo_path = get_new_dir(repo_name)
779 repo_class = get_backend(self.alias)
779 repo_class = get_backend(self.alias)
780 src_url = None
780 src_url = None
781 if _clone_repo:
781 if _clone_repo:
782 src_url = _clone_repo.path
782 src_url = _clone_repo.path
783 repo = repo_class(self._repo_path, create=True, src_url=src_url)
783 repo = repo_class(self._repo_path, create=True, src_url=src_url)
784 self._cleanup_repos.append(repo)
784 self._cleanup_repos.append(repo)
785
785
786 commits = commits or [
786 commits = commits or [
787 {'message': 'Commit %s of %s' % (x, repo_name)}
787 {'message': 'Commit %s of %s' % (x, repo_name)}
788 for x in xrange(number_of_commits)]
788 for x in xrange(number_of_commits)]
789 _add_commits_to_repo(repo, commits)
789 _add_commits_to_repo(repo, commits)
790 return repo
790 return repo
791
791
792 def clone_repo(self, repo):
792 def clone_repo(self, repo):
793 return self.create_repo(_clone_repo=repo)
793 return self.create_repo(_clone_repo=repo)
794
794
795 def cleanup(self):
795 def cleanup(self):
796 for repo in self._cleanup_repos:
796 for repo in self._cleanup_repos:
797 shutil.rmtree(repo.path)
797 shutil.rmtree(repo.path)
798
798
799 def new_repo_path(self):
799 def new_repo_path(self):
800 repo_name = self._next_repo_name()
800 repo_name = self._next_repo_name()
801 self._repo_path = get_new_dir(repo_name)
801 self._repo_path = get_new_dir(repo_name)
802 return self._repo_path
802 return self._repo_path
803
803
804 def _next_repo_name(self):
804 def _next_repo_name(self):
805 return "%s_%s" % (
805 return "%s_%s" % (
806 self.invalid_repo_name.sub('_', self._test_name),
806 self.invalid_repo_name.sub('_', self._test_name),
807 len(self._cleanup_repos))
807 len(self._cleanup_repos))
808
808
809 def add_file(self, repo, filename, content='Test content\n'):
809 def add_file(self, repo, filename, content='Test content\n'):
810 imc = repo.in_memory_commit
810 imc = repo.in_memory_commit
811 imc.add(FileNode(filename, content=content))
811 imc.add(FileNode(filename, content=content))
812 imc.commit(
812 imc.commit(
813 message=u'Automatic commit from vcsbackend fixture',
813 message=u'Automatic commit from vcsbackend fixture',
814 author=u'Automatic')
814 author=u'Automatic')
815
815
816 def ensure_file(self, filename, content='Test content\n'):
816 def ensure_file(self, filename, content='Test content\n'):
817 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
817 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
818 self.add_file(self.repo, filename, content)
818 self.add_file(self.repo, filename, content)
819
819
820
820
821 def _add_commits_to_repo(vcs_repo, commits):
821 def _add_commits_to_repo(vcs_repo, commits):
822 commit_ids = {}
822 commit_ids = {}
823 if not commits:
823 if not commits:
824 return commit_ids
824 return commit_ids
825
825
826 imc = vcs_repo.in_memory_commit
826 imc = vcs_repo.in_memory_commit
827 commit = None
827 commit = None
828
828
829 for idx, commit in enumerate(commits):
829 for idx, commit in enumerate(commits):
830 message = unicode(commit.get('message', 'Commit %s' % idx))
830 message = unicode(commit.get('message', 'Commit %s' % idx))
831
831
832 for node in commit.get('added', []):
832 for node in commit.get('added', []):
833 imc.add(FileNode(node.path, content=node.content))
833 imc.add(FileNode(node.path, content=node.content))
834 for node in commit.get('changed', []):
834 for node in commit.get('changed', []):
835 imc.change(FileNode(node.path, content=node.content))
835 imc.change(FileNode(node.path, content=node.content))
836 for node in commit.get('removed', []):
836 for node in commit.get('removed', []):
837 imc.remove(FileNode(node.path))
837 imc.remove(FileNode(node.path))
838
838
839 parents = [
839 parents = [
840 vcs_repo.get_commit(commit_id=commit_ids[p])
840 vcs_repo.get_commit(commit_id=commit_ids[p])
841 for p in commit.get('parents', [])]
841 for p in commit.get('parents', [])]
842
842
843 operations = ('added', 'changed', 'removed')
843 operations = ('added', 'changed', 'removed')
844 if not any((commit.get(o) for o in operations)):
844 if not any((commit.get(o) for o in operations)):
845 imc.add(FileNode('file_%s' % idx, content=message))
845 imc.add(FileNode('file_%s' % idx, content=message))
846
846
847 commit = imc.commit(
847 commit = imc.commit(
848 message=message,
848 message=message,
849 author=unicode(commit.get('author', 'Automatic')),
849 author=unicode(commit.get('author', 'Automatic')),
850 date=commit.get('date'),
850 date=commit.get('date'),
851 branch=commit.get('branch'),
851 branch=commit.get('branch'),
852 parents=parents)
852 parents=parents)
853
853
854 commit_ids[commit.message] = commit.raw_id
854 commit_ids[commit.message] = commit.raw_id
855
855
856 return commit_ids
856 return commit_ids
857
857
858
858
859 @pytest.fixture
859 @pytest.fixture
860 def reposerver(request):
860 def reposerver(request):
861 """
861 """
862 Allows to serve a backend repository
862 Allows to serve a backend repository
863 """
863 """
864
864
865 repo_server = RepoServer()
865 repo_server = RepoServer()
866 request.addfinalizer(repo_server.cleanup)
866 request.addfinalizer(repo_server.cleanup)
867 return repo_server
867 return repo_server
868
868
869
869
870 class RepoServer(object):
870 class RepoServer(object):
871 """
871 """
872 Utility to serve a local repository for the duration of a test case.
872 Utility to serve a local repository for the duration of a test case.
873
873
874 Supports only Subversion so far.
874 Supports only Subversion so far.
875 """
875 """
876
876
877 url = None
877 url = None
878
878
879 def __init__(self):
879 def __init__(self):
880 self._cleanup_servers = []
880 self._cleanup_servers = []
881
881
882 def serve(self, vcsrepo):
882 def serve(self, vcsrepo):
883 if vcsrepo.alias != 'svn':
883 if vcsrepo.alias != 'svn':
884 raise TypeError("Backend %s not supported" % vcsrepo.alias)
884 raise TypeError("Backend %s not supported" % vcsrepo.alias)
885
885
886 proc = subprocess32.Popen(
886 proc = subprocess32.Popen(
887 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
887 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
888 '--root', vcsrepo.path])
888 '--root', vcsrepo.path])
889 self._cleanup_servers.append(proc)
889 self._cleanup_servers.append(proc)
890 self.url = 'svn://localhost'
890 self.url = 'svn://localhost'
891
891
892 def cleanup(self):
892 def cleanup(self):
893 for proc in self._cleanup_servers:
893 for proc in self._cleanup_servers:
894 proc.terminate()
894 proc.terminate()
895
895
896
896
897 @pytest.fixture
897 @pytest.fixture
898 def pr_util(backend, request, config_stub):
898 def pr_util(backend, request, config_stub):
899 """
899 """
900 Utility for tests of models and for functional tests around pull requests.
900 Utility for tests of models and for functional tests around pull requests.
901
901
902 It gives an instance of :class:`PRTestUtility` which provides various
902 It gives an instance of :class:`PRTestUtility` which provides various
903 utility methods around one pull request.
903 utility methods around one pull request.
904
904
905 This fixture uses `backend` and inherits its parameterization.
905 This fixture uses `backend` and inherits its parameterization.
906 """
906 """
907
907
908 util = PRTestUtility(backend)
908 util = PRTestUtility(backend)
909 request.addfinalizer(util.cleanup)
909 request.addfinalizer(util.cleanup)
910
910
911 return util
911 return util
912
912
913
913
914 class PRTestUtility(object):
914 class PRTestUtility(object):
915
915
916 pull_request = None
916 pull_request = None
917 pull_request_id = None
917 pull_request_id = None
918 mergeable_patcher = None
918 mergeable_patcher = None
919 mergeable_mock = None
919 mergeable_mock = None
920 notification_patcher = None
920 notification_patcher = None
921
921
922 def __init__(self, backend):
922 def __init__(self, backend):
923 self.backend = backend
923 self.backend = backend
924
924
925 def create_pull_request(
925 def create_pull_request(
926 self, commits=None, target_head=None, source_head=None,
926 self, commits=None, target_head=None, source_head=None,
927 revisions=None, approved=False, author=None, mergeable=False,
927 revisions=None, approved=False, author=None, mergeable=False,
928 enable_notifications=True, name_suffix=u'', reviewers=None,
928 enable_notifications=True, name_suffix=u'', reviewers=None,
929 title=u"Test", description=u"Description"):
929 title=u"Test", description=u"Description"):
930 self.set_mergeable(mergeable)
930 self.set_mergeable(mergeable)
931 if not enable_notifications:
931 if not enable_notifications:
932 # mock notification side effect
932 # mock notification side effect
933 self.notification_patcher = mock.patch(
933 self.notification_patcher = mock.patch(
934 'rhodecode.model.notification.NotificationModel.create')
934 'rhodecode.model.notification.NotificationModel.create')
935 self.notification_patcher.start()
935 self.notification_patcher.start()
936
936
937 if not self.pull_request:
937 if not self.pull_request:
938 if not commits:
938 if not commits:
939 commits = [
939 commits = [
940 {'message': 'c1'},
940 {'message': 'c1'},
941 {'message': 'c2'},
941 {'message': 'c2'},
942 {'message': 'c3'},
942 {'message': 'c3'},
943 ]
943 ]
944 target_head = 'c1'
944 target_head = 'c1'
945 source_head = 'c2'
945 source_head = 'c2'
946 revisions = ['c2']
946 revisions = ['c2']
947
947
948 self.commit_ids = self.backend.create_master_repo(commits)
948 self.commit_ids = self.backend.create_master_repo(commits)
949 self.target_repository = self.backend.create_repo(
949 self.target_repository = self.backend.create_repo(
950 heads=[target_head], name_suffix=name_suffix)
950 heads=[target_head], name_suffix=name_suffix)
951 self.source_repository = self.backend.create_repo(
951 self.source_repository = self.backend.create_repo(
952 heads=[source_head], name_suffix=name_suffix)
952 heads=[source_head], name_suffix=name_suffix)
953 self.author = author or UserModel().get_by_username(
953 self.author = author or UserModel().get_by_username(
954 TEST_USER_ADMIN_LOGIN)
954 TEST_USER_ADMIN_LOGIN)
955
955
956 model = PullRequestModel()
956 model = PullRequestModel()
957 self.create_parameters = {
957 self.create_parameters = {
958 'created_by': self.author,
958 'created_by': self.author,
959 'source_repo': self.source_repository.repo_name,
959 'source_repo': self.source_repository.repo_name,
960 'source_ref': self._default_branch_reference(source_head),
960 'source_ref': self._default_branch_reference(source_head),
961 'target_repo': self.target_repository.repo_name,
961 'target_repo': self.target_repository.repo_name,
962 'target_ref': self._default_branch_reference(target_head),
962 'target_ref': self._default_branch_reference(target_head),
963 'revisions': [self.commit_ids[r] for r in revisions],
963 'revisions': [self.commit_ids[r] for r in revisions],
964 'reviewers': reviewers or self._get_reviewers(),
964 'reviewers': reviewers or self._get_reviewers(),
965 'title': title,
965 'title': title,
966 'description': description,
966 'description': description,
967 }
967 }
968 self.pull_request = model.create(**self.create_parameters)
968 self.pull_request = model.create(**self.create_parameters)
969 assert model.get_versions(self.pull_request) == []
969 assert model.get_versions(self.pull_request) == []
970
970
971 self.pull_request_id = self.pull_request.pull_request_id
971 self.pull_request_id = self.pull_request.pull_request_id
972
972
973 if approved:
973 if approved:
974 self.approve()
974 self.approve()
975
975
976 Session().add(self.pull_request)
976 Session().add(self.pull_request)
977 Session().commit()
977 Session().commit()
978
978
979 return self.pull_request
979 return self.pull_request
980
980
981 def approve(self):
981 def approve(self):
982 self.create_status_votes(
982 self.create_status_votes(
983 ChangesetStatus.STATUS_APPROVED,
983 ChangesetStatus.STATUS_APPROVED,
984 *self.pull_request.reviewers)
984 *self.pull_request.reviewers)
985
985
986 def close(self):
986 def close(self):
987 PullRequestModel().close_pull_request(self.pull_request, self.author)
987 PullRequestModel().close_pull_request(self.pull_request, self.author)
988
988
989 def _default_branch_reference(self, commit_message):
989 def _default_branch_reference(self, commit_message):
990 reference = '%s:%s:%s' % (
990 reference = '%s:%s:%s' % (
991 'branch',
991 'branch',
992 self.backend.default_branch_name,
992 self.backend.default_branch_name,
993 self.commit_ids[commit_message])
993 self.commit_ids[commit_message])
994 return reference
994 return reference
995
995
996 def _get_reviewers(self):
996 def _get_reviewers(self):
997 return [
997 return [
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1000 ]
1000 ]
1001
1001
1002 def update_source_repository(self, head=None):
1002 def update_source_repository(self, head=None):
1003 heads = [head or 'c3']
1003 heads = [head or 'c3']
1004 self.backend.pull_heads(self.source_repository, heads=heads)
1004 self.backend.pull_heads(self.source_repository, heads=heads)
1005
1005
1006 def add_one_commit(self, head=None):
1006 def add_one_commit(self, head=None):
1007 self.update_source_repository(head=head)
1007 self.update_source_repository(head=head)
1008 old_commit_ids = set(self.pull_request.revisions)
1008 old_commit_ids = set(self.pull_request.revisions)
1009 PullRequestModel().update_commits(self.pull_request)
1009 PullRequestModel().update_commits(self.pull_request)
1010 commit_ids = set(self.pull_request.revisions)
1010 commit_ids = set(self.pull_request.revisions)
1011 new_commit_ids = commit_ids - old_commit_ids
1011 new_commit_ids = commit_ids - old_commit_ids
1012 assert len(new_commit_ids) == 1
1012 assert len(new_commit_ids) == 1
1013 return new_commit_ids.pop()
1013 return new_commit_ids.pop()
1014
1014
1015 def remove_one_commit(self):
1015 def remove_one_commit(self):
1016 assert len(self.pull_request.revisions) == 2
1016 assert len(self.pull_request.revisions) == 2
1017 source_vcs = self.source_repository.scm_instance()
1017 source_vcs = self.source_repository.scm_instance()
1018 removed_commit_id = source_vcs.commit_ids[-1]
1018 removed_commit_id = source_vcs.commit_ids[-1]
1019
1019
1020 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1020 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1021 # remove the if once that's sorted out.
1021 # remove the if once that's sorted out.
1022 if self.backend.alias == "git":
1022 if self.backend.alias == "git":
1023 kwargs = {'branch_name': self.backend.default_branch_name}
1023 kwargs = {'branch_name': self.backend.default_branch_name}
1024 else:
1024 else:
1025 kwargs = {}
1025 kwargs = {}
1026 source_vcs.strip(removed_commit_id, **kwargs)
1026 source_vcs.strip(removed_commit_id, **kwargs)
1027
1027
1028 PullRequestModel().update_commits(self.pull_request)
1028 PullRequestModel().update_commits(self.pull_request)
1029 assert len(self.pull_request.revisions) == 1
1029 assert len(self.pull_request.revisions) == 1
1030 return removed_commit_id
1030 return removed_commit_id
1031
1031
1032 def create_comment(self, linked_to=None):
1032 def create_comment(self, linked_to=None):
1033 comment = CommentsModel().create(
1033 comment = CommentsModel().create(
1034 text=u"Test comment",
1034 text=u"Test comment",
1035 repo=self.target_repository.repo_name,
1035 repo=self.target_repository.repo_name,
1036 user=self.author,
1036 user=self.author,
1037 pull_request=self.pull_request)
1037 pull_request=self.pull_request)
1038 assert comment.pull_request_version_id is None
1038 assert comment.pull_request_version_id is None
1039
1039
1040 if linked_to:
1040 if linked_to:
1041 PullRequestModel()._link_comments_to_version(linked_to)
1041 PullRequestModel()._link_comments_to_version(linked_to)
1042
1042
1043 return comment
1043 return comment
1044
1044
1045 def create_inline_comment(
1045 def create_inline_comment(
1046 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1046 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1047 comment = CommentsModel().create(
1047 comment = CommentsModel().create(
1048 text=u"Test comment",
1048 text=u"Test comment",
1049 repo=self.target_repository.repo_name,
1049 repo=self.target_repository.repo_name,
1050 user=self.author,
1050 user=self.author,
1051 line_no=line_no,
1051 line_no=line_no,
1052 f_path=file_path,
1052 f_path=file_path,
1053 pull_request=self.pull_request)
1053 pull_request=self.pull_request)
1054 assert comment.pull_request_version_id is None
1054 assert comment.pull_request_version_id is None
1055
1055
1056 if linked_to:
1056 if linked_to:
1057 PullRequestModel()._link_comments_to_version(linked_to)
1057 PullRequestModel()._link_comments_to_version(linked_to)
1058
1058
1059 return comment
1059 return comment
1060
1060
1061 def create_version_of_pull_request(self):
1061 def create_version_of_pull_request(self):
1062 pull_request = self.create_pull_request()
1062 pull_request = self.create_pull_request()
1063 version = PullRequestModel()._create_version_from_snapshot(
1063 version = PullRequestModel()._create_version_from_snapshot(
1064 pull_request)
1064 pull_request)
1065 return version
1065 return version
1066
1066
1067 def create_status_votes(self, status, *reviewers):
1067 def create_status_votes(self, status, *reviewers):
1068 for reviewer in reviewers:
1068 for reviewer in reviewers:
1069 ChangesetStatusModel().set_status(
1069 ChangesetStatusModel().set_status(
1070 repo=self.pull_request.target_repo,
1070 repo=self.pull_request.target_repo,
1071 status=status,
1071 status=status,
1072 user=reviewer.user_id,
1072 user=reviewer.user_id,
1073 pull_request=self.pull_request)
1073 pull_request=self.pull_request)
1074
1074
1075 def set_mergeable(self, value):
1075 def set_mergeable(self, value):
1076 if not self.mergeable_patcher:
1076 if not self.mergeable_patcher:
1077 self.mergeable_patcher = mock.patch.object(
1077 self.mergeable_patcher = mock.patch.object(
1078 VcsSettingsModel, 'get_general_settings')
1078 VcsSettingsModel, 'get_general_settings')
1079 self.mergeable_mock = self.mergeable_patcher.start()
1079 self.mergeable_mock = self.mergeable_patcher.start()
1080 self.mergeable_mock.return_value = {
1080 self.mergeable_mock.return_value = {
1081 'rhodecode_pr_merge_enabled': value}
1081 'rhodecode_pr_merge_enabled': value}
1082
1082
1083 def cleanup(self):
1083 def cleanup(self):
1084 # In case the source repository is already cleaned up, the pull
1084 # In case the source repository is already cleaned up, the pull
1085 # request will already be deleted.
1085 # request will already be deleted.
1086 pull_request = PullRequest().get(self.pull_request_id)
1086 pull_request = PullRequest().get(self.pull_request_id)
1087 if pull_request:
1087 if pull_request:
1088 PullRequestModel().delete(pull_request, pull_request.author)
1088 PullRequestModel().delete(pull_request, pull_request.author)
1089 Session().commit()
1089 Session().commit()
1090
1090
1091 if self.notification_patcher:
1091 if self.notification_patcher:
1092 self.notification_patcher.stop()
1092 self.notification_patcher.stop()
1093
1093
1094 if self.mergeable_patcher:
1094 if self.mergeable_patcher:
1095 self.mergeable_patcher.stop()
1095 self.mergeable_patcher.stop()
1096
1096
1097
1097
1098 @pytest.fixture
1098 @pytest.fixture
1099 def user_admin(baseapp):
1099 def user_admin(baseapp):
1100 """
1100 """
1101 Provides the default admin test user as an instance of `db.User`.
1101 Provides the default admin test user as an instance of `db.User`.
1102 """
1102 """
1103 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1103 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1104 return user
1104 return user
1105
1105
1106
1106
1107 @pytest.fixture
1107 @pytest.fixture
1108 def user_regular(baseapp):
1108 def user_regular(baseapp):
1109 """
1109 """
1110 Provides the default regular test user as an instance of `db.User`.
1110 Provides the default regular test user as an instance of `db.User`.
1111 """
1111 """
1112 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1112 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1113 return user
1113 return user
1114
1114
1115
1115
1116 @pytest.fixture
1116 @pytest.fixture
1117 def user_util(request, db_connection):
1117 def user_util(request, db_connection):
1118 """
1118 """
1119 Provides a wired instance of `UserUtility` with integrated cleanup.
1119 Provides a wired instance of `UserUtility` with integrated cleanup.
1120 """
1120 """
1121 utility = UserUtility(test_name=request.node.name)
1121 utility = UserUtility(test_name=request.node.name)
1122 request.addfinalizer(utility.cleanup)
1122 request.addfinalizer(utility.cleanup)
1123 return utility
1123 return utility
1124
1124
1125
1125
1126 # TODO: johbo: Split this up into utilities per domain or something similar
1126 # TODO: johbo: Split this up into utilities per domain or something similar
1127 class UserUtility(object):
1127 class UserUtility(object):
1128
1128
1129 def __init__(self, test_name="test"):
1129 def __init__(self, test_name="test"):
1130 self._test_name = self._sanitize_name(test_name)
1130 self._test_name = self._sanitize_name(test_name)
1131 self.fixture = Fixture()
1131 self.fixture = Fixture()
1132 self.repo_group_ids = []
1132 self.repo_group_ids = []
1133 self.repos_ids = []
1133 self.repos_ids = []
1134 self.user_ids = []
1134 self.user_ids = []
1135 self.user_group_ids = []
1135 self.user_group_ids = []
1136 self.user_repo_permission_ids = []
1136 self.user_repo_permission_ids = []
1137 self.user_group_repo_permission_ids = []
1137 self.user_group_repo_permission_ids = []
1138 self.user_repo_group_permission_ids = []
1138 self.user_repo_group_permission_ids = []
1139 self.user_group_repo_group_permission_ids = []
1139 self.user_group_repo_group_permission_ids = []
1140 self.user_user_group_permission_ids = []
1140 self.user_user_group_permission_ids = []
1141 self.user_group_user_group_permission_ids = []
1141 self.user_group_user_group_permission_ids = []
1142 self.user_permissions = []
1142 self.user_permissions = []
1143
1143
1144 def _sanitize_name(self, name):
1144 def _sanitize_name(self, name):
1145 for char in ['[', ']']:
1145 for char in ['[', ']']:
1146 name = name.replace(char, '_')
1146 name = name.replace(char, '_')
1147 return name
1147 return name
1148
1148
1149 def create_repo_group(
1149 def create_repo_group(
1150 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1150 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1151 group_name = "{prefix}_repogroup_{count}".format(
1151 group_name = "{prefix}_repogroup_{count}".format(
1152 prefix=self._test_name,
1152 prefix=self._test_name,
1153 count=len(self.repo_group_ids))
1153 count=len(self.repo_group_ids))
1154 repo_group = self.fixture.create_repo_group(
1154 repo_group = self.fixture.create_repo_group(
1155 group_name, cur_user=owner)
1155 group_name, cur_user=owner)
1156 if auto_cleanup:
1156 if auto_cleanup:
1157 self.repo_group_ids.append(repo_group.group_id)
1157 self.repo_group_ids.append(repo_group.group_id)
1158 return repo_group
1158 return repo_group
1159
1159
1160 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1160 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1161 auto_cleanup=True, repo_type='hg'):
1161 auto_cleanup=True, repo_type='hg'):
1162 repo_name = "{prefix}_repository_{count}".format(
1162 repo_name = "{prefix}_repository_{count}".format(
1163 prefix=self._test_name,
1163 prefix=self._test_name,
1164 count=len(self.repos_ids))
1164 count=len(self.repos_ids))
1165
1165
1166 repository = self.fixture.create_repo(
1166 repository = self.fixture.create_repo(
1167 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type)
1167 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type)
1168 if auto_cleanup:
1168 if auto_cleanup:
1169 self.repos_ids.append(repository.repo_id)
1169 self.repos_ids.append(repository.repo_id)
1170 return repository
1170 return repository
1171
1171
1172 def create_user(self, auto_cleanup=True, **kwargs):
1172 def create_user(self, auto_cleanup=True, **kwargs):
1173 user_name = "{prefix}_user_{count}".format(
1173 user_name = "{prefix}_user_{count}".format(
1174 prefix=self._test_name,
1174 prefix=self._test_name,
1175 count=len(self.user_ids))
1175 count=len(self.user_ids))
1176 user = self.fixture.create_user(user_name, **kwargs)
1176 user = self.fixture.create_user(user_name, **kwargs)
1177 if auto_cleanup:
1177 if auto_cleanup:
1178 self.user_ids.append(user.user_id)
1178 self.user_ids.append(user.user_id)
1179 return user
1179 return user
1180
1180
1181 def create_user_with_group(self):
1181 def create_user_with_group(self):
1182 user = self.create_user()
1182 user = self.create_user()
1183 user_group = self.create_user_group(members=[user])
1183 user_group = self.create_user_group(members=[user])
1184 return user, user_group
1184 return user, user_group
1185
1185
1186 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1186 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1187 auto_cleanup=True, **kwargs):
1187 auto_cleanup=True, **kwargs):
1188 group_name = "{prefix}_usergroup_{count}".format(
1188 group_name = "{prefix}_usergroup_{count}".format(
1189 prefix=self._test_name,
1189 prefix=self._test_name,
1190 count=len(self.user_group_ids))
1190 count=len(self.user_group_ids))
1191 user_group = self.fixture.create_user_group(
1191 user_group = self.fixture.create_user_group(
1192 group_name, cur_user=owner, **kwargs)
1192 group_name, cur_user=owner, **kwargs)
1193
1193
1194 if auto_cleanup:
1194 if auto_cleanup:
1195 self.user_group_ids.append(user_group.users_group_id)
1195 self.user_group_ids.append(user_group.users_group_id)
1196 if members:
1196 if members:
1197 for user in members:
1197 for user in members:
1198 UserGroupModel().add_user_to_group(user_group, user)
1198 UserGroupModel().add_user_to_group(user_group, user)
1199 return user_group
1199 return user_group
1200
1200
1201 def grant_user_permission(self, user_name, permission_name):
1201 def grant_user_permission(self, user_name, permission_name):
1202 self._inherit_default_user_permissions(user_name, False)
1202 self._inherit_default_user_permissions(user_name, False)
1203 self.user_permissions.append((user_name, permission_name))
1203 self.user_permissions.append((user_name, permission_name))
1204
1204
1205 def grant_user_permission_to_repo_group(
1205 def grant_user_permission_to_repo_group(
1206 self, repo_group, user, permission_name):
1206 self, repo_group, user, permission_name):
1207 permission = RepoGroupModel().grant_user_permission(
1207 permission = RepoGroupModel().grant_user_permission(
1208 repo_group, user, permission_name)
1208 repo_group, user, permission_name)
1209 self.user_repo_group_permission_ids.append(
1209 self.user_repo_group_permission_ids.append(
1210 (repo_group.group_id, user.user_id))
1210 (repo_group.group_id, user.user_id))
1211 return permission
1211 return permission
1212
1212
1213 def grant_user_group_permission_to_repo_group(
1213 def grant_user_group_permission_to_repo_group(
1214 self, repo_group, user_group, permission_name):
1214 self, repo_group, user_group, permission_name):
1215 permission = RepoGroupModel().grant_user_group_permission(
1215 permission = RepoGroupModel().grant_user_group_permission(
1216 repo_group, user_group, permission_name)
1216 repo_group, user_group, permission_name)
1217 self.user_group_repo_group_permission_ids.append(
1217 self.user_group_repo_group_permission_ids.append(
1218 (repo_group.group_id, user_group.users_group_id))
1218 (repo_group.group_id, user_group.users_group_id))
1219 return permission
1219 return permission
1220
1220
1221 def grant_user_permission_to_repo(
1221 def grant_user_permission_to_repo(
1222 self, repo, user, permission_name):
1222 self, repo, user, permission_name):
1223 permission = RepoModel().grant_user_permission(
1223 permission = RepoModel().grant_user_permission(
1224 repo, user, permission_name)
1224 repo, user, permission_name)
1225 self.user_repo_permission_ids.append(
1225 self.user_repo_permission_ids.append(
1226 (repo.repo_id, user.user_id))
1226 (repo.repo_id, user.user_id))
1227 return permission
1227 return permission
1228
1228
1229 def grant_user_group_permission_to_repo(
1229 def grant_user_group_permission_to_repo(
1230 self, repo, user_group, permission_name):
1230 self, repo, user_group, permission_name):
1231 permission = RepoModel().grant_user_group_permission(
1231 permission = RepoModel().grant_user_group_permission(
1232 repo, user_group, permission_name)
1232 repo, user_group, permission_name)
1233 self.user_group_repo_permission_ids.append(
1233 self.user_group_repo_permission_ids.append(
1234 (repo.repo_id, user_group.users_group_id))
1234 (repo.repo_id, user_group.users_group_id))
1235 return permission
1235 return permission
1236
1236
1237 def grant_user_permission_to_user_group(
1237 def grant_user_permission_to_user_group(
1238 self, target_user_group, user, permission_name):
1238 self, target_user_group, user, permission_name):
1239 permission = UserGroupModel().grant_user_permission(
1239 permission = UserGroupModel().grant_user_permission(
1240 target_user_group, user, permission_name)
1240 target_user_group, user, permission_name)
1241 self.user_user_group_permission_ids.append(
1241 self.user_user_group_permission_ids.append(
1242 (target_user_group.users_group_id, user.user_id))
1242 (target_user_group.users_group_id, user.user_id))
1243 return permission
1243 return permission
1244
1244
1245 def grant_user_group_permission_to_user_group(
1245 def grant_user_group_permission_to_user_group(
1246 self, target_user_group, user_group, permission_name):
1246 self, target_user_group, user_group, permission_name):
1247 permission = UserGroupModel().grant_user_group_permission(
1247 permission = UserGroupModel().grant_user_group_permission(
1248 target_user_group, user_group, permission_name)
1248 target_user_group, user_group, permission_name)
1249 self.user_group_user_group_permission_ids.append(
1249 self.user_group_user_group_permission_ids.append(
1250 (target_user_group.users_group_id, user_group.users_group_id))
1250 (target_user_group.users_group_id, user_group.users_group_id))
1251 return permission
1251 return permission
1252
1252
1253 def revoke_user_permission(self, user_name, permission_name):
1253 def revoke_user_permission(self, user_name, permission_name):
1254 self._inherit_default_user_permissions(user_name, True)
1254 self._inherit_default_user_permissions(user_name, True)
1255 UserModel().revoke_perm(user_name, permission_name)
1255 UserModel().revoke_perm(user_name, permission_name)
1256
1256
1257 def _inherit_default_user_permissions(self, user_name, value):
1257 def _inherit_default_user_permissions(self, user_name, value):
1258 user = UserModel().get_by_username(user_name)
1258 user = UserModel().get_by_username(user_name)
1259 user.inherit_default_permissions = value
1259 user.inherit_default_permissions = value
1260 Session().add(user)
1260 Session().add(user)
1261 Session().commit()
1261 Session().commit()
1262
1262
1263 def cleanup(self):
1263 def cleanup(self):
1264 self._cleanup_permissions()
1264 self._cleanup_permissions()
1265 self._cleanup_repos()
1265 self._cleanup_repos()
1266 self._cleanup_repo_groups()
1266 self._cleanup_repo_groups()
1267 self._cleanup_user_groups()
1267 self._cleanup_user_groups()
1268 self._cleanup_users()
1268 self._cleanup_users()
1269
1269
1270 def _cleanup_permissions(self):
1270 def _cleanup_permissions(self):
1271 if self.user_permissions:
1271 if self.user_permissions:
1272 for user_name, permission_name in self.user_permissions:
1272 for user_name, permission_name in self.user_permissions:
1273 self.revoke_user_permission(user_name, permission_name)
1273 self.revoke_user_permission(user_name, permission_name)
1274
1274
1275 for permission in self.user_repo_permission_ids:
1275 for permission in self.user_repo_permission_ids:
1276 RepoModel().revoke_user_permission(*permission)
1276 RepoModel().revoke_user_permission(*permission)
1277
1277
1278 for permission in self.user_group_repo_permission_ids:
1278 for permission in self.user_group_repo_permission_ids:
1279 RepoModel().revoke_user_group_permission(*permission)
1279 RepoModel().revoke_user_group_permission(*permission)
1280
1280
1281 for permission in self.user_repo_group_permission_ids:
1281 for permission in self.user_repo_group_permission_ids:
1282 RepoGroupModel().revoke_user_permission(*permission)
1282 RepoGroupModel().revoke_user_permission(*permission)
1283
1283
1284 for permission in self.user_group_repo_group_permission_ids:
1284 for permission in self.user_group_repo_group_permission_ids:
1285 RepoGroupModel().revoke_user_group_permission(*permission)
1285 RepoGroupModel().revoke_user_group_permission(*permission)
1286
1286
1287 for permission in self.user_user_group_permission_ids:
1287 for permission in self.user_user_group_permission_ids:
1288 UserGroupModel().revoke_user_permission(*permission)
1288 UserGroupModel().revoke_user_permission(*permission)
1289
1289
1290 for permission in self.user_group_user_group_permission_ids:
1290 for permission in self.user_group_user_group_permission_ids:
1291 UserGroupModel().revoke_user_group_permission(*permission)
1291 UserGroupModel().revoke_user_group_permission(*permission)
1292
1292
1293 def _cleanup_repo_groups(self):
1293 def _cleanup_repo_groups(self):
1294 def _repo_group_compare(first_group_id, second_group_id):
1294 def _repo_group_compare(first_group_id, second_group_id):
1295 """
1295 """
1296 Gives higher priority to the groups with the most complex paths
1296 Gives higher priority to the groups with the most complex paths
1297 """
1297 """
1298 first_group = RepoGroup.get(first_group_id)
1298 first_group = RepoGroup.get(first_group_id)
1299 second_group = RepoGroup.get(second_group_id)
1299 second_group = RepoGroup.get(second_group_id)
1300 first_group_parts = (
1300 first_group_parts = (
1301 len(first_group.group_name.split('/')) if first_group else 0)
1301 len(first_group.group_name.split('/')) if first_group else 0)
1302 second_group_parts = (
1302 second_group_parts = (
1303 len(second_group.group_name.split('/')) if second_group else 0)
1303 len(second_group.group_name.split('/')) if second_group else 0)
1304 return cmp(second_group_parts, first_group_parts)
1304 return cmp(second_group_parts, first_group_parts)
1305
1305
1306 sorted_repo_group_ids = sorted(
1306 sorted_repo_group_ids = sorted(
1307 self.repo_group_ids, cmp=_repo_group_compare)
1307 self.repo_group_ids, cmp=_repo_group_compare)
1308 for repo_group_id in sorted_repo_group_ids:
1308 for repo_group_id in sorted_repo_group_ids:
1309 self.fixture.destroy_repo_group(repo_group_id)
1309 self.fixture.destroy_repo_group(repo_group_id)
1310
1310
1311 def _cleanup_repos(self):
1311 def _cleanup_repos(self):
1312 sorted_repos_ids = sorted(self.repos_ids)
1312 sorted_repos_ids = sorted(self.repos_ids)
1313 for repo_id in sorted_repos_ids:
1313 for repo_id in sorted_repos_ids:
1314 self.fixture.destroy_repo(repo_id)
1314 self.fixture.destroy_repo(repo_id)
1315
1315
1316 def _cleanup_user_groups(self):
1316 def _cleanup_user_groups(self):
1317 def _user_group_compare(first_group_id, second_group_id):
1317 def _user_group_compare(first_group_id, second_group_id):
1318 """
1318 """
1319 Gives higher priority to the groups with the most complex paths
1319 Gives higher priority to the groups with the most complex paths
1320 """
1320 """
1321 first_group = UserGroup.get(first_group_id)
1321 first_group = UserGroup.get(first_group_id)
1322 second_group = UserGroup.get(second_group_id)
1322 second_group = UserGroup.get(second_group_id)
1323 first_group_parts = (
1323 first_group_parts = (
1324 len(first_group.users_group_name.split('/'))
1324 len(first_group.users_group_name.split('/'))
1325 if first_group else 0)
1325 if first_group else 0)
1326 second_group_parts = (
1326 second_group_parts = (
1327 len(second_group.users_group_name.split('/'))
1327 len(second_group.users_group_name.split('/'))
1328 if second_group else 0)
1328 if second_group else 0)
1329 return cmp(second_group_parts, first_group_parts)
1329 return cmp(second_group_parts, first_group_parts)
1330
1330
1331 sorted_user_group_ids = sorted(
1331 sorted_user_group_ids = sorted(
1332 self.user_group_ids, cmp=_user_group_compare)
1332 self.user_group_ids, cmp=_user_group_compare)
1333 for user_group_id in sorted_user_group_ids:
1333 for user_group_id in sorted_user_group_ids:
1334 self.fixture.destroy_user_group(user_group_id)
1334 self.fixture.destroy_user_group(user_group_id)
1335
1335
1336 def _cleanup_users(self):
1336 def _cleanup_users(self):
1337 for user_id in self.user_ids:
1337 for user_id in self.user_ids:
1338 self.fixture.destroy_user(user_id)
1338 self.fixture.destroy_user(user_id)
1339
1339
1340
1340
1341 # TODO: Think about moving this into a pytest-pyro package and make it a
1341 # TODO: Think about moving this into a pytest-pyro package and make it a
1342 # pytest plugin
1342 # pytest plugin
1343 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1343 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1344 def pytest_runtest_makereport(item, call):
1344 def pytest_runtest_makereport(item, call):
1345 """
1345 """
1346 Adding the remote traceback if the exception has this information.
1346 Adding the remote traceback if the exception has this information.
1347
1347
1348 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1348 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1349 to the exception instance.
1349 to the exception instance.
1350 """
1350 """
1351 outcome = yield
1351 outcome = yield
1352 report = outcome.get_result()
1352 report = outcome.get_result()
1353 if call.excinfo:
1353 if call.excinfo:
1354 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1354 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1355
1355
1356
1356
1357 def _add_vcsserver_remote_traceback(report, exc):
1357 def _add_vcsserver_remote_traceback(report, exc):
1358 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1358 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1359
1359
1360 if vcsserver_traceback:
1360 if vcsserver_traceback:
1361 section = 'VCSServer remote traceback ' + report.when
1361 section = 'VCSServer remote traceback ' + report.when
1362 report.sections.append((section, vcsserver_traceback))
1362 report.sections.append((section, vcsserver_traceback))
1363
1363
1364
1364
1365 @pytest.fixture(scope='session')
1365 @pytest.fixture(scope='session')
1366 def testrun():
1366 def testrun():
1367 return {
1367 return {
1368 'uuid': uuid.uuid4(),
1368 'uuid': uuid.uuid4(),
1369 'start': datetime.datetime.utcnow().isoformat(),
1369 'start': datetime.datetime.utcnow().isoformat(),
1370 'timestamp': int(time.time()),
1370 'timestamp': int(time.time()),
1371 }
1371 }
1372
1372
1373
1373
1374 @pytest.fixture(autouse=True)
1374 @pytest.fixture(autouse=True)
1375 def collect_appenlight_stats(request, testrun):
1375 def collect_appenlight_stats(request, testrun):
1376 """
1376 """
1377 This fixture reports memory consumtion of single tests.
1377 This fixture reports memory consumtion of single tests.
1378
1378
1379 It gathers data based on `psutil` and sends them to Appenlight. The option
1379 It gathers data based on `psutil` and sends them to Appenlight. The option
1380 ``--ae`` has te be used to enable this fixture and the API key for your
1380 ``--ae`` has te be used to enable this fixture and the API key for your
1381 application has to be provided in ``--ae-key``.
1381 application has to be provided in ``--ae-key``.
1382 """
1382 """
1383 try:
1383 try:
1384 # cygwin cannot have yet psutil support.
1384 # cygwin cannot have yet psutil support.
1385 import psutil
1385 import psutil
1386 except ImportError:
1386 except ImportError:
1387 return
1387 return
1388
1388
1389 if not request.config.getoption('--appenlight'):
1389 if not request.config.getoption('--appenlight'):
1390 return
1390 return
1391 else:
1391 else:
1392 # Only request the baseapp fixture if appenlight tracking is
1392 # Only request the baseapp fixture if appenlight tracking is
1393 # enabled. This will speed up a test run of unit tests by 2 to 3
1393 # enabled. This will speed up a test run of unit tests by 2 to 3
1394 # seconds if appenlight is not enabled.
1394 # seconds if appenlight is not enabled.
1395 baseapp = request.getfuncargvalue("baseapp")
1395 baseapp = request.getfuncargvalue("baseapp")
1396 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1396 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1397 client = AppenlightClient(
1397 client = AppenlightClient(
1398 url=url,
1398 url=url,
1399 api_key=request.config.getoption('--appenlight-api-key'),
1399 api_key=request.config.getoption('--appenlight-api-key'),
1400 namespace=request.node.nodeid,
1400 namespace=request.node.nodeid,
1401 request=str(testrun['uuid']),
1401 request=str(testrun['uuid']),
1402 testrun=testrun)
1402 testrun=testrun)
1403
1403
1404 client.collect({
1404 client.collect({
1405 'message': "Starting",
1405 'message': "Starting",
1406 })
1406 })
1407
1407
1408 server_and_port = baseapp.config.get_settings()['vcs.server']
1408 server_and_port = baseapp.config.get_settings()['vcs.server']
1409 protocol = baseapp.config.get_settings()['vcs.server.protocol']
1409 protocol = baseapp.config.get_settings()['vcs.server.protocol']
1410 server = create_vcsserver_proxy(server_and_port, protocol)
1410 server = create_vcsserver_proxy(server_and_port, protocol)
1411 with server:
1411 with server:
1412 vcs_pid = server.get_pid()
1412 vcs_pid = server.get_pid()
1413 server.run_gc()
1413 server.run_gc()
1414 vcs_process = psutil.Process(vcs_pid)
1414 vcs_process = psutil.Process(vcs_pid)
1415 mem = vcs_process.memory_info()
1415 mem = vcs_process.memory_info()
1416 client.tag_before('vcsserver.rss', mem.rss)
1416 client.tag_before('vcsserver.rss', mem.rss)
1417 client.tag_before('vcsserver.vms', mem.vms)
1417 client.tag_before('vcsserver.vms', mem.vms)
1418
1418
1419 test_process = psutil.Process()
1419 test_process = psutil.Process()
1420 mem = test_process.memory_info()
1420 mem = test_process.memory_info()
1421 client.tag_before('test.rss', mem.rss)
1421 client.tag_before('test.rss', mem.rss)
1422 client.tag_before('test.vms', mem.vms)
1422 client.tag_before('test.vms', mem.vms)
1423
1423
1424 client.tag_before('time', time.time())
1424 client.tag_before('time', time.time())
1425
1425
1426 @request.addfinalizer
1426 @request.addfinalizer
1427 def send_stats():
1427 def send_stats():
1428 client.tag_after('time', time.time())
1428 client.tag_after('time', time.time())
1429 with server:
1429 with server:
1430 gc_stats = server.run_gc()
1430 gc_stats = server.run_gc()
1431 for tag, value in gc_stats.items():
1431 for tag, value in gc_stats.items():
1432 client.tag_after(tag, value)
1432 client.tag_after(tag, value)
1433 mem = vcs_process.memory_info()
1433 mem = vcs_process.memory_info()
1434 client.tag_after('vcsserver.rss', mem.rss)
1434 client.tag_after('vcsserver.rss', mem.rss)
1435 client.tag_after('vcsserver.vms', mem.vms)
1435 client.tag_after('vcsserver.vms', mem.vms)
1436
1436
1437 mem = test_process.memory_info()
1437 mem = test_process.memory_info()
1438 client.tag_after('test.rss', mem.rss)
1438 client.tag_after('test.rss', mem.rss)
1439 client.tag_after('test.vms', mem.vms)
1439 client.tag_after('test.vms', mem.vms)
1440
1440
1441 client.collect({
1441 client.collect({
1442 'message': "Finished",
1442 'message': "Finished",
1443 })
1443 })
1444 client.send_stats()
1444 client.send_stats()
1445
1445
1446 return client
1446 return client
1447
1447
1448
1448
1449 class AppenlightClient():
1449 class AppenlightClient():
1450
1450
1451 url_template = '{url}?protocol_version=0.5'
1451 url_template = '{url}?protocol_version=0.5'
1452
1452
1453 def __init__(
1453 def __init__(
1454 self, url, api_key, add_server=True, add_timestamp=True,
1454 self, url, api_key, add_server=True, add_timestamp=True,
1455 namespace=None, request=None, testrun=None):
1455 namespace=None, request=None, testrun=None):
1456 self.url = self.url_template.format(url=url)
1456 self.url = self.url_template.format(url=url)
1457 self.api_key = api_key
1457 self.api_key = api_key
1458 self.add_server = add_server
1458 self.add_server = add_server
1459 self.add_timestamp = add_timestamp
1459 self.add_timestamp = add_timestamp
1460 self.namespace = namespace
1460 self.namespace = namespace
1461 self.request = request
1461 self.request = request
1462 self.server = socket.getfqdn(socket.gethostname())
1462 self.server = socket.getfqdn(socket.gethostname())
1463 self.tags_before = {}
1463 self.tags_before = {}
1464 self.tags_after = {}
1464 self.tags_after = {}
1465 self.stats = []
1465 self.stats = []
1466 self.testrun = testrun or {}
1466 self.testrun = testrun or {}
1467
1467
1468 def tag_before(self, tag, value):
1468 def tag_before(self, tag, value):
1469 self.tags_before[tag] = value
1469 self.tags_before[tag] = value
1470
1470
1471 def tag_after(self, tag, value):
1471 def tag_after(self, tag, value):
1472 self.tags_after[tag] = value
1472 self.tags_after[tag] = value
1473
1473
1474 def collect(self, data):
1474 def collect(self, data):
1475 if self.add_server:
1475 if self.add_server:
1476 data.setdefault('server', self.server)
1476 data.setdefault('server', self.server)
1477 if self.add_timestamp:
1477 if self.add_timestamp:
1478 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1478 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1479 if self.namespace:
1479 if self.namespace:
1480 data.setdefault('namespace', self.namespace)
1480 data.setdefault('namespace', self.namespace)
1481 if self.request:
1481 if self.request:
1482 data.setdefault('request', self.request)
1482 data.setdefault('request', self.request)
1483 self.stats.append(data)
1483 self.stats.append(data)
1484
1484
1485 def send_stats(self):
1485 def send_stats(self):
1486 tags = [
1486 tags = [
1487 ('testrun', self.request),
1487 ('testrun', self.request),
1488 ('testrun.start', self.testrun['start']),
1488 ('testrun.start', self.testrun['start']),
1489 ('testrun.timestamp', self.testrun['timestamp']),
1489 ('testrun.timestamp', self.testrun['timestamp']),
1490 ('test', self.namespace),
1490 ('test', self.namespace),
1491 ]
1491 ]
1492 for key, value in self.tags_before.items():
1492 for key, value in self.tags_before.items():
1493 tags.append((key + '.before', value))
1493 tags.append((key + '.before', value))
1494 try:
1494 try:
1495 delta = self.tags_after[key] - value
1495 delta = self.tags_after[key] - value
1496 tags.append((key + '.delta', delta))
1496 tags.append((key + '.delta', delta))
1497 except Exception:
1497 except Exception:
1498 pass
1498 pass
1499 for key, value in self.tags_after.items():
1499 for key, value in self.tags_after.items():
1500 tags.append((key + '.after', value))
1500 tags.append((key + '.after', value))
1501 self.collect({
1501 self.collect({
1502 'message': "Collected tags",
1502 'message': "Collected tags",
1503 'tags': tags,
1503 'tags': tags,
1504 })
1504 })
1505
1505
1506 response = requests.post(
1506 response = requests.post(
1507 self.url,
1507 self.url,
1508 headers={
1508 headers={
1509 'X-appenlight-api-key': self.api_key},
1509 'X-appenlight-api-key': self.api_key},
1510 json=self.stats,
1510 json=self.stats,
1511 )
1511 )
1512
1512
1513 if not response.status_code == 200:
1513 if not response.status_code == 200:
1514 pprint.pprint(self.stats)
1514 pprint.pprint(self.stats)
1515 print(response.headers)
1515 print(response.headers)
1516 print(response.text)
1516 print(response.text)
1517 raise Exception('Sending to appenlight failed')
1517 raise Exception('Sending to appenlight failed')
1518
1518
1519
1519
1520 @pytest.fixture
1520 @pytest.fixture
1521 def gist_util(request, db_connection):
1521 def gist_util(request, db_connection):
1522 """
1522 """
1523 Provides a wired instance of `GistUtility` with integrated cleanup.
1523 Provides a wired instance of `GistUtility` with integrated cleanup.
1524 """
1524 """
1525 utility = GistUtility()
1525 utility = GistUtility()
1526 request.addfinalizer(utility.cleanup)
1526 request.addfinalizer(utility.cleanup)
1527 return utility
1527 return utility
1528
1528
1529
1529
1530 class GistUtility(object):
1530 class GistUtility(object):
1531 def __init__(self):
1531 def __init__(self):
1532 self.fixture = Fixture()
1532 self.fixture = Fixture()
1533 self.gist_ids = []
1533 self.gist_ids = []
1534
1534
1535 def create_gist(self, **kwargs):
1535 def create_gist(self, **kwargs):
1536 gist = self.fixture.create_gist(**kwargs)
1536 gist = self.fixture.create_gist(**kwargs)
1537 self.gist_ids.append(gist.gist_id)
1537 self.gist_ids.append(gist.gist_id)
1538 return gist
1538 return gist
1539
1539
1540 def cleanup(self):
1540 def cleanup(self):
1541 for id_ in self.gist_ids:
1541 for id_ in self.gist_ids:
1542 self.fixture.destroy_gists(str(id_))
1542 self.fixture.destroy_gists(str(id_))
1543
1543
1544
1544
1545 @pytest.fixture
1545 @pytest.fixture
1546 def enabled_backends(request):
1546 def enabled_backends(request):
1547 backends = request.config.option.backends
1547 backends = request.config.option.backends
1548 return backends[:]
1548 return backends[:]
1549
1549
1550
1550
1551 @pytest.fixture
1551 @pytest.fixture
1552 def settings_util(request, db_connection):
1552 def settings_util(request, db_connection):
1553 """
1553 """
1554 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1554 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1555 """
1555 """
1556 utility = SettingsUtility()
1556 utility = SettingsUtility()
1557 request.addfinalizer(utility.cleanup)
1557 request.addfinalizer(utility.cleanup)
1558 return utility
1558 return utility
1559
1559
1560
1560
1561 class SettingsUtility(object):
1561 class SettingsUtility(object):
1562 def __init__(self):
1562 def __init__(self):
1563 self.rhodecode_ui_ids = []
1563 self.rhodecode_ui_ids = []
1564 self.rhodecode_setting_ids = []
1564 self.rhodecode_setting_ids = []
1565 self.repo_rhodecode_ui_ids = []
1565 self.repo_rhodecode_ui_ids = []
1566 self.repo_rhodecode_setting_ids = []
1566 self.repo_rhodecode_setting_ids = []
1567
1567
1568 def create_repo_rhodecode_ui(
1568 def create_repo_rhodecode_ui(
1569 self, repo, section, value, key=None, active=True, cleanup=True):
1569 self, repo, section, value, key=None, active=True, cleanup=True):
1570 key = key or hashlib.sha1(
1570 key = key or hashlib.sha1(
1571 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1571 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1572
1572
1573 setting = RepoRhodeCodeUi()
1573 setting = RepoRhodeCodeUi()
1574 setting.repository_id = repo.repo_id
1574 setting.repository_id = repo.repo_id
1575 setting.ui_section = section
1575 setting.ui_section = section
1576 setting.ui_value = value
1576 setting.ui_value = value
1577 setting.ui_key = key
1577 setting.ui_key = key
1578 setting.ui_active = active
1578 setting.ui_active = active
1579 Session().add(setting)
1579 Session().add(setting)
1580 Session().commit()
1580 Session().commit()
1581
1581
1582 if cleanup:
1582 if cleanup:
1583 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1583 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1584 return setting
1584 return setting
1585
1585
1586 def create_rhodecode_ui(
1586 def create_rhodecode_ui(
1587 self, section, value, key=None, active=True, cleanup=True):
1587 self, section, value, key=None, active=True, cleanup=True):
1588 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1588 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1589
1589
1590 setting = RhodeCodeUi()
1590 setting = RhodeCodeUi()
1591 setting.ui_section = section
1591 setting.ui_section = section
1592 setting.ui_value = value
1592 setting.ui_value = value
1593 setting.ui_key = key
1593 setting.ui_key = key
1594 setting.ui_active = active
1594 setting.ui_active = active
1595 Session().add(setting)
1595 Session().add(setting)
1596 Session().commit()
1596 Session().commit()
1597
1597
1598 if cleanup:
1598 if cleanup:
1599 self.rhodecode_ui_ids.append(setting.ui_id)
1599 self.rhodecode_ui_ids.append(setting.ui_id)
1600 return setting
1600 return setting
1601
1601
1602 def create_repo_rhodecode_setting(
1602 def create_repo_rhodecode_setting(
1603 self, repo, name, value, type_, cleanup=True):
1603 self, repo, name, value, type_, cleanup=True):
1604 setting = RepoRhodeCodeSetting(
1604 setting = RepoRhodeCodeSetting(
1605 repo.repo_id, key=name, val=value, type=type_)
1605 repo.repo_id, key=name, val=value, type=type_)
1606 Session().add(setting)
1606 Session().add(setting)
1607 Session().commit()
1607 Session().commit()
1608
1608
1609 if cleanup:
1609 if cleanup:
1610 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1610 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1611 return setting
1611 return setting
1612
1612
1613 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1613 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1614 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1614 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1615 Session().add(setting)
1615 Session().add(setting)
1616 Session().commit()
1616 Session().commit()
1617
1617
1618 if cleanup:
1618 if cleanup:
1619 self.rhodecode_setting_ids.append(setting.app_settings_id)
1619 self.rhodecode_setting_ids.append(setting.app_settings_id)
1620
1620
1621 return setting
1621 return setting
1622
1622
1623 def cleanup(self):
1623 def cleanup(self):
1624 for id_ in self.rhodecode_ui_ids:
1624 for id_ in self.rhodecode_ui_ids:
1625 setting = RhodeCodeUi.get(id_)
1625 setting = RhodeCodeUi.get(id_)
1626 Session().delete(setting)
1626 Session().delete(setting)
1627
1627
1628 for id_ in self.rhodecode_setting_ids:
1628 for id_ in self.rhodecode_setting_ids:
1629 setting = RhodeCodeSetting.get(id_)
1629 setting = RhodeCodeSetting.get(id_)
1630 Session().delete(setting)
1630 Session().delete(setting)
1631
1631
1632 for id_ in self.repo_rhodecode_ui_ids:
1632 for id_ in self.repo_rhodecode_ui_ids:
1633 setting = RepoRhodeCodeUi.get(id_)
1633 setting = RepoRhodeCodeUi.get(id_)
1634 Session().delete(setting)
1634 Session().delete(setting)
1635
1635
1636 for id_ in self.repo_rhodecode_setting_ids:
1636 for id_ in self.repo_rhodecode_setting_ids:
1637 setting = RepoRhodeCodeSetting.get(id_)
1637 setting = RepoRhodeCodeSetting.get(id_)
1638 Session().delete(setting)
1638 Session().delete(setting)
1639
1639
1640 Session().commit()
1640 Session().commit()
1641
1641
1642
1642
1643 @pytest.fixture
1643 @pytest.fixture
1644 def no_notifications(request):
1644 def no_notifications(request):
1645 notification_patcher = mock.patch(
1645 notification_patcher = mock.patch(
1646 'rhodecode.model.notification.NotificationModel.create')
1646 'rhodecode.model.notification.NotificationModel.create')
1647 notification_patcher.start()
1647 notification_patcher.start()
1648 request.addfinalizer(notification_patcher.stop)
1648 request.addfinalizer(notification_patcher.stop)
1649
1649
1650
1650
1651 @pytest.fixture(scope='session')
1651 @pytest.fixture(scope='session')
1652 def repeat(request):
1652 def repeat(request):
1653 """
1653 """
1654 The number of repetitions is based on this fixture.
1654 The number of repetitions is based on this fixture.
1655
1655
1656 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1656 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1657 tests are not too slow in our default test suite.
1657 tests are not too slow in our default test suite.
1658 """
1658 """
1659 return request.config.getoption('--repeat')
1659 return request.config.getoption('--repeat')
1660
1660
1661
1661
1662 @pytest.fixture
1662 @pytest.fixture
1663 def rhodecode_fixtures():
1663 def rhodecode_fixtures():
1664 return Fixture()
1664 return Fixture()
1665
1665
1666
1666
1667 @pytest.fixture
1667 @pytest.fixture
1668 def context_stub():
1668 def context_stub():
1669 """
1669 """
1670 Stub context object.
1670 Stub context object.
1671 """
1671 """
1672 context = pyramid.testing.DummyResource()
1672 context = pyramid.testing.DummyResource()
1673 return context
1673 return context
1674
1674
1675
1675
1676 @pytest.fixture
1676 @pytest.fixture
1677 def request_stub():
1677 def request_stub():
1678 """
1678 """
1679 Stub request object.
1679 Stub request object.
1680 """
1680 """
1681 from rhodecode.lib.base import bootstrap_request
1681 from rhodecode.lib.base import bootstrap_request
1682 request = bootstrap_request(scheme='https')
1682 request = bootstrap_request(scheme='https')
1683 return request
1683 return request
1684
1684
1685
1685
1686 @pytest.fixture
1686 @pytest.fixture
1687 def config_stub(request, request_stub):
1687 def config_stub(request, request_stub):
1688 """
1688 """
1689 Set up pyramid.testing and return the Configurator.
1689 Set up pyramid.testing and return the Configurator.
1690 """
1690 """
1691 from rhodecode.lib.base import bootstrap_config
1691 from rhodecode.lib.base import bootstrap_config
1692 config = bootstrap_config(request=request_stub)
1692 config = bootstrap_config(request=request_stub)
1693
1693
1694 @request.addfinalizer
1694 @request.addfinalizer
1695 def cleanup():
1695 def cleanup():
1696 pyramid.testing.tearDown()
1696 pyramid.testing.tearDown()
1697
1697
1698 return config
1698 return config
1699
1699
1700
1700
1701 @pytest.fixture
1701 @pytest.fixture
1702 def StubIntegrationType():
1702 def StubIntegrationType():
1703 class _StubIntegrationType(IntegrationTypeBase):
1703 class _StubIntegrationType(IntegrationTypeBase):
1704 """ Test integration type class """
1704 """ Test integration type class """
1705
1705
1706 key = 'test'
1706 key = 'test'
1707 display_name = 'Test integration type'
1707 display_name = 'Test integration type'
1708 description = 'A test integration type for testing'
1708 description = 'A test integration type for testing'
1709 icon = 'test_icon_html_image'
1709 icon = 'test_icon_html_image'
1710
1710
1711 def __init__(self, settings):
1711 def __init__(self, settings):
1712 super(_StubIntegrationType, self).__init__(settings)
1712 super(_StubIntegrationType, self).__init__(settings)
1713 self.sent_events = [] # for testing
1713 self.sent_events = [] # for testing
1714
1714
1715 def send_event(self, event):
1715 def send_event(self, event):
1716 self.sent_events.append(event)
1716 self.sent_events.append(event)
1717
1717
1718 def settings_schema(self):
1718 def settings_schema(self):
1719 class SettingsSchema(colander.Schema):
1719 class SettingsSchema(colander.Schema):
1720 test_string_field = colander.SchemaNode(
1720 test_string_field = colander.SchemaNode(
1721 colander.String(),
1721 colander.String(),
1722 missing=colander.required,
1722 missing=colander.required,
1723 title='test string field',
1723 title='test string field',
1724 )
1724 )
1725 test_int_field = colander.SchemaNode(
1725 test_int_field = colander.SchemaNode(
1726 colander.Int(),
1726 colander.Int(),
1727 title='some integer setting',
1727 title='some integer setting',
1728 )
1728 )
1729 return SettingsSchema()
1729 return SettingsSchema()
1730
1730
1731
1731
1732 integration_type_registry.register_integration_type(_StubIntegrationType)
1732 integration_type_registry.register_integration_type(_StubIntegrationType)
1733 return _StubIntegrationType
1733 return _StubIntegrationType
1734
1734
1735 @pytest.fixture
1735 @pytest.fixture
1736 def stub_integration_settings():
1736 def stub_integration_settings():
1737 return {
1737 return {
1738 'test_string_field': 'some data',
1738 'test_string_field': 'some data',
1739 'test_int_field': 100,
1739 'test_int_field': 100,
1740 }
1740 }
1741
1741
1742
1742
1743 @pytest.fixture
1743 @pytest.fixture
1744 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1744 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1745 stub_integration_settings):
1745 stub_integration_settings):
1746 integration = IntegrationModel().create(
1746 integration = IntegrationModel().create(
1747 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1747 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1748 name='test repo integration',
1748 name='test repo integration',
1749 repo=repo_stub, repo_group=None, child_repos_only=None)
1749 repo=repo_stub, repo_group=None, child_repos_only=None)
1750
1750
1751 @request.addfinalizer
1751 @request.addfinalizer
1752 def cleanup():
1752 def cleanup():
1753 IntegrationModel().delete(integration)
1753 IntegrationModel().delete(integration)
1754
1754
1755 return integration
1755 return integration
1756
1756
1757
1757
1758 @pytest.fixture
1758 @pytest.fixture
1759 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1759 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1760 stub_integration_settings):
1760 stub_integration_settings):
1761 integration = IntegrationModel().create(
1761 integration = IntegrationModel().create(
1762 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1762 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1763 name='test repogroup integration',
1763 name='test repogroup integration',
1764 repo=None, repo_group=test_repo_group, child_repos_only=True)
1764 repo=None, repo_group=test_repo_group, child_repos_only=True)
1765
1765
1766 @request.addfinalizer
1766 @request.addfinalizer
1767 def cleanup():
1767 def cleanup():
1768 IntegrationModel().delete(integration)
1768 IntegrationModel().delete(integration)
1769
1769
1770 return integration
1770 return integration
1771
1771
1772
1772
1773 @pytest.fixture
1773 @pytest.fixture
1774 def repogroup_recursive_integration_stub(request, test_repo_group,
1774 def repogroup_recursive_integration_stub(request, test_repo_group,
1775 StubIntegrationType, stub_integration_settings):
1775 StubIntegrationType, stub_integration_settings):
1776 integration = IntegrationModel().create(
1776 integration = IntegrationModel().create(
1777 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1777 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1778 name='test recursive repogroup integration',
1778 name='test recursive repogroup integration',
1779 repo=None, repo_group=test_repo_group, child_repos_only=False)
1779 repo=None, repo_group=test_repo_group, child_repos_only=False)
1780
1780
1781 @request.addfinalizer
1781 @request.addfinalizer
1782 def cleanup():
1782 def cleanup():
1783 IntegrationModel().delete(integration)
1783 IntegrationModel().delete(integration)
1784
1784
1785 return integration
1785 return integration
1786
1786
1787
1787
1788 @pytest.fixture
1788 @pytest.fixture
1789 def global_integration_stub(request, StubIntegrationType,
1789 def global_integration_stub(request, StubIntegrationType,
1790 stub_integration_settings):
1790 stub_integration_settings):
1791 integration = IntegrationModel().create(
1791 integration = IntegrationModel().create(
1792 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1792 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1793 name='test global integration',
1793 name='test global integration',
1794 repo=None, repo_group=None, child_repos_only=None)
1794 repo=None, repo_group=None, child_repos_only=None)
1795
1795
1796 @request.addfinalizer
1796 @request.addfinalizer
1797 def cleanup():
1797 def cleanup():
1798 IntegrationModel().delete(integration)
1798 IntegrationModel().delete(integration)
1799
1799
1800 return integration
1800 return integration
1801
1801
1802
1802
1803 @pytest.fixture
1803 @pytest.fixture
1804 def root_repos_integration_stub(request, StubIntegrationType,
1804 def root_repos_integration_stub(request, StubIntegrationType,
1805 stub_integration_settings):
1805 stub_integration_settings):
1806 integration = IntegrationModel().create(
1806 integration = IntegrationModel().create(
1807 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1807 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1808 name='test global integration',
1808 name='test global integration',
1809 repo=None, repo_group=None, child_repos_only=True)
1809 repo=None, repo_group=None, child_repos_only=True)
1810
1810
1811 @request.addfinalizer
1811 @request.addfinalizer
1812 def cleanup():
1812 def cleanup():
1813 IntegrationModel().delete(integration)
1813 IntegrationModel().delete(integration)
1814
1814
1815 return integration
1815 return integration
1816
1816
1817
1817
1818 @pytest.fixture
1818 @pytest.fixture
1819 def local_dt_to_utc():
1819 def local_dt_to_utc():
1820 def _factory(dt):
1820 def _factory(dt):
1821 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1821 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1822 dateutil.tz.tzutc()).replace(tzinfo=None)
1822 dateutil.tz.tzutc()).replace(tzinfo=None)
1823 return _factory
1823 return _factory
1824
1824
1825
1825
1826 @pytest.fixture
1826 @pytest.fixture
1827 def disable_anonymous_user(request, baseapp):
1827 def disable_anonymous_user(request, baseapp):
1828 set_anonymous_access(False)
1828 set_anonymous_access(False)
1829
1829
1830 @request.addfinalizer
1830 @request.addfinalizer
1831 def cleanup():
1831 def cleanup():
1832 set_anonymous_access(True)
1832 set_anonymous_access(True)
1833
1833
1834
1834
1835 @pytest.fixture(scope='module')
1835 @pytest.fixture(scope='module')
1836 def rc_fixture(request):
1836 def rc_fixture(request):
1837 return Fixture()
1837 return Fixture()
1838
1838
1839
1839
1840 @pytest.fixture
1840 @pytest.fixture
1841 def repo_groups(request):
1841 def repo_groups(request):
1842 fixture = Fixture()
1842 fixture = Fixture()
1843
1843
1844 session = Session()
1844 session = Session()
1845 zombie_group = fixture.create_repo_group('zombie')
1845 zombie_group = fixture.create_repo_group('zombie')
1846 parent_group = fixture.create_repo_group('parent')
1846 parent_group = fixture.create_repo_group('parent')
1847 child_group = fixture.create_repo_group('parent/child')
1847 child_group = fixture.create_repo_group('parent/child')
1848 groups_in_db = session.query(RepoGroup).all()
1848 groups_in_db = session.query(RepoGroup).all()
1849 assert len(groups_in_db) == 3
1849 assert len(groups_in_db) == 3
1850 assert child_group.group_parent_id == parent_group.group_id
1850 assert child_group.group_parent_id == parent_group.group_id
1851
1851
1852 @request.addfinalizer
1852 @request.addfinalizer
1853 def cleanup():
1853 def cleanup():
1854 fixture.destroy_repo_group(zombie_group)
1854 fixture.destroy_repo_group(zombie_group)
1855 fixture.destroy_repo_group(child_group)
1855 fixture.destroy_repo_group(child_group)
1856 fixture.destroy_repo_group(parent_group)
1856 fixture.destroy_repo_group(parent_group)
1857
1857
1858 return zombie_group, parent_group, child_group
1858 return zombie_group, parent_group, child_group
General Comments 0
You need to be logged in to leave comments. Login now