From 31914da4ca2d6bbace917d7ec6c5925b49f8ab3b Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Oct 2013 22:13:43 -0400 Subject: [PATCH 1/5] - Better error messages for sign up - Show a throbber while working on sign up - Have the front page redirect to the repositories view when logged in --- endpoints/api.py | 17 +++++++++++++++-- static/js/controllers.js | 11 +++++++++++ static/partials/landing.html | 5 ++++- templates/index.html | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index c0876f2a9..368576f8f 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -13,7 +13,7 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission) from endpoints import registry - +import re logger = logging.getLogger(__name__) @@ -55,6 +55,14 @@ def get_logged_in_user(): @app.route('/api/user/', methods=['POST']) def create_user_api(): user_data = request.get_json() + existing_user = model.get_user(user_data['username']) + if existing_user: + error_resp = jsonify({ + 'message': 'The username already exists' + }) + error_resp.status_code = 400 + return error_resp + try: new_user = model.create_user(user_data['username'], user_data['password'], user_data['email']) @@ -62,8 +70,13 @@ def create_user_api(): send_confirmation_email(new_user.username, new_user.email, code.code) return make_response('Created', 201) except model.DataModelException as ex: + message = ex.message + m = re.search('column ([a-zA-Z]+) is not unique', message) + if m and m.group(1): + message = m.group(1) + ' already exists' + error_resp = jsonify({ - 'message': ex.message, + 'message': message, }) error_resp.status_code = 400 return error_resp diff --git a/static/js/controllers.js b/static/js/controllers.js index 23bc13f64..3772662e9 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -100,18 +100,29 @@ function RepoListCtrl($scope, Restangular) { function LandingCtrl($scope, $timeout, Restangular, UserService) { $('.form-signup').popover(); + $('.spin').spin(); $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; + if (currentUser && !currentUser.anonymous) { + document.location = '/#/repository'; + } }, true); $scope.awaitingConfirmation = false; + $scope.registering = false; + $scope.register = function() { + $('.form-signup').popover('hide'); + $scope.registering = true; + var newUserPost = Restangular.one('user/'); newUserPost.customPOST($scope.newUser).then(function() { $scope.awaitingConfirmation = true; + $scope.registering = false; }, function(result) { console.log("Displaying error message."); + $scope.registering = false; $scope.registerError = result.data.message; $timeout(function() { $('.form-signup').popover('show'); diff --git a/static/partials/landing.html b/static/partials/landing.html index fd0fab96b..af41f8917 100644 --- a/static/partials/landing.html +++ b/static/partials/landing.html @@ -9,13 +9,16 @@
- +
+ +
Thank you for registering! We have sent you an activation email. You must verify your email address before you can continue.
diff --git a/templates/index.html b/templates/index.html index 819d3e4f2..4e2ca8e6a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -43,7 +43,7 @@ - Quay + Quay
From f12ed9859cbf6d4231c963c30adfe724a5c3e8d3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Oct 2013 22:28:39 -0400 Subject: [PATCH 2/5] Change it so the front page does appear for signed in users, with a welcome message and a browse button --- static/js/controllers.js | 7 ++++--- static/partials/landing.html | 11 +++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index 3772662e9..b96985308 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -104,14 +104,15 @@ function LandingCtrl($scope, $timeout, Restangular, UserService) { $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; - if (currentUser && !currentUser.anonymous) { - document.location = '/#/repository'; - } }, true); $scope.awaitingConfirmation = false; $scope.registering = false; + $scope.browseRepos = function() { + document.location = '/#/repository'; + }; + $scope.register = function() { $('.form-signup').popover('hide'); $scope.registering = true; diff --git a/static/partials/landing.html b/static/partials/landing.html index af41f8917..e9cf860b3 100644 --- a/static/partials/landing.html +++ b/static/partials/landing.html @@ -3,8 +3,8 @@
Secure hosting for private docker containers
-
Use the docker images your team needs with the safety of private storage
- +
Use the docker images your team needs with the safety of private storage
+
-
Some message about how awesome it is to be a Quay user goes here.
+
+ Welcome {{ user.username }}! +
+
-
+
Secure From 927b280f1ad5b7ebc54f0fb14abd64966704a1da Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 2 Oct 2013 00:28:24 -0400 Subject: [PATCH 3/5] Redo the landing page to: - Show the user's top repos if they have any - Show a link to the guide and the repos list if they do not - Add a getting starting guide - Redo the repos list to show the user's repos and the top 10 public repos separately --- data/model.py | 15 ++++++++-- endpoints/api.py | 19 +++++++++++-- static/css/quay.css | 51 ++++++++++++++++++++++++++++++++++ static/js/app.js | 1 + static/js/controllers.js | 48 ++++++++++++++++++++++++++++---- static/partials/guide.html | 33 ++++++++++++++++++++++ static/partials/landing.html | 40 ++++++++++++++++++++++---- static/partials/repo-list.html | 31 +++++++++++++++++---- templates/index.html | 5 ++-- 9 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 static/partials/guide.html diff --git a/data/model.py b/data/model.py index d48a46a83..931809895 100644 --- a/data/model.py +++ b/data/model.py @@ -96,9 +96,11 @@ def get_token(code): return AccessToken.get(AccessToken.code == code) -def get_visible_repositories(username=None): +def get_visible_repositories(username=None, include_public=True, limit=None, sort=False): query = Repository.select().distinct().join(Visibility) - or_clauses = [(Visibility.name == 'public')] + or_clauses = [] + if include_public: + or_clauses.append((Visibility.name == 'public')); if username: with_perms = query.switch(Repository).join(RepositoryPermission, @@ -106,8 +108,15 @@ def get_visible_repositories(username=None): query = with_perms.join(User) or_clauses.append(User.username == username) - return query.where(reduce(operator.or_, or_clauses)) + if sort: + with_images = query.switch(Repository).join(Image, JOIN_LEFT_OUTER) + query = with_images.order_by(Image.created.desc()) + query = query.where(reduce(operator.or_, or_clauses)) + if limit: + query = query.limit(limit) + + return query def get_matching_repositories(repo_term, username=None): namespace_term = repo_term diff --git a/endpoints/api.py b/endpoints/api.py index 368576f8f..1a623817a 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -125,10 +125,25 @@ def list_repos_api(): 'name': repo_obj.name, 'description': repo_obj.description, } + + limit = request.args.get('limit', None) + include_public = request.args.get('public', 'true') + include_private = request.args.get('private', 'true') + sort = request.args.get('sort', 'false') - username = current_user.db_user.username if current_user.is_authenticated() else None + try: + limit = int(limit) if limit else None + except: + limit = None + + include_public = include_public == 'true' + include_private = include_private == 'true' + sort = sort == 'true' + + username = current_user.db_user.username if current_user.is_authenticated() and include_private else None repos = [repo_view(repo) - for repo in model.get_visible_repositories(username)] + for repo in model.get_visible_repositories( + username, limit = limit, include_public = include_public, sort = sort)] response = { 'repositories': repos } diff --git a/static/css/quay.css b/static/css/quay.css index 205bdcbb0..6f76d5e42 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -62,6 +62,19 @@ margin-bottom: 20px; } +.landing .welcome-message { + text-align: center; +} + +.landing .welcome-message .sub-message { + margin-top: 16px; +} + +.landing .welcome-message .gravatar { + display: inline-block; + border: 1px solid #94C9F7; +} + .landing .sub-message b { color: #94C9F7; } @@ -136,6 +149,32 @@ background: linear-gradient(to bottom, #141414 0%,transparent 15%,transparent 8 z-index: 1; } +.landing .options { + display: inline-block; + padding: 10px; + margin-top: 20px; +} + +.landing .options .option { +} + +.landing .options .or { + margin: 14px; + text-align: center; +} + +.landing .options .or span { + display: inline-block; + border-radius: 50%; + background: #444; + padding: 6px; + width: 48px; + height: 48px; + line-height: 36px; + text-transform: uppercase; + text-align: center; +} + .landing-footer { padding: 20px; @@ -392,6 +431,9 @@ p.editable:hover i { text-decoration: none !important; } +.repo-list { + margin-bottom: 40px; +} .repo-listing { display: block; @@ -400,6 +442,15 @@ p.editable:hover i { padding: 10px; } +.repo-listing:last-child { + border-bottom: 0px; +} + +.landing .repo-listing { + border-bottom: 0px; + margin-bottom: 0px; +} + .repo-listing a { font-size: 1.5em; } diff --git a/static/js/app.js b/static/js/app.js index aa9b8f0e3..f431054ae 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -72,6 +72,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment'], function($pro when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}). when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}). when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). + when('/guide/', {title: 'Getting Started Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). when('/', {title: 'Quay', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}). otherwise({redirectTo: '/'}); }]). diff --git a/static/js/controllers.js b/static/js/controllers.js index b96985308..43624cbe9 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -77,6 +77,9 @@ function HeaderCtrl($scope, UserService) { }); } +function GuideCtrl($scope, Restangular) { +} + function RepoListCtrl($scope, Restangular) { $scope.getCommentFirstLine = function(commentString) { return getMarkedDown(getFirstTextLine(commentString)); @@ -89,12 +92,22 @@ function RepoListCtrl($scope, Restangular) { $('.spin').spin(); $scope.loading = true; + $scope.public_repositories = null; + $scope.private_repositories = null; - // Load the list of repositories. - var repositoryFetch = Restangular.all('repository/'); - repositoryFetch.getList().then(function(resp) { - $scope.repositories = resp.repositories; - $scope.loading = false; + // Load the list of personal repositories. + var repositoryPrivateFetch = Restangular.all('repository/'); + repositoryPrivateFetch.getList({'public': false, 'sort': true}).then(function(resp) { + $scope.private_repositories = resp.repositories; + $scope.loading = !($scope.public_repositories && $scope.private_repositories); + }); + + // Load the list of public repositories. + var options = {'public': true, 'private': false, 'sort': true, 'limit': 10}; + var repositoryPublicFetch = Restangular.all('repository/'); + repositoryPublicFetch.getList(options).then(function(resp) { + $scope.public_repositories = resp.repositories; + $scope.loading = !($scope.public_repositories && $scope.private_repositories); }); } @@ -103,12 +116,20 @@ function LandingCtrl($scope, $timeout, Restangular, UserService) { $('.spin').spin(); $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { + if (!currentUser.anonymous) { + $scope.loadMyRepos(); + } + $scope.user = currentUser; }, true); $scope.awaitingConfirmation = false; $scope.registering = false; + $scope.getCommentFirstLine = function(commentString) { + return getMarkedDown(getFirstTextLine(commentString)); + }; + $scope.browseRepos = function() { document.location = '/#/repository'; }; @@ -130,6 +151,23 @@ function LandingCtrl($scope, $timeout, Restangular, UserService) { }); }); }; + + $scope.loadMyRepos = function() { + $scope.loadingmyrepos = true; + + // Load the list of repositories. + var params = { + 'limit': 5, + 'public': false, + 'sort': true + }; + + var repositoryFetch = Restangular.all('repository/'); + repositoryFetch.getList(params).then(function(resp) { + $scope.myrepos = resp.repositories; + $scope.loadingmyrepos = false; + }); + }; } function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { diff --git a/static/partials/guide.html b/static/partials/guide.html new file mode 100644 index 000000000..74cc3313c --- /dev/null +++ b/static/partials/guide.html @@ -0,0 +1,33 @@ +
+
Warning: Quay requires docker version 0.7 or higher to work
+ +

Getting started guide

+
+ +

Pushing a repository to Quay

+
+ First, tag the image with your repository name:

+
docker tag 0u123imageid quay.io/repo_namespace/repo_name
+
+ Second, push the repository to Quay:

+
docker push quay.io/repo_namespace/repo_name
+
+
+ +

Pulling a repository from Quay

+
+
Note: Private repositories require you to be logged in or the pull will fail. See below for how to sign into Quay if you have never done so before.
+ To pull a repository from Quay, run the following command: +

+
docker pull quay.io/path/to/repository
+
+
+ +

Signing into to Quay Optional

+
+ If you have never pushed a repository to Quay and wish to pull a private repository, you can sign into Quay by running the following command: +

+
docker login quay.io
+
+
+
diff --git a/static/partials/landing.html b/static/partials/landing.html index e9cf860b3..120e0d34a 100644 --- a/static/partials/landing.html +++ b/static/partials/landing.html @@ -1,10 +1,36 @@
-
+
Secure hosting for private docker containers
-
Use the docker images your team needs with the safety of private storage
- +
Use the docker images your team needs with the safety of private storage
+ +
+ +
+
+
+
+
+

Your Top Repositories

+ +
+
+
+ You don't have any private repositories yet! + + + +
+
-
- Welcome {{ user.username }}! +
+ +
Welcome {{ user.username }}!
- +
@@ -74,6 +101,7 @@

Support

diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index a57acccd3..e261a4101 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -3,10 +3,31 @@
-

Repositories

-
- - {{repository.namespace}}/{{repository.name}} -
+
+

Your Repositories

+ + +
+
+

You don't have any repositories yet!

+ Click here to learn how to create a repository +
+ +
+
+ +
+

Top Public Repositories

+
diff --git a/templates/index.html b/templates/index.html index 4e2ca8e6a..d720132a1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -43,13 +43,14 @@ - Quay + Quay