Changeset - 603f5f7c323d
[Not reviewed]
stable
0 2 0
Thomas De Schampheleire - 7 years ago 2019-02-26 21:50:15
thomas.de_schampheleire@nokia.com
pullrequests: prevent XSS in 'Potential Reviewers' list when first and last names cannot be trusted

If a user first or last name contains javascript, these fields need proper
escaping to avoid XSS attacks.

An example scenario is:
- the malicious user creates a repository. This will cause this user to be
listed automatically under 'Potential Reviewers' in pull requests.
- another user creates a pull request on that repository and selects the
suggested reviewer from the 'Potential Reviewers' list.

Reported by Bob Hogg <wombat@rwhogg.site> (thanks!).


Technical note: the other caller of addReviewMember in base.js itself does
_not_ need to be adapted to escape the input values, because the input
values (oData) are _already_ escaped (by the YUI framework).
2 files changed with 12 insertions and 1 deletions:
0 comments (0 inline, 0 general)
kallithea/public/js/base.js
Show inline comments
 
@@ -932,827 +932,831 @@ var initCodeMirror = function(textarea_i
 
    $('#reset').click(function(e){
 
            window.location=resetUrl;
 
        });
 

	
 
    $('#file_enable').click(function(){
 
            $('#editor_container').show();
 
            $('#upload_file_container').hide();
 
            $('#filename_container').show();
 
            $('#set_mode_header').show();
 
        });
 

	
 
    $('#upload_file_enable').click(function(){
 
            $('#editor_container').hide();
 
            $('#upload_file_container').show();
 
            $('#filename_container').hide();
 
            $('#set_mode_header').hide();
 
        });
 

	
 
    return myCodeMirror
 
};
 

	
 
var setCodeMirrorMode = function(codeMirrorInstance, mode) {
 
    CodeMirror.autoLoadMode(codeMirrorInstance, mode);
 
}
 

	
 

	
 
var _getIdentNode = function(n){
 
    //iterate thrugh nodes until matching interesting node
 

	
 
    if (typeof n == 'undefined'){
 
        return -1
 
    }
 

	
 
    if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
 
        return n
 
    }
 
    else{
 
        return _getIdentNode(n.parentNode);
 
    }
 
};
 

	
 
/* generate links for multi line selects that can be shown by files.html page_highlights.
 
 * This is a mouseup handler for hlcode from CodeHtmlFormatter and pygmentize */
 
var getSelectionLink = function(e) {
 
    //get selection from start/to nodes
 
    if (typeof window.getSelection != "undefined") {
 
        var s = window.getSelection();
 

	
 
        var from = _getIdentNode(s.anchorNode);
 
        var till = _getIdentNode(s.focusNode);
 

	
 
        var f_int = parseInt(from.id.replace('L',''));
 
        var t_int = parseInt(till.id.replace('L',''));
 

	
 
        var yoffset = 35;
 
        var ranges = [parseInt(from.id.replace('L','')), parseInt(till.id.replace('L',''))];
 
        if (ranges[0] > ranges[1]){
 
            //highlight from bottom
 
            yoffset = -yoffset;
 
            ranges = [ranges[1], ranges[0]];
 
        }
 
        var $hl_div = $('div#linktt');
 
        // if we select more than 2 lines
 
        if (ranges[0] != ranges[1]){
 
            if ($hl_div.length) {
 
                $hl_div.html('');
 
            } else {
 
                $hl_div = $('<div id="linktt" class="hl-tip-box">');
 
                $('body').prepend($hl_div);
 
            }
 

	
 
            $hl_div.append($('<a>').html(_TM['Selection Link']).prop('href', location.href.substring(0, location.href.indexOf('#')) + '#L' + ranges[0] + '-'+ranges[1]));
 
            var xy = $(till).offset();
 
            $hl_div.css('top', (xy.top + yoffset) + 'px').css('left', xy.left + 'px');
 
            $hl_div.show();
 
        }
 
        else{
 
            $hl_div.hide();
 
        }
 
    }
 
};
 

	
 
var deleteNotification = function(url, notification_id, callbacks){
 
    var success = function(o){
 
            $("#notification_"+notification_id).remove();
 
            _run_callbacks(callbacks);
 
        };
 
    var failure = function(o){
 
            alert("deleteNotification failure");
 
        };
 
    var postData = {'_method': 'delete'};
 
    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
 
    ajaxPOST(sUrl, postData, success, failure);
 
};
 

	
 
var readNotification = function(url, notification_id, callbacks){
 
    var success = function(o){
 
            var $obj = $("#notification_"+notification_id);
 
            $obj.removeClass('unread');
 
            $obj.find('.read-notification').remove();
 
            _run_callbacks(callbacks);
 
        };
 
    var failure = function(o){
 
            alert("readNotification failure");
 
        };
 
    var postData = {'_method': 'put'};
 
    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
 
    ajaxPOST(sUrl, postData, success, failure);
 
};
 

	
 
/**
 
 * Autocomplete functionality
 
 */
 

	
 
// Custom search function for the DataSource of users
 
var autocompleteMatchUsers = function (sQuery, myUsers) {
 
    // Case insensitive matching
 
    var query = sQuery.toLowerCase();
 
    var i = 0;
 
    var l = myUsers.length;
 
    var matches = [];
 

	
 
    // Match against each name of each contact
 
    for (; i < l; i++) {
 
        var contact = myUsers[i];
 
        if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
 
             ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
 
             ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
 
            matches[matches.length] = contact;
 
        }
 
    }
 
    return matches;
 
};
 

	
 
// Custom search function for the DataSource of userGroups
 
var autocompleteMatchGroups = function (sQuery, myGroups) {
 
    // Case insensitive matching
 
    var query = sQuery.toLowerCase();
 
    var i = 0;
 
    var l = myGroups.length;
 
    var matches = [];
 

	
 
    // Match against each name of each group
 
    for (; i < l; i++) {
 
        var matched_group = myGroups[i];
 
        if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
 
            matches[matches.length] = matched_group;
 
        }
 
    }
 
    return matches;
 
};
 

	
 
// Helper highlight function for the formatter
 
var autocompleteHighlightMatch = function (full, snippet, matchindex) {
 
    return full.substring(0, matchindex)
 
        + "<span class='match'>"
 
        + full.substr(matchindex, snippet.length)
 
        + "</span>" + full.substring(matchindex + snippet.length);
 
};
 

	
 
var gravatar = function(link, size, cssclass) {
 
    var elem = '<img alt="" class="{2}" style="width: {0}px; height: {0}px" src="{1}"/>'.format(size, link, cssclass);
 
    if (!link) {
 
        elem = '<i class="icon-user {1}" style="font-size: {0}px;"></i>'.format(size, cssclass);
 
    }
 
    return elem;
 
}
 

	
 
var autocompleteGravatar = function(res, link, size, group) {
 
    var elem = gravatar(link, size, "perm-gravatar-ac");
 
    if (group !== undefined) {
 
        elem = '<i class="perm-gravatar-ac icon-users"></i>';
 
    }
 
    return '<div class="ac-container-wrap">{0}{1}</div>'.format(elem, res);
 
}
 

	
 
// Custom formatter to highlight the matching letters
 
var autocompleteFormatter = function (oResultData, sQuery, sResultMatch) {
 
    var query = sQuery.toLowerCase();
 

	
 
    // group
 
    if (oResultData.grname != undefined) {
 
        var grname = oResultData.grname;
 
        var grmembers = oResultData.grmembers;
 
        var grnameMatchIndex = grname.toLowerCase().indexOf(query);
 
        var grprefix = "{0}: ".format(_TM['Group']);
 
        var grsuffix = " ({0} {1})".format(grmembers, _TM['members']);
 

	
 
        if (grnameMatchIndex > -1) {
 
            return autocompleteGravatar(grprefix + autocompleteHighlightMatch(grname, query, grnameMatchIndex) + grsuffix, null, null, true);
 
        }
 
        return autocompleteGravatar(grprefix + oResultData.grname + grsuffix, null, null, true);
 

	
 
    // users
 
    } else if (oResultData.nname != undefined) {
 
        var fname = oResultData.fname || "";
 
        var lname = oResultData.lname || "";
 
        var nname = oResultData.nname;
 

	
 
        // Guard against null value
 
        var fnameMatchIndex = fname.toLowerCase().indexOf(query),
 
            lnameMatchIndex = lname.toLowerCase().indexOf(query),
 
            nnameMatchIndex = nname.toLowerCase().indexOf(query),
 
            displayfname, displaylname, displaynname, displayname;
 

	
 
        if (fnameMatchIndex > -1) {
 
            displayfname = autocompleteHighlightMatch(fname, query, fnameMatchIndex);
 
        } else {
 
            displayfname = fname;
 
        }
 

	
 
        if (lnameMatchIndex > -1) {
 
            displaylname = autocompleteHighlightMatch(lname, query, lnameMatchIndex);
 
        } else {
 
            displaylname = lname;
 
        }
 

	
 
        if (nnameMatchIndex > -1) {
 
            displaynname = autocompleteHighlightMatch(nname, query, nnameMatchIndex);
 
        } else {
 
            displaynname = nname;
 
        }
 

	
 
        displayname = displaynname;
 
        if (displayfname && displaylname) {
 
            displayname = "{0} {1} ({2})".format(displayfname, displaylname, displayname);
 
        }
 

	
 
        return autocompleteGravatar(displayname, oResultData.gravatar_lnk, oResultData.gravatar_size);
 
    } else {
 
        return '';
 
    }
 
};
 

	
 
// Generate a basic autocomplete instance that can be tweaked further by the caller
 
var autocompleteCreate = function ($inputElement, $container, matchFunc) {
 
    var datasource = new YAHOO.util.FunctionDataSource(matchFunc);
 

	
 
    var autocomplete = new YAHOO.widget.AutoComplete($inputElement[0], $container[0], datasource);
 
    autocomplete.useShadow = false;
 
    autocomplete.resultTypeList = false;
 
    autocomplete.animVert = false;
 
    autocomplete.animHoriz = false;
 
    autocomplete.animSpeed = 0.1;
 
    autocomplete.formatResult = autocompleteFormatter;
 

	
 
    return autocomplete;
 
}
 

	
 
var SimpleUserAutoComplete = function ($inputElement, $container, users_list) {
 

	
 
    var matchUsers = function (sQuery) {
 
        return autocompleteMatchUsers(sQuery, users_list);
 
    }
 

	
 
    var userAC = autocompleteCreate($inputElement, $container, matchUsers);
 

	
 
    // Handler for selection of an entry
 
    var itemSelectHandler = function (sType, aArgs) {
 
        var myAC = aArgs[0]; // reference back to the AC instance
 
        var elLI = aArgs[1]; // reference to the selected LI element
 
        var oData = aArgs[2]; // object literal of selected item's result data
 
        myAC.getInputEl().value = oData.nname;
 
    };
 
    userAC.itemSelectEvent.subscribe(itemSelectHandler);
 
}
 

	
 
var MembersAutoComplete = function ($inputElement, $container, users_list, groups_list) {
 

	
 
    var matchAll = function (sQuery) {
 
        var u = autocompleteMatchUsers(sQuery, users_list);
 
        var g = autocompleteMatchGroups(sQuery, groups_list);
 
        return u.concat(g);
 
    };
 

	
 
    var membersAC = autocompleteCreate($inputElement, $container, matchAll);
 

	
 
    // Handler for selection of an entry
 
    var itemSelectHandler = function (sType, aArgs) {
 
        var nextId = $inputElement.prop('id').split('perm_new_member_name_')[1];
 
        var myAC = aArgs[0]; // reference back to the AC instance
 
        var elLI = aArgs[1]; // reference to the selected LI element
 
        var oData = aArgs[2]; // object literal of selected item's result data
 
        //fill the autocomplete with value
 
        if (oData.nname != undefined) {
 
            //users
 
            myAC.getInputEl().value = oData.nname;
 
            $('#perm_new_member_type_'+nextId).val('user');
 
        } else {
 
            //groups
 
            myAC.getInputEl().value = oData.grname;
 
            $('#perm_new_member_type_'+nextId).val('users_group');
 
        }
 
    };
 
    membersAC.itemSelectEvent.subscribe(itemSelectHandler);
 
}
 

	
 
var MentionsAutoComplete = function ($inputElement, $container, users_list) {
 

	
 
    var matchUsers = function (sQuery) {
 
            var org_sQuery = sQuery;
 
            if(this.mentionQuery == null){
 
                return []
 
            }
 
            sQuery = this.mentionQuery;
 
            return autocompleteMatchUsers(sQuery, users_list);
 
    }
 

	
 
    var mentionsAC = autocompleteCreate($inputElement, $container, matchUsers);
 
    mentionsAC.suppressInputUpdate = true;
 
    // Overwrite formatResult to take into account mentionQuery
 
    mentionsAC.formatResult = function (oResultData, sQuery, sResultMatch) {
 
        var org_sQuery = sQuery;
 
        if (this.dataSource.mentionQuery != null) {
 
            sQuery = this.dataSource.mentionQuery;
 
        }
 
        return autocompleteFormatter(oResultData, sQuery, sResultMatch);
 
    }
 

	
 
    // Handler for selection of an entry
 
    if(mentionsAC.itemSelectEvent){
 
        mentionsAC.itemSelectEvent.subscribe(function (sType, aArgs) {
 
            var myAC = aArgs[0]; // reference back to the AC instance
 
            var elLI = aArgs[1]; // reference to the selected LI element
 
            var oData = aArgs[2]; // object literal of selected item's result data
 
            //Replace the mention name with replaced
 
            var re = new RegExp();
 
            var org = myAC.getInputEl().value;
 
            var chunks = myAC.dataSource.chunks
 
            // replace middle chunk(the search term) with actuall  match
 
            chunks[1] = chunks[1].replace('@'+myAC.dataSource.mentionQuery,
 
                                          '@'+oData.nname+' ');
 
            myAC.getInputEl().value = chunks.join('');
 
            myAC.getInputEl().focus(); // Y U NO WORK !?
 
        });
 
    }
 

	
 
    // in this keybuffer we will gather current value of search !
 
    // since we need to get this just when someone does `@` then we do the
 
    // search
 
    mentionsAC.dataSource.chunks = [];
 
    mentionsAC.dataSource.mentionQuery = null;
 

	
 
    mentionsAC.get_mention = function(msg, max_pos) {
 
        var org = msg;
 
        // Must match utils2.py MENTIONS_REGEX.
 
        // Only matching on string up to cursor, so it must end with $
 
        var re = new RegExp('(?:^|[^a-zA-Z0-9])@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])$');
 
        var chunks  = [];
 

	
 
        // cut first chunk until current pos
 
        var to_max = msg.substr(0, max_pos);
 
        var at_pos = Math.max(0,to_max.lastIndexOf('@')-1);
 
        var msg2 = to_max.substr(at_pos);
 

	
 
        chunks.push(org.substr(0,at_pos)); // prefix chunk
 
        chunks.push(msg2);                 // search chunk
 
        chunks.push(org.substr(max_pos));  // postfix chunk
 

	
 
        // clean up msg2 for filtering and regex match
 
        var msg2 = msg2.lstrip(' ').lstrip('\n');
 

	
 
        if(re.test(msg2)){
 
            var unam = re.exec(msg2)[1];
 
            return [unam, chunks];
 
        }
 
        return [null, null];
 
    };
 

	
 
    $inputElement.keyup(function(e){
 
            var currentMessage = $inputElement.val();
 
            var currentCaretPosition = $inputElement[0].selectionStart;
 

	
 
            var unam = mentionsAC.get_mention(currentMessage, currentCaretPosition);
 
            var curr_search = null;
 
            if(unam[0]){
 
                curr_search = unam[0];
 
            }
 

	
 
            mentionsAC.dataSource.chunks = unam[1];
 
            mentionsAC.dataSource.mentionQuery = curr_search;
 
        });
 
}
 

	
 
// Important: input arguments to addReviewMember should be safe (escaped) for
 
// inclusion into HTML.
 
var addReviewMember = function(id,fname,lname,nname,gravatar_link,gravatar_size){
 
    var displayname = nname;
 
    if ((fname != "") && (lname != "")) {
 
        displayname = "{0} {1} ({2})".format(fname, lname, nname);
 
    }
 
    var gravatarelm = gravatar(gravatar_link, gravatar_size, "");
 
    // WARNING: the HTML below is duplicate with
 
    // kallithea/templates/pullrequests/pullrequest_show.html
 
    // If you change something here it should be reflected in the template too.
 
    var element = (
 
        '     <li id="reviewer_{2}">\n'+
 
        '       <div class="reviewers_member">\n'+
 
        '           <div class="reviewer_status tooltip" title="not_reviewed">\n'+
 
        '             <i class="icon-circle changeset-status-not_reviewed"></i>\n'+
 
        '           </div>\n'+
 
        '         <div class="reviewer_gravatar gravatar">{0}</div>\n'+
 
        '         <div style="float:left;">{1}</div>\n'+
 
        '         <input type="hidden" value="{2}" name="review_members" />\n'+
 
        '         <div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">\n'+
 
        '             <i class="icon-minus-circled"></i>\n'+
 
        '         </div> (add not saved)\n'+
 
        '       </div>\n'+
 
        '     </li>\n'
 
        ).format(gravatarelm, displayname, id);
 
    // check if we don't have this ID already in
 
    var ids = [];
 
    $('#review_members').find('li').each(function() {
 
            ids.push(this.id);
 
        });
 
    if(ids.indexOf('reviewer_'+id) == -1){
 
        //only add if it's not there
 
        $('#review_members').append(element);
 
    }
 
}
 

	
 
var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
 
    var $li = $('#reviewer_{0}'.format(reviewer_id));
 
    $li.find('div div').css("text-decoration", "line-through");
 
    $li.find('input').prop('name', 'review_members_removed');
 
    $li.find('.reviewer_member_remove').replaceWith('&nbsp;(remove not saved)');
 
}
 

	
 
/* activate auto completion of users as PR reviewers */
 
var PullRequestAutoComplete = function ($inputElement, $container, users_list) {
 

	
 
    var matchUsers = function (sQuery) {
 
        return autocompleteMatchUsers(sQuery, users_list);
 
    };
 

	
 
    var reviewerAC = autocompleteCreate($inputElement, $container, matchUsers);
 
    reviewerAC.suppressInputUpdate = true;
 

	
 
    // Handler for selection of an entry
 
    if(reviewerAC.itemSelectEvent){
 
        reviewerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
 
            var myAC = aArgs[0]; // reference back to the AC instance
 
            var elLI = aArgs[1]; // reference to the selected LI element
 
            var oData = aArgs[2]; // object literal of selected item's result data
 

	
 
            // The fields in oData have already been escaped by the YUI
 
            // framework. We thus should not add explicit escaping here anymore.
 
            addReviewMember(oData.id, oData.fname, oData.lname, oData.nname,
 
                            oData.gravatar_lnk, oData.gravatar_size);
 
            myAC.getInputEl().value = '';
 
        });
 
    }
 
}
 

	
 
/**
 
 * Activate .quick_repo_menu
 
 */
 
var quick_repo_menu = function(){
 
    $(".quick_repo_menu").mouseenter(function(e) {
 
            var $menu = $(e.currentTarget).children().first().children().first();
 
            if($menu.hasClass('hidden')){
 
                $menu.removeClass('hidden').addClass('active');
 
                $(e.currentTarget).removeClass('hidden').addClass('active');
 
            }
 
        });
 
    $(".quick_repo_menu").mouseleave(function(e) {
 
            var $menu = $(e.currentTarget).children().first().children().first();
 
            if($menu.hasClass('active')){
 
                $menu.removeClass('active').addClass('hidden');
 
                $(e.currentTarget).removeClass('active').addClass('hidden');
 
            }
 
        });
 
};
 

	
 

	
 
/**
 
 * TABLE SORTING
 
 */
 

	
 
var revisionSort = function(a, b, desc, field) {
 
    var a_ = parseInt(a.getData('last_rev_raw') || 0);
 
    var b_ = parseInt(b.getData('last_rev_raw') || 0);
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var ageSort = function(a, b, desc, field) {
 
    // data is like: <span class="tooltip" date="2014-06-04 18:18:55.325474" title="Wed, 04 Jun 2014 18:18:55">1 day and 23 hours ago</span>
 
    var a_ = $(a.getData(field)).attr('date');
 
    var b_ = $(b.getData(field)).attr('date');
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var lastLoginSort = function(a, b, desc, field) {
 
    var a_ = parseFloat(a.getData('last_login_raw') || 0);
 
    var b_ = parseFloat(b.getData('last_login_raw') || 0);
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var nameSort = function(a, b, desc, field) {
 
    var a_ = a.getData('raw_name') || 0;
 
    var b_ = b.getData('raw_name') || 0;
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var dateSort = function(a, b, desc, field) {
 
    var a_ = parseFloat(a.getData('raw_date') || 0);
 
    var b_ = parseFloat(b.getData('raw_date') || 0);
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var addPermAction = function(_html, users_list, groups_list){
 
    var $last_node = $('.last_new_member').last(); // empty tr between last and add
 
    var next_id = $('.new_members').length;
 
    $last_node.before($('<tr class="new_members">').append(_html.format(next_id)));
 
    MembersAutoComplete($("#perm_new_member_name_"+next_id),
 
            $("#perm_container_"+next_id), users_list, groups_list);
 
}
 

	
 
function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
 
    var success = function (o) {
 
            $('#' + field_id).remove();
 
        };
 
    var failure = function (o) {
 
            alert(_TM['Failed to revoke permission'] + ": " + o.status);
 
        };
 
    var query_params = {
 
        '_method': 'delete'
 
    }
 
    // put extra data into POST
 
    if (extra_data !== undefined && (typeof extra_data === 'object')){
 
        for(var k in extra_data){
 
            query_params[k] = extra_data[k];
 
        }
 
    }
 

	
 
    if (obj_type=='user'){
 
        query_params['user_id'] = obj_id;
 
        query_params['obj_type'] = 'user';
 
    }
 
    else if (obj_type=='user_group'){
 
        query_params['user_group_id'] = obj_id;
 
        query_params['obj_type'] = 'user_group';
 
    }
 

	
 
    ajaxPOST(url, query_params, success, failure);
 
};
 

	
 
/* Multi selectors */
 

	
 
var MultiSelectWidget = function(selected_id, available_id, form_id){
 
    var $availableselect = $('#' + available_id);
 
    var $selectedselect = $('#' + selected_id);
 

	
 
    //fill available only with those not in selected
 
    var $selectedoptions = $selectedselect.children('option');
 
    $availableselect.children('option').filter(function(i, e){
 
            for(var j = 0, node; node = $selectedoptions[j]; j++){
 
                if(node.value == e.value){
 
                    return true;
 
                }
 
            }
 
            return false;
 
        }).remove();
 

	
 
    $('#add_element').click(function(e){
 
            $selectedselect.append($availableselect.children('option:selected'));
 
        });
 
    $('#remove_element').click(function(e){
 
            $availableselect.append($selectedselect.children('option:selected'));
 
        });
 

	
 
    $('#'+form_id).submit(function(){
 
            $selectedselect.children('option').each(function(i, e){
 
                e.selected = 'selected';
 
            });
 
        });
 
}
 

	
 
// custom paginator
 
var YUI_paginator = function(links_per_page, containers){
 

	
 
    (function () {
 

	
 
        var Paginator = YAHOO.widget.Paginator,
 
            l         = YAHOO.lang,
 
            setId     = YAHOO.util.Dom.generateId;
 

	
 
        Paginator.ui.MyFirstPageLink = function (p) {
 
            this.paginator = p;
 

	
 
            p.subscribe('recordOffsetChange',this.update,this,true);
 
            p.subscribe('rowsPerPageChange',this.update,this,true);
 
            p.subscribe('totalRecordsChange',this.update,this,true);
 
            p.subscribe('destroy',this.destroy,this,true);
 

	
 
            // TODO: make this work
 
            p.subscribe('firstPageLinkLabelChange',this.update,this,true);
 
            p.subscribe('firstPageLinkClassChange',this.update,this,true);
 
        };
 

	
 
        Paginator.ui.MyFirstPageLink.init = function (p) {
 
            p.setAttributeConfig('firstPageLinkLabel', {
 
                value : 1,
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('firstPageLinkClass', {
 
                value : 'yui-pg-first',
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('firstPageLinkTitle', {
 
                value : 'First Page',
 
                validator : l.isString
 
            });
 
        };
 

	
 
        // Instance members and methods
 
        Paginator.ui.MyFirstPageLink.prototype = {
 
            current   : null,
 
            leftmost_page: null,
 
            rightmost_page: null,
 
            link      : null,
 
            span      : null,
 
            dotdot    : null,
 
            getPos    : function(cur_page, max_page, items){
 
                var edge = parseInt(items / 2) + 1;
 
                if (cur_page <= edge){
 
                    var radius = Math.max(parseInt(items / 2), items - cur_page);
 
                }
 
                else if ((max_page - cur_page) < edge) {
 
                    var radius = (items - 1) - (max_page - cur_page);
 
                }
 
                else{
 
                    var radius = parseInt(items / 2);
 
                }
 

	
 
                var left = Math.max(1, (cur_page - (radius)));
 
                var right = Math.min(max_page, cur_page + (radius));
 
                return [left, cur_page, right]
 
            },
 
            render : function (id_base) {
 
                var p      = this.paginator,
 
                    c      = p.get('firstPageLinkClass'),
 
                    label  = p.get('firstPageLinkLabel'),
 
                    title  = p.get('firstPageLinkTitle');
 

	
 
                this.link     = document.createElement('a');
 
                this.span     = document.createElement('span');
 
                $(this.span).hide();
 

	
 
                var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                setId(this.link, id_base + '-first-link');
 
                this.link.href      = '#';
 
                this.link.className = c;
 
                this.link.innerHTML = label;
 
                this.link.title     = title;
 
                YUE.on(this.link,'click',this.onClick,this,true);
 

	
 
                setId(this.span, id_base + '-first-span');
 
                this.span.className = c;
 
                this.span.innerHTML = label;
 

	
 
                this.current = p.getCurrentPage() > 1 ? this.link : this.span;
 
                return this.current;
 
            },
 
            update : function (e) {
 
                var p      = this.paginator;
 
                var _pos   = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                if (e && e.prevValue === e.newValue) {
 
                    return;
 
                }
 

	
 
                var par = this.current ? this.current.parentNode : null;
 
                if (this.leftmost_page > 1) {
 
                    if (par && this.current === this.span) {
 
                        par.replaceChild(this.link,this.current);
 
                        this.current = this.link;
 
                    }
 
                } else {
 
                    if (par && this.current === this.link) {
 
                        par.replaceChild(this.span,this.current);
 
                        this.current = this.span;
 
                    }
 
                }
 
            },
 
            destroy : function () {
 
                YUE.purgeElement(this.link);
 
                this.current.parentNode.removeChild(this.current);
 
                this.link = this.span = null;
 
            },
 
            onClick : function (e) {
 
                YUE.stopEvent(e);
 
                this.paginator.setPage(1);
 
            }
 
        };
 

	
 
        })();
 

	
 
    (function () {
 

	
 
        var Paginator = YAHOO.widget.Paginator,
 
            l         = YAHOO.lang,
 
            setId     = YAHOO.util.Dom.generateId;
 

	
 
        Paginator.ui.MyLastPageLink = function (p) {
 
            this.paginator = p;
 

	
 
            p.subscribe('recordOffsetChange',this.update,this,true);
 
            p.subscribe('rowsPerPageChange',this.update,this,true);
 
            p.subscribe('totalRecordsChange',this.update,this,true);
 
            p.subscribe('destroy',this.destroy,this,true);
 

	
 
            // TODO: make this work
 
            p.subscribe('lastPageLinkLabelChange',this.update,this,true);
 
            p.subscribe('lastPageLinkClassChange', this.update,this,true);
 
        };
 

	
 
        Paginator.ui.MyLastPageLink.init = function (p) {
 
            p.setAttributeConfig('lastPageLinkLabel', {
 
                value : -1,
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('lastPageLinkClass', {
 
                value : 'yui-pg-last',
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('lastPageLinkTitle', {
 
                value : 'Last Page',
 
                validator : l.isString
 
            });
 

	
 
        };
 

	
 
        Paginator.ui.MyLastPageLink.prototype = {
 

	
 
            current   : null,
 
            leftmost_page: null,
 
            rightmost_page: null,
 
            link      : null,
 
            span      : null,
 
            dotdot    : null,
 
            na        : null,
 
            getPos    : function(cur_page, max_page, items){
 
                var edge = parseInt(items / 2) + 1;
 
                if (cur_page <= edge){
 
                    var radius = Math.max(parseInt(items / 2), items - cur_page);
 
                }
 
                else if ((max_page - cur_page) < edge) {
 
                    var radius = (items - 1) - (max_page - cur_page);
 
                }
 
                else{
 
                    var radius = parseInt(items / 2);
 
                }
 

	
 
                var left = Math.max(1, (cur_page - (radius)));
 
                var right = Math.min(max_page, cur_page + (radius));
 
                return [left, cur_page, right]
 
            },
 
            render : function (id_base) {
 
                var p      = this.paginator,
 
                    c      = p.get('lastPageLinkClass'),
 
                    label  = p.get('lastPageLinkLabel'),
 
                    last   = p.getTotalPages(),
 
                    title  = p.get('lastPageLinkTitle');
 

	
 
                var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                this.link = document.createElement('a');
 
                this.span = document.createElement('span');
 
                $(this.span).hide();
 

	
 
                this.na   = this.span.cloneNode(false);
 

	
 
                setId(this.link, id_base + '-last-link');
 
                this.link.href      = '#';
 
                this.link.className = c;
 
                this.link.innerHTML = label;
 
                this.link.title     = title;
 
                YUE.on(this.link,'click',this.onClick,this,true);
 

	
 
                setId(this.span, id_base + '-last-span');
 
                this.span.className = c;
 
                this.span.innerHTML = label;
 

	
 
                setId(this.na, id_base + '-last-na');
 

	
 
                if (this.rightmost_page < p.getTotalPages()){
 
                    this.current = this.link;
 
                }
 
                else{
 
                    this.current = this.span;
 
                }
 

	
 
                this.current.innerHTML = p.getTotalPages();
 
                return this.current;
 
            },
 

	
 
            update : function (e) {
 
                var p      = this.paginator;
 

	
 
                var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                if (e && e.prevValue === e.newValue) {
 
                    return;
 
                }
 

	
 
                var par   = this.current ? this.current.parentNode : null,
 
                    after = this.link;
 
                if (par) {
 

	
 
                    // only show the last page if the rightmost one is
 
                    // lower, so we don't have doubled entries at the end
 
                    if (!(this.rightmost_page < p.getTotalPages())){
 
                        after = this.span
 
                    }
 

	
 
                    if (this.current !== after) {
kallithea/templates/pullrequests/pullrequest_show.html
Show inline comments
 
@@ -31,392 +31,399 @@ ${self.repo_context_bar('showpullrequest
 
              (${_('Closed')})
 
          %endif
 
      </div>
 
      <div id="pr-summary" class="fields">
 

	
 
        <div class="field pr-not-edit" style="min-height:37px">
 
          <div class="label-summary">
 
            <label>${_('Description')}:</label>
 
            %if editable:
 
            <div style="margin: 5px">
 
              <a class="btn btn-small" onclick="$('.pr-do-edit').show();$('.pr-not-edit').hide()">${_("Edit")}</a>
 
            </div>
 
            %endif
 
          </div>
 
          <div class="input">
 
            <div style="white-space:pre-wrap; line-height: 14px">${h.urlify_commit(c.pull_request.description, c.pull_request.org_repo.repo_name)}</div>
 
          </div>
 
        </div>
 

	
 
        %if editable:
 
        <div class="pr-do-edit" style="display:none">
 
          <div class="field">
 
              <div class="label-summary">
 
                  <label for="pullrequest_title">${_('Title')}:</label>
 
              </div>
 
              <div class="input">
 
                  ${h.text('pullrequest_title',class_="large",value=c.pull_request.title,placeholder=_('Summarize the changes'))}
 
              </div>
 
          </div>
 

	
 
          <div class="field">
 
              <div class="label-summary label-textarea">
 
                  <label for="pullrequest_desc">${_('Description')}:</label>
 
              </div>
 
              <div class="textarea text-area editor">
 
                  ${h.textarea('pullrequest_desc',size=30,content=c.pull_request.description,placeholder=_('Write a short description on this pull request'))}
 
              </div>
 
          </div>
 
        </div>
 
        %endif
 

	
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Reviewer voting result')}:</label>
 
          </div>
 
          <div class="input">
 
            <div class="changeset-status-container" style="float:none;clear:both">
 
            %if c.current_voting_result:
 
              <span class="changeset-status-ico" style="padding:0px 4px 0px 0px">
 
                  <i class="icon-circle changeset-status-${c.current_voting_result}" title="${_('Pull request status calculated from votes')}"></i></span>
 
              <span class="changeset-status-lbl tooltip" title="${_('Pull request status calculated from votes')}">
 
                %if c.pull_request.is_closed():
 
                    ${_('Closed')},
 
                %endif
 
                ${h.changeset_status_lbl(c.current_voting_result)}
 
              </span>
 
            %endif
 
            </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Still not reviewed by')}:</label>
 
          </div>
 
          <div class="input">
 
            % if len(c.pull_request_pending_reviewers) > 0:
 
                <div class="tooltip" title="${', '.join([x.username for x in c.pull_request_pending_reviewers])}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
 
            % elif len(c.pull_request_reviewers) > 0:
 
                <div>${_('Pull request was reviewed by all reviewers')}</div>
 
            %else:
 
                <div>${_('There are no reviewers')}</div>
 
            %endif
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Origin')}:</label>
 
          </div>
 
          <div class="input">
 
            <div>
 
              ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
 
              %if c.cs_ref_type != 'branch':
 
                ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.cs_branch_name)}
 
              %endif
 
            </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Target')}:</label>
 
          </div>
 
          <div class="input">
 
            %if c.is_range:
 
              ${_("This is just a range of changesets and doesn't have a target or a real merge ancestor.")}
 
            %else:
 
              ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.a_ref_type, c.a_ref_name)}
 
              ## we don't know other rev - c.a_rev is ancestor and not necessarily on other_name_branch branch
 
            %endif
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Pull changes')}:</label>
 
          </div>
 
          <div class="input">
 
              <div>
 
               ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
 
               %if h.is_hg(c.pull_request.org_repo):
 
                 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
 
               %elif h.is_git(c.pull_request.org_repo):
 
                 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
 
               %endif
 
              </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Created on')}:</label>
 
          </div>
 
          <div class="input">
 
              <div>${h.fmt_date(c.pull_request.created_on)}</div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Owner')}:</label>
 
          </div>
 
          <div class="input pr-not-edit">
 
                  <div class="gravatar">
 
                    ${h.gravatar(c.pull_request.owner.email, size=20)}
 
                  </div>
 
                  <span>${c.pull_request.owner.full_name_and_username}</span><br/>
 
                  <span><a href="mailto:${c.pull_request.owner.email}">${c.pull_request.owner.email}</a></span><br/>
 
          </div>
 
          <div class="input pr-do-edit ac" style="display:none">
 
               ${h.text('owner', class_="large", value=c.pull_request.owner.username, placeholder=_('Username'))}
 
               <div id="owner_completion_container"></div>
 
          </div>
 
        </div>
 

	
 
        <div class="field">
 
            <div class="label-summary">
 
              <label>${_('Update')}:</label>
 
            </div>
 
            <div class="input">
 
              <div class="msg-div">${c.update_msg}</div>
 
              %if c.avail_revs:
 
              <div id="updaterevs" style="max-height:200px; overflow-y:auto; overflow-x:hidden; margin-bottom: 10px; padding: 1px 0">
 
                <div style="height:0">
 
                  <canvas id="avail_graph_canvas" style="width:0"></canvas>
 
                </div>
 
                <table id="updaterevs-table" class="noborder" style="padding-left:50px">
 
                  %for cnt, cs in enumerate(c.avail_cs):
 
                    <tr id="chg_available_${cnt+1}">
 
                      %if cs.revision == c.cs_ranges[-1].revision:
 
                        <td>
 
                          %if editable:
 
                            ${h.radio(name='updaterev', value='')}
 
                          %endif
 
                        </td>
 
                        <td colspan="2">${_("Current revision - no change")}</td>
 
                      %else:
 
                        <td>
 
                          %if editable and cs.revision in c.avail_revs:
 
                            ${h.radio(name='updaterev', value=cs.raw_id)}
 
                          %endif
 
                        </td>
 
                        <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</td>
 
                        <td><div class="message" style="white-space:normal; height:1.1em; max-width: 500px; padding:0">${h.urlify_commit(cs.message, c.repo_name)}</div></td>
 
                      %endif
 
                    </tr>
 
                  %endfor
 
                </table>
 
              </div>
 
              %endif
 
              <div class="msg-div">${c.update_msg_other}</div>
 
            </div>
 
        </div>
 
      </div>
 
    </div>
 
    ## REVIEWERS
 
    <div style="float:left; border-left:1px dashed #eee">
 
        <div class="pr-details-title">${_('Pull Request Reviewers')}</div>
 
        <div id="reviewers" style="padding:0px 0px 5px 10px">
 
          ## members goes here !
 
          <div>
 
            <ul id="review_members" class="group_members">
 
            %for member,status in c.pull_request_reviewers:
 
              ## WARNING: the HTML below is duplicate with
 
              ## kallithea/public/js/base.js
 
              ## If you change something here it should be reflected in the template too.
 
              <li id="reviewer_${member.user_id}">
 
                <div class="reviewers_member">
 
                    <div class="reviewer_status tooltip" title="${h.changeset_status_lbl(status.status if status else 'not_reviewed')}">
 
                      <i class="icon-circle changeset-status-${status.status if status else 'not_reviewed'}"></i>
 
                    </div>
 
                  <div class="reviewer_gravatar gravatar">
 
                    ${h.gravatar(member.email, size=14)}
 
                  </div>
 
                  <div style="float:left;">
 
                    ${member.full_name_and_username}
 
                    %if c.pull_request.user_id == member.user_id:
 
                      (${_('Owner')})
 
                    %endif
 
                  </div>
 
                  <input type="hidden" value="${member.user_id}" name="review_members" />
 
                  %if editable:
 
                  <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
 
                      <i class="icon-minus-circled"></i>
 
                  </div>
 
                  %endif
 
                </div>
 
              </li>
 
            %endfor
 
            </ul>
 
          </div>
 
          %if editable:
 
          <div class='ac'>
 
            <div class="reviewer_ac">
 
               ${h.text('user', class_='yui-ac-input',placeholder=_('Type name of reviewer to add'))}
 
               <div id="reviewers_container"></div>
 
            </div>
 
          </div>
 
          %endif
 
        </div>
 

	
 
        %if not c.pull_request_reviewers:
 
        <div class="pr-details-title">${_('Potential Reviewers')}</div>
 
        <div style="margin: 10px 0 10px 10px; max-width: 250px">
 
          <div>
 
            ${_('Click to add the repository owner as reviewer:')}
 
          </div>
 
          <ul style="margin-top: 10px">
 
            %for u in [c.pull_request.other_repo.user]:
 
              <li>
 
                <a class="missing_reviewer missing_reviewer_${u.user_id}"
 
                  user_id="${u.user_id}"
 
                  fname="${u.name}"
 
                  lname="${u.lastname}"
 
                  nname="${u.username}"
 
                  gravatar_lnk="${h.gravatar_url(u.email, size=28)}"
 
                  gravatar_size="14"
 
                  title="Click to add reviewer to the list, then Save Changes.">${u.full_name}</a>
 
              </li>
 
            %endfor
 
          </ul>
 
        </div>
 
        %endif
 
    </div>
 
    <div class="form" style="clear:both">
 
      <div class="fields">
 
        %if editable:
 
          <div class="buttons">
 
            ${h.submit('pr-form-save',_('Save Changes'),class_="btn btn-small")}
 
            ${h.submit('pr-form-clone',_('Save as New Pull Request'),class_="btn btn-small",disabled='disabled')}
 
            ${h.reset('pr-form-reset',_('Cancel Changes'),class_="btn btn-small")}
 
          </div>
 
        %endif
 
      </div>
 
    </div>
 
  ${h.end_form()}
 
</div>
 

	
 
<div class="box">
 
    <div class="title">
 
      <div class="breadcrumbs">${_('Pull Request Content')}</div>
 
    </div>
 
    <div class="table">
 
          <div id="changeset_compare_view_content">
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 
                  ${comment.comment_count(c.inline_cnt, len(c.comments))}
 
              </div>
 
              ##CS
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 
                ${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}
 
              </div>
 
              <%include file="/compare/compare_cs.html" />
 

	
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 
              ${_('Common ancestor')}:
 
              ${h.link_to(h.short_id(c.a_rev),h.url('changeset_home',repo_name=c.a_repo.repo_name,revision=c.a_rev))}
 
              </div>
 

	
 
              ## FILES
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 

	
 
              % if c.limited_diff:
 
                  ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
 
              % else:
 
                  ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
 
              %endif
 

	
 
              </div>
 
              <div class="cs_files">
 
                %if not c.files:
 
                   <span class="empty_data">${_('No files')}</span>
 
                %endif
 
                %for fid, change, f, stat in c.files:
 
                    <div class="cs_${change}">
 
                      <div class="node">
 
                          <i class="icon-diff-${change}"></i>
 
                          ${h.link_to(h.safe_unicode(f),'#' + fid)}
 
                      </div>
 
                      <div class="changes">${h.fancy_file_stats(stat)}</div>
 
                    </div>
 
                %endfor
 
              </div>
 
              % if c.limited_diff:
 
                <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
 
              % endif
 
          </div>
 
    </div>
 
    <script>
 
    var _USERS_AC_DATA = ${c.users_array|n};
 
    var _GROUPS_AC_DATA = ${c.user_groups_array|n};
 
    // TODO: switch this to pyroutes
 
    AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
 
    AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
 

	
 
    pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
 
    pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
 

	
 
    </script>
 

	
 
    ## diff block
 
    <div class="commentable-diff">
 
    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
 
    ${diff_block.diff_block_js()}
 
    %for fid, change, f, stat in c.files:
 
      ${diff_block.diff_block_simple([c.changes[fid]])}
 
    %endfor
 
    % if c.limited_diff:
 
      <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
 
    % endif
 
    </div>
 

	
 
    ## template for inline comment form
 
    ${comment.comment_inline_form()}
 

	
 
    ## render comments and inlines
 
    ${comment.generate_comments()}
 

	
 
    ## main comment form and it status
 
    ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
 
                              pull_request_id=c.pull_request.pull_request_id),
 
                       c.current_voting_result,
 
                       is_pr=True, change_status=c.allowed_to_change_status)}
 

	
 
    <script type="text/javascript">
 
      $(document).ready(function(){
 
          PullRequestAutoComplete($('#user'), $('#reviewers_container'), _USERS_AC_DATA);
 
          SimpleUserAutoComplete($('#owner'), $('#owner_completion_container'), _USERS_AC_DATA);
 

	
 
          $('.code-difftable').on('click', '.add-bubble', function(e){
 
              show_comment_form($(this));
 
          });
 

	
 
          var avail_jsdata = ${c.avail_jsdata|n};
 
          var avail_r = new BranchRenderer('avail_graph_canvas', 'updaterevs-table', 'chg_available_');
 
          avail_r.render(avail_jsdata,40);
 

	
 
          move_comments($(".comments .comments-list-chunk"));
 

	
 
          $('#updaterevs input').change(function(e){
 
              var update = !!e.target.value;
 
              $('#pr-form-save').prop('disabled',update);
 
              $('#pr-form-clone').prop('disabled',!update);
 
          });
 
          var $org_review_members = $('#review_members').clone();
 
          $('#pr-form-reset').click(function(e){
 
              $('.pr-do-edit').hide();
 
              $('.pr-not-edit').show();
 
              $('#pr-form-save').prop('disabled',false);
 
              $('#pr-form-clone').prop('disabled',true);
 
              $('#review_members').html($org_review_members);
 
          });
 

	
 
          // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
 
          if (window.location.hash != "") {
 
              window.location.href = window.location.href;
 
          }
 

	
 
          $('.missing_reviewer').click(function(){
 
            var $this = $(this);
 
            addReviewMember($this.attr('user_id'), $this.attr('fname'), $this.attr('lname'), $this.attr('nname'), $this.attr('gravatar_lnk'), $this.attr('gravatar_size'));
 
            addReviewMember(
 
                $this.attr('user_id'),
 
                $this.attr('fname').html_escape(),
 
                $this.attr('lname').html_escape(),
 
                $this.attr('nname').html_escape(),
 
                $this.attr('gravatar_lnk'),
 
                $this.attr('gravatar_size')
 
            );
 
          });
 
      });
 
    </script>
 

	
 
</div>
 

	
 
</%def>
0 comments (0 inline, 0 general)