##// 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 "<%= dirs.js.src %>/bootstrap.js",
42 "<%= dirs.js.src %>/bootstrap.js",
43 "<%= dirs.js.src %>/i18n_utils.js",
43 "<%= dirs.js.src %>/i18n_utils.js",
44 "<%= dirs.js.src %>/deform.js",
44 "<%= dirs.js.src %>/deform.js",
45 "<%= dirs.js.src %>/ejs.js",
46 "<%= dirs.js.src %>/ejs_templates/utils.js",
45 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
47 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
46 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
48 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
47 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
49 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 83 # defines current db version for migrations
54 __dbversion__ = 85 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
@@ -108,7 +108,7 b' class TestGetPullRequest(object):'
108 'reasons': reasons,
108 'reasons': reasons,
109 'review_status': st[0][1].status if st else 'not_reviewed',
109 'review_status': st[0][1].status if st else 'not_reviewed',
110 }
110 }
111 for reviewer, reasons, mandatory, st in
111 for obj, reviewer, reasons, mandatory, st in
112 pull_request.reviewers_statuses()
112 pull_request.reviewers_statuses()
113 ]
113 ]
114 }
114 }
@@ -133,7 +133,7 b' class TestUpdatePullRequest(object):'
133 removed = [a.username]
133 removed = [a.username]
134
134
135 pull_request = pr_util.create_pull_request(
135 pull_request = pr_util.create_pull_request(
136 reviewers=[(a.username, ['added via API'], False)])
136 reviewers=[(a.username, ['added via API'], False, [])])
137
137
138 id_, params = build_data(
138 id_, params = build_data(
139 self.apikey, 'update_pull_request',
139 self.apikey, 'update_pull_request',
@@ -53,7 +53,6 b' class AdminUserGroupsView(BaseAppView, D'
53 PermissionModel().set_global_permission_choices(
53 PermissionModel().set_global_permission_choices(
54 c, gettext_translator=self.request.translate)
54 c, gettext_translator=self.request.translate)
55
55
56
57 return c
56 return c
58
57
59 # permission check in data loading of
58 # permission check in data loading of
@@ -299,7 +299,7 b' class TestPullrequestsView(object):'
299 pull_request = pr_util.create_pull_request()
299 pull_request = pr_util.create_pull_request()
300 pull_request_id = pull_request.pull_request_id
300 pull_request_id = pull_request.pull_request_id
301 PullRequestModel().update_reviewers(
301 PullRequestModel().update_reviewers(
302 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
302 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
303 pull_request.author)
303 pull_request.author)
304 author = pull_request.user_id
304 author = pull_request.user_id
305 repo = pull_request.target_repo.repo_id
305 repo = pull_request.target_repo.repo_id
@@ -376,6 +376,8 b' class TestPullrequestsView(object):'
376 ('__start__', 'reasons:sequence'),
376 ('__start__', 'reasons:sequence'),
377 ('reason', 'Some reason'),
377 ('reason', 'Some reason'),
378 ('__end__', 'reasons:sequence'),
378 ('__end__', 'reasons:sequence'),
379 ('__start__', 'rules:sequence'),
380 ('__end__', 'rules:sequence'),
379 ('mandatory', 'False'),
381 ('mandatory', 'False'),
380 ('__end__', 'reviewer:mapping'),
382 ('__end__', 'reviewer:mapping'),
381 ('__end__', 'review_members:sequence'),
383 ('__end__', 'review_members:sequence'),
@@ -433,6 +435,8 b' class TestPullrequestsView(object):'
433 ('__start__', 'reasons:sequence'),
435 ('__start__', 'reasons:sequence'),
434 ('reason', 'Some reason'),
436 ('reason', 'Some reason'),
435 ('__end__', 'reasons:sequence'),
437 ('__end__', 'reasons:sequence'),
438 ('__start__', 'rules:sequence'),
439 ('__end__', 'rules:sequence'),
436 ('mandatory', 'False'),
440 ('mandatory', 'False'),
437 ('__end__', 'reviewer:mapping'),
441 ('__end__', 'reviewer:mapping'),
438 ('__end__', 'review_members:sequence'),
442 ('__end__', 'review_members:sequence'),
@@ -460,7 +464,7 b' class TestPullrequestsView(object):'
460
464
461 # Change reviewers and check that a notification was made
465 # Change reviewers and check that a notification was made
462 PullRequestModel().update_reviewers(
466 PullRequestModel().update_reviewers(
463 pull_request.pull_request_id, [(1, [], False)],
467 pull_request.pull_request_id, [(1, [], False, [])],
464 pull_request.author)
468 pull_request.author)
465 assert len(notifications.all()) == 2
469 assert len(notifications.all()) == 2
466
470
@@ -497,6 +501,8 b' class TestPullrequestsView(object):'
497 ('__start__', 'reasons:sequence'),
501 ('__start__', 'reasons:sequence'),
498 ('reason', 'Some reason'),
502 ('reason', 'Some reason'),
499 ('__end__', 'reasons:sequence'),
503 ('__end__', 'reasons:sequence'),
504 ('__start__', 'rules:sequence'),
505 ('__end__', 'rules:sequence'),
500 ('mandatory', 'False'),
506 ('mandatory', 'False'),
501 ('__end__', 'reviewer:mapping'),
507 ('__end__', 'reviewer:mapping'),
502 ('__end__', 'review_members:sequence'),
508 ('__end__', 'review_members:sequence'),
@@ -22,7 +22,7 b' from rhodecode.lib import helpers as h'
22 from rhodecode.lib.utils2 import safe_int
22 from rhodecode.lib.utils2 import safe_int
23
23
24
24
25 def reviewer_as_json(user, reasons=None, mandatory=False):
25 def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None):
26 """
26 """
27 Returns json struct of a reviewer for frontend
27 Returns json struct of a reviewer for frontend
28
28
@@ -34,10 +34,13 b' def reviewer_as_json(user, reasons=None,'
34 return {
34 return {
35 'user_id': user.user_id,
35 'user_id': user.user_id,
36 'reasons': reasons or [],
36 'reasons': reasons or [],
37 'rules': rules or [],
37 'mandatory': mandatory,
38 'mandatory': mandatory,
39 'user_group': user_group,
38 'username': user.username,
40 'username': user.username,
39 'first_name': user.first_name,
41 'first_name': user.first_name,
40 'last_name': user.last_name,
42 'last_name': user.last_name,
43 'user_link': h.link_to_user(user),
41 'gravatar_link': h.gravatar_url(user.email, 14),
44 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
45 }
43
46
@@ -68,7 +71,7 b' def validate_default_reviewers(review_me'
68 reviewer_by_id = {}
71 reviewer_by_id = {}
69 for r in review_members:
72 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
73 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
74 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['rules'])
72
75
73 reviewer_by_id[reviewer_user_id] = entry
76 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
77 reviewers.append(entry)
@@ -2070,3 +2070,8 b' def go_import_header(request, db_repo=No'
2070 # we have a repo and go-get flag,
2070 # we have a repo and go-get flag,
2071 return literal('<meta name="go-import" content="{} {} {}">'.format(
2071 return literal('<meta name="go-import" content="{} {} {}">'.format(
2072 prefix, db_repo.repo_type, clone_url))
2072 prefix, db_repo.repo_type, clone_url))
2073
2074
2075 def reviewer_as_json(*args, **kwargs):
2076 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2077 return _reviewer_as_json(*args, **kwargs)
@@ -21,7 +21,7 b''
21
21
22 import itertools
22 import itertools
23 import logging
23 import logging
24 from collections import defaultdict
24 import collections
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
@@ -68,6 +68,107 b' class ChangesetStatusModel(BaseModel):'
68 q = q.order_by(ChangesetStatus.version.asc())
68 q = q.order_by(ChangesetStatus.version.asc())
69 return q
69 return q
70
70
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 trim_votes=True):
73 """
74 Calculate status based on given group members, and voting rule
75
76
77 group1 - 4 members, 3 required for approval
78 user1 - approved
79 user2 - reject
80 user3 - approved
81 user4 - rejected
82
83 final_state: rejected, reasons not at least 3 votes
84
85
86 group1 - 4 members, 2 required for approval
87 user1 - approved
88 user2 - reject
89 user3 - approved
90 user4 - rejected
91
92 final_state: approved, reasons got at least 2 approvals
93
94 group1 - 4 members, ALL required for approval
95 user1 - approved
96 user2 - reject
97 user3 - approved
98 user4 - rejected
99
100 final_state: rejected, reasons not all approvals
101
102
103 group1 - 4 members, ALL required for approval
104 user1 - approved
105 user2 - approved
106 user3 - approved
107 user4 - approved
108
109 final_state: approved, reason all approvals received
110
111 group1 - 4 members, 5 required for approval
112 (approval should be shorted to number of actual members)
113
114 user1 - approved
115 user2 - approved
116 user3 - approved
117 user4 - approved
118
119 final_state: approved, reason all approvals received
120
121 """
122 group_vote_data = {}
123 got_rule = False
124 members = collections.OrderedDict()
125 for review_obj, user, reasons, mandatory, statuses \
126 in group_statuses_by_reviewers:
127
128 if not got_rule:
129 group_vote_data = review_obj.rule_user_group_data()
130 got_rule = bool(group_vote_data)
131
132 members[user.user_id] = statuses
133
134 if not group_vote_data:
135 return []
136
137 required_votes = group_vote_data['vote_rule']
138 if required_votes == -1:
139 # -1 means all required, so we replace it with how many people
140 # are in the members
141 required_votes = len(members)
142
143 if trim_votes and required_votes > len(members):
144 # we require more votes than we have members in the group
145 # in this case we trim the required votes to the number of members
146 required_votes = len(members)
147
148 approvals = sum([
149 1 for statuses in members.values()
150 if statuses and
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152
153 calculated_votes = []
154 # we have all votes from users, now check if we have enough votes
155 # to fill other
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 if approvals >= required_votes:
158 fill_in = ChangesetStatus.STATUS_APPROVED
159
160 for member, statuses in members.items():
161 if statuses:
162 ver, latest = statuses[0]
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 calculated_votes.append(fill_in)
165 else:
166 calculated_votes.append(latest.status)
167 else:
168 calculated_votes.append(fill_in)
169
170 return calculated_votes
171
71 def calculate_status(self, statuses_by_reviewers):
172 def calculate_status(self, statuses_by_reviewers):
72 """
173 """
73 Given the approval statuses from reviewers, calculates final approval
174 Given the approval statuses from reviewers, calculates final approval
@@ -76,21 +177,45 b' class ChangesetStatusModel(BaseModel):'
76
177
77 :param statuses_by_reviewers:
178 :param statuses_by_reviewers:
78 """
179 """
79 votes = defaultdict(int)
180
181 def group_rule(element):
182 review_obj = element[0]
183 rule_data = review_obj.rule_user_group_data()
184 if rule_data and rule_data['id']:
185 return rule_data['id']
186
187 voting_groups = itertools.groupby(
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191
80 reviewers_number = len(statuses_by_reviewers)
192 reviewers_number = len(statuses_by_reviewers)
81 for user, reasons, mandatory, statuses in statuses_by_reviewers:
193 votes = collections.defaultdict(int)
82 if statuses:
194 for group, group_statuses_by_reviewers in voting_by_groups:
83 ver, latest = statuses[0]
195 if group:
84 votes[latest.status] += 1
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 else:
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
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
89 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
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 return ChangesetStatus.STATUS_APPROVED
216 return ChangesetStatus.STATUS_APPROVED
91
217
92 # all rejected
218 if rejected_votes_count == reviewers_number:
93 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
94 return ChangesetStatus.STATUS_REJECTED
219 return ChangesetStatus.STATUS_REJECTED
95
220
96 return ChangesetStatus.STATUS_UNDER_REVIEW
221 return ChangesetStatus.STATUS_UNDER_REVIEW
@@ -234,7 +359,7 b' class ChangesetStatusModel(BaseModel):'
234 pull_request=pull_request,
359 pull_request=pull_request,
235 with_revisions=True)
360 with_revisions=True)
236
361
237 commit_statuses = defaultdict(list)
362 commit_statuses = collections.defaultdict(list)
238 for st in _commit_statuses:
363 for st in _commit_statuses:
239 commit_statuses[st.author.username] += [st]
364 commit_statuses[st.author.username] += [st]
240
365
@@ -243,17 +368,18 b' class ChangesetStatusModel(BaseModel):'
243 def version(commit_status):
368 def version(commit_status):
244 return commit_status.version
369 return commit_status.version
245
370
246 for o in pull_request.reviewers:
371 for obj in pull_request.reviewers:
247 if not o.user:
372 if not obj.user:
248 continue
373 continue
249 statuses = commit_statuses.get(o.user.username, None)
374 statuses = commit_statuses.get(obj.user.username, None)
250 if statuses:
375 if statuses:
251 statuses = [(x, list(y)[0])
376 status_groups = itertools.groupby(
252 for x, y in (itertools.groupby(
377 sorted(statuses, key=version), version)
253 sorted(statuses, key=version),version))]
378 statuses = [(x, list(y)[0]) for x, y in status_groups]
254
379
255 pull_request_reviewers.append(
380 pull_request_reviewers.append(
256 (o.user, o.reasons, o.mandatory, statuses))
381 (obj, obj.user, obj.reasons, obj.mandatory, statuses))
382
257 return pull_request_reviewers
383 return pull_request_reviewers
258
384
259 def calculated_review_status(self, pull_request, reviewers_statuses=None):
385 def calculated_review_status(self, pull_request, reviewers_statuses=None):
@@ -59,8 +59,7 b' from rhodecode.lib.utils2 import ('
59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re, StrictAttributeDict, cleaned_uri)
61 glob2re, StrictAttributeDict, cleaned_uri)
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
63 JsonRaw
64 from rhodecode.lib.ext_json import json
63 from rhodecode.lib.ext_json import json
65 from rhodecode.lib.caching_query import FromCache
64 from rhodecode.lib.caching_query import FromCache
66 from rhodecode.lib.encrypt import AESCipher
65 from rhodecode.lib.encrypt import AESCipher
@@ -1327,7 +1326,7 b' class UserGroup(Base, BaseModel):'
1327 @hybrid_property
1326 @hybrid_property
1328 def description_safe(self):
1327 def description_safe(self):
1329 from rhodecode.lib import helpers as h
1328 from rhodecode.lib import helpers as h
1330 return h.escape(self.description)
1329 return h.escape(self.user_group_description)
1331
1330
1332 @hybrid_property
1331 @hybrid_property
1333 def group_data(self):
1332 def group_data(self):
@@ -3594,7 +3593,7 b' class _PullRequestBase(BaseModel):'
3594 'reasons': reasons,
3593 'reasons': reasons,
3595 'review_status': st[0][1].status if st else 'not_reviewed',
3594 'review_status': st[0][1].status if st else 'not_reviewed',
3596 }
3595 }
3597 for reviewer, reasons, mandatory, st in
3596 for obj, reviewer, reasons, mandatory, st in
3598 pull_request.reviewers_statuses()
3597 pull_request.reviewers_statuses()
3599 ]
3598 ]
3600 }
3599 }
@@ -3790,10 +3789,34 b' class PullRequestReviewers(Base, BaseMod'
3790 _reasons = Column(
3789 _reasons = Column(
3791 'reason', MutationList.as_mutable(
3790 'reason', MutationList.as_mutable(
3792 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3791 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3792
3793 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3793 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3794 user = relationship('User')
3794 user = relationship('User')
3795 pull_request = relationship('PullRequest')
3795 pull_request = relationship('PullRequest')
3796
3796
3797 rule_data = Column(
3798 'rule_data_json',
3799 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
3800
3801 def rule_user_group_data(self):
3802 """
3803 Returns the voting user group rule data for this reviewer
3804 """
3805
3806 if self.rule_data and 'vote_rule' in self.rule_data:
3807 user_group_data = {}
3808 if 'rule_user_group_entry_id' in self.rule_data:
3809 # means a group with voting rules !
3810 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
3811 user_group_data['name'] = self.rule_data['rule_name']
3812 user_group_data['vote_rule'] = self.rule_data['vote_rule']
3813
3814 return user_group_data
3815
3816 def __unicode__(self):
3817 return u"<%s('id:%s')>" % (self.__class__.__name__,
3818 self.pull_requests_reviewers_id)
3819
3797
3820
3798 class Notification(Base, BaseModel):
3821 class Notification(Base, BaseModel):
3799 __tablename__ = 'notifications'
3822 __tablename__ = 'notifications'
@@ -4086,6 +4109,7 b' class RepoReviewRuleUser(Base, BaseModel'
4086 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4109 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4087 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4110 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4088 )
4111 )
4112
4089 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4113 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4090 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4114 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4091 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4115 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
@@ -4104,17 +4128,28 b' class RepoReviewRuleUserGroup(Base, Base'
4104 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4128 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4105 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4129 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4106 )
4130 )
4131 VOTE_RULE_ALL = -1
4132
4107 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4133 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4108 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4134 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4109 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4135 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4110 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4136 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4137 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4111 users_group = relationship('UserGroup')
4138 users_group = relationship('UserGroup')
4112
4139
4113 def rule_data(self):
4140 def rule_data(self):
4114 return {
4141 return {
4115 'mandatory': self.mandatory
4142 'mandatory': self.mandatory,
4143 'vote_rule': self.vote_rule
4116 }
4144 }
4117
4145
4146 @property
4147 def vote_rule_label(self):
4148 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4149 return 'all must vote'
4150 else:
4151 return 'min. vote {}'.format(self.vote_rule)
4152
4118
4153
4119 class RepoReviewRule(Base, BaseModel):
4154 class RepoReviewRule(Base, BaseModel):
4120 __tablename__ = 'repo_review_rules'
4155 __tablename__ = 'repo_review_rules'
@@ -4225,12 +4260,20 b' class RepoReviewRule(Base, BaseModel):'
4225
4260
4226 for rule_user_group in self.rule_user_groups:
4261 for rule_user_group in self.rule_user_groups:
4227 source_data = {
4262 source_data = {
4263 'user_group_id': rule_user_group.users_group.users_group_id,
4228 'name': rule_user_group.users_group.users_group_name,
4264 'name': rule_user_group.users_group.users_group_name,
4229 'members': len(rule_user_group.users_group.members)
4265 'members': len(rule_user_group.users_group.members)
4230 }
4266 }
4231 for member in rule_user_group.users_group.members:
4267 for member in rule_user_group.users_group.members:
4232 if member.user.active:
4268 if member.user.active:
4233 users[member.user.username] = {
4269 key = member.user.username
4270 if key in users:
4271 # skip this member as we have him already
4272 # this prevents from override the "first" matched
4273 # users with duplicates in multiple groups
4274 continue
4275
4276 users[key] = {
4234 'user': member.user,
4277 'user': member.user,
4235 'source': 'user_group',
4278 'source': 'user_group',
4236 'source_data': source_data,
4279 'source_data': source_data,
@@ -4239,6 +4282,13 b' class RepoReviewRule(Base, BaseModel):'
4239
4282
4240 return users
4283 return users
4241
4284
4285 def user_group_vote_rule(self):
4286 rules = []
4287 if self.rule_user_groups:
4288 for user_group in self.rule_user_groups:
4289 rules.append(user_group)
4290 return rules
4291
4242 def __repr__(self):
4292 def __repr__(self):
4243 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4293 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4244 self.repo_review_rule_id, self.repo)
4294 self.repo_review_rule_id, self.repo)
@@ -584,6 +584,7 b' def PullRequestForm(localizer, repo_id):'
584 class ReviewerForm(formencode.Schema):
584 class ReviewerForm(formencode.Schema):
585 user_id = v.Int(not_empty=True)
585 user_id = v.Int(not_empty=True)
586 reasons = All()
586 reasons = All()
587 rules = All(v.UniqueList(localizer, convert=int)())
587 mandatory = v.StringBoolean()
588 mandatory = v.StringBoolean()
588
589
589 class _PullRequestForm(formencode.Schema):
590 class _PullRequestForm(formencode.Schema):
@@ -51,7 +51,7 b' from rhodecode.model.changeset_status im'
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
@@ -468,7 +468,7 b' class PullRequestModel(BaseModel):'
468 reviewer_ids = set()
468 reviewer_ids = set()
469 # members / reviewers
469 # members / reviewers
470 for reviewer_object in reviewers:
470 for reviewer_object in reviewers:
471 user_id, reasons, mandatory = reviewer_object
471 user_id, reasons, mandatory, rules = reviewer_object
472 user = self._get_user(user_id)
472 user = self._get_user(user_id)
473
473
474 # skip duplicates
474 # skip duplicates
@@ -482,6 +482,33 b' class PullRequestModel(BaseModel):'
482 reviewer.pull_request = pull_request
482 reviewer.pull_request = pull_request
483 reviewer.reasons = reasons
483 reviewer.reasons = reasons
484 reviewer.mandatory = mandatory
484 reviewer.mandatory = mandatory
485
486 # NOTE(marcink): pick only first rule for now
487 rule_id = rules[0] if rules else None
488 rule = RepoReviewRule.get(rule_id) if rule_id else None
489 if rule:
490 review_group = rule.user_group_vote_rule()
491 if review_group:
492 # NOTE(marcink):
493 # again, can be that user is member of more,
494 # but we pick the first same, as default reviewers algo
495 review_group = review_group[0]
496
497 rule_data = {
498 'rule_name':
499 rule.review_rule_name,
500 'rule_user_group_entry_id':
501 review_group.repo_review_rule_users_group_id,
502 'rule_user_group_name':
503 review_group.users_group.users_group_name,
504 'rule_user_group_members':
505 [x.user.username for x in review_group.users_group.members],
506 }
507 # e.g {'vote_rule': -1, 'mandatory': True}
508 rule_data.update(review_group.rule_data())
509
510 reviewer.rule_data = rule_data
511
485 Session().add(reviewer)
512 Session().add(reviewer)
486
513
487 # Set approval status to "Under Review" for all commits which are
514 # Set approval status to "Under Review" for all commits which are
@@ -962,14 +989,14 b' class PullRequestModel(BaseModel):'
962
989
963 :param pull_request: the pr to update
990 :param pull_request: the pr to update
964 :param reviewer_data: list of tuples
991 :param reviewer_data: list of tuples
965 [(user, ['reason1', 'reason2'], mandatory_flag)]
992 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
966 """
993 """
967 pull_request = self.__get_pull_request(pull_request)
994 pull_request = self.__get_pull_request(pull_request)
968 if pull_request.is_closed():
995 if pull_request.is_closed():
969 raise ValueError('This pull request is closed')
996 raise ValueError('This pull request is closed')
970
997
971 reviewers = {}
998 reviewers = {}
972 for user_id, reasons, mandatory in reviewer_data:
999 for user_id, reasons, mandatory, rules in reviewer_data:
973 if isinstance(user_id, (int, basestring)):
1000 if isinstance(user_id, (int, basestring)):
974 user_id = self._get_user(user_id).user_id
1001 user_id = self._get_user(user_id).user_id
975 reviewers[user_id] = {
1002 reviewers[user_id] = {
@@ -74,6 +74,7 b' class UserModel(BaseModel):'
74 'username': user.username,
74 'username': user.username,
75 'email': user.email,
75 'email': user.email,
76 'icon_link': h.gravatar_url(user.email, 30),
76 'icon_link': h.gravatar_url(user.email, 30),
77 'profile_link': h.link_to_user(user),
77 'value_display': h.escape(h.person(user)),
78 'value_display': h.escape(h.person(user)),
78 'value': user.username,
79 'value': user.username,
79 'value_type': 'user',
80 'value_type': 'user',
@@ -26,6 +26,7 b' class ReviewerSchema(colander.MappingSch'
26 username = colander.SchemaNode(types.StrOrIntType())
26 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
27 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29 rules = colander.SchemaNode(colander.List(), missing=[])
29
30
30
31
31 class ReviewerListSchema(colander.SequenceSchema):
32 class ReviewerListSchema(colander.SequenceSchema):
@@ -1324,7 +1324,7 b' table.integrations {'
1324 .reviewers ul li {
1324 .reviewers ul li {
1325 position: relative;
1325 position: relative;
1326 width: 100%;
1326 width: 100%;
1327 margin-bottom: 8px;
1327 padding-bottom: 8px;
1328 }
1328 }
1329
1329
1330 .reviewer_entry {
1330 .reviewer_entry {
@@ -1335,19 +1335,15 b' table.integrations {'
1335 width: 100%;
1335 width: 100%;
1336 overflow: auto;
1336 overflow: auto;
1337 }
1337 }
1338
1338 .reviewer_reason {
1339 .reviewer_reason_container {
1339 padding-left: 20px;
1340 padding-left: 20px;
1340 line-height: 1.5em;
1341 }
1341 }
1342
1343 .reviewer_reason {
1344 }
1345
1346 .reviewer_status {
1342 .reviewer_status {
1347 display: inline-block;
1343 display: inline-block;
1348 vertical-align: top;
1344 vertical-align: top;
1349 width: 7%;
1345 width: 25px;
1350 min-width: 20px;
1346 min-width: 25px;
1351 height: 1.2em;
1347 height: 1.2em;
1352 margin-top: 3px;
1348 margin-top: 3px;
1353 line-height: 1em;
1349 line-height: 1em;
@@ -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 .reviewer_member_mandatory_remove,
1380 .reviewer_member_mandatory_remove,
1375 .reviewer_member_remove {
1381 .reviewer_member_remove {
1376 position: absolute;
1382 position: absolute;
@@ -1386,10 +1392,6 b' table.integrations {'
1386 color: @grey4;
1392 color: @grey4;
1387 }
1393 }
1388
1394
1389 .reviewer_member_mandatory {
1390 padding-top:20px;
1391 }
1392
1393 .reviewer_member_status {
1395 .reviewer_member_status {
1394 margin-top: 5px;
1396 margin-top: 5px;
1395 }
1397 }
@@ -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 .creation_in_progress {
1855 .creation_in_progress {
1861 color: @grey4
1856 color: @grey4
@@ -137,10 +137,10 b' ReviewersController = function () {'
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0) {
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All individual reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
@@ -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 if (data.rules.use_code_authors_for_review) {
167 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
168 self.$rulesList.append(
160 self.addRule(
169 self.addRule(
@@ -227,10 +236,7 b' ReviewersController = function () {'
227 for (var i = 0; i < data.reviewers.length; i++) {
236 for (var i = 0; i < data.reviewers.length; i++) {
228 var reviewer = data.reviewers[i];
237 var reviewer = data.reviewers[i];
229 self.addReviewMember(
238 self.addReviewMember(
230 reviewer.user_id, reviewer.first_name,
239 reviewer, reviewer.reasons, reviewer.mandatory);
231 reviewer.last_name, reviewer.username,
232 reviewer.gravatar_link, reviewer.reasons,
233 reviewer.mandatory);
234 }
240 }
235 $('.calculate-reviewers').hide();
241 $('.calculate-reviewers').hide();
236 prButtonLock(false, null, 'reviewers');
242 prButtonLock(false, null, 'reviewers');
@@ -260,64 +266,22 b' ReviewersController = function () {'
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
266 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 }
267 }
262 };
268 };
269 this.reviewMemberEntry = function() {
263
270
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
271 };
272 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
265 var members = self.$reviewMembers.get(0);
273 var members = self.$reviewMembers.get(0);
266 var reasons_html = '';
274 var id = reviewer_obj.user_id;
267 var reasons_inputs = '';
275 var username = reviewer_obj.username;
276
268 var reasons = reasons || [];
277 var reasons = reasons || [];
269 var mandatory = mandatory || false;
278 var mandatory = mandatory || false;
270
279
271 if (reasons) {
280 // register IDS to check if we don't have this ID already in
272 for (var i = 0; i < reasons.length; i++) {
281 var currentIds = [];
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 }
276 }
277 var tmpl = '' +
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 '<div class="reviewer_status">'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 '</div>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 '<span class="reviewer_name user">{1}</span>'+
285 reasons_html +
286 '<input type="hidden" name="user_id" value="{2}">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 '{3}'+
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290
291 if (mandatory) {
292 tmpl += ''+
293 '<div class="reviewer_member_mandatory_remove">' +
294 '<i class="icon-remove-sign"></i>'+
295 '</div>' +
296 '<input type="hidden" name="mandatory" value="true">'+
297 '<div class="reviewer_member_mandatory">' +
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 '</div>';
300
301 } else {
302 tmpl += ''+
303 '<input type="hidden" name="mandatory" value="false">'+
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 '<i class="icon-remove-sign"></i>'+
306 '</div>';
307 }
308 // continue template
309 tmpl += ''+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 '</li>' ;
312
313 var displayname = "{0} ({1} {2})".format(
314 nname, escapeHtml(fname), escapeHtml(lname));
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 // check if we don't have this ID already in
317 var ids = [];
318 var _els = self.$reviewMembers.find('li').toArray();
282 var _els = self.$reviewMembers.find('li').toArray();
319 for (el in _els){
283 for (el in _els){
320 ids.push(_els[el].id)
284 currentIds.push(_els[el].id)
321 }
285 }
322
286
323 var userAllowedReview = function(userId) {
287 var userAllowedReview = function(userId) {
@@ -333,19 +297,29 b' ReviewersController = function () {'
333
297
334 var userAllowed = userAllowedReview(id);
298 var userAllowed = userAllowedReview(id);
335 if (!userAllowed){
299 if (!userAllowed){
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
300 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
337 }
301 } else {
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
302 // only add if it's not there
303 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
339
304
340 if(shouldAdd) {
305 if (alreadyReviewer) {
341 // only add if it's not there
306 alert(_gettext('User `{0}` already in reviewers').format(username));
342 members.innerHTML += element;
307 } else {
308 members.innerHTML += renderTemplate('reviewMemberEntry', {
309 'member': reviewer_obj,
310 'mandatory': mandatory,
311 'allowed_to_update': true,
312 'review_status': 'not_reviewed',
313 'review_status_label': _gettext('Not Reviewed'),
314 'reasons': reasons
315 });
316 }
343 }
317 }
344
318
345 };
319 };
346
320
347 this.updateReviewers = function(repo_name, pull_request_id){
321 this.updateReviewers = function(repo_name, pull_request_id){
348 var postData = '_method=put&' + $('#reviewers input').serialize();
322 var postData = $('#reviewers input').serialize();
349 _updatePullRequest(repo_name, pull_request_id, postData);
323 _updatePullRequest(repo_name, pull_request_id, postData);
350 };
324 };
351
325
@@ -457,21 +431,30 b' var ReviewerAutoComplete = function(inpu'
457 formatResult: autocompleteFormatResult,
431 formatResult: autocompleteFormatResult,
458 lookupFilter: autocompleteFilterResult,
432 lookupFilter: autocompleteFilterResult,
459 onSelect: function(element, data) {
433 onSelect: function(element, data) {
434 var mandatory = false;
435 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
460
436
461 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
437 // add whole user groups
462 if (data.value_type == 'user_group') {
438 if (data.value_type == 'user_group') {
463 reasons.push(_gettext('member of "{0}"').format(data.value_display));
439 reasons.push(_gettext('member of "{0}"').format(data.value_display));
464
440
465 $.each(data.members, function(index, member_data) {
441 $.each(data.members, function(index, member_data) {
466 reviewersController.addReviewMember(
442 var reviewer = member_data;
467 member_data.id, member_data.first_name, member_data.last_name,
443 reviewer['user_id'] = member_data['id'];
468 member_data.username, member_data.icon_link, reasons);
444 reviewer['gravatar_link'] = member_data['icon_link'];
445 reviewer['user_link'] = member_data['profile_link'];
446 reviewer['rules'] = [];
447 reviewersController.addReviewMember(reviewer, reasons, mandatory);
469 })
448 })
470
449 }
471 } else {
450 // add single user
472 reviewersController.addReviewMember(
451 else {
473 data.id, data.first_name, data.last_name,
452 var reviewer = data;
474 data.username, data.icon_link, reasons);
453 reviewer['user_id'] = data['id'];
454 reviewer['gravatar_link'] = data['icon_link'];
455 reviewer['user_link'] = data['profile_link'];
456 reviewer['rules'] = [];
457 reviewersController.addReviewMember(reviewer, reasons, mandatory);
475 }
458 }
476
459
477 $(inputId).val('');
460 $(inputId).val('');
@@ -1,6 +1,8 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="root.mako"/>
2 <%inherit file="root.mako"/>
3
3
4 <%include file="/ejs_templates/templates.html"/>
5
4 <div class="outerwrapper">
6 <div class="outerwrapper">
5 <!-- HEADER -->
7 <!-- HEADER -->
6 <div class="header">
8 <div class="header">
@@ -432,7 +432,7 b''
432 // generate new DESC of target repo displayed next to select
432 // generate new DESC of target repo displayed next to select
433 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
433 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
434 $('#target_repo_desc').html(
434 $('#target_repo_desc').html(
435 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Use as source</a>".format(repoData['description'], prLink)
435 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
436 );
436 );
437
437
438 // generate dynamic select2 for refs.
438 // generate dynamic select2 for refs.
@@ -340,58 +340,42 b''
340 ## REVIEWERS
340 ## REVIEWERS
341 <div class="reviewers-title block-right">
341 <div class="reviewers-title block-right">
342 <div class="pr-details-title">
342 <div class="pr-details-title">
343 ${_('Pull request reviewers')} / <a href="#toggleReasons" onclick="$('.reviewer_reason').toggle(); return false">${_('show reasons')}</a>
343 ${_('Pull request reviewers')}
344 %if c.allowed_to_update:
344 %if c.allowed_to_update:
345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
346 %endif
346 %endif
347 </div>
347 </div>
348 </div>
348 </div>
349 <div id="reviewers" class="block-right pr-details-content reviewers">
349 <div id="reviewers" class="block-right pr-details-content reviewers">
350 ## members goes here !
350
351 ## members redering block
351 <input type="hidden" name="__start__" value="review_members:sequence">
352 <input type="hidden" name="__start__" value="review_members:sequence">
352 <ul id="review_members" class="group_members">
353 <ul id="review_members" class="group_members">
353 %for member,reasons,mandatory,status in c.pull_request_reviewers:
354
354 <li id="reviewer_${member.user_id}">
355 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
355 <div class="reviewers_member">
356 <script>
356 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
357 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
357 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
358 var status = "${(status[0][1].status if status else 'not_reviewed')}";
358 </div>
359 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
359 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
360 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
360 ${self.gravatar_with_user(member.email, 16)}
361
361 </div>
362 var entry = renderTemplate('reviewMemberEntry', {
362 <input type="hidden" name="__start__" value="reviewer:mapping">
363 'member': member,
363 <input type="hidden" name="__start__" value="reasons:sequence">
364 'mandatory': member.mandatory,
364 % if reasons:
365 'reasons': member.reasons,
365 <div class="reviewer_reason_container">
366 'allowed_to_update': allowed_to_update,
366 %for reason in reasons:
367 'review_status': status,
367 <div class="reviewer_reason" style="display: none">- ${reason}</div>
368 'review_status_label': status_lbl,
368 <input type="hidden" name="reason" value="${reason}">
369 'user_group': member.user_group
369 %endfor
370 });
370 </div>
371 $('#review_members').append(entry)
371 % endif
372 </script>
372 <input type="hidden" name="__end__" value="reasons:sequence">
373
373 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
374 % endfor
374 <input type="hidden" name="mandatory" value="${mandatory}"/>
375
375 <input type="hidden" name="__end__" value="reviewer:mapping">
376 % if mandatory:
377 <div class="reviewer_member_mandatory_remove">
378 <i class="icon-remove-sign"></i>
379 </div>
380 <div class="reviewer_member_mandatory">
381 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
382 </div>
383 % else:
384 %if c.allowed_to_update:
385 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
386 <i class="icon-remove-sign" ></i>
387 </div>
388 %endif
389 % endif
390 </div>
391 </li>
392 %endfor
393 </ul>
376 </ul>
394 <input type="hidden" name="__end__" value="review_members:sequence">
377 <input type="hidden" name="__end__" value="review_members:sequence">
378 ## end members redering block
395
379
396 %if not c.pull_request.is_closed():
380 %if not c.pull_request.is_closed():
397 <div id="add_reviewer" class="ac" style="display: none;">
381 <div id="add_reviewer" class="ac" style="display: none;">
@@ -693,7 +677,7 b''
693 editButton: $('#open_edit_reviewers'),
677 editButton: $('#open_edit_reviewers'),
694 closeButton: $('#close_edit_reviewers'),
678 closeButton: $('#close_edit_reviewers'),
695 addButton: $('#add_reviewer'),
679 addButton: $('#add_reviewer'),
696 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
680 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
697
681
698 init: function() {
682 init: function() {
699 var self = this;
683 var self = this;
@@ -119,7 +119,7 b' class TestPullRequestModel(object):'
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False, [])],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
@@ -128,7 +128,7 b' class TestPullRequestModel(object):'
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False, [])],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
@@ -41,7 +41,8 b' class TestGetUsers(object):'
41 user_util.create_user(active=is_active, lastname='Fake user')
41 user_util.create_user(active=is_active, lastname='Fake user')
42
42
43 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
43 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
44 users = UserModel().get_users()
44 with mock.patch('rhodecode.lib.helpers.link_to_user'):
45 users = UserModel().get_users()
45 fake_users = [u for u in users if u['last_name'] == 'Fake user']
46 fake_users = [u for u in users if u['last_name'] == 'Fake user']
46 assert len(fake_users) == 2
47 assert len(fake_users) == 2
47
48
@@ -61,8 +62,9 b' class TestGetUsers(object):'
61 active=True, lastname=u'Fake {} user'.format(keyword))
62 active=True, lastname=u'Fake {} user'.format(keyword))
62
63
63 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
64 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
64 keyword = keywords[1].lower()
65 with mock.patch('rhodecode.lib.helpers.link_to_user'):
65 users = UserModel().get_users(name_contains=keyword)
66 keyword = keywords[1].lower()
67 users = UserModel().get_users(name_contains=keyword)
66
68
67 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
69 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
68 assert len(fake_users) == 2
70 assert len(fake_users) == 2
@@ -80,7 +82,8 b' class TestGetUsers(object):'
80
82
81 keyword = keywords[1].lower()
83 keyword = keywords[1].lower()
82 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
84 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
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 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
88 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
86 assert len(fake_users) == 2
89 assert len(fake_users) == 2
@@ -95,7 +98,8 b' class TestGetUsers(object):'
95
98
96 user_filter = created_users[-1].username[-2:]
99 user_filter = created_users[-1].username[-2:]
97 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
100 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
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 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
104 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
101 assert len(fake_users) == 1
105 assert len(fake_users) == 1
@@ -108,7 +112,8 b' class TestGetUsers(object):'
108 active=True, lastname='Fake user'))
112 active=True, lastname='Fake user'))
109
113
110 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
114 with mock.patch('rhodecode.lib.helpers.gravatar_url'):
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 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
118 fake_users = [u for u in users if u['last_name'].startswith('Fake')]
114 assert len(fake_users) == 3
119 assert len(fake_users) == 3
@@ -995,8 +995,8 b' class PRTestUtility(object):'
995
995
996 def _get_reviewers(self):
996 def _get_reviewers(self):
997 return [
997 return [
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1000 ]
1000 ]
1001
1001
1002 def update_source_repository(self, head=None):
1002 def update_source_repository(self, head=None):
General Comments 0
You need to be logged in to leave comments. Login now