Merge branch 'koh'
Conflicts: auth/scopes.py requirements-nover.txt requirements.txt static/css/quay.css static/directives/namespace-selector.html static/js/app.js static/partials/manage-application.html templates/oauthorize.html
This commit is contained in:
commit
f3259c862b
57 changed files with 537 additions and 141 deletions
|
@ -8,7 +8,7 @@ ENV HOME /root
|
|||
RUN apt-get update # 10SEP2014
|
||||
|
||||
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62 libjpeg62-dev libevent-2.0.5 libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap-2.4-2 libldap2-dev libsasl2-modules libsasl2-dev libpq5 libpq-dev
|
||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62 libjpeg62-dev libevent-2.0.5 libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap-2.4-2 libldap2-dev libsasl2-modules libsasl2-dev libpq5 libpq-dev libfreetype6-dev
|
||||
|
||||
# Build the python dependencies
|
||||
ADD requirements.txt requirements.txt
|
||||
|
|
2
app.py
2
app.py
|
@ -25,6 +25,7 @@ from data.buildlogs import BuildLogs
|
|||
from data.archivedlogs import LogArchive
|
||||
from data.queue import WorkQueue
|
||||
from data.userevent import UserEventsBuilderModule
|
||||
from avatars.avatars import Avatar
|
||||
|
||||
|
||||
class Config(BaseConfig):
|
||||
|
@ -119,6 +120,7 @@ features.import_features(app.config)
|
|||
|
||||
Principal(app, use_sessions=False)
|
||||
|
||||
avatar = Avatar(app)
|
||||
login_manager = LoginManager(app)
|
||||
mail = Mail(app)
|
||||
storage = Storage(app)
|
||||
|
|
|
@ -76,7 +76,7 @@ IMPLIED_SCOPES = {
|
|||
|
||||
def scopes_from_scope_string(scopes):
|
||||
if not scopes:
|
||||
return {}
|
||||
scopes = ''
|
||||
|
||||
return {ALL_SCOPES.get(scope, None) for scope in scopes.split(',')}
|
||||
|
||||
|
|
0
avatars/__init__.py
Normal file
0
avatars/__init__.py
Normal file
65
avatars/avatars.py
Normal file
65
avatars/avatars.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
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):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
AVATAR_CLASSES = {
|
||||
'gravatar': GravatarAvatar,
|
||||
'local': LocalAvatar
|
||||
}
|
|
@ -19,7 +19,7 @@ def build_requests_session():
|
|||
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
||||
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
|
||||
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
||||
'CONTACT_INFO']
|
||||
'CONTACT_INFO', 'AVATAR_KIND']
|
||||
|
||||
|
||||
def getFrontendVisibleConfig(config_dict):
|
||||
|
@ -46,6 +46,8 @@ class DefaultConfig(object):
|
|||
PREFERRED_URL_SCHEME = 'http'
|
||||
SERVER_HOSTNAME = 'localhost:5000'
|
||||
|
||||
AVATAR_KIND = 'local'
|
||||
|
||||
REGISTRY_TITLE = 'Quay.io'
|
||||
REGISTRY_TITLE_SHORT = 'Quay.io'
|
||||
CONTACT_INFO = [
|
||||
|
|
|
@ -475,7 +475,7 @@ class OAuthApplication(BaseModel):
|
|||
|
||||
name = CharField()
|
||||
description = TextField(default='')
|
||||
gravatar_email = CharField(null=True)
|
||||
avatar_email = CharField(null=True, db_column='gravatar_email')
|
||||
|
||||
|
||||
class OAuthAuthorizationCode(BaseModel):
|
||||
|
|
|
@ -9,6 +9,7 @@ from data.database import (OAuthApplication, OAuthAuthorizationCode, OAuthAccess
|
|||
random_string_generator)
|
||||
from data.model.legacy import get_user
|
||||
from auth import scopes
|
||||
from flask import render_template
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -154,6 +155,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
|||
|
||||
|
||||
def get_token_response(self, response_type, client_id, redirect_uri, **params):
|
||||
|
||||
# Ensure proper response_type
|
||||
if response_type != 'token':
|
||||
err = 'unsupported_response_type'
|
||||
|
@ -161,7 +163,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
|||
|
||||
# Check redirect URI
|
||||
is_valid_redirect_uri = self.validate_redirect_uri(client_id, redirect_uri)
|
||||
if not is_valid_redirect_uri:
|
||||
if redirect_uri != 'display' and not is_valid_redirect_uri:
|
||||
return self._invalid_redirect_uri_response()
|
||||
|
||||
# Check conditions
|
||||
|
@ -196,6 +198,10 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
|||
url = utils.build_url(redirect_uri, params)
|
||||
url += '#access_token=%s&token_type=%s&expires_in=%s' % (access_token, token_type, expires_in)
|
||||
|
||||
if redirect_uri == 'display':
|
||||
return self._make_response(
|
||||
render_template("message.html", message="Access Token: " + access_token))
|
||||
|
||||
return self._make_response(headers={'Location': url}, status_code=302)
|
||||
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import logging
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from app import app
|
||||
from flask import Blueprint, request, make_response, jsonify, session
|
||||
from flask.ext.restful import Resource, abort, Api, reqparse
|
||||
from flask.ext.restful.utils.cors import crossdomain
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from calendar import timegm
|
||||
from email.utils import formatdate
|
||||
from functools import partial, wraps
|
||||
|
@ -52,6 +51,10 @@ class InvalidRequest(ApiException):
|
|||
def __init__(self, error_description, payload=None):
|
||||
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
|
||||
|
||||
class InvalidResponse(ApiException):
|
||||
def __init__(self, error_description, payload=None):
|
||||
ApiException.__init__(self, 'invalid_response', 400, error_description, payload)
|
||||
|
||||
|
||||
class InvalidToken(ApiException):
|
||||
def __init__(self, error_description, payload=None):
|
||||
|
@ -170,6 +173,9 @@ internal_only = add_method_metadata('internal', True)
|
|||
|
||||
def path_param(name, description):
|
||||
def add_param(func):
|
||||
if not func:
|
||||
return func
|
||||
|
||||
if '__api_path_params' not in dir(func):
|
||||
func.__api_path_params = {}
|
||||
func.__api_path_params[name] = {
|
||||
|
@ -346,6 +352,25 @@ def log_action(kind, user_or_orgname, metadata=None, repo=None):
|
|||
metadata=metadata, repository=repo)
|
||||
|
||||
|
||||
def define_json_response(schema_name):
|
||||
def wrapper(func):
|
||||
@add_method_metadata('response_schema', schema_name)
|
||||
@wraps(func)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
schema = self.schemas[schema_name]
|
||||
resp = func(self, *args, **kwargs)
|
||||
|
||||
if app.config['TESTING']:
|
||||
try:
|
||||
validate(resp, schema)
|
||||
except ValidationError as ex:
|
||||
raise InvalidResponse(ex.message)
|
||||
|
||||
return resp
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
import endpoints.api.billing
|
||||
import endpoints.api.build
|
||||
import endpoints.api.discovery
|
||||
|
|
|
@ -4,10 +4,11 @@ from flask import request
|
|||
from app import billing
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||
require_user_admin, show_if, hide_if, abort)
|
||||
require_user_admin, show_if, hide_if, path_param, require_scope, abort)
|
||||
from endpoints.api.subscribe import subscribe, subscription_view
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
from data.billing import PLANS
|
||||
|
||||
|
@ -149,6 +150,7 @@ class UserCard(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/card')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@internal_only
|
||||
@related_user_resource(UserCard)
|
||||
@show_if(features.BILLING)
|
||||
|
@ -171,6 +173,7 @@ class OrganizationCard(ApiResource):
|
|||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrgCard')
|
||||
def get(self, orgname):
|
||||
""" Get the organization's credit card. """
|
||||
|
@ -259,6 +262,7 @@ class UserPlan(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/plan')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@internal_only
|
||||
@related_user_resource(UserPlan)
|
||||
@show_if(features.BILLING)
|
||||
|
@ -285,6 +289,7 @@ class OrganizationPlan(ApiResource):
|
|||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('updateOrgSubscription')
|
||||
@validate_json_request('OrgSubscription')
|
||||
def put(self, orgname):
|
||||
|
@ -299,6 +304,7 @@ class OrganizationPlan(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrgSubscription')
|
||||
def get(self, orgname):
|
||||
""" Fetch any existing subscription for the org. """
|
||||
|
@ -343,11 +349,12 @@ class UserInvoiceList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/invoices')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@related_user_resource(UserInvoiceList)
|
||||
@show_if(features.BILLING)
|
||||
class OrgnaizationInvoiceList(ApiResource):
|
||||
""" Resource for listing an orgnaization's invoices. """
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('listOrgInvoices')
|
||||
def get(self, orgname):
|
||||
""" List the invoices for the specified orgnaization. """
|
||||
|
|
|
@ -8,7 +8,8 @@ from flask import request, redirect
|
|||
from app import app, userfiles as user_files, build_logs, log_archive
|
||||
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||
require_repo_read, require_repo_write, validate_json_request,
|
||||
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
|
||||
ApiResource, internal_only, format_date, api, Unauthorized, NotFound,
|
||||
path_param)
|
||||
from endpoints.common import start_build
|
||||
from endpoints.trigger import BuildTrigger
|
||||
from data import model, database
|
||||
|
@ -95,6 +96,7 @@ def build_status_view(build_obj, can_write=False):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryBuildList(RepositoryParamResource):
|
||||
""" Resource related to creating and listing repository builds. """
|
||||
schemas = {
|
||||
|
@ -199,6 +201,8 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('build_uuid', 'The UUID of the build')
|
||||
class RepositoryBuildStatus(RepositoryParamResource):
|
||||
""" Resource for dealing with repository build status. """
|
||||
@require_repo_read
|
||||
|
@ -215,6 +219,8 @@ class RepositoryBuildStatus(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('build_uuid', 'The UUID of the build')
|
||||
class RepositoryBuildLogs(RepositoryParamResource):
|
||||
""" Resource for loading repository build logs. """
|
||||
@require_repo_write
|
||||
|
|
|
@ -94,13 +94,19 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||
|
||||
new_operation = {
|
||||
'method': method_name,
|
||||
'nickname': method_metadata(method, 'nickname')
|
||||
'nickname': method_metadata(method, 'nickname') or '(unnamed)'
|
||||
}
|
||||
|
||||
if not compact:
|
||||
response_type = 'void'
|
||||
res_schema_name = method_metadata(method, 'response_schema')
|
||||
if res_schema_name:
|
||||
models[res_schema_name] = view_class.schemas[res_schema_name]
|
||||
response_type = res_schema_name
|
||||
|
||||
new_operation.update({
|
||||
'type': 'void',
|
||||
'summary': method.__doc__ if method.__doc__ else '',
|
||||
'type': response_type,
|
||||
'summary': method.__doc__.strip() if method.__doc__ else '',
|
||||
'parameters': parameters,
|
||||
})
|
||||
|
||||
|
@ -134,7 +140,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
||||
new_resource = {
|
||||
'path': swagger_path,
|
||||
'description': view_class.__doc__ if view_class.__doc__ else "",
|
||||
'description': view_class.__doc__.strip() if view_class.__doc__ else "",
|
||||
'operations': operations,
|
||||
'name': fully_qualified_name(view_class),
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ from collections import defaultdict
|
|||
|
||||
from app import storage as store
|
||||
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
|
||||
format_date, NotFound)
|
||||
format_date, NotFound, path_param)
|
||||
from data import model
|
||||
from util.cache import cache_control_flask_restful
|
||||
|
||||
|
@ -40,6 +40,7 @@ def image_view(image, image_map):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryImageList(RepositoryParamResource):
|
||||
""" Resource for listing repository images. """
|
||||
@require_repo_read
|
||||
|
@ -67,6 +68,8 @@ class RepositoryImageList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('image_id', 'The Docker image ID')
|
||||
class RepositoryImage(RepositoryParamResource):
|
||||
""" Resource for handling repository images. """
|
||||
@require_repo_read
|
||||
|
@ -86,6 +89,8 @@ class RepositoryImage(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('image_id', 'The Docker image ID')
|
||||
class RepositoryImageChanges(RepositoryParamResource):
|
||||
""" Resource for handling repository image change lists. """
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ from datetime import datetime, timedelta
|
|||
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
|
||||
RepositoryParamResource, require_repo_admin, related_user_resource,
|
||||
format_date, Unauthorized, NotFound, require_user_admin,
|
||||
internal_only)
|
||||
internal_only, path_param, require_scope)
|
||||
from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from data import model
|
||||
from auth import scopes
|
||||
|
||||
|
||||
def log_view(log):
|
||||
|
@ -63,7 +64,7 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/logs')
|
||||
@internal_only
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryLogs(RepositoryParamResource):
|
||||
""" Resource for fetching logs for the specific repository. """
|
||||
@require_repo_admin
|
||||
|
@ -103,7 +104,7 @@ class UserLogs(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/logs')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@related_user_resource(UserLogs)
|
||||
class OrgLogs(ApiResource):
|
||||
""" Resource for fetching logs for the entire organization. """
|
||||
|
@ -112,6 +113,7 @@ class OrgLogs(ApiResource):
|
|||
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
def get(self, args, orgname):
|
||||
""" List the logs for the specified organization. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
|
|
|
@ -2,18 +2,19 @@ import logging
|
|||
|
||||
from flask import request
|
||||
|
||||
from app import billing as stripe
|
||||
from app import billing as stripe, avatar
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||
require_user_admin, log_action, show_if)
|
||||
require_user_admin, log_action, show_if, path_param,
|
||||
require_scope)
|
||||
from endpoints.api.team import team_view
|
||||
from endpoints.api.user import User, PrivateRepositories
|
||||
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
||||
CreateRepositoryPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
from data.billing import get_plan
|
||||
from util.gravatar import compute_hash
|
||||
|
||||
import features
|
||||
|
||||
|
@ -28,7 +29,7 @@ def org_view(o, teams):
|
|||
view = {
|
||||
'name': o.username,
|
||||
'email': o.email if is_admin else '',
|
||||
'gravatar': compute_hash(o.email),
|
||||
'avatar': avatar.compute_hash(o.email, name=o.username),
|
||||
'teams': {t.name : team_view(o.username, t) for t in teams},
|
||||
'is_admin': is_admin
|
||||
}
|
||||
|
@ -97,7 +98,7 @@ class OrganizationList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@related_user_resource(User)
|
||||
class Organization(ApiResource):
|
||||
""" Resource for managing organizations. """
|
||||
|
@ -118,6 +119,8 @@ class Organization(ApiResource):
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganization')
|
||||
def get(self, orgname):
|
||||
""" Get the details for the specified organization """
|
||||
|
@ -133,6 +136,7 @@ class Organization(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('changeOrganizationDetails')
|
||||
@validate_json_request('UpdateOrg')
|
||||
def put(self, orgname):
|
||||
|
@ -163,11 +167,14 @@ class Organization(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/private')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@internal_only
|
||||
@related_user_resource(PrivateRepositories)
|
||||
@show_if(features.BILLING)
|
||||
class OrgPrivateRepositories(ApiResource):
|
||||
""" Custom verb to compute whether additional private repositories are available. """
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganizationPrivateAllowed')
|
||||
def get(self, orgname):
|
||||
""" Return whether or not this org is allowed to create new private repositories. """
|
||||
|
@ -199,9 +206,11 @@ class OrgPrivateRepositories(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/members')
|
||||
@internal_only
|
||||
@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):
|
||||
""" List the members of the specified organization. """
|
||||
|
@ -232,9 +241,12 @@ class OrgnaizationMemberList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/members/<membername>')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('membername', 'The username of the organization member')
|
||||
class OrganizationMember(ApiResource):
|
||||
""" Resource for managing individual organization members. """
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganizationMember')
|
||||
def get(self, orgname, membername):
|
||||
""" Get information on the specific orgnaization member. """
|
||||
|
@ -265,8 +277,10 @@ class OrganizationMember(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/app/<client_id>')
|
||||
@path_param('client_id', 'The OAuth client ID')
|
||||
class ApplicationInformation(ApiResource):
|
||||
""" Resource that returns public information about a registered application. """
|
||||
|
||||
@nickname('getApplicationInformation')
|
||||
def get(self, client_id):
|
||||
""" Get information on the specified application. """
|
||||
|
@ -274,14 +288,16 @@ class ApplicationInformation(ApiResource):
|
|||
if not application:
|
||||
raise NotFound()
|
||||
|
||||
org_hash = compute_hash(application.organization.email)
|
||||
gravatar = compute_hash(application.gravatar_email) if application.gravatar_email else org_hash
|
||||
org_hash = avatar.compute_hash(application.organization.email,
|
||||
name=application.organization.username)
|
||||
app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
|
||||
application.avatar_email else org_hash)
|
||||
|
||||
return {
|
||||
'name': application.name,
|
||||
'description': application.description,
|
||||
'uri': application.application_uri,
|
||||
'gravatar': gravatar,
|
||||
'avatar': app_hash,
|
||||
'organization': org_view(application.organization, [])
|
||||
}
|
||||
|
||||
|
@ -297,12 +313,12 @@ def app_view(application):
|
|||
'client_id': application.client_id,
|
||||
'client_secret': application.client_secret if is_admin else None,
|
||||
'redirect_uri': application.redirect_uri if is_admin else None,
|
||||
'gravatar_email': application.gravatar_email if is_admin else None,
|
||||
'avatar_email': application.avatar_email if is_admin else None,
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/applications')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
class OrganizationApplications(ApiResource):
|
||||
""" Resource for managing applications defined by an organizations. """
|
||||
schemas = {
|
||||
|
@ -330,15 +346,15 @@ class OrganizationApplications(ApiResource):
|
|||
'type': 'string',
|
||||
'description': 'The human-readable description for the application',
|
||||
},
|
||||
'gravatar_email': {
|
||||
'avatar_email': {
|
||||
'type': 'string',
|
||||
'description': 'The e-mail address of the gravatar to use for the application',
|
||||
'description': 'The e-mail address of the avatar to use for the application',
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganizationApplications')
|
||||
def get(self, orgname):
|
||||
""" List the applications for the specified organization """
|
||||
|
@ -354,6 +370,7 @@ class OrganizationApplications(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('createOrganizationApplication')
|
||||
@validate_json_request('NewApp')
|
||||
def post(self, orgname):
|
||||
|
@ -371,7 +388,7 @@ class OrganizationApplications(ApiResource):
|
|||
app_data.get('application_uri', ''),
|
||||
app_data.get('redirect_uri', ''),
|
||||
description = app_data.get('description', ''),
|
||||
gravatar_email = app_data.get('gravatar_email', None),)
|
||||
avatar_email = app_data.get('avatar_email', None),)
|
||||
|
||||
|
||||
app_data.update({
|
||||
|
@ -386,7 +403,8 @@ class OrganizationApplications(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/applications/<client_id>')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('client_id', 'The OAuth client ID')
|
||||
class OrganizationApplicationResource(ApiResource):
|
||||
""" Resource for managing an application defined by an organizations. """
|
||||
schemas = {
|
||||
|
@ -416,14 +434,15 @@ class OrganizationApplicationResource(ApiResource):
|
|||
'type': 'string',
|
||||
'description': 'The human-readable description for the application',
|
||||
},
|
||||
'gravatar_email': {
|
||||
'avatar_email': {
|
||||
'type': 'string',
|
||||
'description': 'The e-mail address of the gravatar to use for the application',
|
||||
'description': 'The e-mail address of the avatar to use for the application',
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganizationApplication')
|
||||
def get(self, orgname, client_id):
|
||||
""" Retrieves the application with the specified client_id under the specified organization """
|
||||
|
@ -442,6 +461,7 @@ class OrganizationApplicationResource(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('updateOrganizationApplication')
|
||||
@validate_json_request('UpdateApp')
|
||||
def put(self, orgname, client_id):
|
||||
|
@ -462,7 +482,7 @@ class OrganizationApplicationResource(ApiResource):
|
|||
application.application_uri = app_data['application_uri']
|
||||
application.redirect_uri = app_data['redirect_uri']
|
||||
application.description = app_data.get('description', '')
|
||||
application.gravatar_email = app_data.get('gravatar_email', None)
|
||||
application.avatar_email = app_data.get('avatar_email', None)
|
||||
application.save()
|
||||
|
||||
app_data.update({
|
||||
|
@ -475,7 +495,7 @@ class OrganizationApplicationResource(ApiResource):
|
|||
return app_view(application)
|
||||
raise Unauthorized()
|
||||
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('deleteOrganizationApplication')
|
||||
def delete(self, orgname, client_id):
|
||||
""" Deletes the application under this organization. """
|
||||
|
@ -498,6 +518,8 @@ class OrganizationApplicationResource(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/applications/<client_id>/resetclientsecret')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('client_id', 'The OAuth client ID')
|
||||
@internal_only
|
||||
class OrganizationApplicationResetClientSecret(ApiResource):
|
||||
""" Custom verb for resetting the client secret of an application. """
|
||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
|||
from flask import request
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||
log_action, request_error, validate_json_request)
|
||||
log_action, request_error, validate_json_request, path_param)
|
||||
from data import model
|
||||
|
||||
|
||||
|
@ -26,6 +26,7 @@ def wrap_role_view_org(role_json, user, org_members):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/team/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryTeamPermissionList(RepositoryParamResource):
|
||||
""" Resource for repository team permissions. """
|
||||
@require_repo_admin
|
||||
|
@ -41,6 +42,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/user/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryUserPermissionList(RepositoryParamResource):
|
||||
""" Resource for repository user permissions. """
|
||||
@require_repo_admin
|
||||
|
@ -80,6 +82,8 @@ class RepositoryUserPermissionList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('username', 'The username of the user to which the permission applies')
|
||||
class RepositoryUserPermission(RepositoryParamResource):
|
||||
""" Resource for managing individual user permissions. """
|
||||
schemas = {
|
||||
|
@ -175,6 +179,8 @@ class RepositoryUserPermission(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('teamname', 'The name of the team to which the permission applies')
|
||||
class RepositoryTeamPermission(RepositoryParamResource):
|
||||
""" Resource for managing individual team permissions. """
|
||||
schemas = {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from flask import request
|
||||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
log_action, Unauthorized, NotFound, internal_only)
|
||||
log_action, Unauthorized, NotFound, internal_only, path_param,
|
||||
require_scope)
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
|
||||
|
||||
|
@ -54,7 +56,7 @@ def log_prototype_action(action_kind, orgname, prototype, **kwargs):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/prototypes')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
class PermissionPrototypeList(ApiResource):
|
||||
""" Resource for listing and creating permission prototypes. """
|
||||
schemas = {
|
||||
|
@ -115,6 +117,7 @@ class PermissionPrototypeList(ApiResource):
|
|||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganizationPrototypePermissions')
|
||||
def get(self, orgname):
|
||||
""" List the existing prototypes for this organization. """
|
||||
|
@ -131,6 +134,7 @@ class PermissionPrototypeList(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('createOrganizationPrototypePermission')
|
||||
@validate_json_request('NewPrototype')
|
||||
def post(self, orgname):
|
||||
|
@ -179,7 +183,8 @@ class PermissionPrototypeList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/prototypes/<prototypeid>')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('prototypeid', 'The ID of the prototype')
|
||||
class PermissionPrototype(ApiResource):
|
||||
""" Resource for managingin individual permission prototypes. """
|
||||
schemas = {
|
||||
|
@ -204,6 +209,7 @@ class PermissionPrototype(ApiResource):
|
|||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('deleteOrganizationPrototypePermission')
|
||||
def delete(self, orgname, prototypeid):
|
||||
""" Delete an existing permission prototype. """
|
||||
|
@ -224,6 +230,7 @@ class PermissionPrototype(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('updateOrganizationPrototypePermission')
|
||||
@validate_json_request('PrototypeUpdate')
|
||||
def put(self, orgname, prototypeid):
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask import request, abort
|
|||
|
||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||
log_action, validate_json_request, NotFound, internal_only,
|
||||
show_if)
|
||||
path_param, show_if)
|
||||
|
||||
from app import tf
|
||||
from data import model
|
||||
|
@ -28,6 +28,8 @@ def record_view(record):
|
|||
@internal_only
|
||||
@show_if(features.MAILING)
|
||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('email', 'The e-mail address')
|
||||
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
||||
@require_repo_admin
|
||||
|
|
|
@ -7,7 +7,9 @@ from data import model
|
|||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||
require_repo_read, require_repo_write, require_repo_admin,
|
||||
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
||||
request_error, require_scope, Unauthorized, NotFound, InvalidRequest)
|
||||
request_error, require_scope, Unauthorized, NotFound, InvalidRequest,
|
||||
path_param)
|
||||
|
||||
from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission,
|
||||
CreateRepositoryPermission, ReadRepositoryPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
@ -140,6 +142,7 @@ class RepositoryList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class Repository(RepositoryParamResource):
|
||||
"""Operations for managing a specific repository."""
|
||||
schemas = {
|
||||
|
@ -232,6 +235,7 @@ class Repository(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/changevisibility')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryVisibility(RepositoryParamResource):
|
||||
""" Custom verb for changing the visibility of the repository. """
|
||||
schemas = {
|
||||
|
|
|
@ -4,7 +4,8 @@ from flask import request, abort
|
|||
|
||||
from app import notification_queue
|
||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||
log_action, validate_json_request, api, NotFound, request_error)
|
||||
log_action, validate_json_request, api, NotFound, request_error,
|
||||
path_param)
|
||||
from endpoints.notificationevent import NotificationEvent
|
||||
from endpoints.notificationmethod import (NotificationMethod,
|
||||
CannotValidateNotificationMethodException)
|
||||
|
@ -28,6 +29,7 @@ def notification_view(notification):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/notification/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryNotificationList(RepositoryParamResource):
|
||||
""" Resource for dealing with listing and creating notifications on a repository. """
|
||||
schemas = {
|
||||
|
@ -95,6 +97,8 @@ class RepositoryNotificationList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('uuid', 'The UUID of the notification')
|
||||
class RepositoryNotification(RepositoryParamResource):
|
||||
""" Resource for dealing with specific notifications. """
|
||||
@require_repo_admin
|
||||
|
@ -126,6 +130,8 @@ class RepositoryNotification(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('uuid', 'The UUID of the notification')
|
||||
class TestRepositoryNotification(RepositoryParamResource):
|
||||
""" Resource for queuing a test of a notification. """
|
||||
@require_repo_admin
|
||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
|||
from flask import request
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||
log_action, validate_json_request, NotFound)
|
||||
log_action, validate_json_request, NotFound, path_param)
|
||||
from data import model
|
||||
|
||||
|
||||
|
@ -19,6 +19,7 @@ def token_view(token_obj):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tokens/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryTokenList(RepositoryParamResource):
|
||||
""" Resource for creating and listing repository tokens. """
|
||||
schemas = {
|
||||
|
@ -66,6 +67,8 @@ class RepositoryTokenList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('code', 'The token code')
|
||||
class RepositoryToken(RepositoryParamResource):
|
||||
""" Resource for managing individual tokens. """
|
||||
schemas = {
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
|
||||
require_scope)
|
||||
require_scope, path_param)
|
||||
from data import model
|
||||
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
||||
ReadRepositoryPermission, UserAdminPermission,
|
||||
AdministerOrganizationPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from util.gravatar import compute_hash
|
||||
from app import avatar
|
||||
|
||||
|
||||
@resource('/v1/entities/<prefix>')
|
||||
class EntitySearch(ApiResource):
|
||||
""" Resource for searching entities. """
|
||||
@path_param('prefix', 'The prefix of the entities being looked up')
|
||||
@parse_args
|
||||
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
|
||||
default='')
|
||||
|
@ -44,7 +45,7 @@ class EntitySearch(ApiResource):
|
|||
'name': namespace_name,
|
||||
'kind': 'org',
|
||||
'is_org_member': True,
|
||||
'gravatar': compute_hash(organization.email),
|
||||
'avatar': avatar.compute_hash(organization.email, name=organization.username),
|
||||
}]
|
||||
|
||||
except model.InvalidOrganizationException:
|
||||
|
|
|
@ -9,7 +9,7 @@ from flask import request
|
|||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||
log_action, internal_only, NotFound, require_user_admin, format_date,
|
||||
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
||||
query_param, abort, require_fresh_login)
|
||||
query_param, abort, require_fresh_login, path_param)
|
||||
|
||||
from endpoints.api.logs import get_logs
|
||||
|
||||
|
@ -166,6 +166,7 @@ class SuperUserSendRecoveryEmail(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/superuser/users/<username>')
|
||||
@path_param('username', 'The username of the user being managed')
|
||||
@internal_only
|
||||
@show_if(features.SUPER_USERS)
|
||||
class SuperUserManagement(ApiResource):
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from flask import request
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||
RepositoryParamResource, log_action, NotFound, validate_json_request)
|
||||
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
||||
path_param)
|
||||
from endpoints.api.image import image_view
|
||||
from data import model
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('tag', 'The name of the tag')
|
||||
class RepositoryTag(RepositoryParamResource):
|
||||
""" Resource for managing repository tags. """
|
||||
schemas = {
|
||||
|
@ -73,6 +76,8 @@ class RepositoryTag(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('tag', 'The name of the tag')
|
||||
class RepositoryTagImages(RepositoryParamResource):
|
||||
""" Resource for listing the images in a specific repository tag. """
|
||||
@require_repo_read
|
||||
|
|
|
@ -2,13 +2,14 @@ from flask import request
|
|||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
||||
query_param, truthy_bool, parse_args, require_user_admin, show_if)
|
||||
path_param, query_param, truthy_bool, parse_args, require_user_admin,
|
||||
show_if)
|
||||
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
from util.useremails import send_org_invite_email
|
||||
from util.gravatar import compute_hash
|
||||
from app import avatar
|
||||
|
||||
import features
|
||||
|
||||
|
@ -63,7 +64,7 @@ def member_view(member, invited=False):
|
|||
'name': member.username,
|
||||
'kind': 'user',
|
||||
'is_robot': member.robot,
|
||||
'gravatar': compute_hash(member.email) if not member.robot else None,
|
||||
'avatar': avatar.compute_hash(member.email, name=member.username) if not member.robot else None,
|
||||
'invited': invited,
|
||||
}
|
||||
|
||||
|
@ -75,13 +76,14 @@ def invite_view(invite):
|
|||
return {
|
||||
'email': invite.email,
|
||||
'kind': 'invite',
|
||||
'gravatar': compute_hash(invite.email),
|
||||
'avatar': avatar.compute_hash(invite.email),
|
||||
'invited': True
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('teamname', 'The name of the team')
|
||||
class OrganizationTeam(ApiResource):
|
||||
""" Resource for manging an organization's teams. """
|
||||
schemas = {
|
||||
|
@ -110,6 +112,7 @@ class OrganizationTeam(ApiResource):
|
|||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('updateOrganizationTeam')
|
||||
@validate_json_request('TeamDescription')
|
||||
def put(self, orgname, teamname):
|
||||
|
@ -151,6 +154,7 @@ class OrganizationTeam(ApiResource):
|
|||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('deleteOrganizationTeam')
|
||||
def delete(self, orgname, teamname):
|
||||
""" Delete the specified team. """
|
||||
|
@ -164,9 +168,11 @@ class OrganizationTeam(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>/members')
|
||||
@internal_only
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('teamname', 'The name of the team')
|
||||
class TeamMemberList(ApiResource):
|
||||
""" Resource for managing the list of members for a team. """
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@parse_args
|
||||
@query_param('includePending', 'Whether to include pending members', type=truthy_bool, default=False)
|
||||
@nickname('getOrganizationTeamMembers')
|
||||
|
@ -199,8 +205,12 @@ class TeamMemberList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>/members/<membername>')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('teamname', 'The name of the team')
|
||||
@path_param('membername', 'The username of the team member')
|
||||
class TeamMember(ApiResource):
|
||||
""" Resource for managing individual members of a team. """
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('updateOrganizationTeamMember')
|
||||
def put(self, orgname, teamname, membername):
|
||||
|
|
|
@ -8,7 +8,8 @@ from urlparse import urlunparse
|
|||
from app import app
|
||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||
log_action, request_error, query_param, parse_args, internal_only,
|
||||
validate_json_request, api, Unauthorized, NotFound, InvalidRequest)
|
||||
validate_json_request, api, Unauthorized, NotFound, InvalidRequest,
|
||||
path_param)
|
||||
from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus,
|
||||
get_trigger_config)
|
||||
from endpoints.common import start_build
|
||||
|
@ -30,6 +31,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class BuildTriggerList(RepositoryParamResource):
|
||||
""" Resource for listing repository build triggers. """
|
||||
|
||||
|
@ -44,6 +46,8 @@ class BuildTriggerList(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class BuildTrigger(RepositoryParamResource):
|
||||
""" Resource for managing specific build triggers. """
|
||||
|
||||
|
@ -90,6 +94,8 @@ class BuildTrigger(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
@internal_only
|
||||
class BuildTriggerSubdirs(RepositoryParamResource):
|
||||
""" Custom verb for fetching the subdirs which are buildable for a trigger. """
|
||||
|
@ -137,7 +143,8 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
|
||||
@internal_only
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class BuildTriggerActivate(RepositoryParamResource):
|
||||
""" Custom verb for activating a build trigger once all required information has been collected.
|
||||
"""
|
||||
|
@ -234,6 +241,8 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
@internal_only
|
||||
class BuildTriggerAnalyze(RepositoryParamResource):
|
||||
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
|
||||
|
@ -369,6 +378,8 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class ActivateBuildTrigger(RepositoryParamResource):
|
||||
""" Custom verb to manually activate a build trigger. """
|
||||
schemas = {
|
||||
|
@ -424,6 +435,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class TriggerBuildList(RepositoryParamResource):
|
||||
""" Resource to represent builds that were activated from the specified trigger. """
|
||||
@require_repo_admin
|
||||
|
@ -471,6 +484,8 @@ class BuildTriggerFieldValues(RepositoryParamResource):
|
|||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
@internal_only
|
||||
class BuildTriggerSources(RepositoryParamResource):
|
||||
""" Custom verb to fetch the list of build sources for the trigger config. """
|
||||
|
|
|
@ -5,11 +5,11 @@ from flask import request
|
|||
from flask.ext.login import logout_user
|
||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||
|
||||
from app import app, billing as stripe, authentication
|
||||
from app import app, billing as stripe, authentication, avatar
|
||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
||||
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
||||
license_error, require_fresh_login)
|
||||
license_error, require_fresh_login, path_param, define_json_response)
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from endpoints.api.team import try_accept_invite
|
||||
|
@ -20,7 +20,6 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
|
|||
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from util.gravatar import compute_hash
|
||||
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
|
||||
from util.names import parse_single_urn
|
||||
|
||||
|
@ -34,7 +33,7 @@ def user_view(user):
|
|||
admin_org = AdministerOrganizationPermission(o.username)
|
||||
return {
|
||||
'name': o.username,
|
||||
'gravatar': compute_hash(o.email),
|
||||
'avatar': avatar.compute_hash(o.email, name=o.username),
|
||||
'is_org_admin': admin_org.can(),
|
||||
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
|
||||
'preferred_namespace': not (o.stripe_id is None)
|
||||
|
@ -60,13 +59,13 @@ def user_view(user):
|
|||
'verified': user.verified,
|
||||
'anonymous': False,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'gravatar': compute_hash(user.email),
|
||||
'avatar': avatar.compute_hash(user.email, name=user.username),
|
||||
}
|
||||
|
||||
user_admin = UserAdminPermission(user.username)
|
||||
if user_admin.can():
|
||||
user_response.update({
|
||||
'email': user.email,
|
||||
'organizations': [org_view(o) for o in organizations],
|
||||
'logins': [login_view(login) for login in logins],
|
||||
'can_create_repo': True,
|
||||
|
@ -149,10 +148,51 @@ class User(ApiResource):
|
|||
},
|
||||
},
|
||||
},
|
||||
'UserView': {
|
||||
'id': 'UserView',
|
||||
'type': 'object',
|
||||
'description': 'Describes a user',
|
||||
'required': ['verified', 'anonymous', 'avatar'],
|
||||
'properties': {
|
||||
'verified': {
|
||||
'type': 'boolean',
|
||||
'description': 'Whether the user\'s email address has been verified'
|
||||
},
|
||||
'anonymous': {
|
||||
'type': 'boolean',
|
||||
'description': 'true if this user data represents a guest user'
|
||||
},
|
||||
'email': {
|
||||
'type': 'string',
|
||||
'description': 'The user\'s email address',
|
||||
},
|
||||
'avatar': {
|
||||
'type': 'string',
|
||||
'description': 'Avatar hash representing the user\'s icon'
|
||||
},
|
||||
'organizations': {
|
||||
'type': 'array',
|
||||
'description': 'Information about the organizations in which the user is a member'
|
||||
},
|
||||
'logins': {
|
||||
'type': 'array',
|
||||
'description': 'The list of external login providers against which the user has authenticated'
|
||||
},
|
||||
'can_create_repo': {
|
||||
'type': 'boolean',
|
||||
'description': 'Whether the user has permission to create repositories'
|
||||
},
|
||||
'preferred_namespace': {
|
||||
'type': 'boolean',
|
||||
'description': 'If true, the user\'s namespace is the preferred namespace to display'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@require_scope(scopes.READ_USER)
|
||||
@nickname('getLoggedInUser')
|
||||
@define_json_response('UserView')
|
||||
def get(self):
|
||||
""" Get user information for the authenticated user. """
|
||||
user = get_authenticated_user()
|
||||
|
@ -166,6 +206,7 @@ class User(ApiResource):
|
|||
@nickname('changeUserDetails')
|
||||
@internal_only
|
||||
@validate_json_request('UpdateUser')
|
||||
@define_json_response('UserView')
|
||||
def put(self):
|
||||
""" Update a users details such as password or email. """
|
||||
user = get_authenticated_user()
|
||||
|
@ -526,6 +567,7 @@ class UserNotificationList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/user/notifications/<uuid>')
|
||||
@path_param('uuid', 'The uuid of the user notification')
|
||||
@internal_only
|
||||
class UserNotification(ApiResource):
|
||||
schemas = {
|
||||
|
@ -572,10 +614,12 @@ def authorization_view(access_token):
|
|||
'name': oauth_app.name,
|
||||
'description': oauth_app.description,
|
||||
'url': oauth_app.application_uri,
|
||||
'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
|
||||
'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email,
|
||||
name=oauth_app.name),
|
||||
'organization': {
|
||||
'name': oauth_app.organization.username,
|
||||
'gravatar': compute_hash(oauth_app.organization.email)
|
||||
'avatar': avatar.compute_hash(oauth_app.organization.email,
|
||||
name=oauth_app.organization.username)
|
||||
}
|
||||
},
|
||||
'scopes': scopes.get_scope_information(access_token.scope),
|
||||
|
@ -596,6 +640,7 @@ class UserAuthorizationList(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/user/authorizations/<access_token_uuid>')
|
||||
@path_param('access_token_uuid', 'The uuid of the access token')
|
||||
@internal_only
|
||||
class UserAuthorization(ApiResource):
|
||||
@require_user_admin
|
||||
|
|
|
@ -198,6 +198,7 @@ def render_page_template(name, **kwargs):
|
|||
feature_set=json.dumps(features.get_features()),
|
||||
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
|
||||
oauth_set=json.dumps(get_oauth_config()),
|
||||
scope_set=json.dumps(scopes.ALL_SCOPES),
|
||||
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||
google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
|
||||
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||
Blueprint, send_from_directory, jsonify)
|
||||
|
||||
from avatar_generator import Avatar
|
||||
from flask.ext.login import current_user
|
||||
from urlparse import urlparse
|
||||
from health.healthcheck import HealthCheck
|
||||
|
||||
from data import model
|
||||
from data.model.oauth import DatabaseAuthorizationProvider
|
||||
from app import app, billing as stripe, build_logs
|
||||
from app import app, billing as stripe, build_logs, avatar
|
||||
from auth.auth import require_session_login, process_oauth
|
||||
from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
|
@ -17,8 +18,8 @@ from util.seo import render_snapshot
|
|||
from util.cache import no_cache
|
||||
from endpoints.common import common_login, render_page_template, route_show_if, param_required
|
||||
from endpoints.csrf import csrf_protect, generate_csrf_token
|
||||
from endpoints.registry import set_cache_headers
|
||||
from util.names import parse_repository_name
|
||||
from util.gravatar import compute_hash
|
||||
from util.useremails import send_email_changed
|
||||
from auth import scopes
|
||||
|
||||
|
@ -182,6 +183,20 @@ def status():
|
|||
return response
|
||||
|
||||
|
||||
@app.route("/avatar/<avatar_hash>")
|
||||
@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():
|
||||
|
@ -391,7 +406,7 @@ def request_authorization_code():
|
|||
|
||||
if (not current_user.is_authenticated() or
|
||||
not provider.validate_has_scopes(client_id, current_user.db_user().username, scope)):
|
||||
if not provider.validate_redirect_uri(client_id, redirect_uri):
|
||||
if redirect_uri != 'display' and not provider.validate_redirect_uri(client_id, redirect_uri):
|
||||
current_app = provider.get_application_for_client_id(client_id)
|
||||
if not current_app:
|
||||
abort(404)
|
||||
|
@ -411,9 +426,11 @@ def request_authorization_code():
|
|||
'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),
|
||||
'organization': {
|
||||
'name': oauth_app.organization.username,
|
||||
'gravatar': compute_hash(oauth_app.organization.email)
|
||||
'avatar': avatar.compute_hash(oauth_app.organization.email,
|
||||
name=oauth_app.organization.username)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -431,7 +448,6 @@ def request_authorization_code():
|
|||
else:
|
||||
return provider.get_authorization_code(response_type, client_id, redirect_uri, scope=scope)
|
||||
|
||||
|
||||
@web.route('/oauth/access_token', methods=['POST'])
|
||||
@no_cache
|
||||
@param_required('grant_type')
|
||||
|
|
|
@ -23,7 +23,7 @@ redis
|
|||
hiredis
|
||||
docker-py
|
||||
pygithub
|
||||
flask-restful
|
||||
flask-restful==0.2.12
|
||||
jsonschema
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
alembic
|
||||
|
@ -39,4 +39,5 @@ psycopg2
|
|||
pyyaml
|
||||
git+https://github.com/DevTable/aniso8601-fake.git
|
||||
git+https://github.com/DevTable/anunidecode.git
|
||||
git+https://github.com/DevTable/avatar-generator.git
|
||||
gipc
|
||||
|
|
|
@ -15,10 +15,11 @@ PyPDF2==1.23
|
|||
PyYAML==3.11
|
||||
SQLAlchemy==0.9.8
|
||||
Werkzeug==0.9.6
|
||||
alembic==0.7.0
|
||||
git+https://github.com/DevTable/aniso8601-fake.git
|
||||
git+https://github.com/DevTable/anunidecode.git
|
||||
git+https://github.com/DevTable/avatar-generator.git
|
||||
aiowsgi==0.3
|
||||
alembic==0.6.7
|
||||
autobahn==0.9.3-3
|
||||
backports.ssl-match-hostname==3.4.0.2
|
||||
beautifulsoup4==4.3.2
|
||||
|
@ -36,10 +37,10 @@ html5lib==0.999
|
|||
itsdangerous==0.24
|
||||
jsonschema==2.4.0
|
||||
marisa-trie==0.6
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
mixpanel-py==3.2.0
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
paramiko==1.15.1
|
||||
peewee==2.4.2
|
||||
peewee==2.4.3
|
||||
psycopg2==2.5.4
|
||||
py-bcrypt==0.4
|
||||
pycrypto==2.6.1
|
||||
|
|
|
@ -4264,9 +4264,10 @@ pre.command:before {
|
|||
display: block !important;
|
||||
}
|
||||
|
||||
.auth-header > img {
|
||||
.auth-header > .avatar {
|
||||
float: left;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
|
@ -4827,7 +4828,7 @@ i.slack-icon {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.member-listing .gravatar {
|
||||
.member-listing .avatar {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -4887,4 +4888,12 @@ i.slack-icon {
|
|||
half (although it is still not great)
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
#gen-token table {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#gen-token input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="application-info-element" style="padding-bottom: 18px">
|
||||
<div class="auth-header">
|
||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
|
||||
<span class="avatar" size="48" hash="application.avatar"></span>
|
||||
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
||||
<h4>
|
||||
{{ application.organization.name }}
|
||||
|
|
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 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 ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
||||
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
||||
<img class="gravatar" ng-if="showGravatar == 'true' && entity.gravatar" ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||
<span ng-if="showGravatar != 'true' || !entity.gravatar">
|
||||
<span class="avatar" size="avatarSize || 16" hash="entity.avatar" ng-if="showAvatar == 'true' && entity.avatar"></span>
|
||||
<span ng-if="showAvatar != 'true' || !entity.avatar">
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
</span>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
<li class="dropdown" ng-switch-when="false">
|
||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||
<span class="avatar" size="32" hash="user.avatar"></span>
|
||||
{{ user.username }}
|
||||
<span class="notifications-bubble"></span>
|
||||
<b class="caret"></b>
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<span class="namespace-selector-dropdown">
|
||||
<span ng-show="user.organizations.length == 0">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
|
||||
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||
<span class="namespace-name">{{user.username}}</span>
|
||||
</span>
|
||||
|
||||
<div class="btn-group" ng-show="user.organizations.length > 0">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<img src="//www.gravatar.com/avatar/{{ namespaceObj.gravatar }}?s=16&d=identicon" />
|
||||
<span class="avatar" size="16" hash="namespaceObj.avatar"></span>
|
||||
{{namespace}} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="namespace-item" ng-repeat="org in user.organizations"
|
||||
ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''">
|
||||
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)">
|
||||
<img src="//www.gravatar.com/avatar/{{ org.gravatar }}?s=24&d=identicon" />
|
||||
<span class="avatar" size="24" hash="org.avatar"></span>
|
||||
<span class="namespace-name">{{ org.name }}</span>
|
||||
</a>
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
<li class="divider"></li>
|
||||
<li>
|
||||
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
|
||||
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||
<span class="namespace-name">{{ user.username }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="circle" ng-class="getClass(notification)"></div>
|
||||
<div class="message" ng-bind-html="getMessage(notification)"></div>
|
||||
<div class="orginfo" ng-if="notification.organization">
|
||||
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
||||
<span class="avatar" size="24" hash="getAvatar(notification.organization)"></span>
|
||||
<span class="orgname">{{ notification.organization }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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">
|
||||
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
||||
</span>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<td>
|
||||
<div class="current-repo">
|
||||
<img class="dropdown-select-icon github-org-icon"
|
||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
|
||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}">
|
||||
<a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank">{{ state.currentRepo.repo }}</a>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -53,7 +53,7 @@
|
|||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i>
|
||||
<img class="dropdown-select-icon github-org-icon"
|
||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
|
||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/image/empty.png' }}">
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu scrollable-menu" role="menu">
|
||||
|
|
BIN
static/img/empty.png
Normal file
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;
|
||||
}]);
|
||||
|
||||
$provide.factory('AvatarService', ['Config', '$sanitize', 'md5',
|
||||
function(Config, $sanitize, md5) {
|
||||
var avatarService = {};
|
||||
var cache = {};
|
||||
|
||||
avatarService.getAvatar = function(hash, opt_size) {
|
||||
var size = opt_size || 16;
|
||||
switch (Config['AVATAR_KIND']) {
|
||||
case 'local':
|
||||
return '/avatar/' + hash + '?size=' + size;
|
||||
break;
|
||||
|
||||
case 'gravatar':
|
||||
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
avatarService.computeHash = function(opt_email, opt_name) {
|
||||
var email = opt_email || '';
|
||||
var name = opt_name || '';
|
||||
|
||||
var cacheKey = email + ':' + name;
|
||||
if (!cacheKey) { return '-'; }
|
||||
|
||||
if (cache[cacheKey]) {
|
||||
return cache[cacheKey];
|
||||
}
|
||||
|
||||
var hash = md5.createHash(email.toString().toLowerCase());
|
||||
switch (Config['AVATAR_KIND']) {
|
||||
case 'local':
|
||||
if (name) {
|
||||
hash = name[0] + hash;
|
||||
} else if (email) {
|
||||
hash = email[0] + hash;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return cache[cacheKey] = hash;
|
||||
};
|
||||
|
||||
return avatarService;
|
||||
}]);
|
||||
|
||||
$provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
|
||||
function(UtilService, $sanitize, KeyService) {
|
||||
|
@ -1712,6 +1757,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return notificationService;
|
||||
}]);
|
||||
|
||||
$provide.factory('OAuthService', ['$location', 'Config', function($location, Config) {
|
||||
var oauthService = {};
|
||||
oauthService.SCOPES = window.__auth_scopes;
|
||||
return oauthService;
|
||||
}]);
|
||||
|
||||
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
|
||||
var keyService = {}
|
||||
var oauth = window.__oauth;
|
||||
|
@ -2449,8 +2500,8 @@ quayApp.directive('entityReference', function () {
|
|||
scope: {
|
||||
'entity': '=entity',
|
||||
'namespace': '=namespace',
|
||||
'showGravatar': '@showGravatar',
|
||||
'gravatarSize': '@gravatarSize'
|
||||
'showAvatar': '@showAvatar',
|
||||
'avatarSize': '@avatarSize'
|
||||
},
|
||||
controller: function($scope, $element, UserService, UtilService) {
|
||||
$scope.getIsAdmin = function(namespace) {
|
||||
|
@ -4327,8 +4378,7 @@ quayApp.directive('entitySearch', function () {
|
|||
} else if (datum.entity.kind == 'team') {
|
||||
template += '<i class="fa fa-group fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'org') {
|
||||
template += '<i class="fa"><img src="//www.gravatar.com/avatar/' +
|
||||
datum.entity.gravatar + '?s=16&d=identicon"></i>';
|
||||
template += '<i class="fa">' + AvatarService.getAvatar(datum.entity.avatar, 16) + '</i>';
|
||||
}
|
||||
|
||||
template += '<span class="name">' + datum.value + '</span>';
|
||||
|
@ -6068,9 +6118,9 @@ quayApp.directive('notificationView', function () {
|
|||
return NotificationService.getMessage(notification);
|
||||
};
|
||||
|
||||
$scope.getGravatar = function(orgname) {
|
||||
$scope.getAvatar = function(orgname) {
|
||||
var organization = UserService.getOrganization(orgname);
|
||||
return organization['gravatar'] || '';
|
||||
return organization['avatar'] || '';
|
||||
};
|
||||
|
||||
$scope.parseDate = function(dateString) {
|
||||
|
@ -6452,6 +6502,39 @@ quayApp.directive('locationView', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('avatar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/avatar.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'hash': '=hash',
|
||||
'email': '=email',
|
||||
'name': '=name',
|
||||
'size': '=size'
|
||||
},
|
||||
controller: function($scope, $element, AvatarService) {
|
||||
$scope.AvatarService = AvatarService;
|
||||
|
||||
var refreshHash = function() {
|
||||
if (!$scope.name && !$scope.email) { return; }
|
||||
$scope._hash = AvatarService.computeHash($scope.email, $scope.name);
|
||||
};
|
||||
|
||||
$scope.$watch('hash', function(hash) {
|
||||
$scope._hash = hash;
|
||||
});
|
||||
|
||||
$scope.$watch('name', refreshHash);
|
||||
$scope.$watch('email', refreshHash);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('tagSpecificImagesView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
|
|
@ -2697,12 +2697,28 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul
|
|||
}
|
||||
|
||||
|
||||
function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, ApiService) {
|
||||
function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, OAuthService, ApiService, UserService, Config) {
|
||||
var orgname = $routeParams.orgname;
|
||||
var clientId = $routeParams.clientid;
|
||||
|
||||
$scope.Config = Config;
|
||||
$scope.OAuthService = OAuthService;
|
||||
$scope.updating = false;
|
||||
|
||||
$scope.genScopes = {};
|
||||
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.getScopes = function(scopes) {
|
||||
var checked = [];
|
||||
for (var scopeName in scopes) {
|
||||
if (scopes.hasOwnProperty(scopeName) && scopes[scopeName]) {
|
||||
checked.push(scopeName);
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
};
|
||||
|
||||
$scope.askResetClientSecret = function() {
|
||||
$('#resetSecretModal').modal({});
|
||||
};
|
||||
|
@ -2737,8 +2753,8 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
delete $scope.application['description'];
|
||||
}
|
||||
|
||||
if (!$scope.application['gravatar_email']) {
|
||||
delete $scope.application['gravatar_email'];
|
||||
if (!$scope.application['avatar_email']) {
|
||||
delete $scope.application['avatar_email'];
|
||||
}
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
|
||||
|
|
|
@ -58,9 +58,6 @@
|
|||
<small>Co-Founder</small></h3>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/342ea83fd68d33f90b1f06f466d533c6?s=128&d=identicon">
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-10">
|
||||
<p>Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.</p>
|
||||
</div>
|
||||
|
@ -71,9 +68,6 @@
|
|||
<small>Co-Founder</small></h3>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/9fc3232622773fb2e8f71c0027601bc5?s=128&d=mm">
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-10">
|
||||
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. </p>
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<div class="signup-form"></div>
|
||||
</div>
|
||||
<div ng-show="!user.anonymous" class="user-welcome">
|
||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||
<span class="avatar" size="128" hash="user.avatar"></span>
|
||||
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<div class="signup-form"></div>
|
||||
</div>
|
||||
<div ng-show="!user.anonymous" class="user-welcome">
|
||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||
<span class="avatar" size="128" hash="user.avatar"></span>
|
||||
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="auth-header">
|
||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar_email | gravatar }}?s=48&d=identicon">
|
||||
<span class="avatar" size="48" email="application.avatar_email" name="application.name"></span>
|
||||
<h2>{{ application.name || '(Untitled)' }}</h2>
|
||||
<h4>
|
||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
|
||||
<span class="avatar" size="24" hash="organization.avatar" style="vertical-align: middle; margin-right: 4px;"></span>
|
||||
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
|
||||
</h4>
|
||||
</div>
|
||||
|
@ -30,6 +30,7 @@
|
|||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Settings</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#oauth">OAuth Information</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#gen-token">Generate Token</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete Application</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -59,9 +60,9 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group nested">
|
||||
<label for="fieldAppGravatar">Gravatar E-mail (optional)</label>
|
||||
<input type="email" class="form-control" id="fieldAppGravatar" placeholder="Gravatar E-mail" ng-model="application.gravatar_email">
|
||||
<div class="description">An e-mail address representing the <a href="http://en.gravatar.com/" target="_blank">Gravatar</a> for the application. See above for the icon.</div>
|
||||
<label for="fieldAppAvatar">Avatar E-mail (optional)</label>
|
||||
<input type="email" class="form-control" id="fieldAppAvatar" placeholder="Avatar E-mail" ng-model="application.avatar_email">
|
||||
<div class="description">An e-mail address representing the <a href="http://docs.quay.io/glossary/avatar" target="_blank">Avatar</a> for the application. See above for the icon.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
|
||||
|
@ -91,6 +92,31 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Token tab -->
|
||||
<div id="gen-token" class="tab-pane">
|
||||
<div style="margin-bottom: 20px">
|
||||
Click the button below to generate a new <a href="http://tools.ietf.org/html/rfc6749#section-1.4" target="_new">OAuth 2 Access Token</a>.
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px">
|
||||
<strong>Note:</strong> The generated token will act on behalf of user
|
||||
<span class="avatar" hash="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;"></span>
|
||||
{{ user.username }}
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr ng-repeat="(scopeName, scopeInfo) in OAuthService.SCOPES">
|
||||
<td><label onclick="event.stopPropagation()"><input type="checkbox" value="scopeInfo[0]" ng-model="genScopes[scopeName]">{{ scopeInfo[3] }}</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a class="btn btn-success"
|
||||
href="{{ Config.getUrl('/oauth/authorize?response_type=token&client_id=' + application.client_id + '&scope=' + getScopes(genScopes).join(',') + '&redirect_uri=display') }}"
|
||||
ng-disabled="!getScopes(genScopes).length" target="_blank">
|
||||
Generate Access Token
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- OAuth tab -->
|
||||
<div id="oauth" class="tab-pane">
|
||||
<table style="margin-top: 20px;">
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
|
||||
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
|
||||
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization">
|
||||
<img src="//www.gravatar.com/avatar/{{ organizationEmail | gravatar }}?s=24&d=identicon">
|
||||
<span class="avatar" size="24" email="organizationEmail" name="orgname"></span>
|
||||
<input type="email" class="form-control" ng-model="organizationEmail"
|
||||
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required>
|
||||
<button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<h2>Organizations</h2>
|
||||
|
||||
<div class="organization-listing" ng-repeat="organization in user.organizations">
|
||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=32&d=identicon">
|
||||
<span class="avatar" size="32" hash="organization.avatar"></span>
|
||||
<a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||
|
@ -67,10 +67,10 @@
|
|||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span ng-if="member.kind != 'invite'">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||
</span>
|
||||
<span class="invite-listing" ng-if="member.kind == 'invite'">
|
||||
<img class="gravatar"ng-src="//www.gravatar.com/avatar/{{ member.gravatar }}?s=32&d=identicon">
|
||||
<span class="avatar" size="32" hash="member.avatar"></span>
|
||||
{{ member.email }}
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="user-admin container" ng-show="!user.anonymous">
|
||||
<div class="row">
|
||||
<div class="organization-header-element">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
||||
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||
<span class="organization-name">
|
||||
{{ user.username }}
|
||||
</span>
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
<tr class="auth-info" ng-repeat="authInfo in authorizedApps">
|
||||
<td>
|
||||
<img src="//www.gravatar.com/avatar/{{ authInfo.gravatar }}?s=16&d=identicon">
|
||||
<span class="avatar" size="16" hash="authInfo.application.avatar"></span>
|
||||
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
|
||||
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||
{{ authInfo.application.name }}
|
||||
|
@ -291,7 +291,7 @@
|
|||
<div class="form-group">
|
||||
<label for="orgName">Organization Name</label>
|
||||
<div class="existing-data">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
||||
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||
{{ user.username }}</div>
|
||||
<span class="description">This will continue to be the namespace for your repositories</span>
|
||||
</div>
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
window.__features = {{ feature_set|safe }};
|
||||
window.__config = {{ config_set|safe }};
|
||||
window.__oauth = {{ oauth_set|safe }};
|
||||
window.__auth_scopes = {{ scope_set|safe }};
|
||||
window.__token = '{{ csrf_token() }}';
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
|
||||
<div class="container auth-container" ng-if="!user.anonymous">
|
||||
<div class="auth-header">
|
||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
|
||||
<span class="avatar" size="48" hash="'{{ application.avatar }}'"></span>
|
||||
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
||||
<h4>
|
||||
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
|
||||
<span class="avatar" size="24" hash="'{{ application.organization.avatar }}'"
|
||||
style="vertical-align: middle; margin-right: 4px;"></span>
|
||||
<span style="vertical-align: middle">{{ application.organization.name }}</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
|
|
@ -2114,14 +2114,14 @@ class TestOrganizationApplicationResource(ApiTestCase):
|
|||
edit_json = self.putJsonResponse(OrganizationApplicationResource,
|
||||
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
|
||||
data=dict(name="Some App", description="foo", application_uri="bar",
|
||||
redirect_uri="baz", gravatar_email="meh"))
|
||||
redirect_uri="baz", avatar_email="meh"))
|
||||
|
||||
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, edit_json['client_id'])
|
||||
self.assertEquals("Some App", edit_json['name'])
|
||||
self.assertEquals("foo", edit_json['description'])
|
||||
self.assertEquals("bar", edit_json['application_uri'])
|
||||
self.assertEquals("baz", edit_json['redirect_uri'])
|
||||
self.assertEquals("meh", edit_json['gravatar_email'])
|
||||
self.assertEquals("meh", edit_json['avatar_email'])
|
||||
|
||||
# Retrieve the application again.
|
||||
json = self.getJsonResponse(OrganizationApplicationResource,
|
||||
|
|
|
@ -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 util.gravatar import compute_hash
|
||||
from util.names import parse_robot_username
|
||||
from jinja2 import Template, Environment, FileSystemLoader, contextfilter
|
||||
|
||||
|
@ -25,10 +24,10 @@ def user_reference(username):
|
|||
alt = 'Organization' if user.organization else 'User'
|
||||
return """
|
||||
<span>
|
||||
<img src="http://www.gravatar.com/avatar/%s?s=16&d=identicon"
|
||||
<img src="%s"
|
||||
style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s">
|
||||
<b>%s</b>
|
||||
</span>""" % (compute_hash(user.email), alt, username)
|
||||
</span>""" % (avatar.get_url(user.email, 16), alt, username)
|
||||
|
||||
|
||||
def repository_tag_reference(repository_path_and_tag):
|
||||
|
@ -55,10 +54,10 @@ def repository_reference(pair):
|
|||
|
||||
return """
|
||||
<span style="white-space: nowrap;">
|
||||
<img src="http://www.gravatar.com/avatar/%s?s=16&d=identicon" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
|
||||
<img src="%s" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
|
||||
<a href="%s/repository/%s/%s">%s/%s</a>
|
||||
</span>
|
||||
""" % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository)
|
||||
""" % (avatar.get_url(owner.email, 16), get_app_url(), namespace, repository, namespace, repository)
|
||||
|
||||
|
||||
def admin_reference(username):
|
||||
|
|
|
@ -5,7 +5,6 @@ from flask.ext.mail import Message
|
|||
|
||||
from app import mail, app, get_app_url
|
||||
from data import model
|
||||
from util.gravatar import compute_hash
|
||||
from util.jinjautil import get_template_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
Reference in a new issue