diff options
15 files changed, 373 insertions, 36 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index 82a5708b8f9..3f33fd9dc95 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -66,6 +66,7 @@ module.exports = (grunt) -> '<%= BUILD_PATH %>/js/libs/third-party/numeral-languages.js' '<%= BUILD_PATH %>/js/libs/third-party/bootstrap/tooltip.js' '<%= BUILD_PATH %>/js/libs/third-party/bootstrap/dropdown.js' + '<%= BUILD_PATH %>/js/libs/third-party/md5.js' '<%= BUILD_PATH %>/js/libs/select2-jquery-ui-fix.js' '<%= BUILD_PATH %>/js/libs/widgets/base.js' diff --git a/server/sonar-web/src/main/coffee/apps/issues/models/issues.coffee b/server/sonar-web/src/main/coffee/apps/issues/models/issues.coffee index 08926040335..d98b2632120 100644 --- a/server/sonar-web/src/main/coffee/apps/issues/models/issues.coffee +++ b/server/sonar-web/src/main/coffee/apps/issues/models/issues.coffee @@ -43,6 +43,7 @@ define [ project = find r.projects, issue.project subProject = find r.components, issue.subProject rule = find r.rules, issue.rule + assignee = find r.users, issue.assignee, 'login' _.extend issue, index: index @@ -67,25 +68,9 @@ define [ _.extend issue, ruleName: rule.name - if _.isArray(issue.sources) && issue.sources.length > 0 - source = '' - issue.sources.forEach (line) -> - source = line[1] if line[0] == issue.line - _.extend issue, source: source - - - if _.isArray(issue.scm) && issue.scm.length > 0 - scmAuthor = '' - scmDate = '' - - issue.scm.forEach (line) -> - if line[0] == issue.line - scmAuthor = line[1] - scmDate = line[2] - + if assignee _.extend issue, - scmAuthor: scmAuthor - scmDate: scmDate + assigneeEmail: assignee.email issue diff --git a/server/sonar-web/src/main/coffee/components/issue/collections/issues.coffee b/server/sonar-web/src/main/coffee/components/issue/collections/issues.coffee index ddce4332d2e..208a06ffb3b 100644 --- a/server/sonar-web/src/main/coffee/components/issue/collections/issues.coffee +++ b/server/sonar-web/src/main/coffee/components/issue/collections/issues.coffee @@ -47,6 +47,7 @@ define [ component = find r.components, issue.component project = find r.projects, issue.project rule = find r.rules, issue.rule + assignee = find r.users, issue.assignee, 'login' if component _.extend issue, @@ -62,4 +63,8 @@ define [ _.extend issue, ruleName: rule.name + if assignee + _.extend issue, + assigneeEmail: assignee.email + issue diff --git a/server/sonar-web/src/main/coffee/components/issue/templates/issue.hbs b/server/sonar-web/src/main/coffee/components/issue/templates/issue.hbs index dc47a49ddb9..b27f6b53c81 100644 --- a/server/sonar-web/src/main/coffee/components/issue/templates/issue.hbs +++ b/server/sonar-web/src/main/coffee/components/issue/templates/issue.hbs @@ -56,13 +56,20 @@ <div class="issue-meta"> {{#inArray actions "assign"}} <a class="issue-action issue-action-with-options js-issue-assign"> - <span - class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span> <i - class="icon-dropdown"></i> + {{#if assignee}} + {{#ifShowAvatars}} + <span class="text-top">{{avatarHelper assigneeEmail 16}}</span> + {{/ifShowAvatars}} + {{/if}} + <span class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span> <i class="icon-dropdown"></i> </a> {{else}} - <span - class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span> + {{#if assignee}} + {{#ifShowAvatars}} + <span class="text-top">{{avatarHelper assigneeEmail 16}}</span> + {{/ifShowAvatars}} + {{/if}} + <span class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span> {{/inArray}} </div> diff --git a/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js b/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js index 9ee43d9fdfe..5a0915e7b20 100644 --- a/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js +++ b/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js @@ -85,6 +85,7 @@ define([ return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), { user: window.SS.user, userName: window.SS.userName, + userEmail: window.SS.userEmail, isUserAdmin: window.SS.isUserAdmin, canManageGlobalDashboards: !!window.SS.user, diff --git a/server/sonar-web/src/main/js/apps/nav/templates/nav-global-navbar.hbs b/server/sonar-web/src/main/js/apps/nav/templates/nav-global-navbar.hbs index fcaf9cc2672..11284eee4a9 100644 --- a/server/sonar-web/src/main/js/apps/nav/templates/nav-global-navbar.hbs +++ b/server/sonar-web/src/main/js/apps/nav/templates/nav-global-navbar.hbs @@ -71,7 +71,7 @@ {{#if user}} <li class="dropdown"> <a class="dropdown-toggle" data-toggle="dropdown" href="#"> - {{userName}} <span class="icon-dropdown"></span> + {{#ifShowAvatars}}<span class="little-spacer-right">{{avatarHelper userEmail 20}} {{/ifShowAvatars}}</span>{{userName}} <span class="icon-dropdown"></span> </a> <ul class="dropdown-menu dropdown-menu-right"> <li> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs index 1cd782dd9ed..c3d34d75f53 100644 --- a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs @@ -5,6 +5,12 @@ <a class="js-user-deactivate icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a> </div> +{{#ifShowAvatars}} + <div class="display-inline-block text-top big-spacer-right"> + {{avatarHelper email 36}} + </div> +{{/ifShowAvatars}} + <div class="display-inline-block text-top width-30"> <div> <strong class="js-user-name">{{name}}</strong> diff --git a/server/sonar-web/src/main/js/components/common/handlebars-extensions.js b/server/sonar-web/src/main/js/components/common/handlebars-extensions.js index 610d5425dc2..a92f2d19026 100644 --- a/server/sonar-web/src/main/js/components/common/handlebars-extensions.js +++ b/server/sonar-web/src/main/js/components/common/handlebars-extensions.js @@ -562,4 +562,19 @@ } }); + Handlebars.registerHelper('ifShowAvatars', function (options) { + var cond = window.SS && window.SS.lf && window.SS.lf.enableGravatar; + return cond ? options.fn(this) : options.inverse(this); + }); + + Handlebars.registerHelper('avatarHelper', function (email, size) { + var emailHash = window.md5((email || '').trim()), + url = ('' + window.SS.lf.gravatarServerUrl) + .replace('{EMAIL_MD5}', emailHash) + .replace('{SIZE}', size); + return new Handlebars.SafeString( + '<img src="' + url + '" width="' + size + '" height="' + size + '" alt="' + email + '">' + ); + }); + })(); diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs index 123d21e86a2..1f4c3ffeb19 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs +++ b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs @@ -13,7 +13,11 @@ <td class="source-meta source-line-scm" {{#if line}}data-line-number="{{line}}"{{/if}}> {{#ifSCMChanged2 ../source line}} - <div class="source-line-scm-inner" data-author="{{scmAuthor}}"></div> + <div class="source-line-scm-inner" data-author="{{scmAuthor}}"> + {{#gt line 0}} + {{avatarHelper scmAuthor 16}} + {{/gt}} + </div> {{/ifSCMChanged2}} </td> diff --git a/server/sonar-web/src/main/js/libs/application.js b/server/sonar-web/src/main/js/libs/application.js index 2d16dd4ff5d..6b7fc913488 100644 --- a/server/sonar-web/src/main/js/libs/application.js +++ b/server/sonar-web/src/main/js/libs/application.js @@ -549,6 +549,20 @@ function closeModalWindow () { return params; }; + + /** + * Return an md5 hash of a string + * @param s + * @returns {*} + */ + window.getMD5Hash = function (s) { + if (typeof s === 'string') { + return window.md5(s.trim()); + } else { + return null; + } + }; + })(); (function () { diff --git a/server/sonar-web/src/main/js/libs/third-party/md5.js b/server/sonar-web/src/main/js/libs/third-party/md5.js new file mode 100644 index 00000000000..f92ba37a4de --- /dev/null +++ b/server/sonar-web/src/main/js/libs/third-party/md5.js @@ -0,0 +1,274 @@ +/* + * JavaScript MD5 1.0.1 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/*jslint bitwise: true */ +/*global unescape, define */ + +(function ($) { + 'use strict'; + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safe_add(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + + /* + * Bitwise rotate a 32-bit number to the left. + */ + function bit_rol(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); + } + + /* + * These functions implement the four basic operations the algorithm uses. + */ + function md5_cmn(q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); + } + function md5_ff(a, b, c, d, x, s, t) { + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); + } + function md5_gg(a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); + } + function md5_hh(a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t); + } + function md5_ii(a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); + } + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ + function binl_md5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << (len % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var i, olda, oldb, oldc, oldd, + a = 1732584193, + b = -271733879, + c = -1732584194, + d = 271733878; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5_ff(a, b, c, d, x[i], 7, -680876936); + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i], 20, -373897302); + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5_hh(d, a, b, c, x[i], 11, -358537222); + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i], 6, -198630844); + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return [a, b, c, d]; + } + + /* + * Convert an array of little-endian words to a string + */ + function binl2rstr(input) { + var i, + output = ''; + for (i = 0; i < input.length * 32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); + } + return output; + } + + /* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ + function rstr2binl(input) { + var i, + output = []; + output[(input.length >> 2) - 1] = undefined; + for (i = 0; i < output.length; i += 1) { + output[i] = 0; + } + for (i = 0; i < input.length * 8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); + } + return output; + } + + /* + * Calculate the MD5 of a raw string + */ + function rstr_md5(s) { + return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); + } + + /* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ + function rstr_hmac_md5(key, data) { + var i, + bkey = rstr2binl(key), + ipad = [], + opad = [], + hash; + ipad[15] = opad[15] = undefined; + if (bkey.length > 16) { + bkey = binl_md5(bkey, key.length * 8); + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); + } + + /* + * Convert a raw string to a hex string + */ + function rstr2hex(input) { + var hex_tab = '0123456789abcdef', + output = '', + x, + i; + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt(x & 0x0F); + } + return output; + } + + /* + * Encode a string as utf-8 + */ + function str2rstr_utf8(input) { + return unescape(encodeURIComponent(input)); + } + + /* + * Take string arguments and return either raw or hex encoded strings + */ + function raw_md5(s) { + return rstr_md5(str2rstr_utf8(s)); + } + function hex_md5(s) { + return rstr2hex(raw_md5(s)); + } + function raw_hmac_md5(k, d) { + return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)); + } + function hex_hmac_md5(k, d) { + return rstr2hex(raw_hmac_md5(k, d)); + } + + function md5(string, key, raw) { + if (!key) { + if (!raw) { + return hex_md5(string); + } + return raw_md5(string); + } + if (!raw) { + return hex_hmac_md5(key, string); + } + return raw_hmac_md5(key, string); + } + + if (typeof define === 'function' && define.amd) { + define(function () { + return md5; + }); + } else { + $.md5 = md5; + } +}(this)); diff --git a/server/sonar-web/src/main/less/components/source.less b/server/sonar-web/src/main/less/components/source.less index 6e02fe6e7ad..c2f8237c638 100644 --- a/server/sonar-web/src/main/less/components/source.less +++ b/server/sonar-web/src/main/less/components/source.less @@ -193,13 +193,6 @@ .source-line-scm-inner { max-width: 40px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:before { - content: attr(data-author); - } } .source-line-bar { diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb index 7c8dd530efa..322c7bcbbf8 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb @@ -24,10 +24,16 @@ <script> var pageLang = '<%= I18n.locale.to_s.gsub(/-/, '_') -%>'; <%# The two lines below mean that before full removal of Rails, we have to find a way to handle config properties %> - window.SS = typeof window.SS === 'object' ? window.SS : {}; - window.SS.hoursInDay = <%= configuration('sonar.technicalDebt.hoursInDay', 8) %>; - window.SS.user = '<%= current_user.login if current_user -%>'; - window.SS.userName = '<%= current_user.name if current_user -%>'; + window.SS = { + hoursInDay: <%= configuration('sonar.technicalDebt.hoursInDay', 8) %>, + user: '<%= current_user.login if current_user -%>', + userName: '<%= current_user.name if current_user -%>', + userEmail: '<%= current_user.email if current_user -%>', + lf: { + enableGravatar: <%= configuration('sonar.lf.enableGravatar', true) %>, + gravatarServerUrl: '<%= configuration('sonar.lf.gravatarServerUrl') %>' + } + }; </script> <script src="<%= ApplicationController.root_context -%>/js/sonar.js?v=<%= sonar_version -%>"></script> <script> diff --git a/server/sonar-web/src/test/views/layouts/main.jade b/server/sonar-web/src/test/views/layouts/main.jade index 5e4f16cff19..e1891e03b55 100644 --- a/server/sonar-web/src/test/views/layouts/main.jade +++ b/server/sonar-web/src/test/views/layouts/main.jade @@ -20,6 +20,7 @@ html script(src='/js/libs/third-party/numeral-languages.js') script(src='/js/libs/third-party/bootstrap/tooltip.js') script(src='/js/libs/third-party/bootstrap/dropdown.js') + script(src='/js/libs/third-party/md5.js') script(src='/js/libs/select2-jquery-ui-fix.js') script(src='/js/libs/widgets/base.js') script(src='/js/libs/widgets/widget.js') @@ -53,6 +54,16 @@ html jQuery.mockjaxSettings.contentType = 'text/json'; jQuery.mockjaxSettings.responseTime = 50; jQuery(document).ready(function () { $j('.open-modal').modal(); }); + window.SS = { + hoursInDay: 8, + user: '', + userName: '', + userEmail: '', + lf: { + enableGravatar: false, + gravatarServerUrl: '' + } + }; script. requirejs.config({ baseUrl: baseUrl + '/js' }); diff --git a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java index a6711488e2d..8a19cd775c7 100644 --- a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java +++ b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java @@ -59,6 +59,21 @@ public class CorePropertyDefinitions { .category(CoreProperties.CATEGORY_GENERAL) .subCategory(CoreProperties.SUBCATEGORY_LOOKNFEEL) .build(), + PropertyDefinition.builder("sonar.lf.enableGravatar") + .name("Enable support of gravatars") + .description("Gravatars are profile pictures of users based on their email.") + .type(PropertyType.BOOLEAN) + .defaultValue("true") + .category(CoreProperties.CATEGORY_GENERAL) + .subCategory(CoreProperties.SUBCATEGORY_LOOKNFEEL) + .build(), + PropertyDefinition.builder("sonar.lf.gravatarServerUrl") + .name("Gravatar URL") + .description("Optional URL of custom Gravatar service. Accepted variables are {EMAIL_MD5} for MD5 hash of email and {SIZE} for the picture size in pixels.") + .defaultValue("https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon") + .category(CoreProperties.CATEGORY_GENERAL) + .subCategory(CoreProperties.SUBCATEGORY_LOOKNFEEL) + .build(), // ISSUES PropertyDefinition.builder(CoreProperties.DEFAULT_ISSUE_ASSIGNEE) |