Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Users list #4011

Merged
merged 34 commits into from
Apr 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
20a4793
fetch users
octavioamu Mar 20, 2019
dd1c620
add data
octavioamu Mar 20, 2019
b0d47a9
add keywords
octavioamu Mar 20, 2019
1d637fb
change to rest
octavioamu Apr 2, 2019
90c8b2a
backend
octavioamu Apr 3, 2019
5f43187
fix jsonresponse data and add test
danlipert Apr 3, 2019
2517250
merge dans code
danlipert Apr 3, 2019
6678ec6
fix be results
octavioamu Apr 3, 2019
c5db4b0
move to vue to handle data
octavioamu Apr 3, 2019
1dccf15
add infinite pagination + usersearch
octavioamu Apr 4, 2019
16950a3
comment not active filters
octavioamu Apr 4, 2019
f274021
add verify
octavioamu Apr 5, 2019
ed22557
make verification work
owocki Apr 10, 2019
2211791
add verified badge
octavioamu Apr 10, 2019
0cea1d4
fetch users
octavioamu Mar 20, 2019
21a0d1f
add data
octavioamu Mar 20, 2019
cf58dbb
add keywords
octavioamu Mar 20, 2019
8c4f877
change to rest
octavioamu Apr 2, 2019
bf56fd8
backend
octavioamu Apr 3, 2019
9c8f345
fix jsonresponse data and add test
danlipert Apr 3, 2019
aef15a7
fix be results
octavioamu Apr 3, 2019
23b2abe
move to vue to handle data
octavioamu Apr 3, 2019
082942d
add infinite pagination + usersearch
octavioamu Apr 4, 2019
752ffc8
comment not active filters
octavioamu Apr 4, 2019
a6414a7
add verify
octavioamu Apr 5, 2019
7017636
make verification work
owocki Apr 10, 2019
5ff6d7f
add verified badge
octavioamu Apr 10, 2019
2dc8aca
clean
octavioamu Apr 11, 2019
080a229
clean code
octavioamu Apr 11, 2019
f380e1b
Merge branch 'users-list' of github.com:gitcoinco/web into users-list
octavioamu Apr 11, 2019
7b53c57
Merge branch 'master' into users-list
octavioamu Apr 11, 2019
e2b1a45
review comments
octavioamu Apr 11, 2019
cfec57c
Merge branch 'master' into users-list
octavioamu Apr 11, 2019
7c9bae3
Merge branch 'master' into users-list
danlipert Apr 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,10 @@
re_path(r'^cms/', include(wagtailadmin_urls)),
re_path(r'^documents/', include(wagtaildocs_urls)),
re_path(r'', include(wagtail_urls)),

# users
path('users',dashboard.views.users_directory, name='users_directory'),
url(r'^api/v0.1/users_fetch/', dashboard.views.users_fetch, name='users_fetch'),
]

if settings.ENABLE_SILK:
Expand Down
101 changes: 101 additions & 0 deletions app/assets/v2/js/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
let users = [];
let usersPage = 1;
let usersNumPages = '';
let usersHasNext = false;
let numUsers = '';

Vue.mixin({
methods: {
fetchUsers: function(newPage) {
var vm = this;

if (newPage) {
vm.usersPage = newPage;
}
vm.params.page = vm.usersPage;

if (vm.searchTerm) {
vm.params.search = vm.searchTerm;
} else {
delete vm.params['search'];
}
let searchParams = new URLSearchParams(vm.params);

let apiUrlUsers = `/api/v0.1/users_fetch/?${searchParams.toString()}`;

var getUsers = fetchData (apiUrlUsers, 'GET');

$.when(getUsers).then(function(response) {

response.data.forEach(function(item) {
vm.users.push(item);
});

vm.usersNumPages = response.num_pages;
vm.usersHasNext = response.has_next;
vm.numUsers = response.count;

if (vm.usersHasNext) {
vm.usersPage = ++vm.usersPage;

} else {
vm.usersPage = 1;
}
});
},
searchUsers: function() {
vm = this;
vm.users = [];

vm.fetchUsers(1);

},
bottomVisible: function() {
vm = this;

const scrollY = window.scrollY;
const visible = document.documentElement.clientHeight;
const pageHeight = document.documentElement.scrollHeight - 500;
const bottomOfPage = visible + scrollY >= pageHeight;

if (bottomOfPage || pageHeight < visible) {
if (vm.usersHasNext) {
vm.fetchUsers();
vm.usersHasNext = false;
}
}
}
}

});

if (document.getElementById('gc-users-directory')) {
var app = new Vue({
delimiters: [ '[[', ']]' ],
el: '#gc-users-directory',
data: {
users,
usersPage,
usersNumPages,
usersHasNext,
numUsers,
media_url,
searchTerm: null,
bottom: false,
params: {}
},
mounted() {
this.fetchUsers();
},
beforeMount() {
window.addEventListener('scroll', () => {
this.bottom = this.bottomVisible();
}, false);
},
beforeDestroy() {
window.removeEventListener('scroll', () => {
this.bottom = this.bottomVisible();
});
}
});
}
180 changes: 180 additions & 0 deletions app/dashboard/templates/dashboard/users.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
{% comment %}
Copyright (C) 2019 Gitcoin Core

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

{% endcomment %}
{% load i18n static email_obfuscator add_url_schema avatar_tags %}
<!DOCTYPE html>
<html lang="en">

<head>
{% include 'shared/head.html' %}
{% include 'shared/cards.html' %}

<style>
.grid-4 {
display: grid;
grid-template-columns: repeat(1, 1fr);;
grid-gap: 5rem 3rem;
}

@media (min-width: 768px) {
.grid-4 {
grid-template-columns: repeat(2, 1fr);;
}
}

@media (min-width: 992px) {
.grid-4 {
grid-template-columns: repeat(3, 1fr);;
}
}

@media (min-width: 1200px) {
.grid-4 {
grid-template-columns: repeat(4, 1fr);;
}
}

</style>

</head>

<body class="interior {{active}} g-font-muli bg-lightblue">
{% include 'shared/tag_manager_2.html' %}
<div class="container-fluid header dash">
{% include 'shared/top_nav.html' with class='d-md-flex' %}
{% include 'shared/nav.html' %}
</div>
<div class="" id="gc-users-directory" v-cloak>

<div class="container-fluid">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#usersFilters" aria-controls="usersFilters" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="usersFilters">
<!-- <div class="navbar-nav mr-4">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats up with this fat comment? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is for the next version

<label for="">{% trans 'Sort by:' %}</label>
<select name="" id="sort_order">
<option value="-created_on">{% trans 'Newest' %}</option>
<option value="created_on">{% trans 'Oldest' %}</option>
<option value="-popularity">{% trans 'All Time Popularity' %}</option>
<option value="-popularity_week">{% trans 'Trending' %}</option>
<option value="price_finney">{% trans 'Low price' %}</option>
<option value="-price_finney">{% trans 'High price' %}</option>
<option value="name">{% trans 'Name' %}</option>
<option value="-rarity">{% trans 'Rarity' %}</option>
</select>
</div>
<div class="navbar-nav">
<select class="keywords-filter form-control" multiple="multiple" style="width:300px; height: 37px;"></select>
</div>
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Dropdown
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
</li>
</ul> -->
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" v-model="searchTerm" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" @click.prevent="searchUsers()">Search</button>
</form>
</div>
</nav>
</div>
<div class="container" @scroll.passive="onScroll($event)">
<div class="py-5 mt-5">
<div class="grid-4">
<div v-for="user in users" class="card shadow-sm border-0" :key="user.id">
<img class="rounded-circle shadow mx-auto mt-n5" width="140" height="140" :src="`/dynamic/avatar/${ user.handle }`"/>
<div class="card-body">
<h5 class="text-center">
[[ user.data.name || user.handle ]]
<span v-if="user.verification">
<img src="{% static 'v2/images/badge-verify.svg' %}" alt="">
</span>
</h5>
<a :href="`/profile/${ user.handle }`" class="text-center d-block">
@[[ user.handle ]]
</a>
<div v-if="user.keywords">
<template v-for="keyword in user.keywords">
<span class="badge badge--greenlight">[[ keyword ]]</span>[[ ' ' ]]
</template>
</div>
<div class="">
<a :href="`${ user.data.html_url }?tab=repositories`" target="_blank" rel="noopener noreferrer" >
<i class="fab fa-github"></i>
</a>
<a :href="`${ user.data.blog }`" target="_blank" rel="noopener noreferrer" v-if="user.data.blog">
<i class="fas fa-home"></i>
</a>
<a :href="`mailto:${ user.data.email }`" v-if="user.data.email">
<i class="far fa-envelope"></i>
</a>
</div>
<div class="my-2 font-smaller-3" id="job_status">
<a :href="`${media_url}${user.resume}`" download data-toggle="tooltip" title="Download resume" v-if="user.show_job_status">
<i class="fa fa-briefcase mr-2" aria-hidden="true"></i>
</a>
</div>
<small>
Joined: <time :datetime="[[ user.created_on ]]" :title="[[ user.created_on ]]">[[ user.created_on | moment ]]</time>
</small>
</div>
<!-- [[user]] -->
</div>
</div>
</div>
</div>
</div>



{% include 'shared/analytics.html' %}
{% include 'shared/footer_scripts.html' %}
{% include 'shared/footer.html' %}
{% include 'shared/messages.html' %}
<script src="{% static "v2/js/popper.min.js" %}"></script>
<script src="{% static "v2/js/bootstrap.min.js" %}"></script>
<script>
let bootstrapTooltip = $.fn.tooltip.noConflict()
$.fn.runTooltip = bootstrapTooltip;
$('[data-toggle="tooltip"]').runTooltip();

</script>
<script src="{% static "v2/js/users.js" %}"></script>

</body>
</html>
42 changes: 42 additions & 0 deletions app/dashboard/tests/test_users_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""Handle dashboard embed related tests.
Copyright (C) 2018 Gitcoin Core
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

import json

from django.contrib.auth.models import User
from django.test.client import RequestFactory

from dashboard.models import Profile
from dashboard.views import users_fetch
from test_plus.test import TestCase


class UsersListTest(TestCase):
"""Define tests for the user list."""

def setUp(self):
self.request = RequestFactory()
for i in range(20):
user = User.objects.create(password="{}".format(i),
username="user{}".format(i))
profile = Profile.objects.create(user=user, data={}, handle="{}".format(i))

def test_user_list(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! :D

assert json.loads(users_fetch(self.request.get('/api/v0.1/users_fetch/')).content)['count'] == 20
41 changes: 41 additions & 0 deletions app/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,47 @@ def onboard(request, flow):
return TemplateResponse(request, 'ftux/onboard.html', params)


def users_directory(request):
"""Handle displaying users directory page."""

params = {
'active': 'users',
'title': 'Users',
'meta_title': "",
'meta_description': ""
}
return TemplateResponse(request, 'dashboard/users.html', params)


@require_GET
def users_fetch(request):
"""Handle displaying users."""
q = request.GET.get('search', '')
limit = int(request.GET.get('limit', 10))
page = int(request.GET.get('page', 1))
order_by = request.GET.get('order_by', '-created_on')
context = {}
user_list = Profile.objects.all().order_by(order_by).filter(handle__icontains=q).cache()
params = dict()
all_pages = Paginator(user_list, limit)
all_users = []
for user in all_pages.page(page):
profile_json = {}
profile_json = user.to_standard_dict()
if user.avatar_baseavatar_related.exists():
user_avatar = user.avatar_baseavatar_related.first()
profile_json['avatar_id'] = user_avatar.pk
profile_json['avatar_url'] = user_avatar.avatar_url
profile_json['verification'] = user.get_my_verified_check
all_users.append(profile_json)
# dumping and loading the json here quickly passes serialization issues - definitely can be a better solution
params['data'] = json.loads(json.dumps(all_users, default=str))
params['has_next'] = all_pages.page(page).has_next()
params['count'] = all_pages.count
params['num_pages'] = all_pages.num_pages
return JsonResponse(params, status=200, safe=False)


def dashboard(request):
"""Handle displaying the dashboard."""

Expand Down