##// 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
@@ -42,6 +42,8 b''
42 42 "<%= dirs.js.src %>/bootstrap.js",
43 43 "<%= dirs.js.src %>/i18n_utils.js",
44 44 "<%= dirs.js.src %>/deform.js",
45 "<%= dirs.js.src %>/ejs.js",
46 "<%= dirs.js.src %>/ejs_templates/utils.js",
45 47 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
46 48 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
47 49 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 51 EXTENSIONS = {}
52 52
53 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 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
@@ -108,7 +108,7 b' class TestGetPullRequest(object):'
108 108 'reasons': reasons,
109 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 112 pull_request.reviewers_statuses()
113 113 ]
114 114 }
@@ -133,7 +133,7 b' class TestUpdatePullRequest(object):'
133 133 removed = [a.username]
134 134
135 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 138 id_, params = build_data(
139 139 self.apikey, 'update_pull_request',
@@ -53,7 +53,6 b' class AdminUserGroupsView(BaseAppView, D'
53 53 PermissionModel().set_global_permission_choices(
54 54 c, gettext_translator=self.request.translate)
55 55
56
57 56 return c
58 57
59 58 # permission check in data loading of
@@ -299,7 +299,7 b' class TestPullrequestsView(object):'
299 299 pull_request = pr_util.create_pull_request()
300 300 pull_request_id = pull_request.pull_request_id
301 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 303 pull_request.author)
304 304 author = pull_request.user_id
305 305 repo = pull_request.target_repo.repo_id
@@ -376,6 +376,8 b' class TestPullrequestsView(object):'
376 376 ('__start__', 'reasons:sequence'),
377 377 ('reason', 'Some reason'),
378 378 ('__end__', 'reasons:sequence'),
379 ('__start__', 'rules:sequence'),
380 ('__end__', 'rules:sequence'),
379 381 ('mandatory', 'False'),
380 382 ('__end__', 'reviewer:mapping'),
381 383 ('__end__', 'review_members:sequence'),
@@ -433,6 +435,8 b' class TestPullrequestsView(object):'
433 435 ('__start__', 'reasons:sequence'),
434 436 ('reason', 'Some reason'),
435 437 ('__end__', 'reasons:sequence'),
438 ('__start__', 'rules:sequence'),
439 ('__end__', 'rules:sequence'),
436 440 ('mandatory', 'False'),
437 441 ('__end__', 'reviewer:mapping'),
438 442 ('__end__', 'review_members:sequence'),
@@ -460,7 +464,7 b' class TestPullrequestsView(object):'
460 464
461 465 # Change reviewers and check that a notification was made
462 466 PullRequestModel().update_reviewers(
463 pull_request.pull_request_id, [(1, [], False)],
467 pull_request.pull_request_id, [(1, [], False, [])],
464 468 pull_request.author)
465 469 assert len(notifications.all()) == 2
466 470
@@ -497,6 +501,8 b' class TestPullrequestsView(object):'
497 501 ('__start__', 'reasons:sequence'),
498 502 ('reason', 'Some reason'),
499 503 ('__end__', 'reasons:sequence'),
504 ('__start__', 'rules:sequence'),
505 ('__end__', 'rules:sequence'),
500 506 ('mandatory', 'False'),
501 507 ('__end__', 'reviewer:mapping'),
502 508 ('__end__', 'review_members:sequence'),
@@ -22,7 +22,7 b' from rhodecode.lib import helpers as h'
22 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 27 Returns json struct of a reviewer for frontend
28 28
@@ -34,10 +34,13 b' def reviewer_as_json(user, reasons=None,'
34 34 return {
35 35 'user_id': user.user_id,
36 36 'reasons': reasons or [],
37 'rules': rules or [],
37 38 'mandatory': mandatory,
39 'user_group': user_group,
38 40 'username': user.username,
39 41 'first_name': user.first_name,
40 42 'last_name': user.last_name,
43 'user_link': h.link_to_user(user),
41 44 'gravatar_link': h.gravatar_url(user.email, 14),
42 45 }
43 46
@@ -68,7 +71,7 b' def validate_default_reviewers(review_me'
68 71 reviewer_by_id = {}
69 72 for r in review_members:
70 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 76 reviewer_by_id[reviewer_user_id] = entry
74 77 reviewers.append(entry)
@@ -2070,3 +2070,8 b' def go_import_header(request, db_repo=No'
2070 2070 # we have a repo and go-get flag,
2071 2071 return literal('<meta name="go-import" content="{} {} {}">'.format(
2072 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)
@@ -21,7 +21,7 b''
21 21
22 22 import itertools
23 23 import logging
24 from collections import defaultdict
24 import collections
25 25
26 26 from rhodecode.model import BaseModel
27 27 from rhodecode.model.db import (
@@ -68,6 +68,107 b' class ChangesetStatusModel(BaseModel):'
68 68 q = q.order_by(ChangesetStatus.version.asc())
69 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 172 def calculate_status(self, statuses_by_reviewers):
72 173 """
73 174 Given the approval statuses from reviewers, calculates final approval
@@ -76,21 +177,45 b' class ChangesetStatusModel(BaseModel):'
76 177
77 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 192 reviewers_number = len(statuses_by_reviewers)
81 for user, reasons, mandatory, statuses in statuses_by_reviewers:
82 if statuses:
83 ver, latest = statuses[0]
84 votes[latest.status] += 1
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
85 200 else:
86 votes[ChangesetStatus.DEFAULT] += 1
201
202 for review_obj, user, reasons, mandatory, statuses \
203 in group_statuses_by_reviewers:
204 # individual vote
205 if statuses:
206 ver, latest = statuses[0]
207 votes[latest.status] += 1
87 208
88 # all approved
89 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211
212 # TODO(marcink): with group voting, how does rejected work,
213 # do we ever get rejected state ?
214
215 if approved_votes_count == reviewers_number:
90 216 return ChangesetStatus.STATUS_APPROVED
91 217
92 # all rejected
93 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
218 if rejected_votes_count == reviewers_number:
94 219 return ChangesetStatus.STATUS_REJECTED
95 220
96 221 return ChangesetStatus.STATUS_UNDER_REVIEW
@@ -234,7 +359,7 b' class ChangesetStatusModel(BaseModel):'
234 359 pull_request=pull_request,
235 360 with_revisions=True)
236 361
237 commit_statuses = defaultdict(list)
362 commit_statuses = collections.defaultdict(list)
238 363 for st in _commit_statuses:
239 364 commit_statuses[st.author.username] += [st]
240 365
@@ -243,17 +368,18 b' class ChangesetStatusModel(BaseModel):'
243 368 def version(commit_status):
244 369 return commit_status.version
245 370
246 for o in pull_request.reviewers:
247 if not o.user:
371 for obj in pull_request.reviewers:
372 if not obj.user:
248 373 continue
249 statuses = commit_statuses.get(o.user.username, None)
374 statuses = commit_statuses.get(obj.user.username, None)
250 375 if statuses:
251 statuses = [(x, list(y)[0])
252 for x, y in (itertools.groupby(
253 sorted(statuses, key=version),version))]
376 status_groups = itertools.groupby(
377 sorted(statuses, key=version), version)
378 statuses = [(x, list(y)[0]) for x, y in status_groups]
254 379
255 380 pull_request_reviewers.append(
256 (o.user, o.reasons, o.mandatory, statuses))
381 (obj, obj.user, obj.reasons, obj.mandatory, statuses))
382
257 383 return pull_request_reviewers
258 384
259 385 def calculated_review_status(self, pull_request, reviewers_statuses=None):
@@ -59,8 +59,7 b' from rhodecode.lib.utils2 import ('
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re, StrictAttributeDict, cleaned_uri)
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
63 JsonRaw
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
64 63 from rhodecode.lib.ext_json import json
65 64 from rhodecode.lib.caching_query import FromCache
66 65 from rhodecode.lib.encrypt import AESCipher
@@ -1327,7 +1326,7 b' class UserGroup(Base, BaseModel):'
1327 1326 @hybrid_property
1328 1327 def description_safe(self):
1329 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 1331 @hybrid_property
1333 1332 def group_data(self):
@@ -3594,7 +3593,7 b' class _PullRequestBase(BaseModel):'
3594 3593 'reasons': reasons,
3595 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 3597 pull_request.reviewers_statuses()
3599 3598 ]
3600 3599 }
@@ -3790,10 +3789,34 b' class PullRequestReviewers(Base, BaseMod'
3790 3789 _reasons = Column(
3791 3790 'reason', MutationList.as_mutable(
3792 3791 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3792
3793 3793 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3794 3794 user = relationship('User')
3795 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 3821 class Notification(Base, BaseModel):
3799 3822 __tablename__ = 'notifications'
@@ -4086,6 +4109,7 b' class RepoReviewRuleUser(Base, BaseModel'
4086 4109 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4087 4110 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4088 4111 )
4112
4089 4113 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4090 4114 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4091 4115 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
@@ -4104,17 +4128,28 b' class RepoReviewRuleUserGroup(Base, Base'
4104 4128 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4105 4129 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4106 4130 )
4131 VOTE_RULE_ALL = -1
4132
4107 4133 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4108 4134 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4109 4135 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4110 4136 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4137 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4111 4138 users_group = relationship('UserGroup')
4112 4139
4113 4140 def rule_data(self):
4114 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 4154 class RepoReviewRule(Base, BaseModel):
4120 4155 __tablename__ = 'repo_review_rules'
@@ -4225,12 +4260,20 b' class RepoReviewRule(Base, BaseModel):'
4225 4260
4226 4261 for rule_user_group in self.rule_user_groups:
4227 4262 source_data = {
4263 'user_group_id': rule_user_group.users_group.users_group_id,
4228 4264 'name': rule_user_group.users_group.users_group_name,
4229 4265 'members': len(rule_user_group.users_group.members)
4230 4266 }
4231 4267 for member in rule_user_group.users_group.members:
4232 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 4277 'user': member.user,
4235 4278 'source': 'user_group',
4236 4279 'source_data': source_data,
@@ -4239,6 +4282,13 b' class RepoReviewRule(Base, BaseModel):'
4239 4282
4240 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 4292 def __repr__(self):
4243 4293 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4244 4294 self.repo_review_rule_id, self.repo)
@@ -584,6 +584,7 b' def PullRequestForm(localizer, repo_id):'
584 584 class ReviewerForm(formencode.Schema):
585 585 user_id = v.Int(not_empty=True)
586 586 reasons = All()
587 rules = All(v.UniqueList(localizer, convert=int)())
587 588 mandatory = v.StringBoolean()
588 589
589 590 class _PullRequestForm(formencode.Schema):
@@ -51,7 +51,7 b' from rhodecode.model.changeset_status im'
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.db import (
53 53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.notification import NotificationModel, \
57 57 EmailNotificationModel
@@ -468,7 +468,7 b' class PullRequestModel(BaseModel):'
468 468 reviewer_ids = set()
469 469 # members / reviewers
470 470 for reviewer_object in reviewers:
471 user_id, reasons, mandatory = reviewer_object
471 user_id, reasons, mandatory, rules = reviewer_object
472 472 user = self._get_user(user_id)
473 473
474 474 # skip duplicates
@@ -482,6 +482,33 b' class PullRequestModel(BaseModel):'
482 482 reviewer.pull_request = pull_request
483 483 reviewer.reasons = reasons
484 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 512 Session().add(reviewer)
486 513
487 514 # Set approval status to "Under Review" for all commits which are
@@ -962,14 +989,14 b' class PullRequestModel(BaseModel):'
962 989
963 990 :param pull_request: the pr to update
964 991 :param reviewer_data: list of tuples
965 [(user, ['reason1', 'reason2'], mandatory_flag)]
992 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
966 993 """
967 994 pull_request = self.__get_pull_request(pull_request)
968 995 if pull_request.is_closed():
969 996 raise ValueError('This pull request is closed')
970 997
971 998 reviewers = {}
972 for user_id, reasons, mandatory in reviewer_data:
999 for user_id, reasons, mandatory, rules in reviewer_data:
973 1000 if isinstance(user_id, (int, basestring)):
974 1001 user_id = self._get_user(user_id).user_id
975 1002 reviewers[user_id] = {
@@ -74,6 +74,7 b' class UserModel(BaseModel):'
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 'profile_link': h.link_to_user(user),
77 78 'value_display': h.escape(h.person(user)),
78 79 'value': user.username,
79 80 'value_type': 'user',
@@ -26,6 +26,7 b' class ReviewerSchema(colander.MappingSch'
26 26 username = colander.SchemaNode(types.StrOrIntType())
27 27 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
28 28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29 rules = colander.SchemaNode(colander.List(), missing=[])
29 30
30 31
31 32 class ReviewerListSchema(colander.SequenceSchema):
@@ -1324,7 +1324,7 b' table.integrations {'
1324 1324 .reviewers ul li {
1325 1325 position: relative;
1326 1326 width: 100%;
1327 margin-bottom: 8px;
1327 padding-bottom: 8px;
1328 1328 }
1329 1329
1330 1330 .reviewer_entry {
@@ -1335,19 +1335,15 b' table.integrations {'
1335 1335 width: 100%;
1336 1336 overflow: auto;
1337 1337 }
1338
1339 .reviewer_reason_container {
1340 padding-left: 20px;
1338 .reviewer_reason {
1339 padding-left: 20px;
1340 line-height: 1.5em;
1341 1341 }
1342
1343 .reviewer_reason {
1344 }
1345
1346 1342 .reviewer_status {
1347 1343 display: inline-block;
1348 1344 vertical-align: top;
1349 width: 7%;
1350 min-width: 20px;
1345 width: 25px;
1346 min-width: 25px;
1351 1347 height: 1.2em;
1352 1348 margin-top: 3px;
1353 1349 line-height: 1em;
@@ -1370,7 +1366,17 b' table.integrations {'
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 1380 .reviewer_member_mandatory_remove,
1375 1381 .reviewer_member_remove {
1376 1382 position: absolute;
@@ -1386,10 +1392,6 b' table.integrations {'
1386 1392 color: @grey4;
1387 1393 }
1388 1394
1389 .reviewer_member_mandatory {
1390 padding-top:20px;
1391 }
1392
1393 1395 .reviewer_member_status {
1394 1396 margin-top: 5px;
1395 1397 }
@@ -1849,13 +1851,6 b' BIN_FILENODE = 7'
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 1855 .creation_in_progress {
1861 1856 color: @grey4
@@ -137,10 +137,10 b' ReviewersController = function () {'
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0) {
141 141 self.$rulesList.append(
142 142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All individual reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
@@ -155,6 +155,15 b' ReviewersController = function () {'
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 167 if (data.rules.use_code_authors_for_review) {
159 168 self.$rulesList.append(
160 169 self.addRule(
@@ -227,10 +236,7 b' ReviewersController = function () {'
227 236 for (var i = 0; i < data.reviewers.length; i++) {
228 237 var reviewer = data.reviewers[i];
229 238 self.addReviewMember(
230 reviewer.user_id, reviewer.first_name,
231 reviewer.last_name, reviewer.username,
232 reviewer.gravatar_link, reviewer.reasons,
233 reviewer.mandatory);
239 reviewer, reviewer.reasons, reviewer.mandatory);
234 240 }
235 241 $('.calculate-reviewers').hide();
236 242 prButtonLock(false, null, 'reviewers');
@@ -260,64 +266,22 b' ReviewersController = function () {'
260 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 273 var members = self.$reviewMembers.get(0);
266 var reasons_html = '';
267 var reasons_inputs = '';
274 var id = reviewer_obj.user_id;
275 var username = reviewer_obj.username;
276
268 277 var reasons = reasons || [];
269 278 var mandatory = mandatory || false;
270 279
271 if (reasons) {
272 for (var i = 0; i < reasons.length; i++) {
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 = [];
280 // register IDS to check if we don't have this ID already in
281 var currentIds = [];
318 282 var _els = self.$reviewMembers.find('li').toArray();
319 283 for (el in _els){
320 ids.push(_els[el].id)
284 currentIds.push(_els[el].id)
321 285 }
322 286
323 287 var userAllowedReview = function(userId) {
@@ -333,19 +297,29 b' ReviewersController = function () {'
333 297
334 298 var userAllowed = userAllowedReview(id);
335 299 if (!userAllowed){
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
337 }
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
300 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
301 } else {
302 // only add if it's not there
303 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
339 304
340 if(shouldAdd) {
341 // only add if it's not there
342 members.innerHTML += element;
305 if (alreadyReviewer) {
306 alert(_gettext('User `{0}` already in reviewers').format(username));
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 321 this.updateReviewers = function(repo_name, pull_request_id){
348 var postData = '_method=put&' + $('#reviewers input').serialize();
322 var postData = $('#reviewers input').serialize();
349 323 _updatePullRequest(repo_name, pull_request_id, postData);
350 324 };
351 325
@@ -457,21 +431,30 b' var ReviewerAutoComplete = function(inpu'
457 431 formatResult: autocompleteFormatResult,
458 432 lookupFilter: autocompleteFilterResult,
459 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 438 if (data.value_type == 'user_group') {
463 439 reasons.push(_gettext('member of "{0}"').format(data.value_display));
464 440
465 441 $.each(data.members, function(index, member_data) {
466 reviewersController.addReviewMember(
467 member_data.id, member_data.first_name, member_data.last_name,
468 member_data.username, member_data.icon_link, reasons);
442 var reviewer = member_data;
443 reviewer['user_id'] = member_data['id'];
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
471 } else {
472 reviewersController.addReviewMember(
473 data.id, data.first_name, data.last_name,
474 data.username, data.icon_link, reasons);
449 }
450 // add single user
451 else {
452 var reviewer = data;
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 460 $(inputId).val('');
@@ -1,6 +1,8 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 <%include file="/ejs_templates/templates.html"/>
5
4 6 <div class="outerwrapper">
5 7 <!-- HEADER -->
6 8 <div class="header">
@@ -432,7 +432,7 b''
432 432 // generate new DESC of target repo displayed next to select
433 433 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
434 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 438 // generate dynamic select2 for refs.
@@ -340,58 +340,42 b''
340 340 ## REVIEWERS
341 341 <div class="reviewers-title block-right">
342 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 344 %if c.allowed_to_update:
345 345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
346 346 %endif
347 347 </div>
348 348 </div>
349 349 <div id="reviewers" class="block-right pr-details-content reviewers">
350 ## members goes here !
350
351 ## members redering block
351 352 <input type="hidden" name="__start__" value="review_members:sequence">
352 353 <ul id="review_members" class="group_members">
353 %for member,reasons,mandatory,status in c.pull_request_reviewers:
354 <li id="reviewer_${member.user_id}">
355 <div class="reviewers_member">
356 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
357 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
358 </div>
359 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
360 ${self.gravatar_with_user(member.email, 16)}
361 </div>
362 <input type="hidden" name="__start__" value="reviewer:mapping">
363 <input type="hidden" name="__start__" value="reasons:sequence">
364 % if reasons:
365 <div class="reviewer_reason_container">
366 %for reason in reasons:
367 <div class="reviewer_reason" style="display: none">- ${reason}</div>
368 <input type="hidden" name="reason" value="${reason}">
369 %endfor
370 </div>
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
354
355 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
356 <script>
357 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
358 var status = "${(status[0][1].status if status else 'not_reviewed')}";
359 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
360 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
361
362 var entry = renderTemplate('reviewMemberEntry', {
363 'member': member,
364 'mandatory': member.mandatory,
365 'reasons': member.reasons,
366 'allowed_to_update': allowed_to_update,
367 'review_status': status,
368 'review_status_label': status_lbl,
369 'user_group': member.user_group
370 });
371 $('#review_members').append(entry)
372 </script>
373
374 % endfor
375
393 376 </ul>
394 377 <input type="hidden" name="__end__" value="review_members:sequence">
378 ## end members redering block
395 379
396 380 %if not c.pull_request.is_closed():
397 381 <div id="add_reviewer" class="ac" style="display: none;">
@@ -693,7 +677,7 b''
693 677 editButton: $('#open_edit_reviewers'),
694 678 closeButton: $('#close_edit_reviewers'),
695 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 682 init: function() {
699 683 var self = this;
@@ -119,7 +119,7 b' class TestPullRequestModel(object):'
119 119
120 120 def test_get_awaiting_my_review(self, pull_request):
121 121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False, [])],
123 123 pull_request.author)
124 124 prs = PullRequestModel().get_awaiting_my_review(
125 125 pull_request.target_repo, user_id=pull_request.author.user_id)
@@ -128,7 +128,7 b' class TestPullRequestModel(object):'
128 128
129 129 def test_count_awaiting_my_review(self, pull_request):
130 130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False, [])],
132 132 pull_request.author)
133 133 pr_count = PullRequestModel().count_awaiting_my_review(
134 134 pull_request.target_repo, user_id=pull_request.author.user_id)
@@ -41,7 +41,8 b' class TestGetUsers(object):'
41 41 user_util.create_user(active=is_active, lastname='Fake user')
42 42
43 43 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
44 users = UserModel().get_users()
44 with mock.patch('rhodecode.lib.helpers.link_to_user'):
45 users = UserModel().get_users()
45 46 fake_users = [u for u in users if u['last_name'] == 'Fake user']
46 47 assert len(fake_users) == 2
47 48
@@ -61,8 +62,9 b' class TestGetUsers(object):'
61 62 active=True, lastname=u'Fake {} user'.format(keyword))
62 63
63 64 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
64 keyword = keywords[1].lower()
65 users = UserModel().get_users(name_contains=keyword)
65 with mock.patch('rhodecode.lib.helpers.link_to_user'):
66 keyword = keywords[1].lower()
67 users = UserModel().get_users(name_contains=keyword)
66 68
67 69 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
68 70 assert len(fake_users) == 2
@@ -80,7 +82,8 b' class TestGetUsers(object):'
80 82
81 83 keyword = keywords[1].lower()
82 84 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
83 users = UserModel().get_users(name_contains=keyword)
85 with mock.patch('rhodecode.lib.helpers.link_to_user'):
86 users = UserModel().get_users(name_contains=keyword)
84 87
85 88 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
86 89 assert len(fake_users) == 2
@@ -95,7 +98,8 b' class TestGetUsers(object):'
95 98
96 99 user_filter = created_users[-1].username[-2:]
97 100 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
98 users = UserModel().get_users(name_contains=user_filter)
101 with mock.patch('rhodecode.lib.helpers.link_to_user'):
102 users = UserModel().get_users(name_contains=user_filter)
99 103
100 104 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
101 105 assert len(fake_users) == 1
@@ -108,7 +112,8 b' class TestGetUsers(object):'
108 112 active=True, lastname='Fake user'))
109 113
110 114 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
111 users = UserModel().get_users(name_contains='Fake', limit=3)
115 with mock.patch('rhodecode.lib.helpers.link_to_user'):
116 users = UserModel().get_users(name_contains='Fake', limit=3)
112 117
113 118 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
114 119 assert len(fake_users) == 3
@@ -995,8 +995,8 b' class PRTestUtility(object):'
995 995
996 996 def _get_reviewers(self):
997 997 return [
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1000 1000 ]
1001 1001
1002 1002 def update_source_repository(self, head=None):
General Comments 0
You need to be logged in to leave comments. Login now