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 RUN apt-get update # 10SEP2014
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands # 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 # Build the python dependencies
ADD requirements.txt requirements.txt 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.archivedlogs import LogArchive
from data.queue import WorkQueue from data.queue import WorkQueue
from data.userevent import UserEventsBuilderModule from data.userevent import UserEventsBuilderModule
from avatars.avatars import Avatar
class Config(BaseConfig): class Config(BaseConfig):
@ -119,6 +120,7 @@ features.import_features(app.config)
Principal(app, use_sessions=False) Principal(app, use_sessions=False)
avatar = Avatar(app)
login_manager = LoginManager(app) login_manager = LoginManager(app)
mail = Mail(app) mail = Mail(app)
storage = Storage(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', CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', 'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
'CONTACT_INFO'] 'CONTACT_INFO', 'AVATAR_KIND']
def getFrontendVisibleConfig(config_dict): def getFrontendVisibleConfig(config_dict):
@ -46,6 +46,8 @@ class DefaultConfig(object):
PREFERRED_URL_SCHEME = 'http' PREFERRED_URL_SCHEME = 'http'
SERVER_HOSTNAME = 'localhost:5000' SERVER_HOSTNAME = 'localhost:5000'
AVATAR_KIND = 'local'
REGISTRY_TITLE = 'Quay.io' REGISTRY_TITLE = 'Quay.io'
REGISTRY_TITLE_SHORT = 'Quay.io' REGISTRY_TITLE_SHORT = 'Quay.io'
CONTACT_INFO = [ CONTACT_INFO = [

View file

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

View file

@ -2,7 +2,7 @@ import logging
from flask import request 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, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound, related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action, show_if) 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 auth.auth_context import get_authenticated_user
from data import model from data import model
from data.billing import get_plan from data.billing import get_plan
from util.gravatar import compute_hash
import features import features
@ -28,7 +27,7 @@ def org_view(o, teams):
view = { view = {
'name': o.username, 'name': o.username,
'email': o.email if is_admin else '', '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}, 'teams': {t.name : team_view(o.username, t) for t in teams},
'is_admin': is_admin 'is_admin': is_admin
} }
@ -274,14 +273,16 @@ class ApplicationInformation(ApiResource):
if not application: if not application:
raise NotFound() raise NotFound()
org_hash = compute_hash(application.organization.email) org_hash = avatar.compute_hash(application.organization.email,
gravatar = compute_hash(application.gravatar_email) if application.gravatar_email else org_hash name=application.organization.username)
app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
application.avatar_email else org_hash)
return { return {
'name': application.name, 'name': application.name,
'description': application.description, 'description': application.description,
'uri': application.application_uri, 'uri': application.application_uri,
'gravatar': gravatar, 'avatar': app_hash,
'organization': org_view(application.organization, []) 'organization': org_view(application.organization, [])
} }
@ -297,7 +298,7 @@ def app_view(application):
'client_id': application.client_id, 'client_id': application.client_id,
'client_secret': application.client_secret if is_admin else None, 'client_secret': application.client_secret if is_admin else None,
'redirect_uri': application.redirect_uri 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', 'type': 'string',
'description': 'The human-readable description for the application', 'description': 'The human-readable description for the application',
}, },
'gravatar_email': { 'avatar_email': {
'type': 'string', '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('application_uri', ''),
app_data.get('redirect_uri', ''), app_data.get('redirect_uri', ''),
description = app_data.get('description', ''), description = app_data.get('description', ''),
gravatar_email = app_data.get('gravatar_email', None),) avatar_email = app_data.get('avatar_email', None),)
app_data.update({ app_data.update({
@ -416,9 +417,9 @@ class OrganizationApplicationResource(ApiResource):
'type': 'string', 'type': 'string',
'description': 'The human-readable description for the application', 'description': 'The human-readable description for the application',
}, },
'gravatar_email': { 'avatar_email': {
'type': 'string', '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.application_uri = app_data['application_uri']
application.redirect_uri = app_data['redirect_uri'] application.redirect_uri = app_data['redirect_uri']
application.description = app_data.get('description', '') 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() application.save()
app_data.update({ app_data.update({

View file

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

View file

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

View file

@ -5,7 +5,7 @@ from flask import request
from flask.ext.login import logout_user from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity 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, from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, parse_args, log_action, internal_only, NotFound, require_user_admin, parse_args,
query_param, InvalidToken, require_scope, format_date, hide_if, show_if, query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
@ -20,7 +20,6 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
UserAdminPermission, UserReadPermission, SuperUserPermission) UserAdminPermission, UserReadPermission, SuperUserPermission)
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth import scopes 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.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
from util.names import parse_single_urn from util.names import parse_single_urn
@ -34,7 +33,7 @@ def user_view(user):
admin_org = AdministerOrganizationPermission(o.username) admin_org = AdministerOrganizationPermission(o.username)
return { return {
'name': o.username, 'name': o.username,
'gravatar': compute_hash(o.email), 'avatar': avatar.compute_hash(o.email, name=o.username),
'is_org_admin': admin_org.can(), 'is_org_admin': admin_org.can(),
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(), 'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
'preferred_namespace': not (o.stripe_id is None) 'preferred_namespace': not (o.stripe_id is None)
@ -61,7 +60,7 @@ def user_view(user):
'anonymous': False, 'anonymous': False,
'username': user.username, 'username': user.username,
'email': user.email, 'email': user.email,
'gravatar': compute_hash(user.email), 'avatar': avatar.compute_hash(user.email, name=user.username),
} }
user_admin = UserAdminPermission(user.username) user_admin = UserAdminPermission(user.username)
@ -572,10 +571,12 @@ def authorization_view(access_token):
'name': oauth_app.name, 'name': oauth_app.name,
'description': oauth_app.description, 'description': oauth_app.description,
'url': oauth_app.application_uri, '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': { 'organization': {
'name': oauth_app.organization.username, '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), '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, from flask import (abort, redirect, request, url_for, make_response, Response,
Blueprint, send_from_directory, jsonify) Blueprint, send_from_directory, jsonify)
from avatar_generator import Avatar
from flask.ext.login import current_user from flask.ext.login import current_user
from urlparse import urlparse from urlparse import urlparse
from health.healthcheck import HealthCheck from health.healthcheck import HealthCheck
from data import model from data import model
from data.model.oauth import DatabaseAuthorizationProvider 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.auth import require_session_login, process_oauth
from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission
from util.invoice import renderInvoiceToPdf 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.common import common_login, render_page_template, route_show_if, param_required
from endpoints.csrf import csrf_protect, generate_csrf_token from endpoints.csrf import csrf_protect, generate_csrf_token
from util.names import parse_repository_name from util.names import parse_repository_name
from util.gravatar import compute_hash
from util.useremails import send_email_changed from util.useremails import send_email_changed
from auth import scopes from auth import scopes
@ -182,6 +183,18 @@ def status():
return response 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']) @web.route('/tos', methods=['GET'])
@no_cache @no_cache
def tos(): def tos():
@ -413,7 +426,8 @@ def request_authorization_code():
'url': oauth_app.application_uri, 'url': oauth_app.application_uri,
'organization': { 'organization': {
'name': oauth_app.organization.username, '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; margin-bottom: 10px;
} }
.member-listing .gravatar { .member-listing .avatar {
vertical-align: middle; vertical-align: middle;
margin-right: 10px; margin-right: 10px;
} }

View file

@ -1,6 +1,6 @@
<div class="application-info-element" style="padding-bottom: 18px"> <div class="application-info-element" style="padding-bottom: 18px">
<div class="auth-header"> <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> <h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4> <h4>
{{ application.organization.name }} {{ 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> </span>
<span ng-if="entity.kind == 'org'"> <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 class="entity-name">
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span> <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 ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
</span> </span>
</span> </span>
<span ng-if="entity.kind != 'team' && entity.kind != 'org'"> <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 class="avatar" size="avatarSize || 16" hash="entity.avatar" ng-if="showAvatar == 'true' && entity.avatar"></span>
<span ng-if="showGravatar != 'true' || !entity.gravatar"> <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-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> <i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
</span> </span>

View file

@ -35,7 +35,7 @@
<li class="dropdown" ng-switch-when="false"> <li class="dropdown" ng-switch-when="false">
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown"> <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 }} {{ user.username }}
<span class="notifications-bubble"></span> <span class="notifications-bubble"></span>
<b class="caret"></b> <b class="caret"></b>

View file

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

View file

@ -3,7 +3,7 @@
<div class="circle" ng-class="getClass(notification)"></div> <div class="circle" ng-class="getClass(notification)"></div>
<div class="message" ng-bind-html="getMessage(notification)"></div> <div class="message" ng-bind-html="getMessage(notification)"></div>
<div class="orginfo" ng-if="notification.organization"> <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> <span class="orgname">{{ notification.organization }}</span>
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
<div class="organization-header-element"> <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"> <span class="organization-name" ng-show="teamName || clickable">
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a> <a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</span> </span>

View file

@ -9,7 +9,7 @@
<td> <td>
<div class="current-repo"> <div class="current-repo">
<img class="dropdown-select-icon github-org-icon" <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> <a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank">{{ state.currentRepo.repo }}</a>
</div> </div>
</td> </td>
@ -53,7 +53,7 @@
<!-- Icons --> <!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i> <i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i>
<img class="dropdown-select-icon github-org-icon" <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 --> <!-- Dropdown menu -->
<ul class="dropdown-select-menu scrollable-menu" role="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; 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', $provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
function(UtilService, $sanitize, KeyService) { function(UtilService, $sanitize, KeyService) {
@ -2441,8 +2486,8 @@ quayApp.directive('entityReference', function () {
scope: { scope: {
'entity': '=entity', 'entity': '=entity',
'namespace': '=namespace', 'namespace': '=namespace',
'showGravatar': '@showGravatar', 'showAvatar': '@showAvatar',
'gravatarSize': '@gravatarSize' 'avatarSize': '@avatarSize'
}, },
controller: function($scope, $element, UserService, UtilService) { controller: function($scope, $element, UserService, UtilService) {
$scope.getIsAdmin = function(namespace) { $scope.getIsAdmin = function(namespace) {
@ -4319,8 +4364,7 @@ quayApp.directive('entitySearch', function () {
} else if (datum.entity.kind == 'team') { } else if (datum.entity.kind == 'team') {
template += '<i class="fa fa-group fa-lg"></i>'; template += '<i class="fa fa-group fa-lg"></i>';
} else if (datum.entity.kind == 'org') { } else if (datum.entity.kind == 'org') {
template += '<i class="fa"><img src="//www.gravatar.com/avatar/' + template += '<i class="fa">' + AvatarService.getAvatar(datum.entity.avatar, 16) + '</i>';
datum.entity.gravatar + '?s=16&amp;d=identicon"></i>';
} }
template += '<span class="name">' + datum.value + '</span>'; template += '<span class="name">' + datum.value + '</span>';
@ -6043,9 +6087,9 @@ quayApp.directive('notificationView', function () {
return NotificationService.getMessage(notification); return NotificationService.getMessage(notification);
}; };
$scope.getGravatar = function(orgname) { $scope.getAvatar = function(orgname) {
var organization = UserService.getOrganization(orgname); var organization = UserService.getOrganization(orgname);
return organization['gravatar'] || ''; return organization['avatar'] || '';
}; };
$scope.parseDate = function(dateString) { $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 () { quayApp.directive('tagSpecificImagesView', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,

View file

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

View file

@ -58,9 +58,6 @@
<small>Co-Founder</small></h3> <small>Co-Founder</small></h3>
</div> </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"> <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> <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> </div>
@ -71,9 +68,6 @@
<small>Co-Founder</small></h3> <small>Co-Founder</small></h3>
</div> </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"> <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> <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> </div>

View file

@ -47,7 +47,7 @@
<div class="signup-form"></div> <div class="signup-form"></div>
</div> </div>
<div ng-show="!user.anonymous" class="user-welcome"> <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> <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 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> <a class="btn btn-success" href="/new/">Create a new repository</a>

View file

@ -57,7 +57,7 @@
<div class="signup-form"></div> <div class="signup-form"></div>
</div> </div>
<div ng-show="!user.anonymous" class="user-welcome"> <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> <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 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> <a class="btn btn-success" href="/new/">Create a new repository</a>

View file

@ -6,11 +6,11 @@
<!-- Header --> <!-- Header -->
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="auth-header"> <div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.gravatar_email | gravatar }}?s=48&d=identicon"> <h2><span class="avatar" size="48" email="application.avatar_email" name="application.name"></span>
<h2>{{ application.name || '(Untitled)' }}</h2> {{ application.name || '(Untitled)' }}</h2>
<h4> <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> <span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
</h4> </h4>
</div> </div>
@ -59,9 +59,9 @@
</div> </div>
<div class="form-group nested"> <div class="form-group nested">
<label for="fieldAppGravatar">Gravatar E-mail (optional)</label> <label for="fieldAppAvatar">Avatar E-mail (optional)</label>
<input type="email" class="form-control" id="fieldAppGravatar" placeholder="Gravatar E-mail" ng-model="application.gravatar_email"> <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://en.gravatar.com/" target="_blank">Gravatar</a> for the application. See above for the icon.</div> <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>
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;"> <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;"> <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" <form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization"> 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" <input type="email" class="form-control" ng-model="organizationEmail"
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required> 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"> <button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">

View file

@ -23,7 +23,7 @@
<h2>Organizations</h2> <h2>Organizations</h2>
<div class="organization-listing" ng-repeat="organization in user.organizations"> <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> <a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</div> </div>
</div> </div>

View file

@ -36,7 +36,7 @@
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'"> <tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
<td class="user entity"> <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>
<td> <td>
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'" <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'"> <tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
<td class="user entity"> <td class="user entity">
<span ng-if="member.kind != 'invite'"> <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>
<span class="invite-listing" ng-if="member.kind == 'invite'"> <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 }} {{ member.email }}
</span> </span>
</td> </td>

View file

@ -9,7 +9,7 @@
<div class="user-admin container" ng-show="!user.anonymous"> <div class="user-admin container" ng-show="!user.anonymous">
<div class="row"> <div class="row">
<div class="organization-header-element"> <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"> <span class="organization-name">
{{ user.username }} {{ user.username }}
</span> </span>
@ -72,7 +72,7 @@
<tr class="auth-info" ng-repeat="authInfo in authorizedApps"> <tr class="auth-info" ng-repeat="authInfo in authorizedApps">
<td> <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" <a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip> data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }} {{ authInfo.application.name }}
@ -291,7 +291,7 @@
<div class="form-group"> <div class="form-group">
<label for="orgName">Organization Name</label> <label for="orgName">Organization Name</label>
<div class="existing-data"> <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> {{ user.username }}</div>
<span class="description">This will continue to be the namespace for your repositories</span> <span class="description">This will continue to be the namespace for your repositories</span>
</div> </div>

View file

@ -13,10 +13,11 @@
<div class="container auth-container" ng-if="!user.anonymous"> <div class="container auth-container" ng-if="!user.anonymous">
<div class="auth-header"> <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> <h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4> <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> <span style="vertical-align: middle">{{ application.organization.name }}</span>
</h4> </h4>
</div> </div>

View file

@ -2114,14 +2114,14 @@ class TestOrganizationApplicationResource(ApiTestCase):
edit_json = self.putJsonResponse(OrganizationApplicationResource, edit_json = self.putJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID), params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
data=dict(name="Some App", description="foo", application_uri="bar", 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(FAKE_APPLICATION_CLIENT_ID, edit_json['client_id'])
self.assertEquals("Some App", edit_json['name']) self.assertEquals("Some App", edit_json['name'])
self.assertEquals("foo", edit_json['description']) self.assertEquals("foo", edit_json['description'])
self.assertEquals("bar", edit_json['application_uri']) self.assertEquals("bar", edit_json['application_uri'])
self.assertEquals("baz", edit_json['redirect_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. # Retrieve the application again.
json = self.getJsonResponse(OrganizationApplicationResource, 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 data import model
from util.gravatar import compute_hash
from util.names import parse_robot_username from util.names import parse_robot_username
from jinja2 import Template, Environment, FileSystemLoader, contextfilter from jinja2 import Template, Environment, FileSystemLoader, contextfilter
@ -25,10 +24,10 @@ def user_reference(username):
alt = 'Organization' if user.organization else 'User' alt = 'Organization' if user.organization else 'User'
return """ return """
<span> <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"> style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s">
<b>%s</b> <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): def repository_tag_reference(repository_path_and_tag):
@ -55,10 +54,10 @@ def repository_reference(pair):
return """ return """
<span style="white-space: nowrap;"> <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> <a href="%s/repository/%s/%s">%s/%s</a>
</span> </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): 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 app import mail, app, get_app_url
from data import model from data import model
from util.gravatar import compute_hash
from util.jinjautil import get_template_env from util.jinjautil import get_template_env
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)