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 %}