Add repo autocomplete for searching.

This commit is contained in:
Joseph Schorr 2013-09-27 19:21:54 -04:00
parent bf926aceee
commit edaad6eea2
6 changed files with 165 additions and 5 deletions

View file

@ -26,6 +26,9 @@ def get_user(username):
return None
def get_matching_users(username_prefix):
return list(User.select().where(User.username ** (username_prefix + '%')).limit(10))
def verify_user(username, password):
try:
fetched = User.get(User.username == username)
@ -58,6 +61,10 @@ def get_token(code):
return AccessToken.get(AccessToken.code == code)
def get_matching_repositories(repo_term):
return list(Repository.select().where(Repository.name ** ('%' + repo_term + '%') | Repository.namespace ** ('%' + repo_term + '%') | Repository.description ** ('%' + repo_term + '%')).limit(10))
def change_password(user, new_password):
pw_hash = bcrypt.hashpw(new_password, bcrypt.gensalt())
user.password_hash = pw_hash

View file

@ -32,19 +32,44 @@ def get_logged_in_user():
})
@app.route('/api/users/<prefix>', methods=['GET'])
def get_matching_users(prefix):
users = model.get_matching_users(prefix)
return jsonify({
'users': [user.username for user in users]
})
@app.route('/api/repository/', methods=['POST'])
@login_required
def create_repo_api():
pass
@app.route('/api/repository/find/<prefix>', methods=['GET'])
@login_required
def match_repos_api(prefix):
def repo_view(repo):
return {
'namespace': repo.namespace,
'name': repo.name,
'description': repo.description,
}
repos = [repo_view(repo) for repo in model.get_matching_repositories(prefix) if
ReadRepositoryPermission(repo.namespace, repo.name).can()]
response = {
'repositories': repos
}
return jsonify(response)
@app.route('/api/repository/', methods=['GET'])
@login_required
def list_repos_api():
def repo_view(repo_perm):
return {
'namespace': repo_perm.repository.namespace,
'name': repo_perm.repository.name,

View file

@ -1,3 +1,28 @@
#repoSearch {
width: 400px;
}
.repo-mini-listing {
margin: 2px;
}
.repo-mini-listing i {
color: #aaa;
margin-right: 4px;
}
.repo-mini-listing .description {
margin-left: 20px;
color: #aaa;
font-size: 85%;
padding-top: 4px;
border-top: 1px solid #eee;
margin-top: 4px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.editable i {
opacity: 0.2;
font-size: 85%;
@ -146,6 +171,7 @@ p.editable:hover i {
text-decoration: none !important;
}
.repo-listing {
display: block;
margin-bottom: 20px;
@ -193,4 +219,64 @@ p.editable:hover i {
.repo .images {
margin: 10px;
}
.twitter-typeahead .tt-query,
.twitter-typeahead .tt-hint {
margin-bottom: 0;
}
.tt-dropdown-menu {
min-width: 160px;
margin-top: 2px;
padding: 5px 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.2);
*border-right-width: 2px;
*border-bottom-width: 2px;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
}
.tt-suggestion {
display: block;
padding: 3px 20px;
}
.tt-suggestion.tt-is-under-cursor {
color: #fff;
background-color: #0081c2;
background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
background-image: -o-linear-gradient(top, #0088cc, #0077b3);
background-image: linear-gradient(to bottom, #0088cc, #0077b3);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)
}
.tt-suggestion.tt-is-under-cursor a {
color: #fff;
}
.tt-suggestion p {
margin: 0;
}
.twitter-typeahead .tt-hint {
display: block;
height: 34px;
padding: 5px 12px;
font-size: 14px;
line-height: 1.428571429;
border: 1px solid transparent;
}

View file

@ -2,6 +2,42 @@ function HeaderCtrl($scope, UserService) {
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
}, true);
$('#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;
}
},
template: function (datum) {
template = '<div class="repo-mini-listing">';
template += '<i class="icon-hdd icon-large"></i>'
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
if (datum.repo.description) {
template += '<span class="description">' + datum.repo.description + '</span>'
}
template += '</div>'
return template;
},
});
$('#repoSearch').on('typeahead:selected', function (e, datum) {
$('#repoSearch').typeahead('setQuery', '');
document.location = '#/repository/' + datum.repo.namespace + '/' + datum.repo.name
});
}
function RepoListCtrl($scope, Restangular) {

7
static/js/typeahead.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@
<script src="static/lib/angular-moment.min.js"></script>
<script src="static/js/ZeroClipboard.min.js"></script>
<script src="static/js/typeahead.min.js"></script>
<script src="static/js/app.js"></script>
<script src="static/js/controllers.js"></script>
@ -51,13 +52,11 @@
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" class="form-control" placeholder="Find Repo">
<input id="repoSearch" type="text" class="form-control" placeholder="Find Repo">
</div>
</form>
<li class="dropdown" ng-switch-when="false">
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{ user.username }} <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/signout">Sign out</a></li>