Merge pull request #2086 from coreos-inc/user-info
Add collection of user metadata: name and company
This commit is contained in:
commit
45b1148118
14 changed files with 178 additions and 33 deletions
|
@ -222,6 +222,9 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether to proxy all direct download URLs in storage via the registry's nginx.
|
# Feature Flag: Whether to proxy all direct download URLs in storage via the registry's nginx.
|
||||||
FEATURE_PROXY_STORAGE = False
|
FEATURE_PROXY_STORAGE = False
|
||||||
|
|
||||||
|
# Feature Flag: Whether to collect and support user metadata.
|
||||||
|
FEATURE_USER_METADATA = False
|
||||||
|
|
||||||
# The namespace to use for library repositories.
|
# The namespace to use for library repositories.
|
||||||
# Note: This must remain 'library' until Docker removes their hard-coded namespace for libraries.
|
# Note: This must remain 'library' until Docker removes their hard-coded namespace for libraries.
|
||||||
# See: https://github.com/docker/docker/blob/master/registry/session.go#L320
|
# See: https://github.com/docker/docker/blob/master/registry/session.go#L320
|
||||||
|
|
|
@ -337,6 +337,9 @@ class User(BaseModel):
|
||||||
enabled = BooleanField(default=True)
|
enabled = BooleanField(default=True)
|
||||||
invoice_email_address = CharField(null=True, index=True)
|
invoice_email_address = CharField(null=True, index=True)
|
||||||
|
|
||||||
|
name = CharField(null=True)
|
||||||
|
company = CharField(null=True)
|
||||||
|
|
||||||
def delete_instance(self, recursive=False, delete_nullable=False):
|
def delete_instance(self, recursive=False, delete_nullable=False):
|
||||||
# If we are deleting a robot account, only execute the subset of queries necessary.
|
# If we are deleting a robot account, only execute the subset of queries necessary.
|
||||||
if self.robot:
|
if self.robot:
|
||||||
|
@ -372,6 +375,12 @@ class User(BaseModel):
|
||||||
Namespace = User.alias()
|
Namespace = User.alias()
|
||||||
|
|
||||||
|
|
||||||
|
class UserPromptTypes(object):
|
||||||
|
CONFIRM_USERNAME = 'confirm_username'
|
||||||
|
ENTER_NAME = 'enter_name'
|
||||||
|
ENTER_COMPANY = 'enter_company'
|
||||||
|
|
||||||
|
|
||||||
class UserPromptKind(BaseModel):
|
class UserPromptKind(BaseModel):
|
||||||
name = CharField(index=True)
|
name = CharField(index=True)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""Add user metadata fields
|
||||||
|
|
||||||
|
Revision ID: 491a530df230
|
||||||
|
Revises: 6c7014e84a5e
|
||||||
|
Create Date: 2016-11-04 18:03:05.237408
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '491a530df230'
|
||||||
|
down_revision = '6c7014e84a5e'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from util.migrate import UTF8CharField
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('user', sa.Column('company', UTF8CharField(length=255), nullable=True))
|
||||||
|
op.add_column('user', sa.Column('name', UTF8CharField(length=255), nullable=True))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
op.bulk_insert(tables.userpromptkind,
|
||||||
|
[
|
||||||
|
{'name':'enter_name'},
|
||||||
|
{'name':'enter_company'},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'name')
|
||||||
|
op.drop_column('user', 'company')
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.userpromptkind.delete()
|
||||||
|
.where(tables.userpromptkind.c.name == op.inline_literal('enter_name')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.userpromptkind.delete()
|
||||||
|
.where(tables.userpromptkind.c.name == op.inline_literal('enter_company')))
|
||||||
|
)
|
|
@ -6,13 +6,14 @@ import uuid
|
||||||
from peewee import JOIN_LEFT_OUTER, IntegrityError, fn
|
from peewee import JOIN_LEFT_OUTER, IntegrityError, fn
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from data.database import (User, LoginService, FederatedLogin, RepositoryPermission, TeamMember,
|
from data.database import (User, LoginService, FederatedLogin, RepositoryPermission, TeamMember,
|
||||||
Team, Repository, TupleSelector, TeamRole, Namespace, Visibility,
|
Team, Repository, TupleSelector, TeamRole, Namespace, Visibility,
|
||||||
EmailConfirmation, Role, db_for_update, random_string_generator,
|
EmailConfirmation, Role, db_for_update, random_string_generator,
|
||||||
UserRegion, ImageStorageLocation, QueueItem, TeamMemberInvite,
|
UserRegion, ImageStorageLocation, QueueItem, TeamMemberInvite,
|
||||||
ServiceKeyApproval, OAuthApplication, RepositoryBuildTrigger,
|
ServiceKeyApproval, OAuthApplication, RepositoryBuildTrigger,
|
||||||
UserPromptKind, UserPrompt)
|
UserPromptKind, UserPrompt, UserPromptTypes)
|
||||||
from data.model import (DataModelException, InvalidPasswordException, InvalidRobotException,
|
from data.model import (DataModelException, InvalidPasswordException, InvalidRobotException,
|
||||||
InvalidUsernameException, InvalidEmailAddressException,
|
InvalidUsernameException, InvalidEmailAddressException,
|
||||||
TooManyLoginAttemptsException, db_transaction,
|
TooManyLoginAttemptsException, db_transaction,
|
||||||
|
@ -28,17 +29,16 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1)
|
EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1)
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password, salt=None):
|
def hash_password(password, salt=None):
|
||||||
salt = salt or bcrypt.gensalt()
|
salt = salt or bcrypt.gensalt()
|
||||||
return bcrypt.hashpw(password.encode('utf-8'), salt)
|
return bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||||
|
|
||||||
def create_user(username, password, email, auto_verify=False, email_required=True):
|
def create_user(username, password, email, auto_verify=False, email_required=True, prompts=tuple()):
|
||||||
""" Creates a regular user, if allowed. """
|
""" Creates a regular user, if allowed. """
|
||||||
if not validate_password(password):
|
if not validate_password(password):
|
||||||
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
||||||
|
|
||||||
created = create_user_noverify(username, email, email_required=email_required)
|
created = create_user_noverify(username, email, email_required=email_required, prompts=prompts)
|
||||||
created.password_hash = hash_password(password)
|
created.password_hash = hash_password(password)
|
||||||
created.verified = auto_verify
|
created.verified = auto_verify
|
||||||
created.save()
|
created.save()
|
||||||
|
@ -46,7 +46,7 @@ def create_user(username, password, email, auto_verify=False, email_required=Tru
|
||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
def create_user_noverify(username, email, email_required=True):
|
def create_user_noverify(username, email, email_required=True, prompts=tuple()):
|
||||||
if email_required:
|
if email_required:
|
||||||
if not validate_email(email):
|
if not validate_email(email):
|
||||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||||
|
@ -76,7 +76,11 @@ def create_user_noverify(username, email, email_required=True):
|
||||||
logger.debug('Email and username are unique!')
|
logger.debug('Email and username are unique!')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return User.create(username=username, email=email)
|
new_user = User.create(username=username, email=email)
|
||||||
|
for prompt in prompts:
|
||||||
|
create_user_prompt(new_user, prompt)
|
||||||
|
|
||||||
|
return new_user
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise DataModelException(ex.message)
|
raise DataModelException(ex.message)
|
||||||
|
|
||||||
|
@ -103,6 +107,15 @@ def change_password(user, new_password):
|
||||||
notification.delete_notifications_by_kind(user, 'password_required')
|
notification.delete_notifications_by_kind(user, 'password_required')
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_user_prompts(features):
|
||||||
|
prompts = set()
|
||||||
|
if features.USER_METADATA:
|
||||||
|
prompts.add(UserPromptTypes.ENTER_NAME)
|
||||||
|
prompts.add(UserPromptTypes.ENTER_COMPANY)
|
||||||
|
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
|
||||||
def has_user_prompts(user):
|
def has_user_prompts(user):
|
||||||
try:
|
try:
|
||||||
UserPrompt.select().where(UserPrompt.user == user).get()
|
UserPrompt.select().where(UserPrompt.user == user).get()
|
||||||
|
@ -110,6 +123,7 @@ def has_user_prompts(user):
|
||||||
except UserPrompt.DoesNotExist:
|
except UserPrompt.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def has_user_prompt(user, prompt_name):
|
def has_user_prompt(user, prompt_name):
|
||||||
prompt_kind = UserPromptKind.get(name=prompt_name)
|
prompt_kind = UserPromptKind.get(name=prompt_name)
|
||||||
|
|
||||||
|
@ -341,16 +355,28 @@ def list_entity_robot_permission_teams(entity_name, include_permissions=False):
|
||||||
return TupleSelector(query, fields)
|
return TupleSelector(query, fields)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_metadata(user, name=None, company=None):
|
||||||
|
""" Updates the metadata associated with the user, including his/her name and company. """
|
||||||
|
with db_transaction():
|
||||||
|
user.name = name or user.name
|
||||||
|
user.company = company or user.company
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Remove any prompts associated with the user's metadata being needed.
|
||||||
|
remove_user_prompt(user, UserPromptTypes.ENTER_NAME)
|
||||||
|
remove_user_prompt(user, UserPromptTypes.ENTER_COMPANY)
|
||||||
|
|
||||||
|
|
||||||
def create_federated_user(username, email, service_name, service_ident,
|
def create_federated_user(username, email, service_name, service_ident,
|
||||||
set_password_notification, metadata={},
|
set_password_notification, metadata={},
|
||||||
email_required=True, confirm_username=False):
|
email_required=True, prompts=tuple()):
|
||||||
new_user = create_user_noverify(username, email, email_required=email_required)
|
prompts = set(prompts)
|
||||||
|
prompts.add(UserPromptTypes.CONFIRM_USERNAME)
|
||||||
|
|
||||||
|
new_user = create_user_noverify(username, email, email_required=email_required, prompts=prompts)
|
||||||
new_user.verified = True
|
new_user.verified = True
|
||||||
new_user.save()
|
new_user.save()
|
||||||
|
|
||||||
if confirm_username:
|
|
||||||
create_user_prompt(new_user, 'confirm_username')
|
|
||||||
|
|
||||||
service = LoginService.get(LoginService.name == service_name)
|
service = LoginService.get(LoginService.name == service_name)
|
||||||
FederatedLogin.create(user=new_user, service=service,
|
FederatedLogin.create(user=new_user, service=service,
|
||||||
service_ident=service_ident,
|
service_ident=service_ident,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import features
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
@ -49,11 +50,12 @@ class FederatedUsers(object):
|
||||||
logger.error('Unable to pick a username for user: %s', username)
|
logger.error('Unable to pick a username for user: %s', username)
|
||||||
return (None, 'Unable to pick a username. Please report this to your administrator.')
|
return (None, 'Unable to pick a username. Please report this to your administrator.')
|
||||||
|
|
||||||
|
prompts = model.user.get_default_user_prompts(features)
|
||||||
db_user = model.user.create_federated_user(valid_username, email, self._federated_service,
|
db_user = model.user.create_federated_user(valid_username, email, self._federated_service,
|
||||||
username,
|
username,
|
||||||
set_password_notification=False,
|
set_password_notification=False,
|
||||||
email_required=self._requires_email,
|
email_required=self._requires_email,
|
||||||
confirm_username=True)
|
prompts=prompts)
|
||||||
else:
|
else:
|
||||||
# Update the db attributes from the federated service.
|
# Update the db attributes from the federated service.
|
||||||
if email:
|
if email:
|
||||||
|
|
|
@ -260,8 +260,9 @@ class SuperUserList(ApiResource):
|
||||||
# Create the user.
|
# Create the user.
|
||||||
username = user_information['username']
|
username = user_information['username']
|
||||||
email = user_information.get('email')
|
email = user_information.get('email')
|
||||||
|
prompts = model.user.get_default_user_prompts(features)
|
||||||
user = model.user.create_user(username, password, email, auto_verify=not features.MAILING,
|
user = model.user.create_user(username, password, email, auto_verify=not features.MAILING,
|
||||||
email_required=features.MAILING)
|
email_required=features.MAILING, prompts=prompts)
|
||||||
|
|
||||||
# If mailing is turned on, send the user a verification email.
|
# If mailing is turned on, send the user a verification email.
|
||||||
if features.MAILING:
|
if features.MAILING:
|
||||||
|
|
|
@ -17,7 +17,7 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
|
||||||
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import get_plan
|
from data.billing import get_plan
|
||||||
from data.database import Repository as RepositoryTable
|
from data.database import Repository as RepositoryTable, UserPromptTypes
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, require_user_admin, parse_args,
|
log_action, internal_only, require_user_admin, parse_args,
|
||||||
query_param, require_scope, format_date, show_if,
|
query_param, require_scope, format_date, show_if,
|
||||||
|
@ -180,8 +180,8 @@ class User(ApiResource):
|
||||||
},
|
},
|
||||||
'invite_code': {
|
'invite_code': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The optional invite code'
|
'description': 'The optional invite code',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'UpdateUser': {
|
'UpdateUser': {
|
||||||
|
@ -212,7 +212,15 @@ class User(ApiResource):
|
||||||
'invoice_email_address': {
|
'invoice_email_address': {
|
||||||
'type': ['string', 'null'],
|
'type': ['string', 'null'],
|
||||||
'description': 'Custom email address for receiving invoices',
|
'description': 'Custom email address for receiving invoices',
|
||||||
}
|
},
|
||||||
|
'name': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The optional entered name for the user',
|
||||||
|
},
|
||||||
|
'company': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The optional entered company for the user',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'UserView': {
|
'UserView': {
|
||||||
|
@ -326,6 +334,9 @@ class User(ApiResource):
|
||||||
else:
|
else:
|
||||||
model.user.update_email(user, new_email, auto_verify=not features.MAILING)
|
model.user.update_email(user, new_email, auto_verify=not features.MAILING)
|
||||||
|
|
||||||
|
if 'name' in user_data or 'company' in user_data:
|
||||||
|
model.user.update_user_metadata(user, user_data.get('name'), user_data.get('company'))
|
||||||
|
|
||||||
# Check for username rename. A username can be renamed if the feature is enabled OR the user
|
# Check for username rename. A username can be renamed if the feature is enabled OR the user
|
||||||
# currently has a confirm_username prompt.
|
# currently has a confirm_username prompt.
|
||||||
if 'username' in user_data:
|
if 'username' in user_data:
|
||||||
|
@ -370,10 +381,12 @@ class User(ApiResource):
|
||||||
raise request_error(message='Email address is required')
|
raise request_error(message='Email address is required')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
prompts = model.user.get_default_user_prompts(features)
|
||||||
new_user = model.user.create_user(user_data['username'], user_data['password'],
|
new_user = model.user.create_user(user_data['username'], user_data['password'],
|
||||||
user_data.get('email'),
|
user_data.get('email'),
|
||||||
auto_verify=not features.MAILING,
|
auto_verify=not features.MAILING,
|
||||||
email_required=features.MAILING)
|
email_required=features.MAILING,
|
||||||
|
prompts=prompts)
|
||||||
|
|
||||||
email_address_confirmed = handle_invite_code(invite_code, new_user)
|
email_address_confirmed = handle_invite_code(invite_code, new_user)
|
||||||
if features.MAILING and not email_address_confirmed:
|
if features.MAILING and not email_address_confirmed:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from flask import request, redirect, url_for, Blueprint
|
from flask import request, redirect, url_for, Blueprint
|
||||||
|
@ -63,9 +62,11 @@ def conduct_oauth_login(service, user_id, username, email, metadata={}):
|
||||||
new_username = valid
|
new_username = valid
|
||||||
break
|
break
|
||||||
|
|
||||||
|
prompts = model.user.get_default_user_prompts(features)
|
||||||
to_login = model.user.create_federated_user(new_username, email, service_name.lower(),
|
to_login = model.user.create_federated_user(new_username, email, service_name.lower(),
|
||||||
user_id, set_password_notification=True,
|
user_id, set_password_notification=True,
|
||||||
metadata=metadata, confirm_username=True)
|
metadata=metadata,
|
||||||
|
prompts=prompts)
|
||||||
|
|
||||||
# Success, tell analytics
|
# Success, tell analytics
|
||||||
analytics.track(to_login.username, 'register', {'service': service_name.lower()})
|
analytics.track(to_login.username, 'register', {'service': service_name.lower()})
|
||||||
|
|
|
@ -399,7 +399,10 @@ def confirm_email():
|
||||||
user_analytics.change_email(old_email, new_email)
|
user_analytics.change_email(old_email, new_email)
|
||||||
|
|
||||||
common_login(user)
|
common_login(user)
|
||||||
return redirect(url_for('web.user', tab='email') if new_email else url_for('web.index'))
|
if model.user.has_user_prompts(user):
|
||||||
|
return redirect(url_for('web.updateuser'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('web.user', tab='email') if new_email else url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@web.route('/recovery', methods=['GET'])
|
@web.route('/recovery', methods=['GET'])
|
||||||
|
|
|
@ -397,6 +397,8 @@ def initialize_database():
|
||||||
LabelSourceType.create(name='internal')
|
LabelSourceType.create(name='internal')
|
||||||
|
|
||||||
UserPromptKind.create(name='confirm_username')
|
UserPromptKind.create(name='confirm_username')
|
||||||
|
UserPromptKind.create(name='enter_name')
|
||||||
|
UserPromptKind.create(name='enter_company')
|
||||||
|
|
||||||
|
|
||||||
def wipe_database():
|
def wipe_database():
|
||||||
|
|
|
@ -21,7 +21,11 @@
|
||||||
|
|
||||||
UserService.updateUserIn($scope, function(user) {
|
UserService.updateUserIn($scope, function(user) {
|
||||||
if (!user.anonymous) {
|
if (!user.anonymous) {
|
||||||
$location.path('/repository/');
|
if (user.prompts && user.prompts.length) {
|
||||||
|
$location.path('/updateuser/');
|
||||||
|
} else {
|
||||||
|
$location.path('/repository/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
function UpdateUserCtrl($scope, UserService, $location, ApiService) {
|
function UpdateUserCtrl($scope, UserService, $location, ApiService) {
|
||||||
$scope.state = 'loading';
|
$scope.state = 'loading';
|
||||||
|
$scope.metadata = {};
|
||||||
|
|
||||||
UserService.updateUserIn($scope, function(user) {
|
UserService.updateUserIn($scope, function(user) {
|
||||||
if (!user.anonymous) {
|
if (!user.anonymous) {
|
||||||
|
@ -45,15 +46,17 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateUsername = function(username) {
|
$scope.updateUser = function(data) {
|
||||||
$scope.state = 'updating';
|
$scope.state = 'updating';
|
||||||
var data = {
|
ApiService.changeUserDetails(data).then(function() {
|
||||||
'username': username
|
UserService.load(function(updated) {
|
||||||
};
|
if (updated.prompts.length) {
|
||||||
|
$scope.state = 'editing';
|
||||||
ApiService.changeUserDetails(data).then(function() {
|
} else {
|
||||||
window.location = '/';
|
$location.url('/');
|
||||||
}, ApiService.errorDisplay('Could not update username'));
|
}
|
||||||
|
});
|
||||||
|
}, ApiService.errorDisplay('Could not update user information'));
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.hasPrompt = function(user, prompt_name) {
|
$scope.hasPrompt = function(user, prompt_name) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="cor-loader-inline" ng-if="user.anonymous || state == 'updating'"></div>
|
<div class="cor-loader-inline" ng-if="user.anonymous || state == 'updating'"></div>
|
||||||
<!-- TODO: Support additional kinds of prompts here -->
|
|
||||||
|
|
||||||
|
<!-- Confirm username -->
|
||||||
<div class="update-user" ng-show="hasPrompt(user, 'confirm_username') && state != 'updating'">
|
<div class="update-user" ng-show="hasPrompt(user, 'confirm_username') && state != 'updating'">
|
||||||
<h2>Confirm Username</h2>
|
<h2>Confirm Username</h2>
|
||||||
<p>
|
<p>
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
Docker CLI guidelines for use as a namespace in <span class="registry-title"></span>.
|
Docker CLI guidelines for use as a namespace in <span class="registry-title"></span>.
|
||||||
</p>
|
</p>
|
||||||
<p>Please confirm the selected username or enter a different username below:</p>
|
<p>Please confirm the selected username or enter a different username below:</p>
|
||||||
<form name="usernameForm" ng-submit="updateUsername(username)">
|
<form name="usernameForm" ng-submit="updateUser({'username': username})">
|
||||||
<div class="namespace-input" binding="username" is-back-incompat="isBackIncompat"
|
<div class="namespace-input" binding="username" is-back-incompat="isBackIncompat"
|
||||||
namespace-title="Username" style="margin-bottom: 20px;"
|
namespace-title="Username" style="margin-bottom: 20px;"
|
||||||
has-external-error="state == 'existing'"></div>
|
has-external-error="state == 'existing'"></div>
|
||||||
|
@ -33,4 +33,38 @@
|
||||||
<i class="fa fa-exclamation-triangle"></i> Note: Usernames with dots or dashes are incompatible with Docker verion 1.8 or older
|
<i class="fa fa-exclamation-triangle"></i> Note: Usernames with dots or dashes are incompatible with Docker verion 1.8 or older
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enter metadata -->
|
||||||
|
<div class="update-user" ng-show="!hasPrompt(user, 'confirm_username') && (hasPrompt(user, 'enter_name') || hasPrompt(user, 'enter_company')) && state != 'updating'">
|
||||||
|
<h2>Tell us a bit more about yourself</h2>
|
||||||
|
<div>This information will be displayed in your user profile.</div>
|
||||||
|
|
||||||
|
<form name="metadataForm" ng-submit="updateUser(metadata)" style="margin-top: 20px;">
|
||||||
|
<div class="form-group nested">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-container">
|
||||||
|
<input type="text" class="form-control" placeholder="Name" ng-model="metadata.name"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group nested">
|
||||||
|
<label for="firstName">Company</label>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-container">
|
||||||
|
<input type="text" class="form-control" placeholder="Company name" ng-model="metadata.company"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px">
|
||||||
|
<input type="submit" class="btn btn-primary" value="Save Details"
|
||||||
|
ng-disabled="!metadata.name && !metadata.company">
|
||||||
|
<button class="btn btn-default"
|
||||||
|
ng-click="updateUser({'company': '', 'name': ''})">
|
||||||
|
No thanks</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
Binary file not shown.
Reference in a new issue