Add a configurable avatar system and add an internal avatar system for enterprise

This commit is contained in:
Joseph Schorr 2014-11-24 19:25:13 -05:00
parent f6dd8b0a4d
commit e9cac407df
36 changed files with 241 additions and 92 deletions

View file

@ -8,7 +8,7 @@ ENV HOME /root
RUN apt-get update # 10SEP2014
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62 libjpeg62-dev libevent-2.0.5 libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap-2.4-2 libldap2-dev libsasl2-modules libsasl2-dev libpq5 libpq-dev
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62 libjpeg62-dev libevent-2.0.5 libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap-2.4-2 libldap2-dev libsasl2-modules libsasl2-dev libpq5 libpq-dev libfreetype6-dev
# Build the python dependencies
ADD requirements.txt requirements.txt

2
app.py
View file

@ -25,6 +25,7 @@ from data.buildlogs import BuildLogs
from data.archivedlogs import LogArchive
from data.queue import WorkQueue
from data.userevent import UserEventsBuilderModule
from avatars.avatars import Avatar
class Config(BaseConfig):
@ -119,6 +120,7 @@ features.import_features(app.config)
Principal(app, use_sessions=False)
avatar = Avatar(app)
login_manager = LoginManager(app)
mail = Mail(app)
storage = Storage(app)

0
avatars/__init__.py Normal file
View file

63
avatars/avatars.py Normal file
View file

@ -0,0 +1,63 @@
import hashlib
class Avatar(object):
def __init__(self, app=None):
self.app = app
self.state = self._init_app(app)
def _init_app(self, app):
return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')](
app.config['SERVER_HOSTNAME'],
app.config['PREFERRED_URL_SCHEME'])
def __getattr__(self, name):
return getattr(self.state, name, None)
class BaseAvatar(object):
""" Base class for all avatar implementations. """
def __init__(self, server_hostname, preferred_url_scheme):
self.server_hostname = server_hostname
self.preferred_url_scheme = preferred_url_scheme
def get_url(self, email, size=16, name=None):
""" Returns the full URL for viewing the avatar of the given email address, with
an optional size.
"""
raise NotImplementedError
def compute_hash(self, email, name=None):
""" Computes the avatar hash for the given email address. If the name is given and a default
avatar is being computed, the name can be used in place of the email address. """
raise NotImplementedError
class GravatarAvatar(BaseAvatar):
""" Avatar system that uses gravatar for generating avatars. """
def compute_hash(self, email, name=None):
return hashlib.md5(email.strip().lower()).hexdigest()
def get_url(self, email, size=16, name=None):
computed = self.compute_hash(email, name=name)
return '%s://www.gravatar.com/avatar/%s?d=identicon&size=%s' % (self.preferred_url_scheme,
computed, size)
class LocalAvatar(BaseAvatar):
""" Avatar system that uses the local system for generating avatars. """
def compute_hash(self, email, name=None):
if not name and not email:
return ''
prefix = name if name else email
return prefix[0] + hashlib.md5(email.strip().lower()).hexdigest()
def get_url(self, email, size=16, name=None):
computed = self.compute_hash(email, name=name)
return '%s://%s/avatar/%s?size=%s' % (self.preferred_url_scheme, self.server_hostname,
computed, size)
AVATAR_CLASSES = {
'gravatar': GravatarAvatar,
'local': LocalAvatar
}

View file

@ -19,7 +19,7 @@ def build_requests_session():
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
'CONTACT_INFO']
'CONTACT_INFO', 'AVATAR_KIND']
def getFrontendVisibleConfig(config_dict):
@ -46,6 +46,8 @@ class DefaultConfig(object):
PREFERRED_URL_SCHEME = 'http'
SERVER_HOSTNAME = 'localhost:5000'
AVATAR_KIND = 'local'
REGISTRY_TITLE = 'Quay.io'
REGISTRY_TITLE_SHORT = 'Quay.io'
CONTACT_INFO = [

View file

@ -474,7 +474,7 @@ class OAuthApplication(BaseModel):
name = CharField()
description = TextField(default='')
gravatar_email = CharField(null=True)
avatar_email = CharField(null=True, db_column='gravatar_email')
class OAuthAuthorizationCode(BaseModel):

View file

@ -2,7 +2,7 @@ import logging
from flask import request
from app import billing as stripe
from app import billing as stripe, avatar
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action, show_if)
@ -13,7 +13,6 @@ from auth.permissions import (AdministerOrganizationPermission, OrganizationMem
from auth.auth_context import get_authenticated_user
from data import model
from data.billing import get_plan
from util.gravatar import compute_hash
import features
@ -28,7 +27,7 @@ def org_view(o, teams):
view = {
'name': o.username,
'email': o.email if is_admin else '',
'gravatar': compute_hash(o.email),
'avatar': avatar.compute_hash(o.email, name=o.username),
'teams': {t.name : team_view(o.username, t) for t in teams},
'is_admin': is_admin
}
@ -274,14 +273,16 @@ class ApplicationInformation(ApiResource):
if not application:
raise NotFound()
org_hash = compute_hash(application.organization.email)
gravatar = compute_hash(application.gravatar_email) if application.gravatar_email else org_hash
org_hash = avatar.compute_hash(application.organization.email,
name=application.organization.username)
app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
application.avatar_email else org_hash)
return {
'name': application.name,
'description': application.description,
'uri': application.application_uri,
'gravatar': gravatar,
'avatar': app_hash,
'organization': org_view(application.organization, [])
}
@ -297,7 +298,7 @@ def app_view(application):
'client_id': application.client_id,
'client_secret': application.client_secret if is_admin else None,
'redirect_uri': application.redirect_uri if is_admin else None,
'gravatar_email': application.gravatar_email if is_admin else None,
'avatar_email': application.avatar_email if is_admin else None,
}
@ -330,9 +331,9 @@ class OrganizationApplications(ApiResource):
'type': 'string',
'description': 'The human-readable description for the application',
},
'gravatar_email': {
'avatar_email': {
'type': 'string',
'description': 'The e-mail address of the gravatar to use for the application',
'description': 'The e-mail address of the avatar to use for the application',
}
},
},
@ -371,7 +372,7 @@ class OrganizationApplications(ApiResource):
app_data.get('application_uri', ''),
app_data.get('redirect_uri', ''),
description = app_data.get('description', ''),
gravatar_email = app_data.get('gravatar_email', None),)
avatar_email = app_data.get('avatar_email', None),)
app_data.update({
@ -416,9 +417,9 @@ class OrganizationApplicationResource(ApiResource):
'type': 'string',
'description': 'The human-readable description for the application',
},
'gravatar_email': {
'avatar_email': {
'type': 'string',
'description': 'The e-mail address of the gravatar to use for the application',
'description': 'The e-mail address of the avatar to use for the application',
}
},
},
@ -462,7 +463,7 @@ class OrganizationApplicationResource(ApiResource):
application.application_uri = app_data['application_uri']
application.redirect_uri = app_data['redirect_uri']
application.description = app_data.get('description', '')
application.gravatar_email = app_data.get('gravatar_email', None)
application.avatar_email = app_data.get('avatar_email', None)
application.save()
app_data.update({

View file

@ -6,7 +6,7 @@ from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
AdministerOrganizationPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from app import avatar
@resource('/v1/entities/<prefix>')
@ -44,7 +44,7 @@ class EntitySearch(ApiResource):
'name': namespace_name,
'kind': 'org',
'is_org_member': True,
'gravatar': compute_hash(organization.email),
'avatar': avatar.compute_hash(organization.email, name=organization.username),
}]
except model.InvalidOrganizationException:

View file

@ -8,7 +8,7 @@ from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
from util.useremails import send_org_invite_email
from util.gravatar import compute_hash
from app import avatar
import features
@ -63,7 +63,7 @@ def member_view(member, invited=False):
'name': member.username,
'kind': 'user',
'is_robot': member.robot,
'gravatar': compute_hash(member.email) if not member.robot else None,
'avatar': avatar.compute_hash(member.email, name=member.username) if not member.robot else None,
'invited': invited,
}
@ -75,7 +75,7 @@ def invite_view(invite):
return {
'email': invite.email,
'kind': 'invite',
'gravatar': compute_hash(invite.email),
'avatar': avatar.compute_hash(invite.email),
'invited': True
}

View file

@ -5,7 +5,7 @@ from flask import request
from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app, billing as stripe, authentication
from app import app, billing as stripe, authentication, avatar
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, parse_args,
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
@ -20,7 +20,6 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
UserAdminPermission, UserReadPermission, SuperUserPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
from util.names import parse_single_urn
@ -34,7 +33,7 @@ def user_view(user):
admin_org = AdministerOrganizationPermission(o.username)
return {
'name': o.username,
'gravatar': compute_hash(o.email),
'avatar': avatar.compute_hash(o.email, name=o.username),
'is_org_admin': admin_org.can(),
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
'preferred_namespace': not (o.stripe_id is None)
@ -61,7 +60,7 @@ def user_view(user):
'anonymous': False,
'username': user.username,
'email': user.email,
'gravatar': compute_hash(user.email),
'avatar': avatar.compute_hash(user.email, name=user.username),
}
user_admin = UserAdminPermission(user.username)
@ -572,10 +571,12 @@ def authorization_view(access_token):
'name': oauth_app.name,
'description': oauth_app.description,
'url': oauth_app.application_uri,
'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email,
name=oauth_app.name),
'organization': {
'name': oauth_app.organization.username,
'gravatar': compute_hash(oauth_app.organization.email)
'avatar': avatar.compute_hash(oauth_app.organization.email,
name=oauth_app.organization.username)
}
},
'scopes': scopes.get_scope_information(access_token.scope),

View file

@ -3,13 +3,15 @@ import os
from flask import (abort, redirect, request, url_for, make_response, Response,
Blueprint, send_from_directory, jsonify)
from avatar_generator import Avatar
from flask.ext.login import current_user
from urlparse import urlparse
from health.healthcheck import HealthCheck
from data import model
from data.model.oauth import DatabaseAuthorizationProvider
from app import app, billing as stripe, build_logs
from app import app, billing as stripe, build_logs, avatar
from auth.auth import require_session_login, process_oauth
from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission
from util.invoice import renderInvoiceToPdf
@ -18,7 +20,6 @@ from util.cache import no_cache
from endpoints.common import common_login, render_page_template, route_show_if, param_required
from endpoints.csrf import csrf_protect, generate_csrf_token
from util.names import parse_repository_name
from util.gravatar import compute_hash
from util.useremails import send_email_changed
from auth import scopes
@ -182,6 +183,18 @@ def status():
return response
@app.route("/avatar/<avatar_hash>")
def avatar(avatar_hash):
try:
size = int(request.args.get('size', 16))
except ValueError:
size = 16
generated = Avatar.generate(size, avatar_hash)
headers = {'Content-Type': 'image/png'}
return make_response(generated, 200, headers)
@web.route('/tos', methods=['GET'])
@no_cache
def tos():
@ -413,7 +426,8 @@ def request_authorization_code():
'url': oauth_app.application_uri,
'organization': {
'name': oauth_app.organization.username,
'gravatar': compute_hash(oauth_app.organization.email)
'avatar': avatar.compute_hash(oauth_app.organization.email,
name=oauth_app.organization.name)
}
}

View file

@ -4819,7 +4819,7 @@ i.slack-icon {
margin-bottom: 10px;
}
.member-listing .gravatar {
.member-listing .avatar {
vertical-align: middle;
margin-right: 10px;
}

View file

@ -1,6 +1,6 @@
<div class="application-info-element" style="padding-bottom: 18px">
<div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
<span class="avatar" size="48" hash="application.avatar"></span>
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4>
{{ application.organization.name }}

View file

@ -0,0 +1 @@
<img class="avatar-element" ng-src="{{ AvatarService.getAvatar(_hash, size) }}">

View file

@ -7,15 +7,15 @@
</span>
</span>
<span ng-if="entity.kind == 'org'">
<img ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&amp;d=identicon">
<span class="avatar" size="avatarSize || 16" hash="entity.avatar"></span>
<span class="entity-name">
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
</span>
</span>
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
<img class="gravatar" ng-if="showGravatar == 'true' && entity.gravatar" ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&amp;d=identicon">
<span ng-if="showGravatar != 'true' || !entity.gravatar">
<span class="avatar" size="avatarSize || 16" hash="entity.avatar" ng-if="showAvatar == 'true' && entity.avatar"></span>
<span ng-if="showAvatar != 'true' || !entity.avatar">
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
</span>

View file

@ -35,7 +35,7 @@
<li class="dropdown" ng-switch-when="false">
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
<span class="avatar" size="32" hash="user.avatar"></span>
{{ user.username }}
<span class="notifications-bubble"></span>
<b class="caret"></b>

View file

@ -1,20 +1,20 @@
<span class="namespace-selector-dropdown">
<span ng-show="user.organizations.length == 0">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
<span class="avatar" size="24" hash="user.avatar"></span>
<span class="namespace-name">{{user.username}}</span>
</span>
<div class="btn-group" ng-show="user.organizations.length > 0">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ namespaceObj.gravatar }}?s=16&d=identicon" />
<span class="avatar" size="16" hash="namespaceObj.avatar"></span>
{{namespace}} <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li class="namespace-item" ng-repeat="org in user.organizations"
ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''">
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)">
<img src="//www.gravatar.com/avatar/{{ org.gravatar }}?s=24&d=identicon" />
<span class="namespace-name">{{ org.name }}</span>
<span class="avatar" size="24" hash="org.avatar"></span>
<span class="namespace-name">{{ org.name }}</span>
</a>
<i class="fa fa-exclamation-triangle" ng-show="requireCreate && !namespaces[org.name].can_create_repo"
@ -25,7 +25,7 @@
<li class="divider"></li>
<li>
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
<span class="avatar" size="24" hash="user.avatar"></span>
<span class="namespace-name">{{ user.username }}</span>
</a>
</li>

View file

@ -3,7 +3,7 @@
<div class="circle" ng-class="getClass(notification)"></div>
<div class="message" ng-bind-html="getMessage(notification)"></div>
<div class="orginfo" ng-if="notification.organization">
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
<span class="avatar" size="24" hash="getAvatar(notification.organization)"></span>
<span class="orgname">{{ notification.organization }}</span>
</div>
</div>

View file

@ -1,5 +1,5 @@
<div class="organization-header-element">
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&amp;d=identicon">
<span class="avatar" size="24" hash="organization.avatar"></span>
<span class="organization-name" ng-show="teamName || clickable">
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</span>

View file

@ -9,7 +9,7 @@
<td>
<div class="current-repo">
<img class="dropdown-select-icon github-org-icon"
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}">
<a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank">{{ state.currentRepo.repo }}</a>
</div>
</td>
@ -53,7 +53,7 @@
<!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i>
<img class="dropdown-select-icon github-org-icon"
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/image/empty.png' }}">
<!-- Dropdown menu -->
<ul class="dropdown-select-menu scrollable-menu" role="menu">

BIN
static/img/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View file

@ -620,6 +620,51 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return pingService;
}]);
$provide.factory('AvatarService', ['Config', '$sanitize', 'md5',
function(Config, $sanitize, md5) {
var avatarService = {};
var cache = {};
avatarService.getAvatar = function(hash, opt_size) {
var size = opt_size || 16;
switch (Config['AVATAR_KIND']) {
case 'local':
return '/avatar/' + hash + '?size=' + size;
break;
case 'gravatar':
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size;
break;
}
};
avatarService.computeHash = function(opt_email, opt_name) {
var email = opt_email || '';
var name = opt_name || '';
var cacheKey = email + ':' + name;
if (!cacheKey) { return '-'; }
if (cache[cacheKey]) {
return cache[cacheKey];
}
var hash = md5.createHash(email.toString().toLowerCase());
switch (Config['AVATAR_KIND']) {
case 'local':
if (name) {
hash = name[0] + hash;
} else if (email) {
hash = email[0] + hash;
}
break;
}
return cache[cacheKey] = hash;
};
return avatarService;
}]);
$provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
function(UtilService, $sanitize, KeyService) {
@ -2441,8 +2486,8 @@ quayApp.directive('entityReference', function () {
scope: {
'entity': '=entity',
'namespace': '=namespace',
'showGravatar': '@showGravatar',
'gravatarSize': '@gravatarSize'
'showAvatar': '@showAvatar',
'avatarSize': '@avatarSize'
},
controller: function($scope, $element, UserService, UtilService) {
$scope.getIsAdmin = function(namespace) {
@ -4319,8 +4364,7 @@ quayApp.directive('entitySearch', function () {
} else if (datum.entity.kind == 'team') {
template += '<i class="fa fa-group fa-lg"></i>';
} else if (datum.entity.kind == 'org') {
template += '<i class="fa"><img src="//www.gravatar.com/avatar/' +
datum.entity.gravatar + '?s=16&amp;d=identicon"></i>';
template += '<i class="fa">' + AvatarService.getAvatar(datum.entity.avatar, 16) + '</i>';
}
template += '<span class="name">' + datum.value + '</span>';
@ -6043,9 +6087,9 @@ quayApp.directive('notificationView', function () {
return NotificationService.getMessage(notification);
};
$scope.getGravatar = function(orgname) {
$scope.getAvatar = function(orgname) {
var organization = UserService.getOrganization(orgname);
return organization['gravatar'] || '';
return organization['avatar'] || '';
};
$scope.parseDate = function(dateString) {
@ -6427,6 +6471,39 @@ quayApp.directive('locationView', function () {
});
quayApp.directive('avatar', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/avatar.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'hash': '=hash',
'email': '=email',
'name': '=name',
'size': '=size'
},
controller: function($scope, $element, AvatarService) {
$scope.AvatarService = AvatarService;
var refreshHash = function() {
if (!$scope.name && !$scope.email) { return; }
$scope._hash = AvatarService.computeHash($scope.email, $scope.name);
};
$scope.$watch('hash', function(hash) {
$scope._hash = hash;
});
$scope.$watch('name', refreshHash);
$scope.$watch('email', refreshHash);
}
};
return directiveDefinitionObject;
});
quayApp.directive('tagSpecificImagesView', function () {
var directiveDefinitionObject = {
priority: 0,

View file

@ -2736,8 +2736,8 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
delete $scope.application['description'];
}
if (!$scope.application['gravatar_email']) {
delete $scope.application['gravatar_email'];
if (!$scope.application['avatar_email']) {
delete $scope.application['avatar_email'];
}
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {

View file

@ -58,9 +58,6 @@
<small>Co-Founder</small></h3>
</div>
<div class="col-sm-3 col-md-2">
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/342ea83fd68d33f90b1f06f466d533c6?s=128&d=identicon">
</div>
<div class="col-sm-7 col-md-10">
<p>Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.</p>
</div>
@ -71,9 +68,6 @@
<small>Co-Founder</small></h3>
</div>
<div class="col-sm-3 col-md-2">
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/9fc3232622773fb2e8f71c0027601bc5?s=128&d=mm">
</div>
<div class="col-sm-7 col-md-10">
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. </p>
</div>

View file

@ -47,7 +47,7 @@
<div class="signup-form"></div>
</div>
<div ng-show="!user.anonymous" class="user-welcome">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
<span class="avatar" size="128" hash="user.avatar"></span>
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a>

View file

@ -57,7 +57,7 @@
<div class="signup-form"></div>
</div>
<div ng-show="!user.anonymous" class="user-welcome">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
<span class="avatar" size="128" hash="user.avatar"></span>
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a>

View file

@ -6,11 +6,11 @@
<!-- Header -->
<div class="row">
<div class="col-md-12">
<div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.gravatar_email | gravatar }}?s=48&d=identicon">
<h2>{{ application.name || '(Untitled)' }}</h2>
<div class="auth-header">
<h2><span class="avatar" size="48" email="application.avatar_email" name="application.name"></span>
{{ application.name || '(Untitled)' }}</h2>
<h4>
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
<span class="avatar" size="24" hash="organization.avatar" style="vertical-align: middle; margin-right: 4px;"></span>
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
</h4>
</div>
@ -59,9 +59,9 @@
</div>
<div class="form-group nested">
<label for="fieldAppGravatar">Gravatar E-mail (optional)</label>
<input type="email" class="form-control" id="fieldAppGravatar" placeholder="Gravatar E-mail" ng-model="application.gravatar_email">
<div class="description">An e-mail address representing the <a href="http://en.gravatar.com/" target="_blank">Gravatar</a> for the application. See above for the icon.</div>
<label for="fieldAppAvatar">Avatar E-mail (optional)</label>
<input type="email" class="form-control" id="fieldAppAvatar" placeholder="Avatar E-mail" ng-model="application.avatar_email">
<div class="description">An e-mail address representing the <a href="http://docs.quay.io/glossary/avatar" target="_blank">Avatar</a> for the application. See above for the icon.</div>
</div>
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">

View file

@ -43,7 +43,7 @@
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization">
<img src="//www.gravatar.com/avatar/{{ organizationEmail | gravatar }}?s=24&amp;d=identicon">
<span class="avatar" size="24" email="organizationEmail" name="orgname"></span>
<input type="email" class="form-control" ng-model="organizationEmail"
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required>
<button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">

View file

@ -23,7 +23,7 @@
<h2>Organizations</h2>
<div class="organization-listing" ng-repeat="organization in user.organizations">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=32&amp;d=identicon">
<span class="avatar" size="32" hash="organization.avatar"></span>
<a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</div>
</div>

View file

@ -36,7 +36,7 @@
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
<td class="user entity">
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
</td>
<td>
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
@ -67,10 +67,10 @@
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
<td class="user entity">
<span ng-if="member.kind != 'invite'">
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
</span>
<span class="invite-listing" ng-if="member.kind == 'invite'">
<img class="gravatar"ng-src="//www.gravatar.com/avatar/{{ member.gravatar }}?s=32&amp;d=identicon">
<span class="avatar" size="32" hash="member.avatar"></span>
{{ member.email }}
</span>
</td>

View file

@ -9,7 +9,7 @@
<div class="user-admin container" ng-show="!user.anonymous">
<div class="row">
<div class="organization-header-element">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&amp;d=identicon">
<span class="avatar" size="24" hash="user.avatar"></span>
<span class="organization-name">
{{ user.username }}
</span>
@ -72,7 +72,7 @@
<tr class="auth-info" ng-repeat="authInfo in authorizedApps">
<td>
<img src="//www.gravatar.com/avatar/{{ authInfo.gravatar }}?s=16&d=identicon">
<span class="avatar" size="16" hash="authInfo.application.avatar"></span>
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }}
@ -291,7 +291,7 @@
<div class="form-group">
<label for="orgName">Organization Name</label>
<div class="existing-data">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&amp;d=identicon">
<span class="avatar" size="24" hash="user.avatar"></span>
{{ user.username }}</div>
<span class="description">This will continue to be the namespace for your repositories</span>
</div>

View file

@ -13,10 +13,11 @@
<div class="container auth-container" ng-if="!user.anonymous">
<div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
<span class="avatar" size="48" hash="{{ application.avatar }}"></span>
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4>
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
<span class="avatar" size="24" hash="{{ application.organization.avatar }}"
style="vertical-align: middle; margin-right: 4px;"></span>
<span style="vertical-align: middle">{{ application.organization.name }}</span>
</h4>
</div>

View file

@ -2114,14 +2114,14 @@ class TestOrganizationApplicationResource(ApiTestCase):
edit_json = self.putJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
data=dict(name="Some App", description="foo", application_uri="bar",
redirect_uri="baz", gravatar_email="meh"))
redirect_uri="baz", avatar_email="meh"))
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, edit_json['client_id'])
self.assertEquals("Some App", edit_json['name'])
self.assertEquals("foo", edit_json['description'])
self.assertEquals("bar", edit_json['application_uri'])
self.assertEquals("baz", edit_json['redirect_uri'])
self.assertEquals("meh", edit_json['gravatar_email'])
self.assertEquals("meh", edit_json['avatar_email'])
# Retrieve the application again.
json = self.getJsonResponse(OrganizationApplicationResource,

View file

@ -1,5 +0,0 @@
import hashlib
def compute_hash(email_address):
return hashlib.md5(email_address.strip().lower()).hexdigest()

View file

@ -1,6 +1,5 @@
from app import get_app_url
from app import get_app_url, avatar
from data import model
from util.gravatar import compute_hash
from util.names import parse_robot_username
from jinja2 import Template, Environment, FileSystemLoader, contextfilter
@ -25,10 +24,10 @@ def user_reference(username):
alt = 'Organization' if user.organization else 'User'
return """
<span>
<img src="http://www.gravatar.com/avatar/%s?s=16&amp;d=identicon"
<img src="%s"
style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s">
<b>%s</b>
</span>""" % (compute_hash(user.email), alt, username)
</span>""" % (avatar.get_url(user.email, 16), alt, username)
def repository_tag_reference(repository_path_and_tag):
@ -55,10 +54,10 @@ def repository_reference(pair):
return """
<span style="white-space: nowrap;">
<img src="http://www.gravatar.com/avatar/%s?s=16&amp;d=identicon" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
<img src="%s" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
<a href="%s/repository/%s/%s">%s/%s</a>
</span>
""" % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository)
""" % (avatar.get_url(owner.email, 16), get_app_url(), namespace, repository, namespace, repository)
def admin_reference(username):

View file

@ -5,7 +5,6 @@ from flask.ext.mail import Message
from app import mail, app, get_app_url
from data import model
from util.gravatar import compute_hash
from util.jinjautil import get_template_env
logger = logging.getLogger(__name__)