diff --git a/endpoints/api.py b/endpoints/api.py index ddc9bfa5e..4e6c2f53d 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -6,6 +6,7 @@ from functools import wraps from data import model from app import app +from util.email import send_confirmation_email from util.names import parse_repository_name from util.gravatar import compute_hash from auth.permissions import (ReadRepositoryPermission, @@ -36,7 +37,7 @@ def welcome(): return make_response('welcome', 200) -@app.route('/api/user/') +@app.route('/api/user/', methods=['GET']) def get_logged_in_user(): if current_user.is_anonymous(): return jsonify({'anonymous': True}) @@ -51,6 +52,23 @@ def get_logged_in_user(): }) +@app.route('/api/user/', methods=['POST']) +def create_user_api(): + user_data = request.get_json() + try: + new_user = model.create_user(user_data['username'], user_data['password'], + user_data['email']) + code = model.create_confirm_email_code(new_user) + send_confirmation_email(new_user.username, new_user.email, code.code) + return make_response('Created', 201) + except model.DataModelException as ex: + error_resp = jsonify({ + 'message': ex.message, + }) + error_resp.status_code = 400 + return error_resp + + @app.route('/api/users/', methods=['GET']) @api_login_required def get_matching_users(prefix): diff --git a/static/css/quay.css b/static/css/quay.css index 4d7943e8b..fb473d933 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -36,6 +36,18 @@ margin-left: 0px; } +.form-signup input.ng-invalid.ng-dirty { + background-color: #FDD7D9; +} + +.form-signup input.ng-valid.ng-dirty { + background-color: #DDFFEE; +} + +.landing .popover-content { + color: black; +} + .landing .message { font-size: 3.4em; margin-bottom: 10px; diff --git a/static/js/app.js b/static/js/app.js index 9364c133e..aa9b8f0e3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -7,14 +7,14 @@ $.fn.spin = function(opts) { if (spinner) spinner.stop(); if (opts !== false) { options = { - color: $this.css('color') || '#000', - lines: 12, // The number of lines to draw - length: 7, // The length of each line - width: 4, // The line thickness - radius: 10, // The radius of the inner circle - speed: 1, // Rounds per second - trail: 100, // Afterglow percentage - shadow: false // Whether to render a shadow + color: $this.css('color') || '#000', + lines: 12, // The number of lines to draw + length: 7, // The length of each line + width: 4, // The line thickness + radius: 10, // The radius of the inner circle + speed: 1, // Rounds per second + trail: 100, // Afterglow percentage + shadow: false // Whether to render a shadow }; opts = $.extend(options, opts); @@ -54,6 +54,18 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment'], function($pro return userService; }]) }). + directive('match', function($parse) { + return { + require: 'ngModel', + link: function(scope, elem, attrs, ctrl) { + scope.$watch(function() { + return $parse(attrs.match)(scope) === ctrl.$modelValue; + }, function(currentValue) { + ctrl.$setValidity('mismatch', currentValue); + }); + } + }; + }). config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { $routeProvider. when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}). diff --git a/static/js/controllers.js b/static/js/controllers.js index 2ccf35b5f..6079e5768 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2,17 +2,17 @@ function getFirstTextLine(commentString) { if (!commentString) { return; } var lines = commentString.split('\n'); - var MARKDOWN_CHARS = { - '#': true, - '-': true, - '>': true, - '`': true - }; + var MARKDOWN_CHARS = { + '#': true, + '-': true, + '>': true, + '`': true + }; for (var i = 0; i < lines.length; ++i) { // Skip code lines. if (lines[i].indexOf(' ') == 0) { - continue; + continue; } // Skip empty lines. @@ -22,7 +22,7 @@ function getFirstTextLine(commentString) { // Skip control lines. if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) { - continue; + continue; } return getMarkedDown(lines[i]); @@ -43,30 +43,30 @@ function HeaderCtrl($scope, UserService) { $('#repoSearch').typeahead({ name: 'repositories', remote: { - url: '/api/repository/find/%QUERY', - filter: function(data) { - var datums = []; - for (var i = 0; i < data.repositories.length; ++i) { - var repo = data.repositories[i]; - datums.push({ - 'value': repo.name, - 'tokens': [repo.name, repo.namespace], - 'repo': repo - }); - } - return datums; - } + url: '/api/repository/find/%QUERY', + filter: function(data) { + var datums = []; + for (var i = 0; i < data.repositories.length; ++i) { + var repo = data.repositories[i]; + datums.push({ + 'value': repo.name, + 'tokens': [repo.name, repo.namespace], + 'repo': repo + }); + } + return datums; + } }, template: function (datum) { - template = '
'; - template += '' - template += '' + datum.repo.namespace +'/' + datum.repo.name + '' - if (datum.repo.description) { - template += '' + getFirstTextLine(datum.repo.description) + '' - } + template = '
'; + template += '' + template += '' + datum.repo.namespace +'/' + datum.repo.name + '' + if (datum.repo.description) { + template += '' + getFirstTextLine(datum.repo.description) + '' + } - template += '
' - return template; + template += '
' + return template; }, }); @@ -98,8 +98,26 @@ function RepoListCtrl($scope, Restangular) { }); } -function LandingCtrl($scope) { +function LandingCtrl($scope, $timeout, Restangular, UserService) { + $('.form-signup').popover(); + $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { + $scope.user = currentUser; + }, true); + + $scope.awaitingConfirmation = false; + $scope.register = function() { + var newUserPost = Restangular.one('user/'); + newUserPost.customPOST($scope.newUser).then(function() { + $scope.awaitingConfirmation = true; + }, function(result) { + console.log("Displaying error message."); + $scope.registerError = result.data.message; + $timeout(function() { + $('.form-signup').popover('show'); + }); + }); + }; } function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { @@ -108,27 +126,27 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $rootScope.title = 'Loading...'; $scope.showTab = function(tabName) { - for (var i = 0; i < tabs.length; ++i) { - $('#' + tabs[i]).hide(); - $('#' + tabs[i] + '-tab').removeClass('active'); - } + for (var i = 0; i < tabs.length; ++i) { + $('#' + tabs[i]).hide(); + $('#' + tabs[i] + '-tab').removeClass('active'); + } - $('#' + tabName).show(); - $('#' + tabName + '-tab').addClass('active'); + $('#' + tabName).show(); + $('#' + tabName + '-tab').addClass('active'); - if (tabName == 'image-history') { - $scope.listImages(); - } + if (tabName == 'image-history') { + $scope.listImages(); + } }; $scope.editDescription = function() { if (!$scope.repo.can_write) { return; } if (!$scope.markdownDescriptionEditor) { - var converter = Markdown.getSanitizingConverter(); - var editor = new Markdown.Editor(converter, '-description'); - editor.run(); - $scope.markdownDescriptionEditor = editor; + var converter = Markdown.getSanitizingConverter(); + var editor = new Markdown.Editor(converter, '-description'); + editor.run(); + $scope.markdownDescriptionEditor = editor; } $('#wmd-input-description')[0].value = $scope.repo.description; @@ -155,12 +173,12 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { }; $scope.listImages = function() { - if ($scope.imageHistory) { return; } + if ($scope.imageHistory) { return; } - var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images'); - imageFetch.get().then(function(resp) { - $scope.imageHistory = resp.images; - }); + var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images'); + imageFetch.get().then(function(resp) { + $scope.imageHistory = resp.images; + }); }; var namespace = $routeParams.namespace; @@ -178,14 +196,14 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { var clip = new ZeroClipboard($('#copyClipboard'), { 'moviePath': 'static/lib/ZeroClipboard.swf' }); clip.on('complete', function() { - // Resets the animation. - var elem = $('#clipboardCopied')[0]; - elem.style.display = 'none'; - - // Show the notification. - setTimeout(function() { - elem.style.display = 'block'; - }, 1); + // Resets the animation. + var elem = $('#clipboardCopied')[0]; + elem.style.display = 'none'; + + // Show the notification. + setTimeout(function() { + elem.style.display = 'block'; + }, 1); }); $scope.loading = false; @@ -201,35 +219,34 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { var name = $routeParams.name; $('#userSearch').typeahead({ - name: 'users', - remote: { - url: '/api/users/%QUERY', - filter: function(data) { - var datums = []; - for (var i = 0; i < data.users.length; ++i) { - var user = data.users[i]; - datums.push({ - 'value': user, - 'tokens': [user], - 'username': user - }); - } - return datums; - } - }, - template: function (datum) { - template = '
'; - template += '' - template += '' + datum.username + '' - template += '
' - return template; - }, - + name: 'users', + remote: { + url: '/api/users/%QUERY', + filter: function(data) { + var datums = []; + for (var i = 0; i < data.users.length; ++i) { + var user = data.users[i]; + datums.push({ + 'value': user, + 'tokens': [user], + 'username': user + }); + } + return datums; + } + }, + template: function (datum) { + template = '
'; + template += '' + template += '' + datum.username + '' + template += '
' + return template; + }, }); $('#userSearch').on('typeahead:selected', function(e, datum) { - $('#userSearch').typeahead('setQuery', ''); - $scope.addNewPermission(datum.username); + $('#userSearch').typeahead('setQuery', ''); + $scope.addNewPermission(datum.username); }); $scope.addNewPermission = function(username) { @@ -239,34 +256,34 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { // Need the $scope.apply for both the permission stuff to change and for // the XHR call to be made. $scope.$apply(function() { - $scope.addRole(username, 'read') + $scope.addRole(username, 'read') }); }; $scope.deleteRole = function(username) { var permissionDelete = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + username); permissionDelete.customDELETE().then(function() { - delete $scope.permissions[username]; + delete $scope.permissions[username]; }, function(result) { - if (result.status == 409) { - $('#onlyadminModal').modal({}); - } else { - $('#cannotchangeModal').modal({}); - } + if (result.status == 409) { + $('#onlyadminModal').modal({}); + } else { + $('#cannotchangeModal').modal({}); + } }); }; $scope.addRole = function(username, role) { var permission = { - 'role': role + 'role': role }; var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + username); permissionPost.customPOST(permission).then(function() { - $scope.permissions[username] = permission; - $scope.permissions = $scope.permissions; + $scope.permissions[username] = permission; + $scope.permissions = $scope.permissions; }, function(result) { - $('#cannotchangeModal').modal({}); + $('#cannotchangeModal').modal({}); }); }; @@ -277,50 +294,50 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { var permissionPut = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + username); permissionPut.customPUT(permission).then(function() {}, function(result) { - if (result.status == 409) { - permission.role = currentRole; - $('#onlyadminModal').modal({}); - } else { - $('#cannotchangeModal').modal({}); - } + if (result.status == 409) { + permission.role = currentRole; + $('#onlyadminModal').modal({}); + } else { + $('#cannotchangeModal').modal({}); + } }); }; $scope.askChangeAccess = function(newAccess) { - $('#make' + newAccess + 'Modal').modal({}); + $('#make' + newAccess + 'Modal').modal({}); }; $scope.changeAccess = function(newAccess) { - $('#make' + newAccess + 'Modal').modal('hide'); + $('#make' + newAccess + 'Modal').modal('hide'); - var visibility = { - 'visibility': newAccess - }; - var visibilityPost = Restangular.one('repository/' + namespace + '/' + name + '/changevisibility'); - visibilityPost.customPOST(visibility).then(function() { - $scope.repo.is_public = newAccess == 'public'; - }, function() { - $('#cannotchangeModal').modal({}); - }); + var visibility = { + 'visibility': newAccess + }; + var visibilityPost = Restangular.one('repository/' + namespace + '/' + name + '/changevisibility'); + visibilityPost.customPOST(visibility).then(function() { + $scope.repo.is_public = newAccess == 'public'; + }, function() { + $('#cannotchangeModal').modal({}); + }); }; $scope.askDelete = function() { - $('#confirmdeleteModal').modal({}); + $('#confirmdeleteModal').modal({}); }; $scope.deleteRepo = function() { - $('#confirmdeleteModal').modal('hide'); + $('#confirmdeleteModal').modal('hide'); - var deleteAction = Restangular.one('repository/' + namespace + '/' + name); - deleteAction.customDELETE().then(function() { - $scope.repo = null; - - setTimeout(function() { - document.location = '/#/repository'; - }, 1000); - }, function() { - $('#cannotchangeModal').modal({}); - }); + var deleteAction = Restangular.one('repository/' + namespace + '/' + name); + deleteAction.customDELETE().then(function() { + $scope.repo = null; + + setTimeout(function() { + document.location = '/#/repository'; + }, 1000); + }, function() { + $('#cannotchangeModal').modal({}); + }); }; $('.spin').spin(); diff --git a/static/partials/landing.html b/static/partials/landing.html index 17d0f6571..5b8db66e1 100644 --- a/static/partials/landing.html +++ b/static/partials/landing.html @@ -8,12 +8,21 @@
- +
+ +
+
Thank you for registering! We have sent you an activation email. You must verify your email address before you can continue.
+
+
+
+
Some message about how awesome it is to be a Quay user goes here.
+