From 8a510c6102ae6ee0fb1ff91e988aac3cf61217b9 Mon Sep 17 00:00:00 2001 From: kvgarg <co16326.ccet@gmail.com> Date: Sat, 15 Jun 2019 00:43:04 +0530 Subject: [PATCH] gamification/: Redesign the webpage The redesigned webpages provides a enhanced UI/UX design to web-page with additional functionality of searching the contributors. Closes https://github.com/coala/community/issues/260 --- community/urls.py | 4 +- gamification/tests/test_views.py | 2 +- gamification/views.py | 81 ++++++++++- static/css/gamification.css | 165 +++++++++++++++++++++++ static/js/contributors.js | 8 +- static/js/gamification.js | 150 +++++++++++++++++++++ templates/gamification.html | 225 ++++++++++++++++++++++++------- 7 files changed, 579 insertions(+), 56 deletions(-) create mode 100644 static/css/gamification.css create mode 100644 static/js/gamification.js diff --git a/community/urls.py b/community/urls.py index a171c0e8..42384cb3 100644 --- a/community/urls.py +++ b/community/urls.py @@ -11,7 +11,7 @@ from gci.feeds import LatestTasksFeed as gci_tasks_rss from ci_build.view_log import BuildLogsView from data.views import ContributorsListView -from gamification.views import index as gamification_index +from gamification.views import GamificationResults from meta_review.views import ContributorsMetaReview from inactive_issues.inactive_issues_scraper import inactive_issues_json from openhub.views import index as openhub_index @@ -193,7 +193,7 @@ def get_organization(): distill_file='static/unassigned-issues.json', ), distill_url( - r'gamification/$', gamification_index, + r'gamification/$', GamificationResults.as_view(), name='community-gamification', distill_func=get_index, distill_file='gamification/index.html', diff --git a/gamification/tests/test_views.py b/gamification/tests/test_views.py index 64e270c7..fa7a1a22 100644 --- a/gamification/tests/test_views.py +++ b/gamification/tests/test_views.py @@ -25,4 +25,4 @@ def test_view_uses_correct_template(self): def test_all_contributors_on_template(self): resp = self.client.get(reverse('community-gamification')) self.assertEqual(resp.status_code, 200) - self.assertTrue(len(resp.context['participants']) == 10) + self.assertTrue(len(resp.context['gamification_results']) == 10) diff --git a/gamification/views.py b/gamification/views.py index 25b14243..0772782c 100644 --- a/gamification/views.py +++ b/gamification/views.py @@ -1,10 +1,79 @@ -from django.shortcuts import render +import json -from gamification.models import Participant +from django.views.generic import TemplateView +from community.views import get_header_and_footer +from gamification.models import Participant, Level, Badge -def index(request): - Participant.objects.filter(username__startswith='testuser').delete() + +class GamificationResults(TemplateView): + template_name = 'gamification.html' participants = Participant.objects.all() - args = {'participants': participants} - return render(request, 'gamification.html', args) + + def get_users_username(self, users): + """ + :param users: A Queryset, with a field username + :return: A list of usernames + """ + usernames = list() + for user in users: + usernames.append(user.username) + return usernames + + def group_participants_by_score(self): + """ + Divide the participants according to their scores. For example, if + there are 10 contributors who have different scores and there are + possibly 4 ranges i.e. score_gt 80, score_between (70,80), + score_between (60,70) and score_lt 60. So, divide them and put them + in their respective lists. + :return: A Dict, with key as score_range and value a list of + contributors username + """ + scores = set() + for contrib in self.participants: + scores.add(contrib.score) + + scores = list(scores) + scores.sort() + + try: + min_score, max_score = scores[0], scores[-1] + except IndexError: + return dict() + + difference_bw_groups_score = int(max_score/5) + score_ranges = [ + min_score + i * difference_bw_groups_score for i in range(6) + ] + score_ranges[-1] += max_score % 5 + + grouped_participants = dict() + for index, score in enumerate(score_ranges[1:]): + begin_score, end_score = score_ranges[index], score + + filtered_participants = self.participants.filter( + score__range=[begin_score, end_score] + ) + + if begin_score == min_score: + grp_lvl = f'<{end_score}' + elif end_score < max_score: + grp_lvl = f'>={begin_score} and <{end_score}' + else: + grp_lvl = f'>={begin_score}' + + grouped_participants[grp_lvl] = json.dumps( + self.get_users_username(filtered_participants) + ) + return grouped_participants + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context = get_header_and_footer(context) + context['gamification_results'] = self.participants + context['levels'] = Level.objects.all() + context['badges'] = Badge.objects.all() + context['grouped_participants'] = self.group_participants_by_score() + + return context diff --git a/static/css/gamification.css b/static/css/gamification.css new file mode 100644 index 00000000..cf354e03 --- /dev/null +++ b/static/css/gamification.css @@ -0,0 +1,165 @@ +.badge-filter { + width: 150px; +} + +.bottom-center { + position: absolute; + width: 100px; + bottom: 2%; + left: 50%; + transform: translate(-50%, -50%); +} + +.clear-filters { + cursor: pointer; +} + +.gamifier-details { + max-width: 79%; + display: flex; +} + +.gamifier-details-part-1, +.gamifier-details-part-2, +.gamifier-details-part-3 { + max-width: 44%; + max-height: 80%; + padding: 10px 5px; +} + +.gamifier-card { + width: 100%; + min-height: 380px; +} + +.gamifier-image { + width: 20%; +} + +.gamifier-image img{ + width: 90%; + margin-right: 0; + margin-left: 1%; + min-width: 100px; + +} + +.github-icon { + color: white; + background-color: black; + border-radius: 100px; +} + +.gitlab-icon { + color: #e33834; + border-radius: 100px; +} + +.filter-btn { + width: 250px; + margin-top: 3%; + margin-left: 3%; + z-index: 0; +} + +.filter-btn .btn-large { + border-radius: 100px; +} + +.filter-btn .btn { + text-transform: none; + border-radius: 100px; + box-shadow: 0 0 25px 2px black; +} + +.filters-option { + margin: 3% auto auto; + display: none; + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.filter-select-fields { + width: 50%; + min-width: 350px; + box-shadow: 0 0 15px 2px black; + border-radius: 20px; +} + +.level-filter { + width: 145px; +} + +.no-contribs-found { + display: none; + justify-content: center; +} + +.no-contribs-found h5 { + margin: 0; +} + +.score-filter { + width: 175px; +} + +.social-icons { + font-size: 1.5em; +} + +@media only screen and (max-width: 890px){ + + .gamifier-card { + max-width: 100%; + width: auto; + margin: 10px; + } + + .gamifier-details { + max-width: 100%; + padding: 0 10px; + } + + .gamifier-image { + margin: auto; + width: 35%; + } + + .bottom-center { + bottom: 2%; + } + + @media only screen and (max-width: 526px){ + + .gamifier-details-part-3 { + display: none; + } + + .gamifier-details-part-1, + .gamifier-details-part-2 { + max-width: 50%; + } + + .gamifier-image { + width: 50%; + } + + } +} + +@-webkit-keyframes fade-in { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +@keyframes fade-in { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +.fade-in { + -webkit-animation-name: fade-in; + animation-name: fade-in; +} diff --git a/static/js/contributors.js b/static/js/contributors.js index beaa9d64..16b04828 100644 --- a/static/js/contributors.js +++ b/static/js/contributors.js @@ -35,8 +35,12 @@ $(document).ready(function(){ else { $('.search-results').css('display', 'block'); close_icon.css('display', 'block'); - var search_by_login = $('[login^=' + searched_keyword +']'); - var search_by_name = $('[name^=' + searched_keyword +']'); + var search_by_login = $( + '.contributor-card[login^=' + searched_keyword +']' + ); + var search_by_name = $( + '.contributor-card[name^=' + searched_keyword +']' + ); var results_tbody_tr = $('.search-results-tbody tr'); results_tbody_tr.remove(); if(search_by_login.length + search_by_name.length === 0 ){ diff --git a/static/js/gamification.js b/static/js/gamification.js new file mode 100644 index 00000000..9d096ab2 --- /dev/null +++ b/static/js/gamification.js @@ -0,0 +1,150 @@ +$(document).ready(function () { + + var search_input = $('#search'); + var score_range_selector = $('.score-range-selector'); + var level_selector = $('.level-selector'); + var badge_selector = $('.badge-selector'); + var filter_button = $('.filter-btn .btn-large'); + + function toggleGamificationCards(display){ + $('.gamifier-card').css('display', display); + } + + function toggleFilterButton(disabled){ + if(disabled){ + filter_button.attr('disabled', 'disabled'); + } + else { + filter_button.removeAttr('disabled'); + } + } + + search_input.on('keypress keyup', function () { + var value = search_input.val(); + if(value){ + toggleFilterButton(true); + } + else { + toggleFilterButton(false); + } + }); + + $('.fa-close').on('click', function () { + toggleFilterButton(false); + }); + + function getContributorsBasedOnFilterSelector(id_prefix, filter_option){ + var filtered_users = []; + if(filter_option.val()){ + var spans = Object.values( + $('.contributors-cards #'+id_prefix+'-'+filter_option.val()) + ); + if(spans.length > 0){ + spans.forEach(function (span) { + if(span.attributes !== undefined){ + filtered_users.push(span.attributes.login.value); + } + }); + } + } + return filtered_users; + } + + function getCommonContribs(in_range_users, at_level_users, has_badges_users){ + var all_contribs = []; + if (score_range_selector.val() !== "[]"){ + all_contribs = in_range_users; + } + if(level_selector.val() !== ""){ + if (all_contribs.length === 0){ + all_contribs = at_level_users; + } + else { + all_contribs = all_contribs.filter(function(username){ + return at_level_users.includes(username); + } + ); + } + } + if (badge_selector.val() !== ""){ + if (all_contribs.length === 0){ + all_contribs = has_badges_users; + } + else { + all_contribs = all_contribs.filter(function(username){ + return has_badges_users.includes(username); + } + ); + } + } + return all_contribs; + } + + function filterUsersAndToggleCards(){ + var in_range_users = JSON.parse(score_range_selector.val()); + var at_level_users = getContributorsBasedOnFilterSelector( + 'level', level_selector + ); + var has_badges_users = getContributorsBasedOnFilterSelector( + 'badge', badge_selector + ); + + if(score_range_selector.val() === "[]" && level_selector.val() === "" && + badge_selector.val() === ""){ + toggleGamificationCards('flex'); + } + else { + toggleGamificationCards('none'); + var contributors = getCommonContribs( + in_range_users, at_level_users, has_badges_users + ); + + if(contributors.length > 0){ + $('.no-contribs-found').css('display', 'none'); + contributors.forEach(function (username) { + $('[login='+username+']').css('display', 'flex'); + }); + } + else { + $('.no-contribs-found').css('display', 'flex'); + $('.no-contribs-found .search-message').text( + 'No contributors found for you selected filter(s). Please' + + ' try different filter options!' + ); + } + } + } + + score_range_selector.on('change', function () { + filterUsersAndToggleCards(); + }); + + level_selector.on('change', function () { + filterUsersAndToggleCards(); + }); + + badge_selector.on('change', function () { + filterUsersAndToggleCards(); + }); + + $('.filter-btn').on('click', function () { + var filters_option = $('.filters-option'); + var el_display = filters_option.css('display'); + if(el_display === 'flex'){ + filters_option.css('display', 'none'); + } + else { + filters_option.css('display', 'flex'); + } + }); + + $('.clear-filters').on('click', function () { + score_range_selector.val("[]"); + level_selector.val(""); + badge_selector.val(""); + $('select').formSelect(); + $('.no-contribs-found').css('display', 'none'); + toggleGamificationCards('flex'); + }); + +}); \ No newline at end of file diff --git a/templates/gamification.html b/templates/gamification.html index 48becb65..8ea29026 100644 --- a/templates/gamification.html +++ b/templates/gamification.html @@ -1,50 +1,185 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <!-- Required meta tags --> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> - <!-- Bootstrap CSS --> - <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> - <title>Newcomers Data</title> - </head> - <body> - <h1>The gamification leaderboard</h1> - <p>Note: All the datetime is in UTC</p> - <hr> - <ul> - {% for participant in participants %} - <div class="container"> - <div class="row"> - <div class="col-sm-6 col-md-4"> - <div class="thumbnail"> - <div class="caption"> - <p>Username: {{ participant.username }}</p> - <p><a href="//github.com/{{ participant.username }}"> - GitHub Profile</a></p> - <p><a href="//gitlab.com/{{ participant.username }}"> - GitLab Profile</a></p> - <p><a href="//www.openhub.net/accounts/{{ participant.username }}">OpenHub Profile</a></p> - <p>Score: {{ participant.score }}</p> - <p>Level: {{ participant.level.name }}</p> - <p>Activities Performed: - {% for activity in participant.activities.all %} - <p>{{ forloop.counter }}. {{ activity.name }}, performed_at: - {{ activity.performed_at }} updated_at: {{ activity.updated_at }} - </p> - {% endfor %}{# for activity in participant.activities.all #} - <p>Badges Earned: - {% for badge in participant.badges.all %} - <p>{{ forloop.counter }}.{{ badge.name }}</p> - {% endfor %}{# for badge in participant.badges.all #} - </p> +{% extends 'base.html' %} +{% load staticfiles %} +{% block title %} + Community | Gamification Leaderboard +{% endblock %} + +{% block add_css_files %} + <link rel="stylesheet" href="{% static 'css/contributors.css' %}"> + <link rel="stylesheet" href="{% static 'css/meta-reviewers.css' %}"> + <link rel="stylesheet" href="{% static 'css/gamification.css' %}"> +{% endblock %} + +{% block add_js_files %} + <script src="{% static 'js/contributors.js' %}"></script> + <script src="{% static 'js/gamification.js' %}"></script> +{% endblock %} + +{% block main-content %} + + <div class="web-page-details apply-flex center-content"> + <h3 style="padding-right: 15px">~</h3> + <h3 class="page-name"> + Contributors Gamification Leaderboard + </h3> + <h3 style="padding-left: 15px">~</h3> + </div> + + <div class="apply-flex center-content"> + <p class="container web-page-description"> + The leaderboard is based upon the gamification system which automates the + recognition of activities performed by community contributors. Based on activities + performed, various parameters are calculated. + </p> + </div> + + <div class="contributors-section apply-flex center-content"> + <div class="form-fields"> + <form> + <div class="input-field apply-flex center-content search-field"> + <i class="fa fa-search social-icons"></i> + <input id="search" type="search" autocomplete="off" placeholder="Search by username or name" required> + <i class="fa fa-close social-icons"></i> + </div> + </form> + <div class="search-results"> + <table> + <thead> + <tr> + <th>Search Results</th> + </tr> + </thead> + <tbody class="search-results-tbody large-font"> + <tr> + <td> + No results found! + </td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="filter-btn apply-flex center-content"> + <a id="filters" class="waves-effect waves-light btn-large"> + <b>Filter Participants</b> + </a> + </div> + </div> + <div class="filters-option apply-flex center-content fade-in"> + <div class="filter-select-fields apply-flex evenly-spread-content"> + <label class="score-filter"> + <select class="score-range-selector" name="score-filter"> + <option value="[]" selected>Choose a score range</option> + {% for group, val in grouped_participants.items %} + <option value="{{ val }}">{{ group }}</option> + {% endfor %} + </select> + </label> + <label class="level-filter"> + <select class="level-selector" name="level-filter"> + <option value="" selected>Choose any level</option> + {% for level in levels %} + <option value="{{ level.number }}">{{ level.name }}</option> + {% endfor %} + </select> + </label> + <label class="badge-filter"> + <select class="badge-selector" name="badge-filter"> + <option value="" selected>Choose any badge</option> + {% for badge in badges %} + <option value="{{ badge.number }}">{{ badge.name }}</option> + {% endfor %} + </select> + </label> + <i class="fa fa-eraser clear-filters"> Filters</i> + </div> + </div> + + <div class="contributors-cards column-flex"> + <div class="no-contribs-found custom-green-color-font"> + <h5 class="search-message"></h5> + <i class="fa fa-smile-o fa-2x"></i> + </div> + {% for contributor in gamification_results %} + <div class="contributor-card meta-reviewer gamifier-card apply-flex" login="{{ contributor.username }}"> + <div class="contributor-image meta-reviewer-image gamifier-image"> + <img src="//github.com/{{ contributor.username }}.png/?size=400" + alt="user-image"> + <div class="bottom-center large-font bold-text social-links"> + <a href="//github.com/{{ contributor.username }}" target="_blank"> + <i class="fa fa-github github-icon social-icons" aria-hidden="true"></i> + </a> + <a href="//gitlab.com/{{ contributor.username }}" target="_blank"> + <i class="fa fa-gitlab social-icons gitlab-icon" aria-hidden="true"></i> + </a> + <a href="//www.openhub.net/accounts/{{ contributor.username }}" target="_blank" class="social-icons">OH</a> + </div> + </div> + <div class="gamifier-details"> + <div class="gamifier-details-part-1 column-flex"> + <div class="column-flex large-font gray-font-color"> + <span> + <b>Username:</b> + {{ contributor.username }} + </span> + <span> + <b>Score:</b> + {{ contributor.score }} + </span> + <span id="level-{{ contributor.level.number }}" login="{{ contributor.username }}"> + <b>Level:</b> + {{ contributor.level.name }} + </span> + <div> + {% if contributor.badges.all %} + <b>Badges Earned:</b> + <div class="column-flex"> + {% for badge in contributor.badges.all %} + <span id="badge-{{ badge.number }}" login="{{ contributor.username }}"> + <b>-></b> + {{ badge.name }} + </span> + {% endfor %}{# for badge in contributor.badges.all #} </div> + {% else %}{# if contributor.badges.all #} + <span> + <b>No badges earned!<i class="fa fa-frown-o"></i></b> + </span> + {% endif %}{# if contributor.badges.all #} + </div> + <div class="column-flex"> + <b>Some Activities permormed:</b> + {% for activity in contributor.activities.all|slice:":2" %} + <span> + <b>{{ forloop.counter }}.</b> {{ activity.name }}, on {{ activity.performed_at }} + </span> + {% endfor %}{# for activity in contributor.activities.all|slice:":2" #} </div> </div> </div> + + {% if contributor.activities.all|slice:"2:6" %} + <div class="gamifier-details-part-2 column-flex large-font gray-font-color"> + {% for activity in contributor.activities.all|slice:"2:6" %} + <span> + <b>{{ forloop.counter|add:2 }}.</b> {{ activity.name }}, on {{ activity.performed_at }} + </span> + {% endfor %}{# for activity in contributor.activities.all|slice:"2:6" #} + </div> + {% endif %}{# if contributor.activities.all|slice:"2:6" #} + + {% if contributor.activities.all|slice:"6:10" %} + <div class="gamifier-details-part-3 column-flex large-font gray-font-color"> + {% for activity in contributor.activities.all|slice:"6:10" %} + <span> + <b>{{ forloop.counter|add:6 }}.</b> {{ activity.name }}, on: {{ activity.performed_at }} + </span> + {% endfor %}{# for activity in contributor.activities.all|slice:"6:10" #} + </div> + {% endif %}{# if contributor.activities.all|slice:"6:10" #} </div> - <hr> - {% endfor %}{# for participant in participants #} - </ul> - </body> -</html> + </div> + {% endfor %}{# for contributor in gamification_results #} + </div> + +{% endblock %}