Add a configurable avatar system and add an internal avatar system for enterprise
This commit is contained in:
parent
f6dd8b0a4d
commit
e9cac407df
36 changed files with 241 additions and 92 deletions
|
@ -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
2
app.py
|
@ -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
0
avatars/__init__.py
Normal file
63
avatars/avatars.py
Normal file
63
avatars/avatars.py
Normal 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
|
||||||
|
}
|
|
@ -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 = [
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
1
static/directives/avatar.html
Normal file
1
static/directives/avatar.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<img class="avatar-element" ng-src="{{ AvatarService.getAvatar(_hash, size) }}">
|
|
@ -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' }}&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' }}&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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="organization-header-element">
|
<div class="organization-header-element">
|
||||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&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>
|
||||||
|
|
|
@ -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
BIN
static/img/empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 B |
|
@ -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&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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;">
|
||||||
|
|
|
@ -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&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">
|
||||||
|
|
|
@ -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&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>
|
||||||
|
|
|
@ -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&d=identicon">
|
<span class="avatar" size="32" hash="member.avatar"></span>
|
||||||
{{ member.email }}
|
{{ member.email }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -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&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&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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import hashlib
|
|
||||||
|
|
||||||
|
|
||||||
def compute_hash(email_address):
|
|
||||||
return hashlib.md5(email_address.strip().lower()).hexdigest()
|
|
|
@ -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&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&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):
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
Reference in a new issue