Merge branch 'master' into fix_build

This commit is contained in:
yackob03 2014-01-02 14:33:23 -05:00
commit 96fdae4f0d
23 changed files with 1323 additions and 692 deletions

View file

@ -386,7 +386,6 @@ def get_matching_teams(team_prefix, organization):
def get_matching_users(username_prefix, robot_namespace=None, def get_matching_users(username_prefix, robot_namespace=None,
organization=None): organization=None):
Org = User.alias()
direct_user_query = (User.username ** (username_prefix + '%') & direct_user_query = (User.username ** (username_prefix + '%') &
(User.organization == False) & (User.robot == False)) (User.organization == False) & (User.robot == False))
@ -396,14 +395,16 @@ def get_matching_users(username_prefix, robot_namespace=None,
(User.username ** (robot_prefix + '%') & (User.username ** (robot_prefix + '%') &
(User.robot == True))) (User.robot == True)))
query = User.select(User.username, Org.username, User.robot).where(direct_user_query) query = (User
.select(User.username, fn.Sum(Team.id), User.robot)
.group_by(User.username)
.where(direct_user_query))
if organization: if organization:
with_team = query.join(TeamMember, JOIN_LEFT_OUTER).join(Team, query = (query
JOIN_LEFT_OUTER) .join(TeamMember, JOIN_LEFT_OUTER)
with_org = with_team.join(Org, JOIN_LEFT_OUTER, .join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
on=(Org.id == Team.organization)) (Team.organization == organization))))
query = with_org.where((Org.id == organization) | (Org.id >> None))
class MatchingUserResult(object): class MatchingUserResult(object):
@ -411,7 +412,7 @@ def get_matching_users(username_prefix, robot_namespace=None,
self.username = args[0] self.username = args[0]
self.is_robot = args[2] self.is_robot = args[2]
if organization: if organization:
self.is_org_member = (args[1] == organization.username) self.is_org_member = (args[1] != None)
else: else:
self.is_org_member = None self.is_org_member = None

View file

@ -1,20 +1,13 @@
import json PLANS = [
import itertools # Deprecated Plans
USER_PLANS = [
{
'title': 'Open Source',
'price': 0,
'privateRepos': 0,
'stripeId': 'free',
'audience': 'Share with the world',
},
{ {
'title': 'Micro', 'title': 'Micro',
'price': 700, 'price': 700,
'privateRepos': 5, 'privateRepos': 5,
'stripeId': 'micro', 'stripeId': 'micro',
'audience': 'For smaller teams', 'audience': 'For smaller teams',
'bus_features': False,
'deprecated': True,
}, },
{ {
'title': 'Basic', 'title': 'Basic',
@ -22,6 +15,8 @@ USER_PLANS = [
'privateRepos': 10, 'privateRepos': 10,
'stripeId': 'small', 'stripeId': 'small',
'audience': 'For your basic team', 'audience': 'For your basic team',
'bus_features': False,
'deprecated': True,
}, },
{ {
'title': 'Medium', 'title': 'Medium',
@ -29,6 +24,8 @@ USER_PLANS = [
'privateRepos': 20, 'privateRepos': 20,
'stripeId': 'medium', 'stripeId': 'medium',
'audience': 'For medium teams', 'audience': 'For medium teams',
'bus_features': False,
'deprecated': True,
}, },
{ {
'title': 'Large', 'title': 'Large',
@ -36,16 +33,28 @@ USER_PLANS = [
'privateRepos': 50, 'privateRepos': 50,
'stripeId': 'large', 'stripeId': 'large',
'audience': 'For larger teams', 'audience': 'For larger teams',
'bus_features': False,
'deprecated': True,
}, },
]
BUSINESS_PLANS = [ # Active plans
{ {
'title': 'Open Source', 'title': 'Open Source',
'price': 0, 'price': 0,
'privateRepos': 0, 'privateRepos': 0,
'stripeId': 'bus-free', 'stripeId': 'free',
'audience': 'Committment to FOSS', 'audience': 'Committment to FOSS',
'bus_features': False,
'deprecated': False,
},
{
'title': 'Personal',
'price': 1200,
'privateRepos': 5,
'stripeId': 'personal',
'audience': 'Individuals',
'bus_features': False,
'deprecated': False,
}, },
{ {
'title': 'Skiff', 'title': 'Skiff',
@ -53,6 +62,8 @@ BUSINESS_PLANS = [
'privateRepos': 10, 'privateRepos': 10,
'stripeId': 'bus-micro', 'stripeId': 'bus-micro',
'audience': 'For startups', 'audience': 'For startups',
'bus_features': True,
'deprecated': False,
}, },
{ {
'title': 'Yacht', 'title': 'Yacht',
@ -60,6 +71,8 @@ BUSINESS_PLANS = [
'privateRepos': 20, 'privateRepos': 20,
'stripeId': 'bus-small', 'stripeId': 'bus-small',
'audience': 'For small businesses', 'audience': 'For small businesses',
'bus_features': True,
'deprecated': False,
}, },
{ {
'title': 'Freighter', 'title': 'Freighter',
@ -67,6 +80,8 @@ BUSINESS_PLANS = [
'privateRepos': 50, 'privateRepos': 50,
'stripeId': 'bus-medium', 'stripeId': 'bus-medium',
'audience': 'For normal businesses', 'audience': 'For normal businesses',
'bus_features': True,
'deprecated': False,
}, },
{ {
'title': 'Tanker', 'title': 'Tanker',
@ -74,14 +89,16 @@ BUSINESS_PLANS = [
'privateRepos': 125, 'privateRepos': 125,
'stripeId': 'bus-large', 'stripeId': 'bus-large',
'audience': 'For large businesses', 'audience': 'For large businesses',
'bus_features': True,
'deprecated': False,
}, },
] ]
def get_plan(id): def get_plan(plan_id):
""" Returns the plan with the given ID or None if none. """ """ Returns the plan with the given ID or None if none. """
for plan in itertools.chain(USER_PLANS, BUSINESS_PLANS): for plan in PLANS:
if plan['stripeId'] == id: if plan['stripeId'] == plan_id:
return plan return plan
return None return None

View file

@ -12,7 +12,7 @@ from collections import defaultdict
from data import model from data import model
from data.queue import dockerfile_build_queue from data.queue import dockerfile_build_queue
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan from data.plans import PLANS, get_plan
from app import app from app import app
from util.email import send_confirmation_email, send_recovery_email from util.email import send_confirmation_email, send_recovery_email
from util.names import parse_repository_name, format_robot_username from util.names import parse_repository_name, format_robot_username
@ -26,19 +26,53 @@ from auth.permissions import (ReadRepositoryPermission,
OrganizationMemberPermission, OrganizationMemberPermission,
ViewTeamPermission) ViewTeamPermission)
from endpoints import registry from endpoints import registry
from endpoints.web import common_login from endpoints.common import common_login
from util.cache import cache_control from util.cache import cache_control
from datetime import datetime, timedelta from datetime import datetime, timedelta
store = app.config['STORAGE'] store = app.config['STORAGE']
user_files = app.config['USERFILES'] user_files = app.config['USERFILES']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
route_data = None
def get_route_data():
global route_data
if route_data:
return route_data
routes = []
for rule in app.url_map.iter_rules():
if rule.rule.startswith('/api/'):
endpoint_method = globals()[rule.endpoint]
is_internal = '__internal_call' in dir(endpoint_method)
is_org_api = '__user_call' in dir(endpoint_method)
methods = list(rule.methods.difference(['HEAD', 'OPTIONS']))
route = {
'name': rule.endpoint,
'methods': methods,
'path': rule.rule,
'parameters': list(rule.arguments)
}
if is_org_api:
route['user_method'] = endpoint_method.__user_call
routes.append(route)
route_data = {
'endpoints': routes
}
return route_data
def log_action(kind, user_or_orgname, metadata={}, repo=None): def log_action(kind, user_or_orgname, metadata={}, repo=None):
performer = current_user.db_user() performer = current_user.db_user()
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr, model.log_action(kind, user_or_orgname, performer=performer,
metadata=metadata, repository=repo) ip=request.remote_addr, metadata=metadata, repository=repo)
def api_login_required(f): def api_login_required(f):
@wraps(f) @wraps(f)
@ -58,26 +92,51 @@ def api_login_required(f):
return decorated_view return decorated_view
def internal_api_call(f):
@wraps(f)
def decorated_view(*args, **kwargs):
return f(*args, **kwargs)
decorated_view.__internal_call = True
return decorated_view
def org_api_call(user_call_name):
def internal_decorator(f):
@wraps(f)
def decorated_view(*args, **kwargs):
return f(*args, **kwargs)
decorated_view.__user_call = user_call_name
return decorated_view
return internal_decorator
@app.errorhandler(model.DataModelException) @app.errorhandler(model.DataModelException)
def handle_dme(ex): def handle_dme(ex):
return make_response(ex.message, 400) return make_response(ex.message, 400)
@app.errorhandler(KeyError) @app.errorhandler(KeyError)
def handle_dme(ex): def handle_dme_key_error(ex):
return make_response(ex.message, 400) return make_response(ex.message, 400)
@app.route('/api/discovery')
def discovery():
return jsonify(get_route_data())
@app.route('/api/') @app.route('/api/')
@internal_api_call
def welcome(): def welcome():
return make_response('welcome', 200) return make_response('welcome', 200)
@app.route('/api/plans/') @app.route('/api/plans/')
def plans_list(): def list_plans():
return jsonify({ return jsonify({
'user': USER_PLANS, 'plans': PLANS,
'business': BUSINESS_PLANS,
}) })
@ -107,6 +166,7 @@ def user_view(user):
@app.route('/api/user/', methods=['GET']) @app.route('/api/user/', methods=['GET'])
@internal_api_call
def get_logged_in_user(): def get_logged_in_user():
if current_user.is_anonymous(): if current_user.is_anonymous():
return jsonify({'anonymous': True}) return jsonify({'anonymous': True})
@ -120,6 +180,7 @@ def get_logged_in_user():
@app.route('/api/user/private', methods=['GET']) @app.route('/api/user/private', methods=['GET'])
@api_login_required @api_login_required
@internal_api_call
def get_user_private_count(): def get_user_private_count():
user = current_user.db_user() user = current_user.db_user()
private_repos = model.get_private_repo_count(user.username) private_repos = model.get_private_repo_count(user.username)
@ -140,6 +201,7 @@ def get_user_private_count():
@app.route('/api/user/convert', methods=['POST']) @app.route('/api/user/convert', methods=['POST'])
@api_login_required @api_login_required
@internal_api_call
def convert_user_to_organization(): def convert_user_to_organization():
user = current_user.db_user() user = current_user.db_user()
convert_data = request.get_json() convert_data = request.get_json()
@ -164,7 +226,7 @@ def convert_user_to_organization():
# Subscribe the organization to the new plan. # Subscribe the organization to the new plan.
plan = convert_data['plan'] plan = convert_data['plan']
subscribe(user, plan, None, BUSINESS_PLANS) subscribe(user, plan, None, True) # Require business plans
# Convert the user to an organization. # Convert the user to an organization.
model.convert_user_to_organization(user, model.get_user(admin_username)) model.convert_user_to_organization(user, model.get_user(admin_username))
@ -172,11 +234,11 @@ def convert_user_to_organization():
# And finally login with the admin credentials. # And finally login with the admin credentials.
return conduct_signin(admin_username, admin_password) return conduct_signin(admin_username, admin_password)
@app.route('/api/user/', methods=['PUT']) @app.route('/api/user/', methods=['PUT'])
@api_login_required @api_login_required
@internal_api_call
def change_user_details(): def change_user_details():
user = current_user.db_user() user = current_user.db_user()
@ -203,7 +265,8 @@ def change_user_details():
@app.route('/api/user/', methods=['POST']) @app.route('/api/user/', methods=['POST'])
def create_user_api(): @internal_api_call
def create_new_user():
user_data = request.get_json() user_data = request.get_json()
existing_user = model.get_user(user_data['username']) existing_user = model.get_user(user_data['username'])
@ -229,7 +292,8 @@ def create_user_api():
@app.route('/api/signin', methods=['POST']) @app.route('/api/signin', methods=['POST'])
def signin_api(): @internal_api_call
def signin_user():
signin_data = request.get_json() signin_data = request.get_json()
username = signin_data['username'] username = signin_data['username']
@ -263,6 +327,7 @@ def conduct_signin(username, password):
@app.route("/api/signout", methods=['POST']) @app.route("/api/signout", methods=['POST'])
@api_login_required @api_login_required
@internal_api_call
def logout(): def logout():
logout_user() logout_user()
identity_changed.send(app, identity=AnonymousIdentity()) identity_changed.send(app, identity=AnonymousIdentity())
@ -270,7 +335,8 @@ def logout():
@app.route("/api/recovery", methods=['POST']) @app.route("/api/recovery", methods=['POST'])
def send_recovery(): @internal_api_call
def request_recovery_email():
email = request.get_json()['email'] email = request.get_json()['email']
code = model.create_reset_password_email_code(email) code = model.create_reset_password_email_code(email)
send_recovery_email(email, code.code) send_recovery_email(email, code.code)
@ -355,7 +421,8 @@ def team_view(orgname, team):
@app.route('/api/organization/', methods=['POST']) @app.route('/api/organization/', methods=['POST'])
@api_login_required @api_login_required
def create_organization_api(): @internal_api_call
def create_organization():
org_data = request.get_json() org_data = request.get_json()
existing = None existing = None
@ -419,6 +486,7 @@ def get_organization(orgname):
@app.route('/api/organization/<orgname>', methods=['PUT']) @app.route('/api/organization/<orgname>', methods=['PUT'])
@api_login_required @api_login_required
@org_api_call('change_user_details')
def change_organization_details(orgname): def change_organization_details(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
@ -427,7 +495,7 @@ def change_organization_details(orgname):
except model.InvalidOrganizationException: except model.InvalidOrganizationException:
abort(404) abort(404)
org_data = request.get_json(); org_data = request.get_json()
if 'invoice_email' in org_data: if 'invoice_email' in org_data:
logger.debug('Changing invoice_email for organization: %s', org.username) logger.debug('Changing invoice_email for organization: %s', org.username)
model.change_invoice_email(org, org_data['invoice_email']) model.change_invoice_email(org, org_data['invoice_email'])
@ -477,7 +545,7 @@ def get_organization_member(orgname, membername):
abort(404) abort(404)
member_dict = None member_dict = None
member_teams = model.get_organization_members_with_teams(org, membername = membername) member_teams = model.get_organization_members_with_teams(org, membername=membername)
for member in member_teams: for member in member_teams:
if not member_dict: if not member_dict:
member_dict = {'username': member.user.username, member_dict = {'username': member.user.username,
@ -496,6 +564,7 @@ def get_organization_member(orgname, membername):
@app.route('/api/organization/<orgname>/private', methods=['GET']) @app.route('/api/organization/<orgname>/private', methods=['GET'])
@api_login_required @api_login_required
@internal_api_call
def get_organization_private_allowed(orgname): def get_organization_private_allowed(orgname):
permission = CreateRepositoryPermission(orgname) permission = CreateRepositoryPermission(orgname)
if permission.can(): if permission.can():
@ -551,17 +620,20 @@ def update_organization_team(orgname, teamname):
log_action('org_create_team', orgname, {'team': teamname}) log_action('org_create_team', orgname, {'team': teamname})
if is_existing: if is_existing:
if 'description' in details and team.description != details['description']: if ('description' in details and
team.description != details['description']):
team.description = details['description'] team.description = details['description']
team.save() team.save()
log_action('org_set_team_description', orgname, {'team': teamname, 'description': team.description}) log_action('org_set_team_description', orgname,
{'team': teamname, 'description': team.description})
if 'role' in details: if 'role' in details:
role = model.get_team_org_role(team).name role = model.get_team_org_role(team).name
if role != details['role']: if role != details['role']:
team = model.set_team_org_permission(team, details['role'], team = model.set_team_org_permission(team, details['role'],
current_user.db_user().username) current_user.db_user().username)
log_action('org_set_team_role', orgname, {'team': teamname, 'role': details['role']}) log_action('org_set_team_role', orgname,
{'team': teamname, 'role': details['role']})
resp = jsonify(team_view(orgname, team)) resp = jsonify(team_view(orgname, team))
if not is_existing: if not is_existing:
@ -629,7 +701,8 @@ def update_organization_team_member(orgname, teamname, membername):
# Add the user to the team. # Add the user to the team.
model.add_user_to_team(user, team) model.add_user_to_team(user, team)
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname}) log_action('org_add_team_member', orgname,
{'member': membername, 'team': teamname})
return jsonify(member_view(user)) return jsonify(member_view(user))
abort(403) abort(403)
@ -644,7 +717,8 @@ def delete_organization_team_member(orgname, teamname, membername):
# Remote the user from the team. # Remote the user from the team.
invoking_user = current_user.db_user().username invoking_user = current_user.db_user().username
model.remove_user_from_team(orgname, teamname, membername, invoking_user) model.remove_user_from_team(orgname, teamname, membername, invoking_user)
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname}) log_action('org_remove_team_member', orgname,
{'member': membername, 'team': teamname})
return make_response('Deleted', 204) return make_response('Deleted', 204)
abort(403) abort(403)
@ -652,7 +726,7 @@ def delete_organization_team_member(orgname, teamname, membername):
@app.route('/api/repository', methods=['POST']) @app.route('/api/repository', methods=['POST'])
@api_login_required @api_login_required
def create_repo_api(): def create_repo():
owner = current_user.db_user() owner = current_user.db_user()
req = request.get_json() req = request.get_json()
namespace_name = req['namespace'] if 'namespace' in req else owner.username namespace_name = req['namespace'] if 'namespace' in req else owner.username
@ -673,7 +747,9 @@ def create_repo_api():
repo.description = req['description'] repo.description = req['description']
repo.save() repo.save()
log_action('create_repo', namespace_name, {'repo': repository_name, 'namespace': namespace_name}, repo=repo) log_action('create_repo', namespace_name,
{'repo': repository_name, 'namespace': namespace_name},
repo=repo)
return jsonify({ return jsonify({
'namespace': namespace_name, 'namespace': namespace_name,
'name': repository_name 'name': repository_name
@ -683,7 +759,7 @@ def create_repo_api():
@app.route('/api/find/repository', methods=['GET']) @app.route('/api/find/repository', methods=['GET'])
def match_repos_api(): def find_repos():
prefix = request.args.get('query', '') prefix = request.args.get('query', '')
def repo_view(repo): def repo_view(repo):
@ -706,7 +782,7 @@ def match_repos_api():
@app.route('/api/repository/', methods=['GET']) @app.route('/api/repository/', methods=['GET'])
def list_repos_api(): def list_repos():
def repo_view(repo_obj): def repo_view(repo_obj):
return { return {
'namespace': repo_obj.namespace, 'namespace': repo_obj.namespace,
@ -749,7 +825,7 @@ def list_repos_api():
@app.route('/api/repository/<path:repository>', methods=['PUT']) @app.route('/api/repository/<path:repository>', methods=['PUT'])
@api_login_required @api_login_required
@parse_repository_name @parse_repository_name
def update_repo_api(namespace, repository): def update_repo(namespace, repository):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
if permission.can(): if permission.can():
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
@ -758,7 +834,8 @@ def update_repo_api(namespace, repository):
repo.description = values['description'] repo.description = values['description']
repo.save() repo.save()
log_action('set_repo_description', namespace, {'repo': repository, 'description': values['description']}, log_action('set_repo_description', namespace,
{'repo': repository, 'description': values['description']},
repo=repo) repo=repo)
return jsonify({ return jsonify({
'success': True 'success': True
@ -771,14 +848,15 @@ def update_repo_api(namespace, repository):
methods=['POST']) methods=['POST'])
@api_login_required @api_login_required
@parse_repository_name @parse_repository_name
def change_repo_visibility_api(namespace, repository): def change_repo_visibility(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository) permission = AdministerRepositoryPermission(namespace, repository)
if permission.can(): if permission.can():
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
if repo: if repo:
values = request.get_json() values = request.get_json()
model.set_repository_visibility(repo, values['visibility']) model.set_repository_visibility(repo, values['visibility'])
log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']}, log_action('change_repo_visibility', namespace,
{'repo': repository, 'visibility': values['visibility']},
repo=repo) repo=repo)
return jsonify({ return jsonify({
'success': True 'success': True
@ -795,7 +873,8 @@ def delete_repository(namespace, repository):
if permission.can(): if permission.can():
model.purge_repository(namespace, repository) model.purge_repository(namespace, repository)
registry.delete_repository_storage(namespace, repository) registry.delete_repository_storage(namespace, repository)
log_action('delete_repo', namespace, {'repo': repository, 'namespace': namespace}) log_action('delete_repo', namespace,
{'repo': repository, 'namespace': namespace})
return make_response('Deleted', 204) return make_response('Deleted', 204)
abort(403) abort(403)
@ -813,7 +892,7 @@ def image_view(image):
@app.route('/api/repository/<path:repository>', methods=['GET']) @app.route('/api/repository/<path:repository>', methods=['GET'])
@parse_repository_name @parse_repository_name
def get_repo_api(namespace, repository): def get_repo(namespace, repository):
logger.debug('Get repo: %s/%s' % (namespace, repository)) logger.debug('Get repo: %s/%s' % (namespace, repository))
def tag_view(tag): def tag_view(tag):
@ -912,7 +991,8 @@ def request_repo_build(namespace, repository):
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id})) dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
log_action('build_dockerfile', namespace, log_action('build_dockerfile', namespace,
{'repo': repository, 'namespace': namespace, 'fileid': dockerfile_id}, repo=repo) {'repo': repository, 'namespace': namespace,
'fileid': dockerfile_id}, repo=repo)
resp = jsonify({ resp = jsonify({
'started': True 'started': True
@ -943,7 +1023,8 @@ def create_webhook(namespace, repository):
resp.headers['Location'] = url_for('get_webhook', repository=repo_string, resp.headers['Location'] = url_for('get_webhook', repository=repo_string,
public_id=webhook.public_id) public_id=webhook.public_id)
log_action('add_repo_webhook', namespace, log_action('add_repo_webhook', namespace,
{'repo': repository, 'webhook_id': webhook.public_id}, repo=repo) {'repo': repository, 'webhook_id': webhook.public_id},
repo=repo)
return resp return resp
abort(403) # Permissions denied abort(403) # Permissions denied
@ -994,6 +1075,7 @@ def delete_webhook(namespace, repository, public_id):
@app.route('/api/filedrop/', methods=['POST']) @app.route('/api/filedrop/', methods=['POST'])
@api_login_required @api_login_required
@internal_api_call
def get_filedrop_url(): def get_filedrop_url():
mime_type = request.get_json()['mimeType'] mime_type = request.get_json()['mimeType']
(url, file_id) = user_files.prepare_for_drop(mime_type) (url, file_id) = user_files.prepare_for_drop(mime_type)
@ -1143,7 +1225,8 @@ def list_repo_user_permissions(namespace, repository):
current_func = role_view_func current_func = role_view_func
def wrapped_role_org_view(repo_perm): def wrapped_role_org_view(repo_perm):
return wrap_role_view_org(current_func(repo_perm), repo_perm.user, org_members) return wrap_role_view_org(current_func(repo_perm), repo_perm.user,
org_members)
role_view_func = wrapped_role_org_view role_view_func = wrapped_role_org_view
@ -1228,7 +1311,8 @@ def change_user_permissions(namespace, repository, username):
return error_resp return error_resp
log_action('change_repo_permission', namespace, log_action('change_repo_permission', namespace,
{'username': username, 'repo': repository, 'role': new_permission['role']}, {'username': username, 'repo': repository,
'role': new_permission['role']},
repo=model.get_repository(namespace, repository)) repo=model.get_repository(namespace, repository))
resp = jsonify(perm_view) resp = jsonify(perm_view)
@ -1255,7 +1339,8 @@ def change_team_permissions(namespace, repository, teamname):
new_permission['role']) new_permission['role'])
log_action('change_repo_permission', namespace, log_action('change_repo_permission', namespace,
{'team': teamname, 'repo': repository, 'role': new_permission['role']}, {'team': teamname, 'repo': repository,
'role': new_permission['role']},
repo=model.get_repository(namespace, repository)) repo=model.get_repository(namespace, repository))
resp = jsonify(role_view(perm)) resp = jsonify(role_view(perm))
@ -1282,7 +1367,8 @@ def delete_user_permissions(namespace, repository, username):
error_resp.status_code = 400 error_resp.status_code = 400
return error_resp return error_resp
log_action('delete_repo_permission', namespace, {'username': username, 'repo': repository}, log_action('delete_repo_permission', namespace,
{'username': username, 'repo': repository},
repo=model.get_repository(namespace, repository)) repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204) return make_response('Deleted', 204)
@ -1299,7 +1385,8 @@ def delete_team_permissions(namespace, repository, teamname):
if permission.can(): if permission.can():
model.delete_team_permission(teamname, namespace, repository) model.delete_team_permission(teamname, namespace, repository)
log_action('delete_repo_permission', namespace, {'team': teamname, 'repo': repository}, log_action('delete_repo_permission', namespace,
{'team': teamname, 'repo': repository},
repo=model.get_repository(namespace, repository)) repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204) return make_response('Deleted', 204)
@ -1353,7 +1440,8 @@ def create_token(namespace, repository):
token = model.create_delegate_token(namespace, repository, token = model.create_delegate_token(namespace, repository,
token_params['friendlyName']) token_params['friendlyName'])
log_action('add_repo_accesstoken', namespace, {'repo': repository, 'token': token_params['friendlyName']}, log_action('add_repo_accesstoken', namespace,
{'repo': repository, 'token': token_params['friendlyName']},
repo = model.get_repository(namespace, repository)) repo = model.get_repository(namespace, repository))
resp = jsonify(token_view(token)) resp = jsonify(token_view(token))
@ -1378,7 +1466,8 @@ def change_token(namespace, repository, code):
new_permission['role']) new_permission['role'])
log_action('change_repo_permission', namespace, log_action('change_repo_permission', namespace,
{'repo': repository, 'token': token.friendly_name, 'code': code, 'role': new_permission['role']}, {'repo': repository, 'token': token.friendly_name, 'code': code,
'role': new_permission['role']},
repo = model.get_repository(namespace, repository)) repo = model.get_repository(namespace, repository))
resp = jsonify(token_view(token)) resp = jsonify(token_view(token))
@ -1397,7 +1486,8 @@ def delete_token(namespace, repository, code):
token = model.delete_delegate_token(namespace, repository, code) token = model.delete_delegate_token(namespace, repository, code)
log_action('delete_repo_accesstoken', namespace, log_action('delete_repo_accesstoken', namespace,
{'repo': repository, 'token': token.friendly_name, 'code': code}, {'repo': repository, 'token': token.friendly_name,
'code': code},
repo = model.get_repository(namespace, repository)) repo = model.get_repository(namespace, repository))
return make_response('Deleted', 204) return make_response('Deleted', 204)
@ -1416,14 +1506,17 @@ def subscription_view(stripe_subscription, used_repos):
@app.route('/api/user/card', methods=['GET']) @app.route('/api/user/card', methods=['GET'])
@api_login_required @api_login_required
def get_user_card_api(): @internal_api_call
def get_user_card():
user = current_user.db_user() user = current_user.db_user()
return get_card(user) return get_card(user)
@app.route('/api/organization/<orgname>/card', methods=['GET']) @app.route('/api/organization/<orgname>/card', methods=['GET'])
@api_login_required @api_login_required
def get_org_card_api(orgname): @internal_api_call
@org_api_call('get_user_card')
def get_org_card(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
organization = model.get_organization(orgname) organization = model.get_organization(orgname)
@ -1434,7 +1527,8 @@ def get_org_card_api(orgname):
@app.route('/api/user/card', methods=['POST']) @app.route('/api/user/card', methods=['POST'])
@api_login_required @api_login_required
def set_user_card_api(): @internal_api_call
def set_user_card():
user = current_user.db_user() user = current_user.db_user()
token = request.get_json()['token'] token = request.get_json()['token']
response = set_card(user, token) response = set_card(user, token)
@ -1444,7 +1538,8 @@ def set_user_card_api():
@app.route('/api/organization/<orgname>/card', methods=['POST']) @app.route('/api/organization/<orgname>/card', methods=['POST'])
@api_login_required @api_login_required
def set_org_card_api(orgname): @org_api_call('set_user_card')
def set_org_card(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
organization = model.get_organization(orgname) organization = model.get_organization(orgname)
@ -1486,21 +1581,22 @@ def get_card(user):
if default_card: if default_card:
card_info = { card_info = {
'owner': card.name, 'owner': default_card.name,
'type': card.type, 'type': default_card.type,
'last4': card.last4 'last4': default_card.last4
} }
return jsonify({'card': card_info}) return jsonify({'card': card_info})
@app.route('/api/user/plan', methods=['PUT']) @app.route('/api/user/plan', methods=['PUT'])
@api_login_required @api_login_required
def subscribe_api(): @internal_api_call
def update_user_subscription():
request_data = request.get_json() request_data = request.get_json()
plan = request_data['plan'] plan = request_data['plan']
token = request_data['token'] if 'token' in request_data else None token = request_data['token'] if 'token' in request_data else None
user = current_user.db_user() user = current_user.db_user()
return subscribe(user, plan, token, USER_PLANS) return subscribe(user, plan, token, False) # Business features not required
def carderror_response(e): def carderror_response(e):
@ -1511,15 +1607,22 @@ def carderror_response(e):
return resp return resp
def subscribe(user, plan, token, accepted_plans): def subscribe(user, plan, token, require_business_plan):
plan_found = None plan_found = None
for plan_obj in accepted_plans: for plan_obj in PLANS:
if plan_obj['stripeId'] == plan: if plan_obj['stripeId'] == plan:
plan_found = plan_obj plan_found = plan_obj
if not plan_found: if not plan_found or plan_found['deprecated']:
logger.warning('Plan not found or deprecated: %s', plan)
abort(404) abort(404)
if (require_business_plan and not plan_found['bus_features'] and not
plan_found['price'] == 0):
logger.warning('Business attempting to subscribe to personal plan: %s',
user.username)
abort(400)
private_repos = model.get_private_repo_count(user.username) private_repos = model.get_private_repo_count(user.username)
# This is the default response # This is the default response
@ -1578,9 +1681,32 @@ def subscribe(user, plan, token, accepted_plans):
return resp return resp
@app.route('/api/user/invoices', methods=['GET'])
@api_login_required
def list_user_invoices():
user = current_user.db_user()
if not user.stripe_id:
abort(404)
return get_invoices(user.stripe_id)
@app.route('/api/organization/<orgname>/invoices', methods=['GET']) @app.route('/api/organization/<orgname>/invoices', methods=['GET'])
@api_login_required @api_login_required
def org_invoices_api(orgname): @org_api_call('list_user_invoices')
def list_org_invoices(orgname):
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
if not organization.stripe_id:
abort(404)
return get_invoices(organization.stripe_id)
abort(403)
def get_invoices(customer_id):
def invoice_view(i): def invoice_view(i):
return { return {
'id': i.id, 'id': i.id,
@ -1596,37 +1722,32 @@ def org_invoices_api(orgname):
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None 'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
} }
permission = AdministerOrganizationPermission(orgname) invoices = stripe.Invoice.all(customer=customer_id, count=12)
if permission.can(): return jsonify({
organization = model.get_organization(orgname) 'invoices': [invoice_view(i) for i in invoices.data]
if not organization.stripe_id: })
abort(404)
invoices = stripe.Invoice.all(customer=organization.stripe_id, count=12)
return jsonify({
'invoices': [invoice_view(i) for i in invoices.data]
})
abort(403)
@app.route('/api/organization/<orgname>/plan', methods=['PUT']) @app.route('/api/organization/<orgname>/plan', methods=['PUT'])
@api_login_required @api_login_required
def subscribe_org_api(orgname): @internal_api_call
@org_api_call('update_user_subscription')
def update_org_subscription(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
request_data = request.get_json() request_data = request.get_json()
plan = request_data['plan'] plan = request_data['plan']
token = request_data['token'] if 'token' in request_data else None token = request_data['token'] if 'token' in request_data else None
organization = model.get_organization(orgname) organization = model.get_organization(orgname)
return subscribe(organization, plan, token, BUSINESS_PLANS) return subscribe(organization, plan, token, True) # Business plan required
abort(403) abort(403)
@app.route('/api/user/plan', methods=['GET']) @app.route('/api/user/plan', methods=['GET'])
@api_login_required @api_login_required
def get_subscription(): @internal_api_call
def get_user_subscription():
user = current_user.db_user() user = current_user.db_user()
private_repos = model.get_private_repo_count(user.username) private_repos = model.get_private_repo_count(user.username)
@ -1644,6 +1765,8 @@ def get_subscription():
@app.route('/api/organization/<orgname>/plan', methods=['GET']) @app.route('/api/organization/<orgname>/plan', methods=['GET'])
@api_login_required @api_login_required
@internal_api_call
@org_api_call('get_user_subscription')
def get_org_subscription(orgname): def get_org_subscription(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
@ -1656,7 +1779,7 @@ def get_org_subscription(orgname):
return jsonify(subscription_view(cus.subscription, private_repos)) return jsonify(subscription_view(cus.subscription, private_repos))
return jsonify({ return jsonify({
'plan': 'bus-free', 'plan': 'free',
'usedPrivateRepos': private_repos, 'usedPrivateRepos': private_repos,
}) })
@ -1682,6 +1805,7 @@ def get_user_robots():
@app.route('/api/organization/<orgname>/robots', methods=['GET']) @app.route('/api/organization/<orgname>/robots', methods=['GET'])
@api_login_required @api_login_required
@org_api_call('get_user_robots')
def get_org_robots(orgname): def get_org_robots(orgname):
permission = OrganizationMemberPermission(orgname) permission = OrganizationMemberPermission(orgname)
if permission.can(): if permission.can():
@ -1695,7 +1819,7 @@ def get_org_robots(orgname):
@app.route('/api/user/robots/<robot_shortname>', methods=['PUT']) @app.route('/api/user/robots/<robot_shortname>', methods=['PUT'])
@api_login_required @api_login_required
def create_robot(robot_shortname): def create_user_robot(robot_shortname):
parent = current_user.db_user() parent = current_user.db_user()
robot, password = model.create_robot(robot_shortname, parent) robot, password = model.create_robot(robot_shortname, parent)
resp = jsonify(robot_view(robot.username, password)) resp = jsonify(robot_view(robot.username, password))
@ -1707,6 +1831,7 @@ def create_robot(robot_shortname):
@app.route('/api/organization/<orgname>/robots/<robot_shortname>', @app.route('/api/organization/<orgname>/robots/<robot_shortname>',
methods=['PUT']) methods=['PUT'])
@api_login_required @api_login_required
@org_api_call('create_user_robot')
def create_org_robot(orgname, robot_shortname): def create_org_robot(orgname, robot_shortname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
@ -1722,7 +1847,7 @@ def create_org_robot(orgname, robot_shortname):
@app.route('/api/user/robots/<robot_shortname>', methods=['DELETE']) @app.route('/api/user/robots/<robot_shortname>', methods=['DELETE'])
@api_login_required @api_login_required
def delete_robot(robot_shortname): def delete_user_robot(robot_shortname):
parent = current_user.db_user() parent = current_user.db_user()
model.delete_robot(format_robot_username(parent.username, robot_shortname)) model.delete_robot(format_robot_username(parent.username, robot_shortname))
log_action('delete_robot', parent.username, {'robot': robot_shortname}) log_action('delete_robot', parent.username, {'robot': robot_shortname})
@ -1732,6 +1857,7 @@ def delete_robot(robot_shortname):
@app.route('/api/organization/<orgname>/robots/<robot_shortname>', @app.route('/api/organization/<orgname>/robots/<robot_shortname>',
methods=['DELETE']) methods=['DELETE'])
@api_login_required @api_login_required
@org_api_call('delete_user_robot')
def delete_org_robot(orgname, robot_shortname): def delete_org_robot(orgname, robot_shortname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
@ -1743,27 +1869,27 @@ def delete_org_robot(orgname, robot_shortname):
def log_view(log): def log_view(log):
view = { view = {
'kind': log.kind.name, 'kind': log.kind.name,
'metadata': json.loads(log.metadata_json), 'metadata': json.loads(log.metadata_json),
'ip': log.ip, 'ip': log.ip,
'datetime': log.datetime, 'datetime': log.datetime,
}
if log.performer:
view['performer'] = {
'username': log.performer.username,
'is_robot': log.performer.robot,
} }
if log.performer: return view
view['performer'] = {
'username': log.performer.username,
'is_robot': log.performer.robot,
}
return view
@app.route('/api/repository/<path:repository>/logs', methods=['GET']) @app.route('/api/repository/<path:repository>/logs', methods=['GET'])
@api_login_required @api_login_required
@parse_repository_name @parse_repository_name
def repo_logs_api(namespace, repository): def list_repo_logs(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository) permission = AdministerRepositoryPermission(namespace, repository)
if permission.can(): if permission.can():
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
@ -1779,19 +1905,33 @@ def repo_logs_api(namespace, repository):
@app.route('/api/organization/<orgname>/logs', methods=['GET']) @app.route('/api/organization/<orgname>/logs', methods=['GET'])
@api_login_required @api_login_required
def org_logs_api(orgname): @org_api_call('list_user_logs')
def list_org_logs(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
performer_name = request.args.get('performer', None) performer_name = request.args.get('performer', None)
start_time = request.args.get('starttime', None) start_time = request.args.get('starttime', None)
end_time = request.args.get('endtime', None) end_time = request.args.get('endtime', None)
return get_logs(orgname, start_time, end_time, performer_name=performer_name) return get_logs(orgname, start_time, end_time,
performer_name=performer_name)
abort(403) abort(403)
def get_logs(namespace, start_time, end_time, performer_name=None, repository=None): @app.route('/api/user/logs', methods=['GET'])
@api_login_required
def list_user_logs():
performer_name = request.args.get('performer', None)
start_time = request.args.get('starttime', None)
end_time = request.args.get('endtime', None)
return get_logs(current_user.db_user().username, start_time, end_time,
performer_name=performer_name)
def get_logs(namespace, start_time, end_time, performer_name=None,
repository=None):
performer = None performer = None
if performer_name: if performer_name:
performer = model.get_user(performer_name) performer = model.get_user(performer_name)
@ -1815,7 +1955,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None, repository=No
if not end_time: if not end_time:
end_time = datetime.today() end_time = datetime.today()
logs = model.list_logs(namespace, start_time, end_time, performer = performer, repository=repository) logs = model.list_logs(namespace, start_time, end_time, performer=performer,
repository=repository)
return jsonify({ return jsonify({
'start_time': start_time, 'start_time': start_time,
'end_time': end_time, 'end_time': end_time,

48
endpoints/common.py Normal file
View file

@ -0,0 +1,48 @@
import logging
from flask.ext.login import login_user, UserMixin
from flask.ext.principal import identity_changed
from data import model
from app import app, login_manager
from auth.permissions import QuayDeferredPermissionUser
logger = logging.getLogger(__name__)
@login_manager.user_loader
def load_user(username):
logger.debug('Loading user: %s' % username)
return _LoginWrappedDBUser(username)
class _LoginWrappedDBUser(UserMixin):
def __init__(self, db_username, db_user=None):
self._db_username = db_username
self._db_user = db_user
def db_user(self):
if not self._db_user:
self._db_user = model.get_user(self._db_username)
return self._db_user
def is_authenticated(self):
return self.db_user() is not None
def is_active(self):
return self.db_user().verified
def get_id(self):
return unicode(self._db_username)
def common_login(db_user):
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
logger.debug('Successfully signed in as: %s' % db_user.username)
new_identity = QuayDeferredPermissionUser(db_user.username, 'username')
identity_changed.send(app, identity=new_identity)
return True
else:
logger.debug('User could not be logged in, inactive?.')
return False

View file

@ -4,53 +4,30 @@ import stripe
from flask import (abort, redirect, request, url_for, render_template, from flask import (abort, redirect, request, url_for, render_template,
make_response, Response) make_response, Response)
from flask.ext.login import login_user, UserMixin from flask.ext.login import current_user
from flask.ext.principal import identity_changed
from urlparse import urlparse from urlparse import urlparse
from data import model from data import model
from app import app, login_manager, mixpanel from app import app, mixpanel
from auth.permissions import (QuayDeferredPermissionUser, from auth.permissions import AdministerOrganizationPermission
AdministerOrganizationPermission)
from util.invoice import renderInvoiceToPdf from util.invoice import renderInvoiceToPdf
from util.seo import render_snapshot from util.seo import render_snapshot
from endpoints.api import get_route_data
from endpoints.common import common_login
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class _LoginWrappedDBUser(UserMixin): def render_page_template(name):
def __init__(self, db_username, db_user=None): return render_template(name, route_data = get_route_data())
self._db_username = db_username
self._db_user = db_user
def db_user(self):
if not self._db_user:
self._db_user = model.get_user(self._db_username)
return self._db_user
def is_authenticated(self):
return self.db_user() is not None
def is_active(self):
return self.db_user().verified
def get_id(self):
return unicode(self._db_username)
@login_manager.user_loader
def load_user(username):
logger.debug('Loading user: %s' % username)
return _LoginWrappedDBUser(username)
@app.route('/', methods=['GET'], defaults={'path': ''}) @app.route('/', methods=['GET'], defaults={'path': ''})
@app.route('/repository/<path:path>', methods=['GET']) @app.route('/repository/<path:path>', methods=['GET'])
@app.route('/organization/<path:path>', methods=['GET']) @app.route('/organization/<path:path>', methods=['GET'])
def index(path): def index(path):
return render_template('index.html') return render_page_template('index.html')
@app.route('/snapshot', methods=['GET']) @app.route('/snapshot', methods=['GET'])
@ -81,6 +58,7 @@ def guide():
def organizations(): def organizations():
return index('') return index('')
@app.route('/user/') @app.route('/user/')
def user(): def user():
return index('') return index('')
@ -119,45 +97,47 @@ def status():
@app.route('/tos', methods=['GET']) @app.route('/tos', methods=['GET'])
def tos(): def tos():
return render_template('tos.html') return render_page_template('tos.html')
@app.route('/disclaimer', methods=['GET']) @app.route('/disclaimer', methods=['GET'])
def disclaimer(): def disclaimer():
return render_template('disclaimer.html') return render_page_template('disclaimer.html')
@app.route('/privacy', methods=['GET']) @app.route('/privacy', methods=['GET'])
def privacy(): def privacy():
return render_template('privacy.html') return render_page_template('privacy.html')
@app.route('/receipt', methods=['GET']) @app.route('/receipt', methods=['GET'])
def receipt(): def receipt():
if not current_user.is_authenticated():
abort(401)
return
id = request.args.get('id') id = request.args.get('id')
if id: if id:
invoice = stripe.Invoice.retrieve(id) invoice = stripe.Invoice.retrieve(id)
if invoice: if invoice:
org = model.get_user_or_org_by_customer_id(invoice.customer) user_or_org = model.get_user_or_org_by_customer_id(invoice.customer)
if org and org.organization:
admin_org = AdministerOrganizationPermission(org.username) if user_or_org:
if admin_org.can(): if user_or_org.organization:
file_data = renderInvoiceToPdf(invoice, org) admin_org = AdministerOrganizationPermission(user_or_org.username)
return Response(file_data, if not admin_org.can():
mimetype="application/pdf", abort(404)
headers={"Content-Disposition": return
"attachment;filename=receipt.pdf"}) else:
abort(404) if not user_or_org.username == current_user.db_user().username:
abort(404)
return
def common_login(db_user): file_data = renderInvoiceToPdf(invoice, user_or_org)
if login_user(_LoginWrappedDBUser(db_user.username, db_user)): return Response(file_data,
logger.debug('Successfully signed in as: %s' % db_user.username) mimetype="application/pdf",
new_identity = QuayDeferredPermissionUser(db_user.username, 'username') headers={"Content-Disposition": "attachment;filename=receipt.pdf"})
identity_changed.send(app, identity=new_identity) abort(404)
return True
else:
logger.debug('User could not be logged in, inactive?.')
return False
@app.route('/oauth2/github/callback', methods=['GET']) @app.route('/oauth2/github/callback', methods=['GET'])
@ -215,12 +195,12 @@ def github_oauth_callback():
mixpanel.alias(to_login.username, state) mixpanel.alias(to_login.username, state)
except model.DataModelException, ex: except model.DataModelException, ex:
return render_template('githuberror.html', error_message=ex.message) return render_page_template('githuberror.html', error_message=ex.message)
if common_login(to_login): if common_login(to_login):
return redirect(url_for('index')) return redirect(url_for('index'))
return render_template('githuberror.html') return render_page_template('githuberror.html')
@app.route('/confirm', methods=['GET']) @app.route('/confirm', methods=['GET'])

View file

@ -1,19 +1,14 @@
import logging import logging
import requests
import stripe import stripe
from flask import (abort, redirect, request, url_for, render_template, from flask import request, make_response
make_response)
from flask.ext.login import login_user, UserMixin, login_required
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
from data import model from data import model
from app import app, login_manager, mixpanel from app import app
from auth.permissions import QuayDeferredPermissionUser
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
from util.invoice import renderInvoiceToHtml from util.invoice import renderInvoiceToHtml
from util.email import send_invoice_email from util.email import send_invoice_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,10 +28,10 @@ def stripe_webhook():
# Find the user associated with the customer ID. # Find the user associated with the customer ID.
user = model.get_user_or_org_by_customer_id(customer_id) user = model.get_user_or_org_by_customer_id(customer_id)
if user and user.invoice_email: if user and user.invoice_email:
# Lookup the invoice. # Lookup the invoice.
invoice = stripe.Invoice.retrieve(invoice_id) invoice = stripe.Invoice.retrieve(invoice_id)
if invoice: if invoice:
invoice_html = renderInvoiceToHtml(invoice, user) invoice_html = renderInvoiceToHtml(invoice, user)
send_invoice_email(user.email, invoice_html) send_invoice_email(user.email, invoice_html)
return make_response('Okay') return make_response('Okay')

View file

@ -3,6 +3,12 @@
margin: 0; margin: 0;
} }
@media (max-width: 410px) {
.olrk-normal {
display: none;
}
}
.resource-view-element { .resource-view-element {
position: relative; position: relative;
} }
@ -104,10 +110,6 @@ html, body {
word-wrap: normal !important; word-wrap: normal !important;
} }
.code-info {
border-bottom: 1px dashed #aaa;
}
.toggle-icon { .toggle-icon {
font-size: 22px; font-size: 22px;
padding: 6px; padding: 6px;
@ -662,49 +664,165 @@ i.toggle-icon:hover {
.plans-list .plan { .plans-list .plan {
vertical-align: top; vertical-align: top;
padding: 10px;
border: 1px solid #eee;
border-top: 4px solid #94C9F7;
font-size: 1.4em; font-size: 1.4em;
margin-top: 5px;
}
.plans-list .plan.small {
border: 1px solid #ddd;
border-top: 4px solid #428bca;
margin-top: 0px;
font-size: 1.6em;
}
.plans-list .plan.business-plan {
border: 1px solid #eee;
border-top: 4px solid #94F794;
} }
.plans-list .plan.bus-small { .plans-list .plan.bus-small {
border: 1px solid #ddd; border-top: 6px solid #46ac39;
border-top: 4px solid #47A447; margin-top: -10px;
margin-top: 0px; }
font-size: 1.6em;
.plans-list .plan.bus-small .plan-box {
background: black !important;
} }
.plans-list .plan:last-child { .plans-list .plan:last-child {
margin-right: 0px; margin-right: 0px;
} }
.plans-list .plan .plan-box {
background: #444;
padding: 10px;
color: white;
}
.plans-list .plan .plan-title { .plans-list .plan .plan-title {
text-transform: uppercase;
padding-top: 25px;
padding-bottom: 20px;
margin-bottom: 10px; margin-bottom: 10px;
display: block;
font-weight: bold; font-weight: bold;
border-bottom: 1px solid #eee;
}
.visible-sm-inline {
display: none;
}
.hidden-sm-inline {
display: inline;
}
@media (max-width: 991px) and (min-width: 768px) {
.visible-sm-inline {
display: inline;
}
.hidden-sm-inline {
display: none;
}
}
.plans-list .plan-box .description {
color: white;
margin-top: 6px;
font-size: 12px !important;
}
.plans-list .plan button {
margin-top: 6px;
margin-bottom: 6px;
}
.plans-list .plan.bus-small button {
font-size: 1em;
}
.plans-list .features-bar {
padding-top: 248px;
}
.plans-list .features-bar .feature .count {
padding: 10px;
}
.plans-list .features-bar .feature {
height: 43px;
text-align: right;
white-space: nowrap;
}
.context-tooltip {
border-bottom: 1px dotted black;
cursor: default;
}
.plans-list .features-bar .feature i {
margin-left: 16px;
float: right;
width: 16px;
font-size: 16px;
text-align: center;
margin-top: 2px;
}
.plans-list .plan .features {
padding: 6px;
background: #eee;
padding-bottom: 0px;
}
.plans-list .plan .feature {
text-align: center;
padding: 10px;
border-bottom: 1px solid #ddd;
font-size: 14px;
}
.plans-list .plan .feature:after {
content: "";
border-radius: 50%;
display: inline-block;
width: 16px;
height: 16px;
}
.plans-list .plan .visible-xs .feature {
text-align: left;
}
.plans-list .plan .visible-xs .feature:after {
float: left;
margin-right: 10px;
}
.plans-list .plan .feature.notpresent {
color: #ccc;
}
.plans-list .plan .feature.present:after {
background: #428bca;
}
.plans-list .plan.business-plan .feature.present:after {
background: #46ac39;
}
.plans-list .plan .count, .plans-list .features-bar .count {
background: white;
border-bottom: 0px;
text-align: center !important;
font-size: 14px;
padding: 10px;
}
.plans-list .plan .count b, .plans-list .features-bar .count b {
font-size: 1.5em;
display: block;
}
.plans-list .plan .feature:last-child {
border-bottom: 0px;
}
.plans-list .plan-price {
margin-bottom: 10px;
} }
.plan-price { .plan-price {
margin-bottom: 10px;
display: block; display: block;
font-weight: bold; font-weight: bold;
font-size: 1.8em; font-size: 1.8em;
position: relative; position: relative;
} }
@ -732,7 +850,8 @@ i.toggle-icon:hover {
.plans-list .plan .description { .plans-list .plan .description {
font-size: 1em; font-size: 1em;
font-size: 16px; font-size: 16px;
margin-bottom: 10px; height: 34px;
} }
.plans-list .plan .smaller { .plans-list .plan .smaller {
@ -1926,28 +2045,28 @@ p.editable:hover i {
display: inline-block; display: inline-block;
} }
.org-admin .invoice-title { .billing-invoices-element .invoice-title {
padding: 6px; padding: 6px;
cursor: pointer; cursor: pointer;
} }
.org-admin .invoice-status .success { .billing-invoices-element .invoice-status .success {
color: green; color: green;
} }
.org-admin .invoice-status .pending { .billing-invoices-element .invoice-status .pending {
color: steelblue; color: steelblue;
} }
.org-admin .invoice-status .danger { .billing-invoices-element .invoice-status .danger {
color: red; color: red;
} }
.org-admin .invoice-amount:before { .billing-invoices-element .invoice-amount:before {
content: '$'; content: '$';
} }
.org-admin .invoice-details { .billing-invoices-element .invoice-details {
margin-left: 10px; margin-left: 10px;
margin-bottom: 10px; margin-bottom: 10px;
@ -1956,21 +2075,21 @@ p.editable:hover i {
border-left: 2px solid #eee !important; border-left: 2px solid #eee !important;
} }
.org-admin .invoice-details td { .billing-invoices-element .invoice-details td {
border: 0px solid transparent !important; border: 0px solid transparent !important;
} }
.org-admin .invoice-details dl { .billing-invoices-element .invoice-details dl {
margin: 0px; margin: 0px;
} }
.org-admin .invoice-details dd { .billing-invoices-element .invoice-details dd {
margin-left: 10px; margin-left: 10px;
padding: 6px; padding: 6px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.org-admin .invoice-title:hover { .billing-invoices-element .invoice-title:hover {
color: steelblue; color: steelblue;
} }
@ -2096,6 +2215,14 @@ p.editable:hover i {
margin-bottom: 0px; margin-bottom: 0px;
} }
.plan-manager-element .plans-list-table .deprecated-plan {
color: #aaa;
}
.plan-manager-element .plans-list-table .deprecated-plan-label {
font-size: 0.7em;
}
.plans-table-element table { .plans-table-element table {
margin: 20px; margin: 20px;
border: 1px solid #eee; border: 1px solid #eee;

View file

@ -0,0 +1,53 @@
<div class="billing-invoices-element">
<div ng-show="loading">
<div class="quay-spinner"></div>
</div>
<div ng-show="!loading && !invoices">
No invoices have been created
</div>
<div ng-show="!loading && invoices">
<table class="table">
<thead>
<th>Billing Date/Time</th>
<th>Amount Due</th>
<th>Status</th>
<th></th>
</thead>
<tbody class="invoice" ng-repeat="invoice in invoices">
<tr class="invoice-title">
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
<td>
<span class="invoice-status">
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
<span class="danger" ng-show="!invoice.paid && invoice.attempted && invoice.closed">Payment failed</span>
<span class="danger" ng-show="!invoice.paid && invoice.attempted && !invoice.closed">Payment failed - Will retry soon</span>
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
</span>
</td>
<td>
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
</a>
</td>
</tr>
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
<td colspan="3">
<dl class="dl-normal">
<dt>Billing Period</dt>
<dd>
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
</dd>
</dl>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View file

@ -7,7 +7,7 @@
<div class="panel-body"> <div class="panel-body">
<div class="quay-spinner" ng-show="!currentCard || changingCard"></div> <div class="quay-spinner" ng-show="!currentCard || changingCard"></div>
<div class="current-card" ng-show="currentCard && !changingCard"> <div class="current-card" ng-show="currentCard && !changingCard">
<img src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4"> <img ng-src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4">
<span class="no-card-outline" ng-show="!currentCard.last4"></span> <span class="no-card-outline" ng-show="!currentCard.last4"></span>
<span class="last4" ng-show="currentCard.last4">****-****-****-<b>{{ currentCard.last4 }}</b></span> <span class="last4" ng-show="currentCard.last4">****-****-****-<b>{{ currentCard.last4 }}</b></span>

View file

@ -38,7 +38,10 @@
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown"> <a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" /> <img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
{{ user.username }} {{ user.username }}
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan"> <span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan"
bs-tooltip="(user.askForPassword ? 'A password is needed for this account<br>' : '') + (overPlan ? 'You are using more private repositories than your plan allows' : '')"
data-placement="left"
data-container="body">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} {{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
</span> </span>
<b class="caret"></b> <b class="caret"></b>

View file

@ -32,14 +32,23 @@
<td></td> <td></td>
</thead> </thead>
<tr ng-repeat="plan in plans" ng-class="(subscribedPlan.stripeId === plan.stripeId) ? getActiveSubClass() : ''"> <tr ng-repeat="plan in plans" ng-show="isPlanVisible(plan, subscribedPlan)"
<td>{{ plan.title }}</td> ng-class="{'active':(subscribedPlan.stripeId === plan.stripeId), 'deprecated-plan':plan.deprecated}">
<td>
{{ plan.title }}
<div class="deprecated-plan-label" ng-show="plan.deprecated">
<span class="context-tooltip" title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
</div>
</td>
<td>{{ plan.privateRepos }}</td> <td>{{ plan.privateRepos }}</td>
<td><div class="plan-price">${{ plan.price / 100 }}</div></td> <td><div class="plan-price">${{ plan.price / 100 }}</div></td>
<td class="controls"> <td class="controls">
<div ng-switch='plan.stripeId'> <div ng-switch='plan.deprecated'>
<div ng-switch-when='bus-free'> <div ng-switch-when='true'>
<button class="btn button-hidden">Hidden!</button> <button class="btn btn-danger" ng-click="cancelSubscription()">
<span class="quay-spinner" ng-show="planChanging"></span>
<span ng-show="!planChanging">Cancel</span>
</button>
</div> </div>
<div ng-switch-default> <div ng-switch-default>
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId" <button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"

View file

@ -1,6 +1,18 @@
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function getFirstTextLine(commentString) { function getFirstTextLine(commentString) {
if (!commentString) { return ''; } if (!commentString) { return ''; }
@ -34,11 +46,8 @@ function getFirstTextLine(commentString) {
return ''; return '';
} }
function createRobotAccount(Restangular, is_org, orgname, name, callback) { function createRobotAccount(ApiService, is_org, orgname, name, callback) {
var url = is_org ? getRestUrl('organization', orgname, 'robots', name) : ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
getRestUrl('user/robots', name);
var createRobot = Restangular.one(url);
createRobot.customPUT().then(callback, function(resp) {
bootbox.dialog({ bootbox.dialog({
"message": resp.data ? resp.data : 'The robot account could not be created', "message": resp.data ? resp.data : 'The robot account could not be created',
"title": "Cannot create robot account", "title": "Cannot create robot account",
@ -52,14 +61,18 @@ function createRobotAccount(Restangular, is_org, orgname, name, callback) {
}); });
} }
function createOrganizationTeam(Restangular, orgname, teamname, callback) { function createOrganizationTeam(ApiService, orgname, teamname, callback) {
var createTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname));
var data = { var data = {
'name': teamname, 'name': teamname,
'role': 'member' 'role': 'member'
}; };
var params = {
'orgname': orgname,
'teamname': teamname
};
createTeam.customPOST(data).then(callback, function() { ApiService.updateOrganizationTeam(data, params).then(callback, function() {
bootbox.dialog({ bootbox.dialog({
"message": resp.data ? resp.data : 'The team could not be created', "message": resp.data ? resp.data : 'The team could not be created',
"title": "Cannot create team", "title": "Cannot create team",
@ -73,17 +86,6 @@ function createOrganizationTeam(Restangular, orgname, teamname, callback) {
}); });
} }
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function getMarkedDown(string) { function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || ''); return Markdown.getSanitizingConverter().makeHtml(string || '');
} }
@ -91,6 +93,137 @@ function getMarkedDown(string) {
// Start the application code itself. // Start the application code itself.
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide, cfpLoadingBarProvider) { quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false; cfpLoadingBarProvider.includeSpinner = false;
$provide.factory('ApiService', ['Restangular', function(Restangular) {
var apiService = {};
var getResource = function(path) {
var resource = {};
resource.url = path;
resource.withOptions = function(options) {
this.options = options;
return this;
};
resource.get = function(processor, opt_errorHandler) {
var options = this.options;
var performer = Restangular.one(this.url);
var result = {
'loading': true,
'value': null,
'hasError': false
};
performer.get(options).then(function(resp) {
result.value = processor(resp);
result.loading = false;
}, function(resp) {
result.hasError = true;
result.loading = false;
if (opt_errorHandler) {
opt_errorHandler(resp);
}
});
return result;
};
return resource;
};
var formatMethodName = function(endpointName) {
var formatted = '';
for (var i = 0; i < endpointName.length; ++i) {
var c = endpointName[i];
if (c == '_') {
c = endpointName[i + 1].toUpperCase();
i++;
}
formatted += c;
}
return formatted;
};
var buildUrl = function(path, parameters) {
// We already have /api/ on the URLs, so remove them from the paths.
path = path.substr('/api/'.length, path.length);
var url = '';
for (var i = 0; i < path.length; ++i) {
var c = path[i];
if (c == '<') {
var end = path.indexOf('>', i);
var varName = path.substr(i + 1, end - i - 1);
var colon = varName.indexOf(':');
var isPathVar = false;
if (colon > 0) {
isPathVar = true;
varName = varName.substr(colon + 1);
}
if (!parameters[varName]) {
throw new Error('Missing parameter: ' + varName);
}
url += isPathVar ? parameters[varName] : encodeURI(parameters[varName]);
i = end;
continue;
}
url += c;
}
return url;
};
var getGenericMethodName = function(userMethodName) {
return formatMethodName(userMethodName.replace('_user', ''));
};
var buildMethodsForEndpoint = function(endpoint) {
var method = endpoint.methods[0].toLowerCase();
var methodName = formatMethodName(endpoint['name']);
apiService[methodName] = function(opt_options, opt_parameters) {
return Restangular.one(buildUrl(endpoint['path'], opt_parameters))['custom' + method.toUpperCase()](opt_options);
};
if (method == 'get') {
apiService[methodName + 'AsResource'] = function(opt_parameters) {
return getResource(buildUrl(endpoint['path'], opt_parameters));
};
}
if (endpoint['user_method']) {
apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters) {
if (orgname) {
if (orgname.name) {
orgname = orgname.name;
}
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {});
return apiService[methodName](opt_options, params);
} else {
return apiService[formatMethodName(endpoint['user_method'])](opt_options, opt_parameters);
}
};
}
};
// Construct the methods for each API endpoint.
if (!window.__endpoints) {
return apiService;
}
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpoint = window.__endpoints[i];
buildMethodsForEndpoint(endpoint);
}
return apiService;
}]);
$provide.factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) { $provide.factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) {
var cookieService = {}; var cookieService = {};
@ -113,7 +246,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
return cookieService; return cookieService;
}]); }]);
$provide.factory('UserService', ['Restangular', 'CookieService', function(Restangular, CookieService) { $provide.factory('UserService', ['ApiService', 'CookieService', function(ApiService, CookieService) {
var userResponse = { var userResponse = {
verified: false, verified: false,
anonymous: true, anonymous: true,
@ -139,8 +272,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
}; };
userService.load = function(opt_callback) { userService.load = function(opt_callback) {
var userFetch = Restangular.one('user/'); ApiService.getLoggedInUser().then(function(loadedUser) {
userFetch.get().then(function(loadedUser) {
userResponse = loadedUser; userResponse = loadedUser;
if (!userResponse.anonymous) { if (!userResponse.anonymous) {
@ -198,48 +330,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
return userService; return userService;
}]); }]);
$provide.factory('ApiService', ['Restangular', function(Restangular) {
var apiService = {}
apiService.at = function(locationPieces) {
var location = getRestUrl.apply(this, arguments);
var info = {
'url': location,
'caller': Restangular.one(location),
'withOptions': function(options) {
info.options = options;
return info;
},
'get': function(processor, opt_errorHandler) {
var options = info.options;
var caller = info.caller;
var result = {
'loading': true,
'value': null,
'hasError': false
};
caller.get(options).then(function(resp) {
result.value = processor(resp);
result.loading = false;
}, function(resp) {
result.hasError = true;
result.loading = false;
if (opt_errorHandler) {
opt_errorHandler(resp);
}
});
return result;
}
};
return info;
};
return apiService;
}]);
$provide.factory('KeyService', ['$location', function($location) { $provide.factory('KeyService', ['$location', function($location) {
var keyService = {} var keyService = {}
@ -254,13 +344,17 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
return keyService; return keyService;
}]); }]);
$provide.factory('PlanService', ['Restangular', 'KeyService', 'UserService', 'CookieService', $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
function(Restangular, KeyService, UserService, CookieService) { function(KeyService, UserService, CookieService, ApiService) {
var plans = null; var plans = null;
var planDict = {}; var planDict = {};
var planService = {}; var planService = {};
var listeners = []; var listeners = [];
planService.getFreePlan = function() {
return 'free';
};
planService.registerListener = function(obj, callback) { planService.registerListener = function(obj, callback) {
listeners.push({'obj': obj, 'callback': callback}); listeners.push({'obj': obj, 'callback': callback});
}; };
@ -278,6 +372,27 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
CookieService.putSession('quay.notedplan', planId); CookieService.putSession('quay.notedplan', planId);
}; };
planService.isOrgCompatible = function(plan) {
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
};
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.handleNotedPlan = function() { planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan(); var planId = planService.getAndResetNotedPlan();
if (!planId) { return; } if (!planId) { return; }
@ -288,13 +403,11 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
} }
planService.getPlan(planId, function(plan) { planService.getPlan(planId, function(plan) {
planService.isBusinessPlan(planId, function(bus) { if (planService.isOrgCompatible(plan)) {
if (bus) { document.location = '/organizations/new/?plan=' + planId;
document.location = '/organizations/new/?plan=' + planId; } else {
} else { document.location = '/user?plan=' + planId;
document.location = '/user?plan=' + planId; }
}
});
}); });
}); });
}; };
@ -330,72 +443,53 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
return; return;
} }
var getPlans = Restangular.one('plans'); ApiService.listPlans().then(function(data) {
getPlans.get().then(function(data) {
var i = 0; var i = 0;
for(i = 0; i < data.user.length; i++) { for(i = 0; i < data.plans.length; i++) {
planDict[data.user[i].stripeId] = data.user[i]; planDict[data.plans[i].stripeId] = data.plans[i];
} }
for(i = 0; i < data.business.length; i++) { plans = data.plans;
planDict[data.business[i].stripeId] = data.business[i];
}
plans = data;
callback(plans); callback(plans);
}, function() { callback([]); }); }, function() { callback([]); });
}; };
planService.getMatchingBusinessPlan = function(callback) { planService.getPlans = function(callback, opt_includePersonal) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.isBusinessPlan = function(planId, callback) {
planService.verifyLoaded(function() { planService.verifyLoaded(function() {
planSource = plans.business; var filtered = [];
for (var i = 0; i < planSource.length; i++) { for (var i = 0; i < plans.length; ++i) {
var plan = planSource[i]; var plan = plans[i];
if (plan.stripeId == planId) { if (plan['deprecated']) { continue; }
callback(true); if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
return; filtered.push(plan);
}
} }
callback(false); callback(filtered);
}); });
}; };
planService.getPlans = function(callback) {
planService.verifyLoaded(callback);
};
planService.getPlan = function(planId, callback) { planService.getPlan = function(planId, callback) {
planService.getPlanIncludingDeprecated(planId, function(plan) {
if (!plan['deprecated']) {
callback(plan);
}
});
};
planService.getPlanIncludingDeprecated = function(planId, callback) {
planService.verifyLoaded(function() { planService.verifyLoaded(function() {
if (planDict[planId]) { if (planDict[planId]) {
callback(planDict[planId]); callback(planDict[planId]);
} }
}); });
}; };
planService.getMinimumPlan = function(privateCount, isBusiness, callback) { planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.verifyLoaded(function() { planService.getPlans(function(plans) {
var planSource = plans.user; for (var i = 0; i < plans.length; i++) {
if (isBusiness) { var plan = plans[i];
planSource = plans.business; if (isBusiness && !planService.isOrgCompatible(plan)) {
} continue;
}
for (var i = 0; i < planSource.length; i++) {
var plan = planSource[i];
if (plan.privateRepos >= privateCount) { if (plan.privateRepos >= privateCount) {
callback(plan); callback(plan);
return; return;
@ -407,13 +501,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
}; };
planService.getSubscription = function(orgname, success, failure) { planService.getSubscription = function(orgname, success, failure) {
var url = planService.getSubscriptionUrl(orgname); ApiService.getSubscription(orgname).then(success, failure);
var getSubscription = Restangular.one(url);
getSubscription.get().then(success, failure);
};
planService.getSubscriptionUrl = function(orgname) {
return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
}; };
planService.setSubscription = function(orgname, planId, success, failure, opt_token) { planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
@ -425,9 +513,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
subscriptionDetails['token'] = opt_token.id; subscriptionDetails['token'] = opt_token.id;
} }
var url = planService.getSubscriptionUrl(orgname); ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
var createSubscriptionRequest = Restangular.one(url);
createSubscriptionRequest.customPUT(subscriptionDetails).then(function(resp) {
success(resp); success(resp);
planService.getPlan(planId, function(plan) { planService.getPlan(planId, function(plan) {
for (var i = 0; i < listeners.length; ++i) { for (var i = 0; i < listeners.length; ++i) {
@ -438,9 +524,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
}; };
planService.getCardInfo = function(orgname, callback) { planService.getCardInfo = function(orgname, callback) {
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; ApiService.getCard(orgname).then(function(resp) {
var getCard = Restangular.one(url);
getCard.customGET().then(function(resp) {
callback(resp.card); callback(resp.card);
}, function() { }, function() {
callback({'is_valid': false}); callback({'is_valid': false});
@ -453,6 +537,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
} }
planService.getPlan(planId, function(plan) { planService.getPlan(planId, function(plan) {
if (orgname && !planService.isOrgCompatible(plan)) { return; }
planService.getCardInfo(orgname, function(cardInfo) { planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && !cardInfo.last4) { if (plan.price > 0 && !cardInfo.last4) {
planService.showSubscribeDialog($scope, orgname, planId, callbacks); planService.showSubscribeDialog($scope, orgname, planId, callbacks);
@ -485,12 +571,10 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
'token': token.id 'token': token.id
}; };
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
var changeCardRequest = Restangular.one(url); planService.handleCardError(resp);
changeCardRequest.customPOST(cardInfo).then(callbacks['success'], function(resp) { callbacks['failure'](resp);
planService.handleCardError(resp); });
callbacks['failure'](resp);
});
}); });
}; };
@ -732,10 +816,9 @@ quayApp.directive('userSetup', function () {
'signInStarted': '&signInStarted', 'signInStarted': '&signInStarted',
'signedIn': '&signedIn' 'signedIn': '&signedIn'
}, },
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
$scope.sendRecovery = function() { $scope.sendRecovery = function() {
var signinPost = Restangular.one('recovery'); ApiService.requestRecoveryEmail($scope.recovery).then(function() {
signinPost.customPOST($scope.recovery).then(function() {
$scope.invalidEmail = false; $scope.invalidEmail = false;
$scope.sent = true; $scope.sent = true;
}, function(result) { }, function(result) {
@ -765,7 +848,7 @@ quayApp.directive('signinForm', function () {
'signInStarted': '&signInStarted', 'signInStarted': '&signInStarted',
'signedIn': '&signedIn' 'signedIn': '&signedIn'
}, },
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
$scope.showGithub = function() { $scope.showGithub = function() {
$scope.markStarted(); $scope.markStarted();
@ -792,8 +875,7 @@ quayApp.directive('signinForm', function () {
$scope.signin = function() { $scope.signin = function() {
$scope.markStarted(); $scope.markStarted();
var signinPost = Restangular.one('signin'); ApiService.signinUser($scope.user).then(function() {
signinPost.customPOST($scope.user).then(function() {
$scope.needsEmailVerification = false; $scope.needsEmailVerification = false;
$scope.invalidCredentials = false; $scope.invalidCredentials = false;
@ -833,7 +915,7 @@ quayApp.directive('signupForm', function () {
scope: { scope: {
}, },
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
$('.form-signup').popover(); $('.form-signup').popover();
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) { angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
@ -850,8 +932,7 @@ quayApp.directive('signupForm', function () {
$('.form-signup').popover('hide'); $('.form-signup').popover('hide');
$scope.registering = true; $scope.registering = true;
var newUserPost = Restangular.one('user/'); ApiService.createNewUser($scope.newUser).then(function() {
newUserPost.customPOST($scope.newUser).then(function() {
$scope.awaitingConfirmation = true; $scope.awaitingConfirmation = true;
$scope.registering = false; $scope.registering = false;
@ -904,7 +985,7 @@ quayApp.directive('dockerAuthDialog', function () {
'shown': '=shown', 'shown': '=shown',
'counter': '=counter' 'counter': '=counter'
}, },
controller: function($scope, $element, Restangular) { controller: function($scope, $element) {
$scope.isDownloadSupported = function() { $scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){} try { return !!new Blob(); } catch(e){}
return false; return false;
@ -962,6 +1043,53 @@ quayApp.filter('visibleLogFilter', function () {
}); });
quayApp.directive('billingInvoices', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/billing-invoices.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user',
'visible': '=visible'
},
controller: function($scope, $element, $sce, ApiService) {
$scope.loading = false;
$scope.invoiceExpanded = {};
$scope.toggleInvoice = function(id) {
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
};
var update = function() {
var hasValidUser = !!$scope.user;
var hasValidOrg = !!$scope.organization;
var isValid = hasValidUser || hasValidOrg;
if (!$scope.visible || !isValid) {
return;
}
$scope.loading = true;
ApiService.listInvoices($scope.organization).then(function(resp) {
$scope.invoices = resp.invoices;
$scope.loading = false;
});
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
$scope.$watch('visible', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('logsView', function () { quayApp.directive('logsView', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -976,7 +1104,7 @@ quayApp.directive('logsView', function () {
'repository': '=repository', 'repository': '=repository',
'performer': '=performer' 'performer': '=performer'
}, },
controller: function($scope, $element, $sce, Restangular) { controller: function($scope, $element, $sce, Restangular, ApiService) {
$scope.loading = true; $scope.loading = true;
$scope.logs = null; $scope.logs = null;
$scope.kindsAllowed = null; $scope.kindsAllowed = null;
@ -1092,6 +1220,8 @@ quayApp.directive('logsView', function () {
$scope.loading = true; $scope.loading = true;
// Note: We construct the URLs here manually because we also use it for the download
// path.
var url = getRestUrl('user/logs'); var url = getRestUrl('user/logs');
if ($scope.organization) { if ($scope.organization) {
url = getRestUrl('organization', $scope.organization.name, 'logs'); url = getRestUrl('organization', $scope.organization.name, 'logs');
@ -1195,7 +1325,7 @@ quayApp.directive('robotsManager', function () {
'organization': '=organization', 'organization': '=organization',
'user': '=user' 'user': '=user'
}, },
controller: function($scope, $element, Restangular) { controller: function($scope, $element, ApiService) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN; $scope.ROBOT_PATTERN = ROBOT_PATTERN;
$scope.robots = null; $scope.robots = null;
$scope.loading = false; $scope.loading = false;
@ -1220,7 +1350,7 @@ quayApp.directive('robotsManager', function () {
$scope.createRobot = function(name) { $scope.createRobot = function(name) {
if (!name) { return; } if (!name) { return; }
createRobotAccount(Restangular, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name, createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
function(created) { function(created) {
$scope.robots.push(created); $scope.robots.push(created);
}); });
@ -1228,11 +1358,7 @@ quayApp.directive('robotsManager', function () {
$scope.deleteRobot = function(info) { $scope.deleteRobot = function(info) {
var shortName = $scope.getShortenedName(info.name); var shortName = $scope.getShortenedName(info.name);
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', shortName) : ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
getRestUrl('user/robots', shortName);
var deleteRobot = Restangular.one(url);
deleteRobot.customDELETE().then(function(resp) {
for (var i = 0; i < $scope.robots.length; ++i) { for (var i = 0; i < $scope.robots.length; ++i) {
if ($scope.robots[i].name == info.name) { if ($scope.robots[i].name == info.name) {
$scope.robots.splice(i, 1); $scope.robots.splice(i, 1);
@ -1258,9 +1384,7 @@ quayApp.directive('robotsManager', function () {
if ($scope.loading) { return; } if ($scope.loading) { return; }
$scope.loading = true; $scope.loading = true;
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots') : 'user/robots'; ApiService.getRobots($scope.organization).then(function(resp) {
var getRobots = Restangular.one(url);
getRobots.customGET($scope.obj).then(function(resp) {
$scope.robots = resp.robots; $scope.robots = resp.robots;
$scope.loading = false; $scope.loading = false;
}); });
@ -1493,7 +1617,7 @@ quayApp.directive('headerBar', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
}, },
controller: function($scope, $element, $location, UserService, PlanService, Restangular) { controller: function($scope, $element, $location, UserService, PlanService, ApiService) {
$scope.overPlan = false; $scope.overPlan = false;
var checkOverPlan = function() { var checkOverPlan = function() {
@ -1502,8 +1626,7 @@ quayApp.directive('headerBar', function () {
return; return;
} }
var checkPrivate = Restangular.one('user/private'); ApiService.getUserPrivateCount().then(function(resp) {
checkPrivate.customGET().then(function(resp) {
$scope.overPlan = resp.privateCount > resp.reposAllowed; $scope.overPlan = resp.privateCount > resp.reposAllowed;
}); });
}; };
@ -1515,10 +1638,9 @@ quayApp.directive('headerBar', function () {
PlanService.registerListener(this, checkOverPlan); PlanService.registerListener(this, checkOverPlan);
$scope.signout = function() { $scope.signout = function() {
var signoutPost = Restangular.one('signout'); ApiService.logout().then(function() {
signoutPost.customPOST().then(function() { UserService.load();
UserService.load(); $location.path('/');
$location.path('/');
}); });
}; };
@ -1549,7 +1671,7 @@ quayApp.directive('entitySearch', function () {
'includeTeams': '=includeTeams', 'includeTeams': '=includeTeams',
'isOrganization': '=isOrganization' 'isOrganization': '=isOrganization'
}, },
controller: function($scope, $element, Restangular, UserService) { controller: function($scope, $element, Restangular, UserService, ApiService) {
$scope.lazyLoading = true; $scope.lazyLoading = true;
$scope.isAdmin = false; $scope.isAdmin = false;
@ -1559,16 +1681,12 @@ quayApp.directive('entitySearch', function () {
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace); $scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
if ($scope.isOrganization && $scope.includeTeams) { if ($scope.isOrganization && $scope.includeTeams) {
var url = getRestUrl('organization', $scope.namespace); ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
var getOrganization = Restangular.one(url);
getOrganization.customGET().then(function(resp) {
$scope.teams = resp.teams; $scope.teams = resp.teams;
}); });
} }
var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
var getRobots = Restangular.one(url);
getRobots.customGET().then(function(resp) {
$scope.robots = resp.robots; $scope.robots = resp.robots;
$scope.lazyLoading = false; $scope.lazyLoading = false;
}, function() { }, function() {
@ -1588,7 +1706,7 @@ quayApp.directive('entitySearch', function () {
return; return;
} }
createOrganizationTeam(Restangular, $scope.namespace, teamname, function(created) { createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) {
$scope.setEntity(created.name, 'team', false); $scope.setEntity(created.name, 'team', false);
$scope.teams[teamname] = created; $scope.teams[teamname] = created;
}); });
@ -1607,7 +1725,7 @@ quayApp.directive('entitySearch', function () {
return; return;
} }
createRobotAccount(Restangular, $scope.isOrganization, $scope.namespace, robotname, function(created) { createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) {
$scope.setEntity(created.name, 'user', true); $scope.setEntity(created.name, 'user', true);
$scope.robots.push(created); $scope.robots.push(created);
}); });
@ -1733,7 +1851,7 @@ quayApp.directive('billingOptions', function () {
'user': '=user', 'user': '=user',
'organization': '=organization' 'organization': '=organization'
}, },
controller: function($scope, $element, PlanService, Restangular) { controller: function($scope, $element, PlanService, ApiService) {
$scope.invoice_email = false; $scope.invoice_email = false;
$scope.currentCard = null; $scope.currentCard = null;
@ -1803,9 +1921,7 @@ quayApp.directive('billingOptions', function () {
var save = function() { var save = function() {
$scope.working = true; $scope.working = true;
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name) : 'user/'; ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
var conductSave = Restangular.one(url);
conductSave.customPUT($scope.obj).then(function(resp) {
$scope.working = false; $scope.working = false;
}); });
}; };
@ -1840,11 +1956,19 @@ quayApp.directive('planManager', function () {
'readyForPlan': '&readyForPlan', 'readyForPlan': '&readyForPlan',
'planChanged': '&planChanged' 'planChanged': '&planChanged'
}, },
controller: function($scope, $element, PlanService, Restangular) { controller: function($scope, $element, PlanService, ApiService) {
var hasSubscription = false; var hasSubscription = false;
$scope.getActiveSubClass = function() { $scope.isPlanVisible = function(plan, subscribedPlan) {
return 'active'; if (plan['deprecated']) {
return plan == subscribedPlan;
}
if ($scope.organization && !PlanService.isOrgCompatible(plan)) {
return false;
}
return true;
}; };
$scope.changeSubscription = function(planId) { $scope.changeSubscription = function(planId) {
@ -1865,17 +1989,17 @@ quayApp.directive('planManager', function () {
}; };
$scope.cancelSubscription = function() { $scope.cancelSubscription = function() {
$scope.changeSubscription(getFreePlan()); $scope.changeSubscription(PlanService.getFreePlan());
}; };
var subscribedToPlan = function(sub) { var subscribedToPlan = function(sub) {
$scope.subscription = sub; $scope.subscription = sub;
if (sub.plan != getFreePlan()) { if (sub.plan != PlanService.getFreePlan()) {
hasSubscription = true; hasSubscription = true;
} }
PlanService.getPlan(sub.plan, function(subscribedPlan) { PlanService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
$scope.subscribedPlan = subscribedPlan; $scope.subscribedPlan = subscribedPlan;
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos; $scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
@ -1905,22 +2029,13 @@ quayApp.directive('planManager', function () {
}); });
}; };
var getFreePlan = function() {
for (var i = 0; i < $scope.plans.length; ++i) {
if ($scope.plans[i].price == 0) {
return $scope.plans[i].stripeId;
}
}
return 'free';
};
var update = function() { var update = function() {
$scope.planLoading = true; $scope.planLoading = true;
if (!$scope.plans) { return; } if (!$scope.plans) { return; }
PlanService.getSubscription($scope.organization, subscribedToPlan, function() { PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
// User/Organization has no subscription. // User/Organization has no subscription.
subscribedToPlan({ 'plan': getFreePlan() }); subscribedToPlan({ 'plan': PlanService.getFreePlan() });
}); });
}; };
@ -1929,13 +2044,13 @@ quayApp.directive('planManager', function () {
if (!$scope.user && !$scope.organization) { return; } if (!$scope.user && !$scope.organization) { return; }
$scope.loadingPlans = true; $scope.loadingPlans = true;
PlanService.getPlans(function(plans) { PlanService.verifyLoaded(function(plans) {
$scope.plans = plans[$scope.organization ? 'business' : 'user']; $scope.plans = plans;
update(); update();
if ($scope.readyForPlan) { if ($scope.readyForPlan) {
var planRequested = $scope.readyForPlan(); var planRequested = $scope.readyForPlan();
if (planRequested && planRequested != getFreePlan()) { if (planRequested && planRequested != PlanService.getFreePlan()) {
$scope.changeSubscription(planRequested); $scope.changeSubscription(planRequested);
} }
} }
@ -2101,15 +2216,27 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
// Check if we need to redirect based on a previously chosen plan. // Check if we need to redirect based on a previously chosen plan.
PlanService.handleNotedPlan(); PlanService.handleNotedPlan();
var changeTab = function(activeTab) { var changeTab = function(activeTab, opt_timeout) {
var checkCount = 0;
$timeout(function() { $timeout(function() {
if (checkCount > 5) { return; }
checkCount++;
$('a[data-toggle="tab"]').each(function(index) { $('a[data-toggle="tab"]').each(function(index) {
var tabName = this.getAttribute('data-target').substr(1); var tabName = this.getAttribute('data-target').substr(1);
if (tabName == activeTab) { if (tabName != activeTab) {
this.click(); return;
} }
if (this.clientWidth == 0) {
changeTab(activeTab, 500);
return;
}
this.click();
}); });
}); }, opt_timeout);
}; };
var resetDefaultTab = function() { var resetDefaultTab = function() {

13
static/js/bootstrap.js vendored Normal file
View file

@ -0,0 +1,13 @@
$.ajax({
type: 'GET',
async: false,
url: '/api/discovery',
success: function(data) {
window.__endpoints = data.endpoints;
},
error: function() {
setTimeout(function() {
$('#couldnotloadModal').modal({});
}, 250);
}
});

View file

@ -22,7 +22,7 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
// Load the list of plans. // Load the list of plans.
PlanService.getPlans(function(plans) { PlanService.getPlans(function(plans) {
$scope.plans = plans; $scope.plans = plans;
}); }, /* include the personal plan */ true);
// Monitor any user changes and place the current user into the scope. // Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
@ -33,19 +33,10 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
}; };
$scope.buyNow = function(plan) { $scope.buyNow = function(plan) {
PlanService.notePlan(plan);
if ($scope.user && !$scope.user.anonymous) { if ($scope.user && !$scope.user.anonymous) {
document.location = '/user?plan=' + plan; PlanService.handleNotedPlan();
} else { } else {
PlanService.notePlan(plan);
$('#signinModal').modal({});
}
};
$scope.createOrg = function(plan) {
if ($scope.user && !$scope.user.anonymous) {
document.location = '/organizations/new/?plan=' + plan;
} else {
PlanService.notePlan(plan);
$('#signinModal').modal({}); $('#signinModal').modal({});
} }
}; };
@ -76,14 +67,15 @@ function RepoListCtrl($scope, Restangular, UserService, ApiService) {
} }
var options = {'public': false, 'sort': true, 'namespace': namespace}; var options = {'public': false, 'sort': true, 'namespace': namespace};
$scope.user_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
$scope.user_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories; return resp.repositories;
}); });
}; };
var loadPublicRepos = function() { var loadPublicRepos = function() {
var options = {'public': true, 'private': false, 'sort': true, 'limit': 10}; var options = {'public': true, 'private': false, 'sort': true, 'limit': 10};
$scope.public_repositories = ApiService.at('repository').withOptions(options).get(function(resp) { $scope.public_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories; return resp.repositories;
}); });
}; };
@ -127,7 +119,7 @@ function LandingCtrl($scope, UserService, ApiService) {
} }
var options = {'limit': 4, 'public': false, 'sort': true, 'namespace': namespace }; var options = {'limit': 4, 'public': false, 'sort': true, 'namespace': namespace };
$scope.my_repositories = ApiService.at('repository').withOptions(options).get(function(resp) { $scope.my_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories; return resp.repositories;
}); });
}; };
@ -178,7 +170,8 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}; };
$scope.loadImageChanges = function(image) { $scope.loadImageChanges = function(image) {
$scope.currentImageChangeResource = ApiService.at('repository', namespace, name, 'image', image.id, 'changes').get(function(ci) { var params = {'repository': namespace + '/' + name, 'image_id': image.id};
$scope.currentImageChangeResource = ApiService.getImageChangesAsResource(params).get(function(ci) {
$scope.currentImageChanges = ci; $scope.currentImageChanges = ci;
}); });
}; };
@ -249,8 +242,9 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}; };
var fetchRepository = function() { var fetchRepository = function() {
var params = {'repository': namespace + '/' + name};
$rootScope.title = 'Loading Repository...'; $rootScope.title = 'Loading Repository...';
$scope.repository = ApiService.at('repository', namespace, name).get(function(repo) { $scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
// Set the repository object. // Set the repository object.
$scope.repo = repo; $scope.repo = repo;
@ -292,6 +286,7 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}; };
var getBuildInfo = function(repo) { var getBuildInfo = function(repo) {
// Note: We use restangular manually here because we need to turn off the loading bar.
var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/'); var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
buildInfo.withHttpConfig({ buildInfo.withHttpConfig({
'ignoreLoadingBar': true 'ignoreLoadingBar': true
@ -321,7 +316,8 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}; };
var listImages = function() { var listImages = function() {
$scope.imageHistory = ApiService.at('repository', namespace, name, 'image').get(function(resp) { var params = {'repository': namespace + '/' + name};
$scope.imageHistory = ApiService.listRepositoryImagesAsResource(params).get(function(resp) {
// Dispose of any existing tree. // Dispose of any existing tree.
if ($scope.tree) { if ($scope.tree) {
$scope.tree.dispose(); $scope.tree.dispose();
@ -361,11 +357,6 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
} }
function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) { function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) {
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
var namespace = $routeParams.namespace; var namespace = $routeParams.namespace;
var name = $routeParams.name; var name = $routeParams.name;
@ -452,8 +443,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
'friendlyName': $scope.newToken.friendlyName 'friendlyName': $scope.newToken.friendlyName
}; };
var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); var params = {'repository': namespace + '/' + name};
permissionPost.customPOST(friendlyName).then(function(newToken) { ApiService.createToken(friendlyName, params).then(function(newToken) {
$scope.newToken.friendlyName = ''; $scope.newToken.friendlyName = '';
$scope.createTokenForm.$setPristine(); $scope.createTokenForm.$setPristine();
$scope.tokens[newToken.code] = newToken; $scope.tokens[newToken.code] = newToken;
@ -461,8 +452,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
}; };
$scope.deleteToken = function(tokenCode) { $scope.deleteToken = function(tokenCode) {
var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode); var params = {
deleteAction.customDELETE().then(function() { 'repository': namespace + '/' + name,
'code': tokenCode
};
ApiService.deleteToken(null, params).then(function() {
delete $scope.tokens[tokenCode]; delete $scope.tokens[tokenCode];
}); });
}; };
@ -472,8 +467,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
'role': newAccess 'role': newAccess
}; };
var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode); var params = {
deleteAction.customPUT(role).then(function(updated) { 'repository': namespace + '/' + name,
'code': tokenCode
};
ApiService.changeToken(role, params).then(function(updated) {
$scope.tokens[updated.code] = updated; $scope.tokens[updated.code] = updated;
}); });
}; };
@ -495,8 +494,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var visibility = { var visibility = {
'visibility': newAccess 'visibility': newAccess
}; };
var visibilityPost = Restangular.one('repository/' + namespace + '/' + name + '/changevisibility');
visibilityPost.customPOST(visibility).then(function() { var params = {
'repository': namespace + '/' + name
};
ApiService.changeRepoVisibility(visibility, params).then(function() {
$scope.repo.is_public = newAccess == 'public'; $scope.repo.is_public = newAccess == 'public';
}, function() { }, function() {
$('#cannotchangeModal').modal({}); $('#cannotchangeModal').modal({});
@ -510,8 +513,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.deleteRepo = function() { $scope.deleteRepo = function() {
$('#confirmdeleteModal').modal('hide'); $('#confirmdeleteModal').modal('hide');
var deleteAction = Restangular.one('repository/' + namespace + '/' + name); var params = {
deleteAction.customDELETE().then(function() { 'repository': namespace + '/' + name
};
ApiService.deleteRepository(null, params).then(function() {
$scope.repo = null; $scope.repo = null;
setTimeout(function() { setTimeout(function() {
@ -523,8 +529,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
}; };
$scope.loadWebhooks = function() { $scope.loadWebhooks = function() {
var params = {
'repository': namespace + '/' + name
};
$scope.newWebhook = {}; $scope.newWebhook = {};
$scope.webhooksResource = ApiService.at('repository', namespace, name, 'webhook').get(function(resp) { $scope.webhooksResource = ApiService.listWebhooksAsResource(params).get(function(resp) {
$scope.webhooks = resp.webhooks; $scope.webhooks = resp.webhooks;
return $scope.webhooks; return $scope.webhooks;
}); });
@ -535,8 +545,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
return; return;
} }
var newWebhook = Restangular.one('repository/' + namespace + '/' + name + '/webhook/'); var params = {
newWebhook.customPOST($scope.newWebhook).then(function(resp) { 'repository': namespace + '/' + name
};
ApiService.createWebhook($scope.newWebhook, params).then(function(resp) {
$scope.webhooks.push(resp); $scope.webhooks.push(resp);
$scope.newWebhook.url = ''; $scope.newWebhook.url = '';
$scope.createWebhookForm.$setPristine(); $scope.createWebhookForm.$setPristine();
@ -544,15 +557,22 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
}; };
$scope.deleteWebhook = function(webhook) { $scope.deleteWebhook = function(webhook) {
var deleteWebhookReq = Restangular.one('repository/' + namespace + '/' + name + '/webhook/' + webhook.public_id); var params = {
deleteWebhookReq.customDELETE().then(function(resp) { 'repository': namespace + '/' + name,
'public_id': webhook.public_id
};
ApiService.deleteWebhook(null, params).then(function(resp) {
$scope.webhooks.splice($scope.webhooks.indexOf(webhook), 1); $scope.webhooks.splice($scope.webhooks.indexOf(webhook), 1);
}); });
}; };
var fetchTokens = function() { var fetchTokens = function() {
var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); var params = {
tokensFetch.get().then(function(resp) { 'repository': namespace + '/' + name
};
ApiService.listRepoTokens(null, params).then(function(resp) {
$scope.tokens = resp.tokens; $scope.tokens = resp.tokens;
}, function() { }, function() {
$scope.tokens = null; $scope.tokens = null;
@ -569,7 +589,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
}; };
var fetchRepository = function() { var fetchRepository = function() {
$scope.repository = ApiService.at('repository', namespace, name).get(function(repo) { var params = {
'repository': namespace + '/' + name
};
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repo = repo; $scope.repo = repo;
$rootScope.title = 'Settings - ' + namespace + '/' + name; $rootScope.title = 'Settings - ' + namespace + '/' + name;
@ -581,6 +605,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
fetchPermissions('team'); fetchPermissions('team');
fetchTokens(); fetchTokens();
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
return $scope.repo; return $scope.repo;
}); });
}; };
@ -589,13 +618,14 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
fetchRepository(); fetchRepository();
} }
function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, UserService, KeyService, $routeParams) { function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, KeyService, $routeParams) {
if ($routeParams['migrate']) { if ($routeParams['migrate']) {
$('#migrateTab').tab('show') $('#migrateTab').tab('show')
} }
UserService.updateUserIn($scope, function(user) { UserService.updateUserIn($scope, function(user) {
$scope.askForPassword = user.askForPassword; $scope.askForPassword = user.askForPassword;
$scope.cuser = jQuery.extend({}, user);
}); });
$scope.readyForPlan = function() { $scope.readyForPlan = function() {
@ -611,8 +641,22 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
$('.form-change-pw').popover(); $('.form-change-pw').popover();
$scope.logsShown = 0;
$scope.invoicesShown = 0;
$scope.loadLogs = function() {
if (!$scope.hasPaidBusinessPlan) { return; }
$scope.logsShown++;
};
$scope.loadInvoices = function() {
if (!$scope.hasPaidBusinessPlan) { return; }
$scope.invoicesShown++;
};
$scope.planChanged = function(plan) { $scope.planChanged = function(plan) {
$scope.hasPaidPlan = plan && plan.price > 0; $scope.hasPaidPlan = plan && plan.price > 0;
$scope.hasPaidBusinessPlan = PlanService.isOrgCompatible(plan) && plan.price > 0;
}; };
$scope.showConvertForm = function() { $scope.showConvertForm = function() {
@ -621,7 +665,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
}); });
PlanService.getPlans(function(plans) { PlanService.getPlans(function(plans) {
$scope.orgPlans = plans.business; $scope.orgPlans = plans;
}); });
$scope.convertStep = 1; $scope.convertStep = 1;
@ -640,8 +684,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
'plan': $scope.org.plan.stripeId 'plan': $scope.org.plan.stripeId
}; };
var convertAccount = Restangular.one('user/convert'); ApiService.convertUserToOrganization(data).then(function(resp) {
convertAccount.customPOST(data).then(function(resp) {
UserService.load(); UserService.load();
$location.path('/'); $location.path('/');
}, function(resp) { }, function(resp) {
@ -658,14 +701,14 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
$('.form-change-pw').popover('hide'); $('.form-change-pw').popover('hide');
$scope.updatingUser = true; $scope.updatingUser = true;
$scope.changePasswordSuccess = false; $scope.changePasswordSuccess = false;
var changePasswordPost = Restangular.one('user/');
changePasswordPost.customPUT($scope.user).then(function() { ApiService.changeUserDetails($scope.cuser).then(function() {
$scope.updatingUser = false; $scope.updatingUser = false;
$scope.changePasswordSuccess = true; $scope.changePasswordSuccess = true;
// Reset the form // Reset the form
$scope.user.password = ''; $scope.cuser.password = '';
$scope.user.repeatPassword = ''; $scope.cuser.repeatPassword = '';
$scope.changePasswordForm.$setPristine(); $scope.changePasswordForm.$setPristine();
// Reload the user. // Reload the user.
@ -681,7 +724,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
}; };
} }
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, Restangular) { function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService) {
var namespace = $routeParams.namespace; var namespace = $routeParams.namespace;
var name = $routeParams.name; var name = $routeParams.name;
var imageid = $routeParams.image; var imageid = $routeParams.image;
@ -737,7 +780,12 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, R
}; };
var fetchImage = function() { var fetchImage = function() {
$scope.image = ApiService.at('repository', namespace, name, 'image', imageid).get(function(image) { var params = {
'repository': namespace + '/' + name,
'image_id': imageid
};
$scope.image = ApiService.getImageAsResource(params).get(function(image) {
$scope.repo = { $scope.repo = {
'name': name, 'name': name,
'namespace': namespace 'namespace': namespace
@ -757,8 +805,12 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, R
}; };
var fetchChanges = function() { var fetchChanges = function() {
var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes'); var params = {
changesFetch.get().then(function(changes) { 'repository': namespace + '/' + name,
'image_id': imageid
};
ApiService.getImageChanges(null, params).then(function(changes) {
var combinedChanges = []; var combinedChanges = [];
var addCombinedChanges = function(c, kind) { var addCombinedChanges = function(c, kind) {
for (var i = 0; i < c.length; ++i) { for (var i = 0; i < c.length; ++i) {
@ -786,7 +838,7 @@ function V1Ctrl($scope, $location, UserService) {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
} }
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangular, PlanService) { function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService) {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
$scope.repo = { $scope.repo = {
@ -817,8 +869,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
if (isUserNamespace) { if (isUserNamespace) {
// Load the user's subscription information in case they want to create a private // Load the user's subscription information in case they want to create a private
// repository. // repository.
var checkPrivateAllowed = Restangular.one('user/private'); ApiService.getUserPrivateCount().then(function(resp) {
checkPrivateAllowed.get().then(function(resp) {
if (resp.privateCount + 1 > resp.reposAllowed) { if (resp.privateCount + 1 > resp.reposAllowed) {
PlanService.getMinimumPlan(resp.privateCount + 1, false, function(minimum) { PlanService.getMinimumPlan(resp.privateCount + 1, false, function(minimum) {
$scope.planRequired = minimum; $scope.planRequired = minimum;
@ -831,8 +882,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
$scope.checkingPlan = false; $scope.checkingPlan = false;
}); });
} else { } else {
var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private'); ApiService.getOrganizationPrivateAllowed(null, {'orgname': namespace}).then(function(resp) {
checkPrivateAllowed.get().then(function(resp) {
$scope.planRequired = resp.privateAllowed ? null : {}; $scope.planRequired = resp.privateAllowed ? null : {};
$scope.checkingPlan = false; $scope.checkingPlan = false;
}, function() { }, function() {
@ -863,8 +913,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
'description': repo.description 'description': repo.description
}; };
var createPost = Restangular.one('repository'); ApiService.createRepo(data).then(function(created) {
createPost.customPOST(data).then(function(created) {
$scope.creating = false; $scope.creating = false;
$scope.created = created; $scope.created = created;
@ -907,9 +956,12 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
'file_id': fileId 'file_id': fileId
}; };
var startBuildCall = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/'); var params = {
startBuildCall.customPOST(data).then(function(resp) { 'repository': repo.namespace + '/' + repo.name
$location.path('/repository/' + repo.namespace + '/' + repo.name); };
ApiService.requestRepoBuild(data, params).then(function(resp) {
$location.path('/repository/' + params.repository);
}, function() { }, function() {
$('#couldnotbuildModal').modal(); $('#couldnotbuildModal').modal();
}); });
@ -958,8 +1010,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
'mimeType': mimeType 'mimeType': mimeType
}; };
var getUploadUrl = Restangular.one('filedrop/'); var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
getUploadUrl.customPOST(data).then(function(resp) {
conductUpload(repo, file, resp.url, resp.file_id, mimeType); conductUpload(repo, file, resp.url, resp.file_id, mimeType);
}, function() { }, function() {
$('#couldnotbuildModal').modal(); $('#couldnotbuildModal').modal();
@ -987,14 +1038,9 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
}; };
} }
function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) { function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
var orgname = $routeParams.orgname; var orgname = $routeParams.orgname;
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
$scope.TEAM_PATTERN = TEAM_PATTERN; $scope.TEAM_PATTERN = TEAM_PATTERN;
$rootScope.title = 'Loading...'; $rootScope.title = 'Loading...';
@ -1008,10 +1054,14 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
var previousRole = $scope.organization.teams[teamname].role; var previousRole = $scope.organization.teams[teamname].role;
$scope.organization.teams[teamname].role = role; $scope.organization.teams[teamname].role = role;
var updateTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); var params = {
'orgname': orgname,
'teamname': teamname
};
var data = $scope.organization.teams[teamname]; var data = $scope.organization.teams[teamname];
updateTeam.customPUT(data).then(function(resp) { ApiService.updateOrganizationTeam(data, params).then(function(resp) {
}, function(resp) { }, function(resp) {
$scope.organization.teams[teamname].role = previousRole; $scope.organization.teams[teamname].role = previousRole;
$scope.roleError = resp.data || ''; $scope.roleError = resp.data || '';
@ -1032,7 +1082,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
return; return;
} }
createOrganizationTeam(Restangular, orgname, teamname, function(created) { createOrganizationTeam(ApiService, orgname, teamname, function(created) {
$scope.organization.teams[teamname] = created; $scope.organization.teams[teamname] = created;
}); });
}; };
@ -1047,8 +1097,12 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
if (!$scope.currentDeleteTeam) { return; } if (!$scope.currentDeleteTeam) { return; }
var teamname = $scope.currentDeleteTeam; var teamname = $scope.currentDeleteTeam;
var deleteAction = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); var params = {
deleteAction.customDELETE().then(function() { 'orgname': orgname,
'teamname': teamname
};
ApiService.deleteOrganizationTeam(null, params).then(function() {
delete $scope.organization.teams[teamname]; delete $scope.organization.teams[teamname];
$scope.currentDeleteTeam = null; $scope.currentDeleteTeam = null;
}, function() { }, function() {
@ -1058,10 +1112,15 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
}; };
var loadOrganization = function() { var loadOrganization = function() {
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) { $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org; $scope.organization = org;
$rootScope.title = orgname; $rootScope.title = orgname;
$rootScope.description = 'Viewing organization ' + orgname; $rootScope.description = 'Viewing organization ' + orgname;
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
}); });
}; };
@ -1074,17 +1133,12 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
// Load the list of plans. // Load the list of plans.
PlanService.getPlans(function(plans) { PlanService.getPlans(function(plans) {
$scope.plans = plans.business; $scope.plans = plans;
$scope.plan_map = {}; $scope.plan_map = {};
var addPlans = function(plans) { for (var i = 0; i < plans.length; ++i) {
for (var i = 0; i < plans.length; ++i) { $scope.plan_map[plans[i].stripeId] = plans[i];
$scope.plan_map[plans[i].stripeId] = plans[i]; }
}
};
addPlans(plans.user);
addPlans(plans.business);
}); });
$scope.orgname = orgname; $scope.orgname = orgname;
@ -1092,37 +1146,29 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
$scope.membersFound = null; $scope.membersFound = null;
$scope.invoiceLoading = true; $scope.invoiceLoading = true;
$scope.logsShown = 0; $scope.logsShown = 0;
$scope.invoicesShown = 0;
$scope.loadLogs = function() { $scope.loadLogs = function() {
$scope.logsShown++; $scope.logsShown++;
}; };
$scope.loadInvoices = function() {
$scope.invoicesShown++;
};
$scope.planChanged = function(plan) { $scope.planChanged = function(plan) {
$scope.hasPaidPlan = plan && plan.price > 0; $scope.hasPaidPlan = plan && plan.price > 0;
}; };
$scope.loadInvoices = function() {
if ($scope.invoices) { return; }
$scope.invoiceLoading = true;
var getInvoices = Restangular.one(getRestUrl('organization', orgname, 'invoices'));
getInvoices.get().then(function(resp) {
$scope.invoiceExpanded = {};
$scope.invoices = resp.invoices;
$scope.invoiceLoading = false;
});
};
$scope.toggleInvoice = function(id) {
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
};
$scope.loadMembers = function() { $scope.loadMembers = function() {
if ($scope.membersFound) { return; } if ($scope.membersFound) { return; }
$scope.membersLoading = true; $scope.membersLoading = true;
var getMembers = Restangular.one(getRestUrl('organization', orgname, 'members')); var params = {
getMembers.get().then(function(resp) { 'orgname': orgname
};
ApiService.getOrganizationMembers(null, params).then(function(resp) {
var membersArray = []; var membersArray = [];
for (var key in resp.members) { for (var key in resp.members) {
if (resp.members.hasOwnProperty(key)) { if (resp.members.hasOwnProperty(key)) {
@ -1136,7 +1182,7 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
}; };
var loadOrganization = function() { var loadOrganization = function() {
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) { $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
if (org && org.is_admin) { if (org && org.is_admin) {
$scope.organization = org; $scope.organization = org;
$rootScope.title = orgname + ' (Admin)'; $rootScope.title = orgname + ' (Admin)';
@ -1150,11 +1196,6 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
} }
function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) { function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
var teamname = $routeParams.teamname; var teamname = $routeParams.teamname;
var orgname = $routeParams.orgname; var orgname = $routeParams.orgname;
@ -1166,8 +1207,13 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
$scope.addNewMember = function(member) { $scope.addNewMember = function(member) {
if ($scope.members[member.name]) { return; } if ($scope.members[member.name]) { return; }
var addMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', member.name)); var params = {
addMember.customPOST().then(function(resp) { 'orgname': orgname,
'teamname': teamname,
'membername': member.name
};
ApiService.updateOrganizationTeamMember(null, params).then(function(resp) {
$scope.members[member.name] = resp; $scope.members[member.name] = resp;
}, function() { }, function() {
$('#cannotChangeMembersModal').modal({}); $('#cannotChangeMembersModal').modal({});
@ -1175,8 +1221,13 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
}; };
$scope.removeMember = function(username) { $scope.removeMember = function(username) {
var removeMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', username)); var params = {
removeMember.customDELETE().then(function(resp) { 'orgname': orgname,
'teamname': teamname,
'membername': username
};
ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) {
delete $scope.members[username]; delete $scope.members[username];
}, function() { }, function() {
$('#cannotChangeMembersModal').modal({}); $('#cannotChangeMembersModal').modal({});
@ -1186,16 +1237,20 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
$scope.updateForDescription = function(content) { $scope.updateForDescription = function(content) {
$scope.organization.teams[teamname].description = content; $scope.organization.teams[teamname].description = content;
var updateTeam = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname)); var params = {
var data = $scope.organization.teams[teamname]; 'orgname': orgname,
updateTeam.customPUT(data).then(function(resp) { 'teamname': teamname
};
var teaminfo = $scope.organization.teams[teamname];
ApiService.updateOrganizationTeam(teaminfo, params).then(function(resp) {
}, function() { }, function() {
$('#cannotChangeTeamModal').modal({}); $('#cannotChangeTeamModal').modal({});
}); });
}; };
var loadOrganization = function() { var loadOrganization = function() {
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) { $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org; $scope.organization = org;
$scope.team = $scope.organization.teams[teamname]; $scope.team = $scope.organization.teams[teamname];
$rootScope.title = teamname + ' (' + $scope.orgname + ')'; $rootScope.title = teamname + ' (' + $scope.orgname + ')';
@ -1206,11 +1261,22 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
}; };
var loadMembers = function() { var loadMembers = function() {
$scope.membersResource = ApiService.at('organization', $scope.orgname, 'team', teamname, 'members').get(function(resp) { var params = {
'orgname': orgname,
'teamname': teamname
};
$scope.membersResource = ApiService.getOrganizationTeamMembersAsResource(params).get(function(resp) {
$scope.members = resp.members; $scope.members = resp.members;
$scope.canEditMembers = resp.can_edit; $scope.canEditMembers = resp.can_edit;
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
return resp.members; return resp.members;
}); });
}; };
// Load the organization. // Load the organization.
@ -1219,18 +1285,17 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
function OrgsCtrl($scope, UserService) { function OrgsCtrl($scope, UserService) {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
browserchrome.update(); browserchrome.update();
} }
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, Restangular) { function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService) {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
var requested = $routeParams['plan']; var requested = $routeParams['plan'];
// Load the list of plans. // Load the list of plans.
PlanService.getPlans(function(plans) { PlanService.getPlans(function(plans) {
$scope.plans = plans.business; $scope.plans = plans;
$scope.currentPlan = null; $scope.currentPlan = null;
if (requested) { if (requested) {
PlanService.getPlan(requested, function(plan) { PlanService.getPlan(requested, function(plan) {
@ -1263,8 +1328,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
'email': org.email 'email': org.email
}; };
var createPost = Restangular.one('organization/'); ApiService.createOrganization(data).then(function(created) {
createPost.customPOST(data).then(function(created) {
$scope.created = created; $scope.created = created;
// Reset the organizations list. // Reset the organizations list.
@ -1311,14 +1375,19 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul
$scope.ready = false; $scope.ready = false;
var loadOrganization = function() { var loadOrganization = function() {
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) { $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org; $scope.organization = org;
return org; return org;
}); });
}; };
var loadMemberInfo = function() { var loadMemberInfo = function() {
$scope.memberResource = ApiService.at('organization', $scope.orgname, 'members', membername).get(function(resp) { var params = {
'orgname': orgname,
'membername': membername
};
$scope.memberResource = ApiService.getOrganizationMemberAsResource(params).get(function(resp) {
$scope.memberInfo = resp.member; $scope.memberInfo = resp.member;
$rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')'; $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';

View file

@ -101,16 +101,16 @@ Email: my@email.com</pre>
as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br> as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
<pre> <pre>
{ {
<span class="code-info" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2, <span class="context-tooltip" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
<span class="code-info" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu", <span class="context-tooltip" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
<span class="code-info" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu", <span class="context-tooltip" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
<span class="code-info" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu", <span class="context-tooltip" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
<span class="code-info" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: { <span class="context-tooltip" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
"latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc" "latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
}, },
<span class="code-info" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable", <span class="context-tooltip" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
<span class="code-info" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private", <span class="context-tooltip" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
<span class="code-info" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu" <span class="context-tooltip" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
} }
</pre> </pre>
</div> </div>

View file

@ -5,7 +5,7 @@
<div ng-show="user.anonymous"> <div ng-show="user.anonymous">
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1> <h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3> <h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
<div class="sellcall"><a href="/plans/">Private repository plans starting at $7/mo</a></div> <div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
</div> </div>
<div ng-show="!user.anonymous"> <div ng-show="!user.anonymous">

View file

@ -72,7 +72,8 @@
</div> </div>
<div class="button-bar"> <div class="button-bar">
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"> <button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
analytics-on analytics-event="create_organization">
Create Organization Create Organization
</button> </button>
</div> </div>

View file

@ -74,7 +74,11 @@
<!-- Payment --> <!-- Payment -->
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace"> <div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
<div class="alert alert-warning"> <div class="alert alert-warning">
In order to make this repository private, youll need to upgrade your plan to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month. In order to make this repository private, youll need to upgrade your plan to
<b style="border-bottom: 1px dotted black;" bs-tooltip="'<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories'">
{{ planRequired.title }}
</b>.
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
</div> </div>
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a> <a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
<div class="quay-spinner" ng-show="planChanging"></div> <div class="quay-spinner" ng-show="planChanging"></div>

View file

@ -40,60 +40,7 @@
<!-- Billing History tab --> <!-- Billing History tab -->
<div id="billing" class="tab-pane"> <div id="billing" class="tab-pane">
<div ng-show="invoiceLoading"> <div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
<div class="quay-spinner"></div>
</div>
<div ng-show="!invoiceLoading && !invoices">
No invoices have been created
</div>
<div ng-show="!invoiceLoading && invoices">
<table class="table">
<thead>
<th>Billing Date/Time</th>
<th>Amount Due</th>
<th>Status</th>
<th></th>
</thead>
<tbody class="invoice" ng-repeat="invoice in invoices">
<tr class="invoice-title">
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
<td>
<span class="invoice-status">
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
<span class="danger" ng-show="!invoice.paid && invoice.attempted && invoice.closed">Payment failed</span>
<span class="danger" ng-show="!invoice.paid && invoice.attempted && !invoice.closed">Payment failed - Will retry soon</span>
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
</span>
</td>
<td>
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
</a>
</td>
</tr>
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
<td colspan="3">
<dl class="dl-normal">
<dt>Billing Period</dt>
<dd>
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
</dd>
<dt>Plan</dt>
<dd>
<span>{{ invoice.plan ? plan_map[invoice.plan].title : '(N/A)' }}</span>
</dd>
</dl>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- Members tab --> <!-- Members tab -->

View file

@ -1,44 +1,107 @@
<div class="container plans content-container"> <div class="container plans content-container">
<div class="callout">
Plans &amp; Pricing
</div>
<div class="all-plans">
All plans include <span class="feature">unlimited public repositories</span> and <span class="feature">unlimited sharing</span>. All paid plans have a <span class="feature">14-day free trial</span>.
</div>
<div class="row plans-list"> <div class="row plans-list">
<div class="col-xs-0 col-lg-1"></div> <div class="col-sm-2">
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.user"> <div class="features-bar hidden-xs">
<div class="plan" ng-class="plan.stripeId"> <div class="visible-lg" style="height: 50px"></div>
<div class="plan-title">{{ plan.title }}</div> <div class="visible-md visible-sm" style="height: 70px"></div>
<div class="plan-price">${{ plan.price/100 }}</div>
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div> <div class="feature">
<div class="description">{{ plan.audience }}</div> <span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
<div class="smaller">SSL secured connections</div> title="All plans have unlimited public repositories">
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button> <span class="hidden-sm-inline">Public Repositories</span>
</div> <span class="visible-sm-inline">Public Repos</span>
</span>
<i class="fa fa-hdd visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="SSL encryption is enabled end-to-end for all operations">
SSL Encryption
</span>
<i class="fa fa-lock visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
Robot accounts
</span>
<i class="fa fa-wrench visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Repository images can be built directly from Dockerfiles">
Dockerfile Build
</span>
<i class="fa fa-upload visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
Teams
</span>
<i class="fa fa-group visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
Logging
</span>
<i class="fa fa-bar-chart-o visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Administrators can view and download the full invoice history for their organization">
Invoice History
</span>
<i class="fa fa-calendar visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="All plans have a 14-day free trial">
<span class="hidden-sm-inline">14-Day Free Trial</span>
<span class="visible-sm-inline">14-Day Trial</span>
</span>
<i class="fa fa-clock-o visible-lg"></i>
</div>
</div>
</div> </div>
</div> <div class="col-sm-2 plan-container" ng-repeat="plan in plans" ng-show="plan.price > 0 && !plan.deprecated">
<div class="plan" ng-class="plan.stripeId + ' ' + (plan.bus_features ? 'business-plan' : '')">
<div class="plan-box">
<div class="plan-title">{{ plan.title }}</div>
<div class="plan-price">${{ plan.price/100 }}</div>
<div class="callout"> <div class="description">{{ plan.audience }}</div>
Business Plan Pricing </div>
</div>
<div class="all-plans"> <div class="features hidden-xs">
All business plans include all of the personal plan features, plus: <span class="business-feature">organizations</span> and <span class="business-feature">teams</span> with <span class="business-feature">delegated access</span> to the organization. All business plans have a <span class="business-feature">14-day free trial</span>. <div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
</div> <div class="feature present"></div>
<div class="feature present"></div>
<div class="feature present"></div>
<div class="feature present"></div>
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
<div class="feature present"></div>
</div>
<div class="features visible-xs">
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
<div class="feature present">Unlimited Public Repositories</div>
<div class="feature present">SSL Encryption</div>
<div class="feature present">Robot accounts</div>
<div class="feature present">Dockerfile Build</div>
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
<div class="feature present">14-Day Free Trial</div>
</div>
<button class="btn btn-block" ng-class="plan.bus_features ? 'btn-success' : 'btn-primary'"
ng-click="buyNow(plan.stripeId)">Start <span class="hidden-sm-inline">Free</span> Trial</button>
<div class="row plans-list">
<div class="col-xs-0 col-lg-1"></div>
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.business">
<div class="plan business-plan" ng-class="plan.stripeId">
<div class="plan-title">{{ plan.title }}</div>
<div class="plan-price">${{ plan.price/100 }}</div>
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
<div class="description">{{ plan.audience }}</div>
<div class="smaller">SSL secured connections</div>
<button class="btn btn-success btn-block" ng-click="createOrg(plan.stripeId)">Sign Up Now</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -27,16 +27,23 @@
<div class="col-md-2"> <div class="col-md-2">
<ul class="nav nav-pills nav-stacked"> <ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li> <li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing">Billing Options</a></li> <li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Set Password</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Set Password</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
</ul> </ul>
</div> </div>
<!-- Content --> <!-- Content -->
<div class="col-md-10"> <div class="col-md-10">
<div class="tab-content"> <div class="tab-content">
<!-- Logs tab -->
<div id="logs" class="tab-pane">
<div class="logs-view" user="user" visible="logsShown"></div>
</div>
<!-- Plans tab --> <!-- Plans tab -->
<div id="plan" class="tab-pane active"> <div id="plan" class="tab-pane active">
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div> <div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
@ -53,9 +60,9 @@
<div ng-show="!updatingUser"> <div ng-show="!updatingUser">
<form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual" <form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering"> data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required> <input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required>
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword" <input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword"
match="user.password" required> match="cuser.password" required>
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit" <button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="change_pass">Change Password</button> analytics-on analytics-event="change_pass">Change Password</button>
</form> </form>
@ -69,10 +76,15 @@
</div> </div>
<!-- Billing options tab --> <!-- Billing options tab -->
<div id="billing" class="tab-pane"> <div id="billingoptions" class="tab-pane">
<div class="billing-options" user="user"></div> <div class="billing-options" user="user"></div>
</div> </div>
<!-- Billing History tab -->
<div id="billing" class="tab-pane">
<div class="billing-invoices" user="user" visible="invoicesShown"></div>
</div>
<!-- Convert to organization tab --> <!-- Convert to organization tab -->
<div id="migrate" class="tab-pane"> <div id="migrate" class="tab-pane">
<!-- Step 0 --> <!-- Step 0 -->
@ -86,11 +98,11 @@
</div> </div>
<div class="panel-body" ng-show="user.organizations.length == 0"> <div class="panel-body" ng-show="user.organizations.length == 0">
<div class="alert alert-danger"> <div class="alert alert-warning">
Converting a user account into an organization <b>cannot be undone</b>.<br> Here be many fire-breathing dragons! Note: Converting a user account into an organization <b>cannot be undone</b>
</div> </div>
<button class="btn btn-danger" ng-click="showConvertForm()">Start conversion process</button> <button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process</button>
</div> </div>
</div> </div>
@ -113,7 +125,7 @@
ng-model="org.adminUser" required autofocus> ng-model="org.adminUser" required autofocus>
<input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password" <input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password"
ng-model="org.adminPassword" required> ng-model="org.adminPassword" required>
<span class="description">The username and password for an <b>existing account</b> that will become administrator of the organization</span> <span class="description">The username and password for the account that will become administrator of the organization</span>
</div> </div>
<!-- Plans Table --> <!-- Plans Table -->
@ -123,7 +135,8 @@
</div> </div>
<div class="button-bar"> <div class="button-bar">
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"> <button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"
analytics-on analytics-event="convert_to_organization">
Convert To Organization Convert To Organization
</button> </button>
</div> </div>

View file

@ -65,6 +65,10 @@
{% endblock %} {% endblock %}
<script type="text/javascript">
window.__endpoints = {{ route_data|safe }}.endpoints;
</script>
<script src="static/js/app.js"></script> <script src="static/js/app.js"></script>
<script src="static/js/controllers.js"></script> <script src="static/js/controllers.js"></script>
<script src="static/js/graphing.js"></script> <script src="static/js/graphing.js"></script>
@ -122,6 +126,22 @@ var isProd = document.location.hostname === 'quay.io';
</nav> </nav>
</div> </div>
<!-- Modal message dialog -->
<div class="modal fade" id="couldnotloadModal" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Uh Oh...</h4>
</div>
<div class="modal-body">
Something went wrong when trying to load Quay.io! Please report this to <a href="mailto:support@quay.io">support@quay.io</a>.
</div>
<div class="modal-footer">
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- begin olark code --> <!-- begin olark code -->
{% if request.host == 'quay.io' %} {% if request.host == 'quay.io' %}
<script data-cfasync="false" type='text/javascript'>/*<![CDATA[*/window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){ <script data-cfasync="false" type='text/javascript'>/*<![CDATA[*/window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){

View file

@ -105,20 +105,20 @@ def build_specs():
return [ return [
TestSpec(url_for('welcome'), 200, 200, 200, 200), TestSpec(url_for('welcome'), 200, 200, 200, 200),
TestSpec(url_for('plans_list'), 200, 200, 200, 200), TestSpec(url_for('list_plans'), 200, 200, 200, 200),
TestSpec(url_for('get_logged_in_user'), 200, 200, 200, 200), TestSpec(url_for('get_logged_in_user'), 200, 200, 200, 200),
TestSpec(url_for('change_user_details'), TestSpec(url_for('change_user_details'),
401, 200, 200, 200).set_method('PUT'), 401, 200, 200, 200).set_method('PUT'),
TestSpec(url_for('create_user_api'), 201, 201, 201, TestSpec(url_for('create_new_user'), 201, 201, 201,
201).set_method('POST').set_data_from_obj(NEW_USER_DETAILS), 201).set_method('POST').set_data_from_obj(NEW_USER_DETAILS),
TestSpec(url_for('signin_api'), 200, 200, 200, TestSpec(url_for('signin_user'), 200, 200, 200,
200).set_method('POST').set_data_from_obj(SIGNIN_DETAILS), 200).set_method('POST').set_data_from_obj(SIGNIN_DETAILS),
TestSpec(url_for('send_recovery'), 201, 201, 201, TestSpec(url_for('request_recovery_email'), 201, 201, 201,
201).set_method('POST').set_data_from_obj(SEND_RECOVERY_DETAILS), 201).set_method('POST').set_data_from_obj(SEND_RECOVERY_DETAILS),
TestSpec(url_for('get_matching_users', prefix='dev'), 401, 200, 200, 200), TestSpec(url_for('get_matching_users', prefix='dev'), 401, 200, 200, 200),
@ -161,29 +161,29 @@ def build_specs():
teamname=ORG_READERS, membername=ORG_OWNER), teamname=ORG_READERS, membername=ORG_OWNER),
admin_code=400).set_method('DELETE'), admin_code=400).set_method('DELETE'),
(TestSpec(url_for('create_repo_api')) (TestSpec(url_for('create_repo'))
.set_method('POST') .set_method('POST')
.set_data_from_obj(NEW_ORG_REPO_DETAILS)), .set_data_from_obj(NEW_ORG_REPO_DETAILS)),
TestSpec(url_for('match_repos_api'), 200, 200, 200, 200), TestSpec(url_for('find_repos'), 200, 200, 200, 200),
TestSpec(url_for('list_repos_api'), 200, 200, 200, 200), TestSpec(url_for('list_repos'), 200, 200, 200, 200),
TestSpec(url_for('update_repo_api', repository=PUBLIC_REPO), TestSpec(url_for('update_repo', repository=PUBLIC_REPO),
admin_code=403).set_method('PUT'), admin_code=403).set_method('PUT'),
(TestSpec(url_for('update_repo_api', repository=ORG_REPO)) (TestSpec(url_for('update_repo', repository=ORG_REPO))
.set_method('PUT') .set_method('PUT')
.set_data_from_obj(UPDATE_REPO_DETAILS)), .set_data_from_obj(UPDATE_REPO_DETAILS)),
(TestSpec(url_for('update_repo_api', repository=PRIVATE_REPO)) (TestSpec(url_for('update_repo', repository=PRIVATE_REPO))
.set_method('PUT') .set_method('PUT')
.set_data_from_obj(UPDATE_REPO_DETAILS)), .set_data_from_obj(UPDATE_REPO_DETAILS)),
(TestSpec(url_for('change_repo_visibility_api', repository=PUBLIC_REPO), (TestSpec(url_for('change_repo_visibility', repository=PUBLIC_REPO),
admin_code=403).set_method('POST') admin_code=403).set_method('POST')
.set_data_from_obj(CHANGE_VISIBILITY_DETAILS)), .set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
(TestSpec(url_for('change_repo_visibility_api', repository=ORG_REPO)) (TestSpec(url_for('change_repo_visibility', repository=ORG_REPO))
.set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)), .set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
(TestSpec(url_for('change_repo_visibility_api', repository=PRIVATE_REPO)) (TestSpec(url_for('change_repo_visibility', repository=PRIVATE_REPO))
.set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)), .set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
TestSpec(url_for('delete_repository', repository=PUBLIC_REPO), TestSpec(url_for('delete_repository', repository=PUBLIC_REPO),
@ -193,11 +193,11 @@ def build_specs():
TestSpec(url_for('delete_repository', repository=PRIVATE_REPO), TestSpec(url_for('delete_repository', repository=PRIVATE_REPO),
admin_code=204).set_method('DELETE'), admin_code=204).set_method('DELETE'),
TestSpec(url_for('get_repo_api', repository=PUBLIC_REPO), TestSpec(url_for('get_repo', repository=PUBLIC_REPO),
200, 200, 200,200), 200, 200, 200,200),
TestSpec(url_for('get_repo_api', repository=ORG_REPO), TestSpec(url_for('get_repo', repository=ORG_REPO),
403, 403, 200, 200), 403, 403, 200, 200),
TestSpec(url_for('get_repo_api', repository=PRIVATE_REPO), TestSpec(url_for('get_repo', repository=PRIVATE_REPO),
403, 403, 200, 200), 403, 403, 200, 200),
TestSpec(url_for('get_repo_builds', repository=PUBLIC_REPO), TestSpec(url_for('get_repo_builds', repository=PUBLIC_REPO),
@ -403,20 +403,20 @@ def build_specs():
TestSpec(url_for('delete_token', repository=PRIVATE_REPO, TestSpec(url_for('delete_token', repository=PRIVATE_REPO,
code=FAKE_TOKEN), admin_code=400).set_method('DELETE'), code=FAKE_TOKEN), admin_code=400).set_method('DELETE'),
TestSpec(url_for('subscribe_api'), 401, 400, 400, 400).set_method('PUT'), TestSpec(url_for('update_user_subscription'), 401, 400, 400, 400).set_method('PUT'),
TestSpec(url_for('subscribe_org_api', orgname=ORG), TestSpec(url_for('update_org_subscription', orgname=ORG),
401, 403, 403, 400).set_method('PUT'), 401, 403, 403, 400).set_method('PUT'),
TestSpec(url_for('get_subscription'), 401, 200, 200, 200), TestSpec(url_for('get_user_subscription'), 401, 200, 200, 200),
TestSpec(url_for('get_org_subscription', orgname=ORG)), TestSpec(url_for('get_org_subscription', orgname=ORG)),
TestSpec(url_for('repo_logs_api', repository=PUBLIC_REPO), admin_code=403), TestSpec(url_for('list_repo_logs', repository=PUBLIC_REPO), admin_code=403),
TestSpec(url_for('repo_logs_api', repository=ORG_REPO)), TestSpec(url_for('list_repo_logs', repository=ORG_REPO)),
TestSpec(url_for('repo_logs_api', repository=PRIVATE_REPO)), TestSpec(url_for('list_repo_logs', repository=PRIVATE_REPO)),
TestSpec(url_for('org_logs_api', orgname=ORG)), TestSpec(url_for('list_org_logs', orgname=ORG)),
] ]