diff --git a/avatars/avatars.py b/avatars/avatars.py
index 40935df10..220cae9cb 100644
--- a/avatars/avatars.py
+++ b/avatars/avatars.py
@@ -1,4 +1,5 @@
import hashlib
+import math
class Avatar(object):
def __init__(self, app=None):
@@ -7,8 +8,7 @@ class Avatar(object):
def _init_app(self, app):
return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')](
- app.config['SERVER_HOSTNAME'],
- app.config['PREFERRED_URL_SCHEME'])
+ app.config['PREFERRED_URL_SCHEME'], app.config['AVATAR_COLORS'], app.config['HTTPCLIENT'])
def __getattr__(self, name):
return getattr(self.state, name, None)
@@ -16,48 +16,83 @@ class Avatar(object):
class BaseAvatar(object):
""" Base class for all avatar implementations. """
- def __init__(self, server_hostname, preferred_url_scheme):
- self.server_hostname = server_hostname
+ def __init__(self, preferred_url_scheme, colors, http_client):
self.preferred_url_scheme = preferred_url_scheme
+ self.colors = colors
+ self.http_client = http_client
- 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.
+ def get_mail_html(self, name, email_or_id, size=16, kind='user'):
+ """ Returns the full HTML and CSS for viewing the avatar of the given name and email address,
+ with an optional size.
"""
- raise NotImplementedError
+ data = self.get_data(name, email_or_id, kind)
+ url = self._get_url(data['hash'], size) if kind != 'team' else None
+ font_size = size - 6
- 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
+ if url is not None:
+ # Try to load the gravatar. If we get a non-404 response, then we use it in place of
+ # the CSS avatar.
+ response = self.http_client.get(url)
+ if response.status_code == 200:
+ return """
""" % (url, size, size, kind)
+
+ radius = '50%' if kind == 'team' else '0%'
+ letter = 'Ω' if kind == 'team' and data['name'] == 'owners' else data['name'].upper()[0]
+
+ return """
+
+ %s
+
+""" % (size, size, data['color'], font_size, size, radius, letter)
+
+ def get_data_for_user(self, user):
+ return self.get_data(user.username, user.email, 'robot' if user.robot else 'user')
+
+ def get_data_for_team(self, team):
+ return self.get_data(team.name, team.name, 'team')
+
+ def get_data_for_org(self, org):
+ return self.get_data(org.username, org.email, 'org')
+
+ def get_data(self, name, email_or_id, kind='user'):
+ """ Computes and returns the full data block for the avatar:
+ {
+ 'name': name,
+ 'hash': The gravatar hash, if any.
+ 'color': The color for the avatar
+ }
+ """
+ colors = self.colors
+ hash_value = hashlib.md5(email_or_id.strip().lower()).hexdigest()
+
+ byte_count = int(math.ceil(math.log(len(colors), 16)))
+ byte_data = hash_value[0:byte_count]
+ hash_color = colors[int(byte_data, 16) % len(colors)]
+
+ return {
+ 'name': name,
+ 'hash': hash_value,
+ 'color': hash_color,
+ 'kind': kind
+ }
+
+ def _get_url(self, hash_value, size):
+ """ Returns the URL for displaying the overlay avatar. """
+ return None
class GravatarAvatar(BaseAvatar):
""" Avatar system that uses gravatar for generating avatars. """
- def compute_hash(self, email, name=None):
- email = email or ""
- 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)
+ def _get_url(self, hash_value, size=16):
+ return '%s://www.gravatar.com/avatar/%s?d=404&size=%s' % (self.preferred_url_scheme,
+ hash_value, size)
class LocalAvatar(BaseAvatar):
""" Avatar system that uses the local system for generating avatars. """
- def compute_hash(self, email, name=None):
- email = email or ""
- 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)
-
+ pass
AVATAR_CLASSES = {
'gravatar': GravatarAvatar,
diff --git a/config.py b/config.py
index 2d50138af..6fe1c4042 100644
--- a/config.py
+++ b/config.py
@@ -45,8 +45,6 @@ class DefaultConfig(object):
PREFERRED_URL_SCHEME = 'http'
SERVER_HOSTNAME = 'localhost:5000'
- AVATAR_KIND = 'local'
-
REGISTRY_TITLE = 'CoreOS Enterprise Registry'
REGISTRY_TITLE_SHORT = 'Enterprise Registry'
@@ -205,3 +203,11 @@ class DefaultConfig(object):
# Signed registry grant token expiration in seconds
SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull
+
+ # The various avatar background colors.
+ AVATAR_KIND = 'local'
+ AVATAR_COLORS = ['#969696', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728',
+ '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2',
+ '#7f7f7f', '#c7c7c7', '#bcbd22', '#1f77b4', '#17becf', '#9edae5', '#393b79',
+ '#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b',
+ '#8c6d31', '#ad494a', '#e7ba52', '#a55194']
diff --git a/data/model/legacy.py b/data/model/legacy.py
index 2fcbfcf68..f0d0c32fe 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -310,11 +310,54 @@ def _list_entity_robots(entity_name):
.where(User.robot == True, User.username ** (entity_name + '+%')))
-def list_entity_robot_tuples(entity_name):
- return (_list_entity_robots(entity_name)
- .select(User.username, FederatedLogin.service_ident)
- .tuples())
+class _TupleWrapper(object):
+ def __init__(self, data, fields):
+ self._data = data
+ self._fields = fields
+ def get(self, field):
+ return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)]
+
+
+class TupleSelector(object):
+ """ Helper class for selecting tuples from a peewee query and easily accessing
+ them as if they were objects.
+ """
+ def __init__(self, query, fields):
+ self._query = query.select(*fields).tuples()
+ self._fields = [field.name + ':' + field.model_class.__name__ for field in fields]
+
+ def __iter__(self):
+ return self._build_iterator()
+
+ def _build_iterator(self):
+ for tuple_data in self._query:
+ yield _TupleWrapper(tuple_data, self._fields)
+
+
+
+def list_entity_robot_permission_teams(entity_name):
+ query = (_list_entity_robots(entity_name)
+ .join(RepositoryPermission, JOIN_LEFT_OUTER,
+ on=(RepositoryPermission.user == FederatedLogin.user))
+ .join(Repository, JOIN_LEFT_OUTER)
+ .switch(User)
+ .join(TeamMember, JOIN_LEFT_OUTER)
+ .join(Team, JOIN_LEFT_OUTER))
+
+ fields = [User.username, FederatedLogin.service_ident, Repository.name, Team.name]
+ return TupleSelector(query, fields)
+
+
+def list_robot_permissions(robot_name):
+ return (RepositoryPermission.select(RepositoryPermission, User, Repository)
+ .join(Repository)
+ .join(Visibility)
+ .switch(RepositoryPermission)
+ .join(Role)
+ .switch(RepositoryPermission)
+ .join(User)
+ .where(User.username == robot_name, User.robot == True))
def convert_user_to_organization(user, admin_user):
# Change the user to an organization.
@@ -654,13 +697,13 @@ def get_matching_users(username_prefix, robot_namespace=None,
(User.robot == True)))
query = (User
- .select(User.username, User.robot)
- .group_by(User.username, User.robot)
+ .select(User.username, User.email, User.robot)
+ .group_by(User.username, User.email, User.robot)
.where(direct_user_query))
if organization:
query = (query
- .select(User.username, User.robot, fn.Sum(Team.id))
+ .select(User.username, User.email, User.robot, fn.Sum(Team.id))
.join(TeamMember, JOIN_LEFT_OUTER)
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
(Team.organization == organization))))
@@ -669,9 +712,11 @@ def get_matching_users(username_prefix, robot_namespace=None,
class MatchingUserResult(object):
def __init__(self, *args):
self.username = args[0]
- self.is_robot = args[1]
+ self.email = args[1]
+ self.robot = args[2]
+
if organization:
- self.is_org_member = (args[2] != None)
+ self.is_org_member = (args[3] != None)
else:
self.is_org_member = None
@@ -1039,7 +1084,8 @@ def get_all_repo_teams(namespace_name, repository_name):
def get_all_repo_users(namespace_name, repository_name):
- return (RepositoryPermission.select(User.username, User.robot, Role.name, RepositoryPermission)
+ return (RepositoryPermission.select(User.username, User.email, User.robot, Role.name,
+ RepositoryPermission)
.join(User)
.switch(RepositoryPermission)
.join(Role)
diff --git a/emails/teaminvite.html b/emails/teaminvite.html
index 3d8ff9c14..128bbe00f 100644
--- a/emails/teaminvite.html
+++ b/emails/teaminvite.html
@@ -4,7 +4,7 @@
Invitation to join team: {{ teamname }}
-{{ inviter | user_reference }} has invited you to join the team {{ teamname }} under organization {{ organization | user_reference }}.
+{{ inviter | user_reference }} has invited you to join the team {{ teamname | team_reference }} under organization {{ organization | user_reference }}.
diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py
index 4302bd62f..3cb98fb84 100644
--- a/endpoints/api/organization.py
+++ b/endpoints/api/organization.py
@@ -24,16 +24,22 @@ logger = logging.getLogger(__name__)
def org_view(o, teams):
- admin_org = AdministerOrganizationPermission(o.username)
- is_admin = admin_org.can()
+ is_admin = AdministerOrganizationPermission(o.username).can()
+ is_member = OrganizationMemberPermission(o.username).can()
+
view = {
'name': o.username,
'email': o.email if is_admin else '',
- 'avatar': avatar.compute_hash(o.email, name=o.username),
- 'teams': {t.name : team_view(o.username, t) for t in teams},
- 'is_admin': is_admin
+ 'avatar': avatar.get_data_for_user(o),
+ 'is_admin': is_admin,
+ 'is_member': is_member
}
+ if teams is not None:
+ teams = sorted(teams, key=lambda team:team.id)
+ view['teams'] = {t.name : team_view(o.username, t) for t in teams}
+ view['ordered_teams'] = [team.name for team in teams]
+
if is_admin:
view['invoice_email'] = o.invoice_email
@@ -129,17 +135,17 @@ class Organization(ApiResource):
@nickname('getOrganization')
def get(self, orgname):
""" Get the details for the specified organization """
- permission = OrganizationMemberPermission(orgname)
- if permission.can():
- try:
- org = model.get_organization(orgname)
- except model.InvalidOrganizationException:
- raise NotFound()
+ try:
+ org = model.get_organization(orgname)
+ except model.InvalidOrganizationException:
+ raise NotFound()
+ teams = None
+ if OrganizationMemberPermission(orgname).can():
teams = model.get_teams_within_org(org)
- return org_view(org, teams)
- raise Unauthorized()
+ return org_view(org, teams)
+
@require_scope(scopes.ORG_ADMIN)
@nickname('changeOrganizationDetails')
@@ -218,7 +224,7 @@ class OrgPrivateRepositories(ApiResource):
@path_param('orgname', 'The name of the organization')
class OrgnaizationMemberList(ApiResource):
""" Resource for listing the members of an organization. """
-
+
@require_scope(scopes.ORG_ADMIN)
@nickname('getOrganizationMembers')
def get(self, orgname):
@@ -297,16 +303,14 @@ class ApplicationInformation(ApiResource):
if not application:
raise NotFound()
- org_hash = avatar.compute_hash(application.organization.email,
- name=application.organization.username)
- app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
- application.avatar_email else org_hash)
+ app_email = application.avatar_email or application.organization.email
+ app_data = avatar.get_data(application.name, app_email, 'app')
return {
'name': application.name,
'description': application.description,
'uri': application.application_uri,
- 'avatar': app_hash,
+ 'avatar': app_data,
'organization': org_view(application.organization, [])
}
diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py
index c8a473d9c..4c0b62074 100644
--- a/endpoints/api/permission.py
+++ b/endpoints/api/permission.py
@@ -2,6 +2,7 @@ import logging
from flask import request
+from app import avatar
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, request_error, validate_json_request, path_param)
from data import model
@@ -16,7 +17,10 @@ def role_view(repo_perm_obj):
}
def wrap_role_view_user(role_json, user):
+ role_json['name'] = user.username
role_json['is_robot'] = user.robot
+ if not user.robot:
+ role_json['avatar'] = avatar.get_data_for_user(user)
return role_json
@@ -25,6 +29,12 @@ def wrap_role_view_org(role_json, user, org_members):
return role_json
+def wrap_role_view_team(role_json, team):
+ role_json['name'] = team.name
+ role_json['avatar'] = avatar.get_data_for_team(team)
+ return role_json
+
+
@resource('/v1/repository//permissions/team/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryTeamPermissionList(RepositoryParamResource):
@@ -35,8 +45,11 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
""" List all team permission. """
repo_perms = model.get_all_repo_teams(namespace, repository)
+ def wrapped_role_view(repo_perm):
+ return wrap_role_view_team(role_view(repo_perm), repo_perm.team)
+
return {
- 'permissions': {repo_perm.team.name: role_view(repo_perm)
+ 'permissions': {repo_perm.team.name: wrapped_role_view(repo_perm)
for repo_perm in repo_perms}
}
@@ -232,7 +245,7 @@ class RepositoryTeamPermission(RepositoryParamResource):
'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
- return role_view(perm), 200
+ return wrap_role_view_team(role_view(perm), perm.team), 200
@require_repo_admin
@nickname('deleteTeamPermissions')
diff --git a/endpoints/api/prototype.py b/endpoints/api/prototype.py
index 343913c3a..de0c97483 100644
--- a/endpoints/api/prototype.py
+++ b/endpoints/api/prototype.py
@@ -7,6 +7,7 @@ from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
+from app import avatar
def prototype_view(proto, org_members):
@@ -16,6 +17,7 @@ def prototype_view(proto, org_members):
'is_robot': user.robot,
'kind': 'user',
'is_org_member': user.robot or user.username in org_members,
+ 'avatar': avatar.get_data_for_user(user)
}
if proto.delegate_user:
@@ -24,6 +26,7 @@ def prototype_view(proto, org_members):
delegate_view = {
'name': proto.delegate_team.name,
'kind': 'team',
+ 'avatar': avatar.get_data_for_team(proto.delegate_team)
}
return {
diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py
index b7614a356..ecb26e573 100644
--- a/endpoints/api/robot.py
+++ b/endpoints/api/robot.py
@@ -5,16 +5,63 @@ from auth.permissions import AdministerOrganizationPermission, OrganizationMembe
from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
+from data.database import User, Team, Repository, FederatedLogin
from util.names import format_robot_username
-
+from flask import abort
+from app import avatar
def robot_view(name, token):
return {
'name': name,
- 'token': token,
+ 'token': token
}
+def permission_view(permission):
+ return {
+ 'repository': {
+ 'name': permission.repository.name,
+ 'is_public': permission.repository.visibility.name == 'public'
+ },
+ 'role': permission.role.name
+ }
+
+
+def robots_list(prefix):
+ tuples = model.list_entity_robot_permission_teams(prefix)
+
+ robots = {}
+ robot_teams = set()
+
+ for robot_tuple in tuples:
+ robot_name = robot_tuple.get(User.username)
+ if not robot_name in robots:
+ robots[robot_name] = {
+ 'name': robot_name,
+ 'token': robot_tuple.get(FederatedLogin.service_ident),
+ 'teams': [],
+ 'repositories': []
+ }
+
+ team_name = robot_tuple.get(Team.name)
+ repository_name = robot_tuple.get(Repository.name)
+
+ if team_name is not None:
+ check_key = robot_name + ':' + team_name
+ if not check_key in robot_teams:
+ robot_teams.add(check_key)
+
+ robots[robot_name]['teams'].append({
+ 'name': team_name,
+ 'avatar': avatar.get_data(team_name, team_name, 'team')
+ })
+
+ if repository_name is not None:
+ if not repository_name in robots[robot_name]['repositories']:
+ robots[robot_name]['repositories'].append(repository_name)
+
+ return {'robots': robots.values()}
+
@resource('/v1/user/robots')
@internal_only
class UserRobotList(ApiResource):
@@ -24,10 +71,7 @@ class UserRobotList(ApiResource):
def get(self):
""" List the available robots for the user. """
user = get_authenticated_user()
- robots = model.list_entity_robot_tuples(user.username)
- return {
- 'robots': [robot_view(name, password) for name, password in robots]
- }
+ return robots_list(user.username)
@resource('/v1/user/robots/')
@@ -73,10 +117,7 @@ class OrgRobotList(ApiResource):
""" List the organization's robots. """
permission = OrganizationMemberPermission(orgname)
if permission.can():
- robots = model.list_entity_robot_tuples(orgname)
- return {
- 'robots': [robot_view(name, password) for name, password in robots]
- }
+ return robots_list(orgname)
raise Unauthorized()
@@ -125,6 +166,47 @@ class OrgRobot(ApiResource):
raise Unauthorized()
+@resource('/v1/user/robots//permissions')
+@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
+@internal_only
+class UserRobotPermissions(ApiResource):
+ """ Resource for listing the permissions a user's robot has in the system. """
+ @require_user_admin
+ @nickname('getUserRobotPermissions')
+ def get(self, robot_shortname):
+ """ Returns the list of repository permissions for the user's robot. """
+ parent = get_authenticated_user()
+ robot, password = model.get_robot(robot_shortname, parent)
+ permissions = model.list_robot_permissions(robot.username)
+
+ return {
+ 'permissions': [permission_view(permission) for permission in permissions]
+ }
+
+
+@resource('/v1/organization//robots//permissions')
+@path_param('orgname', 'The name of the organization')
+@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
+@related_user_resource(UserRobotPermissions)
+class OrgRobotPermissions(ApiResource):
+ """ Resource for listing the permissions an org's robot has in the system. """
+ @require_user_admin
+ @nickname('getOrgRobotPermissions')
+ def get(self, orgname, robot_shortname):
+ """ Returns the list of repository permissions for the org's robot. """
+ permission = AdministerOrganizationPermission(orgname)
+ if permission.can():
+ parent = model.get_organization(orgname)
+ robot, password = model.get_robot(robot_shortname, parent)
+ permissions = model.list_robot_permissions(robot.username)
+
+ return {
+ 'permissions': [permission_view(permission) for permission in permissions]
+ }
+
+ abort(403)
+
+
@resource('/v1/user/robots//regenerate')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
diff --git a/endpoints/api/search.py b/endpoints/api/search.py
index 0e3561745..76223ac1c 100644
--- a/endpoints/api/search.py
+++ b/endpoints/api/search.py
@@ -45,7 +45,7 @@ class EntitySearch(ApiResource):
'name': namespace_name,
'kind': 'org',
'is_org_member': True,
- 'avatar': avatar.compute_hash(organization.email, name=organization.username),
+ 'avatar': avatar.get_data_for_org(organization),
}]
except model.InvalidOrganizationException:
@@ -63,7 +63,8 @@ class EntitySearch(ApiResource):
result = {
'name': team.name,
'kind': 'team',
- 'is_org_member': True
+ 'is_org_member': True,
+ 'avatar': avatar.get_data_for_team(team)
}
return result
@@ -71,11 +72,12 @@ class EntitySearch(ApiResource):
user_json = {
'name': user.username,
'kind': 'user',
- 'is_robot': user.is_robot,
+ 'is_robot': user.robot,
+ 'avatar': avatar.get_data_for_user(user)
}
if organization is not None:
- user_json['is_org_member'] = user.is_robot or user.is_org_member
+ user_json['is_org_member'] = user.robot or user.is_org_member
return user_json
diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py
index 2c7daf633..01dbc5cb2 100644
--- a/endpoints/api/superuser.py
+++ b/endpoints/api/superuser.py
@@ -108,7 +108,7 @@ def user_view(user):
'username': user.username,
'email': user.email,
'verified': user.verified,
- 'avatar': avatar.compute_hash(user.email, name=user.username),
+ 'avatar': avatar.get_data_for_user(user),
'super_user': superusers.is_superuser(user.username)
}
diff --git a/endpoints/api/team.py b/endpoints/api/team.py
index 91f225fa1..ce42f5e94 100644
--- a/endpoints/api/team.py
+++ b/endpoints/api/team.py
@@ -52,11 +52,11 @@ def team_view(orgname, team):
view_permission = ViewTeamPermission(orgname, team.name)
role = model.get_team_org_role(team).name
return {
- 'id': team.id,
'name': team.name,
'description': team.description,
'can_view': view_permission.can(),
- 'role': role
+ 'role': role,
+ 'avatar': avatar.get_data_for_team(team)
}
def member_view(member, invited=False):
@@ -64,7 +64,7 @@ def member_view(member, invited=False):
'name': member.username,
'kind': 'user',
'is_robot': member.robot,
- 'avatar': avatar.compute_hash(member.email, name=member.username) if not member.robot else None,
+ 'avatar': avatar.get_data_for_user(member),
'invited': invited,
}
@@ -76,7 +76,7 @@ def invite_view(invite):
return {
'email': invite.email,
'kind': 'invite',
- 'avatar': avatar.compute_hash(invite.email),
+ 'avatar': avatar.get_data(invite.email, invite.email, 'user'),
'invited': True
}
diff --git a/endpoints/api/user.py b/endpoints/api/user.py
index b5d260516..c3812e717 100644
--- a/endpoints/api/user.py
+++ b/endpoints/api/user.py
@@ -36,7 +36,7 @@ def user_view(user):
admin_org = AdministerOrganizationPermission(o.username)
return {
'name': o.username,
- 'avatar': avatar.compute_hash(o.email, name=o.username),
+ 'avatar': avatar.get_data_for_org(o),
'is_org_admin': admin_org.can(),
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
'preferred_namespace': not (o.stripe_id is None)
@@ -62,7 +62,7 @@ def user_view(user):
'verified': user.verified,
'anonymous': False,
'username': user.username,
- 'avatar': avatar.compute_hash(user.email, name=user.username),
+ 'avatar': avatar.get_data_for_user(user)
}
user_admin = UserAdminPermission(user.username)
@@ -176,8 +176,8 @@ class User(ApiResource):
'description': 'The user\'s email address',
},
'avatar': {
- 'type': 'string',
- 'description': 'Avatar hash representing the user\'s icon'
+ 'type': 'object',
+ 'description': 'Avatar data representing the user\'s icon'
},
'organizations': {
'type': 'array',
@@ -665,17 +665,16 @@ class UserNotification(ApiResource):
def authorization_view(access_token):
oauth_app = access_token.application
+ app_email = oauth_app.avatar_email or oauth_app.organization.email
return {
'application': {
'name': oauth_app.name,
'description': oauth_app.description,
'url': oauth_app.application_uri,
- 'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email,
- name=oauth_app.name),
+ 'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
'organization': {
'name': oauth_app.organization.username,
- 'avatar': avatar.compute_hash(oauth_app.organization.email,
- name=oauth_app.organization.username)
+ 'avatar': avatar.get_data_for_org(oauth_app.organization)
}
},
'scopes': scopes.get_scope_information(access_token.scope),
diff --git a/endpoints/web.py b/endpoints/web.py
index c3af01e44..1359fd3bc 100644
--- a/endpoints/web.py
+++ b/endpoints/web.py
@@ -3,7 +3,6 @@ import logging
from flask import (abort, redirect, request, url_for, make_response, Response,
Blueprint, send_from_directory, jsonify, send_file)
-from avatar_generator import Avatar
from flask.ext.login import current_user
from urlparse import urlparse
from health.healthcheck import get_healthchecker
@@ -210,20 +209,6 @@ def endtoend_health():
return response
-@app.route("/avatar/")
-@set_cache_headers
-def render_avatar(avatar_hash, headers):
- try:
- size = int(request.args.get('size', 16))
- except ValueError:
- size = 16
-
- generated = Avatar.generate(size, avatar_hash, "PNG")
- resp = make_response(generated, 200, {'Content-Type': 'image/png'})
- resp.headers.extend(headers)
- return resp
-
-
@web.route('/tos', methods=['GET'])
@no_cache
def tos():
@@ -449,15 +434,16 @@ def request_authorization_code():
# Load the application information.
oauth_app = provider.get_application_for_client_id(client_id)
+ app_email = oauth_app.email or organization.email
+
oauth_app_view = {
'name': oauth_app.name,
'description': oauth_app.description,
'url': oauth_app.application_uri,
- 'avatar': avatar.compute_hash(oauth_app.avatar_email, name=oauth_app.name),
+ 'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
'organization': {
'name': oauth_app.organization.username,
- 'avatar': avatar.compute_hash(oauth_app.organization.email,
- name=oauth_app.organization.username)
+ 'avatar': avatar.get_data_for_org(oauth_app.organization)
}
}
diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index b74923d4d..37cc03d69 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -773,10 +773,17 @@
padding: 10px;
}
+.co-table.no-lines td {
+ border-bottom: 0px;
+ padding: 6px;
+}
+
.co-table thead td {
+ color: #999;
+ font-size: 90%;
text-transform: uppercase;
- font-size: 16px;
- color: #666;
+ font-weight: 300;
+ padding-top: 0px !important;
}
.co-table thead td a {
@@ -813,11 +820,45 @@
width: 30px;
}
+.co-table td.caret-col {
+ width: 10px;
+ padding-left: 6px;
+ padding-right: 0px;
+ color: #aaa;
+}
+
+.co-table td.caret-col i.fa {
+ cursor: pointer;
+}
+
+.co-table .add-row-spacer td {
+ padding: 5px;
+}
+
.co-table .add-row td {
+ padding-top: 10px;
border-top: 2px solid #eee;
border-bottom: none;
}
+.co-table tr.co-table-header-row td {
+ font-size: 12px;
+ text-transform: uppercase;
+ color: #ccc;
+ border-bottom: none;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 4px;
+}
+
+.co-table tr.co-table-header-row td i.fa {
+ margin-right: 4px;
+}
+
+.co-table tr.indented-row td:first-child {
+ padding-left: 28px;
+}
+
.cor-checkable-menu {
display: inline-block;
}
diff --git a/static/css/directives/ui/application-manager.css b/static/css/directives/ui/application-manager.css
new file mode 100644
index 000000000..8184f9183
--- /dev/null
+++ b/static/css/directives/ui/application-manager.css
@@ -0,0 +1,7 @@
+.application-manager-element .co-table {
+ margin-top: 20px;
+}
+
+.application-manager-element i.fa {
+ margin-right: 4px;
+}
diff --git a/static/css/directives/ui/avatar.css b/static/css/directives/ui/avatar.css
new file mode 100644
index 000000000..0ba53cad5
--- /dev/null
+++ b/static/css/directives/ui/avatar.css
@@ -0,0 +1,30 @@
+.avatar-element {
+ display: inline-block;
+ vertical-align: middle;
+ color: white !important;
+ text-align: center;
+ position: relative;
+ background: white;
+ overflow: hidden;
+}
+
+.avatar-element.team {
+ border-radius: 50%;
+}
+
+.avatar-element img {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+}
+
+.avatar-element .letter {
+ cursor: default !important;
+ font-style: normal !important;
+ font-weight: normal !important;
+ font-variant: normal !important;
+}
+
+a .avatar-element .letter {
+ cursor: pointer !important;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/billing-invoices.css b/static/css/directives/ui/billing-invoices.css
new file mode 100644
index 000000000..10011237c
--- /dev/null
+++ b/static/css/directives/ui/billing-invoices.css
@@ -0,0 +1,25 @@
+
+.billing-invoices-element .invoice-title {
+ padding: 6px;
+ cursor: pointer;
+}
+
+.billing-invoices-element .invoice-status .success {
+ color: green;
+}
+
+.billing-invoices-element .invoice-status .pending {
+ color: steelblue;
+}
+
+.billing-invoices-element .invoice-status .danger {
+ color: red;
+}
+
+.billing-invoices-element .invoice-amount:before {
+ content: '$';
+}
+
+.billing-invoices-element .fa-download {
+ color: #aaa;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/entity-reference.css b/static/css/directives/ui/entity-reference.css
new file mode 100644
index 000000000..5885f0d8c
--- /dev/null
+++ b/static/css/directives/ui/entity-reference.css
@@ -0,0 +1,7 @@
+.entity-reference .new-entity-reference .entity-name {
+ margin-left: 6px;
+}
+
+.entity-reference .new-entity-reference .fa-wrench {
+ width: 16px;
+}
diff --git a/static/css/directives/ui/entity-search.css b/static/css/directives/ui/entity-search.css
new file mode 100644
index 000000000..eaab63120
--- /dev/null
+++ b/static/css/directives/ui/entity-search.css
@@ -0,0 +1,52 @@
+.entity-search-element {
+ position: relative;
+ display: block;
+}
+
+.entity-search-element .entity-reference {
+ position: absolute !important;
+ top: 7px;
+ left: 8px;
+ right: 36px;
+ z-index: 0;
+ pointer-events: none;
+}
+
+.entity-search-element .entity-reference .entity-reference-element {
+ pointer-events: none;
+}
+
+.entity-search-element .entity-reference-element i.fa-exclamation-triangle {
+ pointer-events: all;
+}
+
+.entity-search-element .entity-reference .entity-name {
+ display: none;
+}
+
+.entity-search-element input {
+ vertical-align: middle;
+ width: 100%;
+}
+
+.entity-search-element.persistent input {
+ padding-left: 28px;
+ padding-right: 28px;
+}
+
+.entity-search-element .twitter-typeahead {
+ vertical-align: middle;
+ display: block !important;
+ margin-right: 36px;
+}
+
+.entity-search-element .dropdown {
+ vertical-align: middle;
+ position: absolute;
+ top: 0px;
+ right: 0px;
+}
+
+.entity-search-element .menuitem .avatar {
+ margin-right: 4px;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/prototype-manager.css b/static/css/directives/ui/prototype-manager.css
new file mode 100644
index 000000000..41ccb9824
--- /dev/null
+++ b/static/css/directives/ui/prototype-manager.css
@@ -0,0 +1,8 @@
+.prototype-manager-element i.fa {
+ margin-right: 4px;
+}
+
+.prototype-manager-element td {
+ padding: 10px !important;
+ vertical-align: middle !important;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/repository-permissions-table.css b/static/css/directives/ui/repository-permissions-table.css
index 8e69007a2..437e6d57e 100644
--- a/static/css/directives/ui/repository-permissions-table.css
+++ b/static/css/directives/ui/repository-permissions-table.css
@@ -1,3 +1,15 @@
.repository-permissions-table #add-entity-permission {
padding-left: 0px;
+}
+
+.repository-permissions-table .user-permission-entity {
+ position: relative;
+}
+
+.repository-permissions-table .outside-org {
+ position: absolute;
+ top: 15px;
+ left: -2px;
+ font-size: 16px;
+ color: #E8BB03;
}
\ No newline at end of file
diff --git a/static/css/directives/ui/robots-manager.css b/static/css/directives/ui/robots-manager.css
new file mode 100644
index 000000000..b2eb42bcf
--- /dev/null
+++ b/static/css/directives/ui/robots-manager.css
@@ -0,0 +1,80 @@
+.robots-manager-element .robot a {
+ font-size: 16px;
+ cursor: pointer;
+}
+
+.robots-manager-element .robot .prefix {
+ color: #aaa;
+}
+
+.robots-manager-element .robot i {
+ margin-right: 10px;
+}
+
+.robots-manager-element .popup-input-button i.fa {
+ margin-right: 4px;
+}
+
+.robots-manager-element .empty {
+ color: #ccc;
+}
+
+.robots-manager-element tr.open td {
+ border-bottom: 1px solid transparent;
+}
+
+.robots-manager-element .permissions-table-wrapper {
+ margin-left: 0px;
+ border-left: 2px solid #ccc;
+ padding-left: 20px;
+}
+
+.robots-manager-element .permissions-table tbody tr:last-child td {
+ border-bottom: 0px;
+}
+
+.robots-manager-element .permissions-display-row {
+ position: relative;
+ padding-bottom: 20px;
+}
+
+.robots-manager-element .permissions-display-row td:first-child {
+ min-width: 300px;
+}
+
+.robots-manager-element .repo-circle {
+ color: #999;
+ display: inline-block;
+ position: relative;
+ background: #eee;
+ padding: 4px;
+ border-radius: 50%;
+ display: inline-block;
+ width: 46px;
+ height: 46px;
+ margin-right: 6px;
+}
+
+.robots-manager-element .repo-circle .fa-hdd-o {
+ font-size: 1.7em;
+}
+
+.robots-manager-element .repo-circle.no-background .fa-hdd-o {
+ font-size: 1.7em;
+}
+
+.robots-manager-element .repo-circle .fa-lock {
+ width: 16px;
+ height: 16px;
+ line-height: 16px;
+ font-size: 12px !important;
+}
+
+.robots-manager-element .repo-circle.no-background .fa-lock {
+ bottom: 5px;
+ right: 2px;
+}
+
+.robots-manager-element .member-perm-summary {
+ margin-right: 14px;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/teams-manager.css b/static/css/directives/ui/teams-manager.css
new file mode 100644
index 000000000..a36edf5ce
--- /dev/null
+++ b/static/css/directives/ui/teams-manager.css
@@ -0,0 +1,55 @@
+.teams-manager .popup-input-button {
+ float: right;
+}
+
+.teams-manager .manager-header {
+ border-bottom: 1px solid #eee;
+ margin-bottom: 10px;
+}
+
+.teams-manager .cor-options-menu {
+ display: inline-block;
+ margin-left: 10px;
+}
+
+.teams-manager .header-col .info-icon {
+ font-size: 16px;
+}
+
+.teams-manager .header-col .header-text {
+ text-transform: uppercase;
+ font-size: 14px;
+ color: #aaa !important;
+ display: inline-block;
+ padding-top: 4px;
+}
+
+.teams-manager .control-col {
+ padding-top: 6px;
+}
+
+.teams-manager .team-listing {
+ margin-bottom: 10px;
+}
+
+.teams-manager .team-listing .avatar {
+ margin-right: 6px;
+}
+
+.teams-manager .team-member-list .fa {
+ color: #ccc;
+}
+
+.teams-manager .team-member-list {
+ position: relative;
+ min-height: 20px;
+ padding: 4px;
+ padding-left: 40px;
+}
+
+.teams-manager .team-member-list .team-member-more {
+ vertical-align: middle;
+ padding-left: 6px;
+ color: #aaa;
+ font-size: 14px;
+}
diff --git a/static/css/pages/org-view.css b/static/css/pages/org-view.css
new file mode 100644
index 000000000..1efd78fd1
--- /dev/null
+++ b/static/css/pages/org-view.css
@@ -0,0 +1,25 @@
+.org-view .organization-name {
+ vertical-align: middle;
+ margin-left: 6px;
+}
+
+.org-view h3 {
+ margin-bottom: 20px;
+ margin-top: 0px;
+}
+
+.org-view .section-description-header {
+ padding-left: 40px;
+ position: relative;
+ margin-bottom: 20px;
+}
+
+.org-view .section-description-header:before {
+ font-family: FontAwesome;
+ content: "\f05a";
+ position: absolute;
+ top: 2px;
+ left: 6px;
+ font-size: 27px;
+ color: #888;
+}
\ No newline at end of file
diff --git a/static/css/pages/team-view.css b/static/css/pages/team-view.css
new file mode 100644
index 000000000..26df5412d
--- /dev/null
+++ b/static/css/pages/team-view.css
@@ -0,0 +1,51 @@
+.team-view .co-main-content-panel {
+ padding: 20px;
+}
+
+.team-view .team-name {
+ vertical-align: middle;
+ margin-left: 6px;
+}
+
+.team-view .team-view-header {
+ border-bottom: 1px solid #eee;
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+}
+
+.team-view .team-view-header > h3 {
+ margin-top: 10px;
+}
+
+.team-view .team-view-header .popover {
+ max-width: none !important;
+}
+
+.team-view .team-view-header .popover.bottom-right .arrow:after {
+ border-bottom-color: #f7f7f7;
+ top: 2px;
+}
+
+.team-view .team-view-header .popover-content {
+ font-size: 14px;
+ padding-top: 6px;
+ min-width: 500px;
+}
+
+.team-view .team-view-header .popover-content input {
+ background: white;
+}
+
+.team-view .team-view-add-element .help-text {
+ font-size: 13px;
+ color: #ccc;
+ margin-top: 10px;
+}
+
+.team-view .co-table-header-row td {
+ padding-top: 20px !important;
+}
+
+.team-view .co-table-header-row:first-child td {
+ padding-top: 10px !important;
+}
\ No newline at end of file
diff --git a/static/css/quay.css b/static/css/quay.css
index 9d10c888d..3d674254e 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -372,55 +372,6 @@ nav.navbar-default .navbar-nav>li>a.active {
top: 70px;
}
-.entity-search-element {
- position: relative;
- display: block;
-}
-
-.entity-search-element .entity-reference {
- position: absolute !important;
- top: 7px;
- left: 8px;
- right: 36px;
- z-index: 0;
- pointer-events: none;
-}
-
-.entity-search-element .entity-reference .entity-reference-element {
- pointer-events: none;
-}
-
-.entity-search-element .entity-reference-element i.fa-exclamation-triangle {
- pointer-events: all;
-}
-
-.entity-search-element .entity-reference .entity-name {
- display: none;
-}
-
-.entity-search-element input {
- vertical-align: middle;
- width: 100%;
-}
-
-.entity-search-element.persistent input {
- padding-left: 28px;
- padding-right: 28px;
-}
-
-.entity-search-element .twitter-typeahead {
- vertical-align: middle;
- display: block !important;
- margin-right: 36px;
-}
-
-.entity-search-element .dropdown {
- vertical-align: middle;
- position: absolute;
- top: 0px;
- right: 0px;
-}
-
.dropdown-menu i.fa {
margin-right: 6px;
position: relative;
@@ -574,27 +525,6 @@ i.toggle-icon:hover {
visibility: hidden;
}
-.robots-manager-element {
- max-width: 800px;
-}
-
-.robots-manager-element .alert {
- margin-bottom: 20px;
-}
-
-.robots-manager-element .robot a {
- font-size: 16px;
- cursor: pointer;
-}
-
-.robots-manager-element .robot .prefix {
- color: #aaa;
-}
-
-.robots-manager-element .robot i {
- margin-right: 10px;
-}
-
.logs-view-element .header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
@@ -2330,6 +2260,7 @@ p.editable:hover i {
.copy-box-element input {
border: 0px;
padding-right: 32px;
+ cursor: pointer !important;
}
.copy-box-element .copy-container .copy-icon {
@@ -3336,64 +3267,6 @@ p.editable:hover i {
max-width: 100%;
}
-.billing-invoices-element .invoice-title {
- padding: 6px;
- cursor: pointer;
-}
-
-.billing-invoices-element .invoice-status .success {
- color: green;
-}
-
-.billing-invoices-element .invoice-status .pending {
- color: steelblue;
-}
-
-.billing-invoices-element .invoice-status .danger {
- color: red;
-}
-
-.billing-invoices-element .invoice-amount:before {
- content: '$';
-}
-
-.billing-invoices-element .invoice-details {
- margin-left: 10px;
- margin-bottom: 10px;
-
- padding: 4px;
- padding-left: 6px;
- border-left: 2px solid #eee !important;
-}
-
-.billing-invoices-element .invoice-details td {
- border: 0px solid transparent !important;
-}
-
-.billing-invoices-element .invoice-details dl {
- margin: 0px;
-}
-
-.billing-invoices-element .invoice-details dd {
- margin-left: 10px;
- padding: 6px;
- margin-bottom: 10px;
-}
-
-.billing-invoices-element .invoice-title:hover {
- color: steelblue;
-}
-
-.prototype-manager-element thead th {
- padding: 4px;
- color: #666;
-}
-
-.prototype-manager-element td {
- padding: 10px !important;
- vertical-align: middle !important;
-}
-
.org-list h2 {
margin-bottom: 20px;
}
@@ -5004,3 +4877,13 @@ i.rocket-icon {
text-align: center;
}
+.manager-header {
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+}
+
+.manager-header h3 {
+ margin-bottom: 10px;
+}
+
diff --git a/static/directives/anchor.html b/static/directives/anchor.html
new file mode 100644
index 000000000..563fbe581
--- /dev/null
+++ b/static/directives/anchor.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/static/directives/application-info.html b/static/directives/application-info.html
index 241fec279..960bee7ed 100644
--- a/static/directives/application-info.html
+++ b/static/directives/application-info.html
@@ -1,6 +1,6 @@