diff --git a/.ci/build.sh b/.ci/build.sh
index 1f01164d..56c2be06 100755
--- a/.ci/build.sh
+++ b/.ci/build.sh
@@ -27,6 +27,7 @@ python manage.py fetch_deployed_data _site $ISSUES_JSON \
python manage.py migrate
python manage.py import_contributors_data
+python manage.py create_org_cluster_map_and_activity_graph org_map
python manage.py import_issues_data
python manage.py import_merge_requests_data
python manage.py create_config_data
diff --git a/.coafile b/.coafile
index edfbf0ec..3b4c1c5b 100644
--- a/.coafile
+++ b/.coafile
@@ -1,6 +1,6 @@
[all]
files = **.py, **.js, **.sh
-ignore = .git/**, **/__pycache__/**, gci/client.py, */migrations/**, private/*
+ignore = .git/**, **/__pycache__/**, gci/client.py, */migrations/**, private/*, openhub/**, **/leaflet_dist/**
max_line_length = 80
use_spaces = True
preferred_quotation = '
@@ -42,6 +42,7 @@ files = static/**/*.js
bears = JSHintBear
allow_unused_variables = True
javascript_strictness = False
+environment_jquery = True
[all.yml]
bears = YAMLLintBear
diff --git a/.gitignore b/.gitignore
index 6ca4d51a..2592fe2b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,7 @@ coverage.xml
*.log
local_settings.py
db.sqlite3
+db.sqlite3-journal
# Flask stuff:
instance/
@@ -272,6 +273,7 @@ flycheck_*.el
# Session
Session.vim
+Sessionx.vim
# Temporary
.netrwhist
@@ -429,11 +431,7 @@ DerivedData/
*.perspectivev3
!default.perspectivev3
-## Xcode Patch
-*.xcodeproj/*
-!*.xcodeproj/project.pbxproj
-!*.xcodeproj/xcshareddata/
-!*.xcworkspace/contents.xcworkspacedata
+## Gcc Patch
/*.gcno
# Eclipse rules
diff --git a/.moban.yaml b/.moban.yaml
index 574c2d69..f9db2b3c 100644
--- a/.moban.yaml
+++ b/.moban.yaml
@@ -9,13 +9,13 @@ packages:
- gci
- gsoc
- gamification
- - log
+ - ci_build
- meta_review
- model
- - twitter
- unassigned_issues
dependencies:
+ - getorg~=0.3.1
- git+https://gitlab.com/coala/coala-utils.git
- git-url-parse
- django>2.1,<2.2
diff --git a/.nocover.yaml b/.nocover.yaml
index 987773ce..4757eb64 100644
--- a/.nocover.yaml
+++ b/.nocover.yaml
@@ -8,11 +8,10 @@ nocover_file_globs:
- community/git.py
- gci/*.py
- gsoc/*.py
- - log/*.py
+ - ci_build/*.py
- meta_review/handler.py
- model/*.py
- openhub/*.py
- - twitter/*.py
# Optional coverage. Once off scripts.
- inactive_issues/inactive_issues_scraper.py
- unassigned_issues/unassigned_issues_scraper.py
diff --git a/.travis.yml b/.travis.yml
index 61015858..255cb955 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,5 @@
language: python
-python: 3.6
+python: 3.6.3
cache:
pip: true
diff --git a/activity/scraper.py b/activity/scraper.py
index 069bc668..9d8fc794 100644
--- a/activity/scraper.py
+++ b/activity/scraper.py
@@ -136,7 +136,7 @@ def get_data(self):
return self.data
-def activity_json(request):
+def activity_json(filename):
org_name = get_org_name()
@@ -152,4 +152,5 @@ def activity_json(request):
real_data = Scraper(parsed_json['issues'], datetime.datetime.today())
real_data = real_data.get_data()
- return HttpResponse(json.dumps(real_data))
+ with open(filename, 'w+') as f:
+ json.dump(real_data, f, indent=4)
diff --git a/ci_build/view_log.py b/ci_build/view_log.py
new file mode 100644
index 00000000..8c476ae3
--- /dev/null
+++ b/ci_build/view_log.py
@@ -0,0 +1,131 @@
+import re
+import json
+import os
+import sys
+
+from django.views.generic import TemplateView
+
+from community.views import get_header_and_footer
+from community.git import (
+ get_org_name,
+ get_owner,
+ get_deploy_url,
+ get_upstream_deploy_url
+)
+
+
+class BuildLogsView(TemplateView):
+ template_name = 'build_logs.html'
+
+ def copy_build_logs_json(self, ci_build_jsons):
+ """
+ :param ci_build_jsons: A dict of directories path
+ :return: A boolean, whether the build file is copied
+ """
+ if os.path.isfile(ci_build_jsons['public_path']):
+ if sys.platform == 'linux':
+ os.popen('cp {} {}'.format(
+ ci_build_jsons['site_path'],
+ ci_build_jsons['public_path']))
+ os.popen('cp {} {}'.format(
+ ci_build_jsons['site_path'],
+ ci_build_jsons['static_path']))
+ else:
+ os.popen('copy {} {}'.format(
+ ci_build_jsons['site_path'],
+ ci_build_jsons['public_path']))
+ os.popen('copy {} {}'.format(
+ ci_build_jsons['site_path'],
+ ci_build_jsons['static_path']))
+ return True
+ return False
+
+ def create_and_copy_build_logs_json(self, logs, level_specific_logs):
+ """
+ Create a build logs detailed json file in ./_site directory and copy
+ that file in the ./static and ./public/static directories
+ :param logs: A list of all lines in build log file
+ :param level_specific_logs: A dict containing logs divided in their
+ respective categories
+ :return: A boolean, whether the files were copied or not
+ """
+ ci_build_jsons = {
+ 'site_path': './_site/ci-build-detailed-logs.json',
+ 'public_path': './public/static/ci-build-detailed-logs.json',
+ 'static_path': './static/ci-build-detailed-logs.json'
+ }
+ with open(ci_build_jsons['site_path'], 'w+') as build_logs_file:
+ data = {
+ 'logs': logs,
+ 'logs_level_Specific': level_specific_logs
+ }
+ json.dump(data, build_logs_file, indent=4)
+ return self.copy_build_logs_json(ci_build_jsons)
+
+ def get_build_logs(self, log_file_path):
+ """
+ :param log_file_path: build logs file path
+ :return: a tuple of two where the first element in tuple refers to
+ a list of build logs in the file, and the second element is a dict
+ which categorizes the build logs into 5 categories - INFO, DEBUG,
+ WARNING, ERROR nad CRITICAL
+ """
+ log_lines = []
+ log_level_specific_lines = {
+ 'INFO': [],
+ 'DEBUG': [],
+ 'WARNING': [],
+ 'ERROR': [],
+ 'CRITICAL': []
+ }
+ with open(log_file_path) as log_file:
+ previous_found_level = None
+ for line in log_file:
+ log_lines.append(line)
+ levels = re.findall(r'\[[A-Z]+]', line)
+ if levels:
+ level = levels[0]
+ level = previous_found_level = level[1:-1]
+ log_level_specific_lines[level].append(line)
+ elif previous_found_level:
+ log_level_specific_lines[previous_found_level].append(
+ line)
+ return log_lines, log_level_specific_lines
+
+ def check_build_logs_stored(self):
+ """
+ Check whether the build logs json file is copied to _site and public
+ directories or not
+ :return: A Boolean
+ """
+ log_file_path = './_site/community.log'
+ log_file_exists = os.path.isfile(log_file_path)
+ if log_file_exists:
+ logs, level_specific_logs = self.get_build_logs(log_file_path)
+ return self.create_and_copy_build_logs_json(logs,
+ level_specific_logs)
+ return False
+
+ def get_build_info(self):
+ """
+ Get the information about build, like who deployed the website i.e.
+ owner, name of the organization or user etc.
+ :return: A dict having information about build related details
+ """
+ data = {
+ 'Org name': get_org_name(),
+ 'Owner': get_owner(),
+ 'Deploy URL': get_deploy_url(),
+ }
+ try:
+ data['Upstream deploy URL'] = get_upstream_deploy_url()
+ except RuntimeError:
+ data['Upstream deploy URL'] = 'Not found'
+ return data
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context = get_header_and_footer(context)
+ context['build_info'] = self.get_build_info()
+ context['logs_stored'] = self.check_build_logs_stored()
+ return context
diff --git a/community/git.py b/community/git.py
index fabe06d8..7d5541fc 100644
--- a/community/git.py
+++ b/community/git.py
@@ -49,7 +49,7 @@ def get_config_remote(name='origin'):
raise KeyError('No git remotes found')
-def get_remote_url():
+def get_remote_url(name='origin'):
"""Obtain a parsed remote URL.
Uses CI environment variables or git remotes.
@@ -58,7 +58,7 @@ def get_remote_url():
# It only sets the REPOSITORY_URL
url = os.environ.get('REPOSITORY_URL')
if not url:
- remote = get_config_remote()
+ remote = get_config_remote(name)
assert remote[0][0] == 'url'
url = remote[0][1]
@@ -146,7 +146,7 @@ def get_upstream_repo():
"""Obtain the parent slug of the repository.
"""
try:
- remote = get_config_remote(name='upstream')
+ remote = get_remote_url(name='origin')
except KeyError:
remote = None
diff --git a/community/urls.py b/community/urls.py
index ed936b9d..42384cb3 100644
--- a/community/urls.py
+++ b/community/urls.py
@@ -5,17 +5,14 @@
from django_distill import distill_url
from django.conf.urls.static import static
from django.conf import settings
-from django.views.generic import TemplateView
-from community.views import HomePageView, info
-from gci.views import index as gci_index
+from community.views import HomePageView
+from gci.views import GCIStudentsList
from gci.feeds import LatestTasksFeed as gci_tasks_rss
-from activity.scraper import activity_json
-from twitter.view_twitter import index as twitter_index
-from log.view_log import index as log_index
-from data.views import index as contributors_index
-from gamification.views import index as gamification_index
-from meta_review.views import index as meta_review_index
+from ci_build.view_log import BuildLogsView
+from data.views import ContributorsListView
+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
from model.views import index as model_index
@@ -81,24 +78,6 @@ def get_organization():
distill_func=get_index,
distill_file='index.html',
),
- distill_url(
- 'info.txt', info,
- name='index',
- distill_func=get_index,
- distill_file='info.txt',
- ),
- distill_url(
- r'static/activity-data.json', activity_json,
- name='activity_json',
- distill_func=get_index,
- distill_file='static/activity-data.json',
- ),
- distill_url(
- r'activity/', TemplateView.as_view(template_name='activity.html'),
- name='activity',
- distill_func=get_index,
- distill_file='activity/index.html',
- ),
distill_url(
r'gci/tasks/rss.xml', gci_tasks_rss(),
name='gci-tasks-rss',
@@ -106,31 +85,25 @@ def get_organization():
distill_file='gci/tasks/rss.xml',
),
distill_url(
- r'gci/', gci_index,
+ r'gci/', GCIStudentsList.as_view(),
name='community-gci',
distill_func=get_index,
distill_file='gci/index.html',
),
distill_url(
- r'twitter/', twitter_index,
- name='twitter',
- distill_func=get_index,
- distill_file='twitter/index.html',
- ),
- distill_url(
- r'log/', log_index,
- name='log',
+ r'ci/build/', BuildLogsView.as_view(),
+ name='ci_build',
distill_func=get_index,
- distill_file='log/index.html',
+ distill_file='ci/build/index.html',
),
distill_url(
- r'contributors/$', contributors_index,
+ r'contributors/$', ContributorsListView.as_view(),
name='community-data',
distill_func=get_index,
distill_file='contributors/index.html',
),
distill_url(
- r'meta-review/$', meta_review_index,
+ r'meta-review/$', ContributorsMetaReview.as_view(),
name='meta_review_data',
distill_func=get_index,
distill_file='meta-review/index.html',
@@ -220,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/community/views.py b/community/views.py
index 595c02ed..38c83d23 100644
--- a/community/views.py
+++ b/community/views.py
@@ -1,43 +1,112 @@
-from django.http import HttpResponse
-from django.views.generic.base import TemplateView
+import logging
+
+import requests
from trav import Travis
+from django.views.generic.base import TemplateView
+
from .git import (
- get_deploy_url,
get_org_name,
- get_owner,
- get_upstream_deploy_url,
+ get_remote_url
)
+from data.models import Team
+from gamification.models import Participant as GamificationParticipant
+from meta_review.models import Participant as MetaReviewer
+
+
+def initialize_org_context_details():
+ org_name = get_org_name()
+ org_details = {
+ 'name': org_name,
+ 'blog_url': f'https://blog.{org_name}.io/',
+ 'twitter_url': f'https://twitter.com/{org_name}_io/',
+ 'facebook_url': f'https://www.facebook.com/{org_name}Analyzer',
+ 'repo_url': get_remote_url().href,
+ 'docs': f'https://{org_name}.io/docs',
+ 'newcomer_docs': f'https://{org_name}.io/newcomer',
+ 'coc': f'https://{org_name}.io/coc',
+ 'logo_url': (f'https://api.{org_name}.io/en/latest/_static/images/'
+ f'{org_name}_logo.svg'),
+ 'gitter_chat': f'https://gitter.im/{org_name}/{org_name}/',
+ 'github_core_repo': f'https://github.com/{org_name}/{org_name}/',
+ 'licence_type': 'GNU AGPL v3.0'
+ }
+ return org_details
+
+
+def get_header_and_footer(context):
+ context['isTravis'] = Travis.TRAVIS
+ context['travisLink'] = Travis.TRAVIS_BUILD_WEB_URL
+ context['org'] = initialize_org_context_details()
+ print('Running on Travis: {}, build link: {}'.format(context['isTravis'],
+ context['travisLink']
+ ))
+ return context
class HomePageView(TemplateView):
template_name = 'index.html'
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context['isTravis'] = Travis.TRAVIS
- context['travisLink'] = Travis.TRAVIS_BUILD_WEB_URL
+ def get_team_details(self, org_name):
+ teams = [
+ f'{org_name} newcomers',
+ f'{org_name} developers',
+ f'{org_name} admins'
+ ]
+ team_details = {}
+ for team_name in teams:
+ team = Team.objects.get(name=team_name)
+ contributors_count = team.contributors.count()
+ team_details[
+ team_name.replace(org_name, '').strip().capitalize()
+ ] = contributors_count
+ return team_details
- print('Running on Travis: {}, build link: {}'.format(
- context['isTravis'],
- context['travisLink']))
+ def get_quote_of_the_day(self):
- return context
+ try:
+ qod = requests.get('http://quotes.rest/qod?category=inspire')
+ qod.raise_for_status()
+ except requests.HTTPError as err:
+ error_info = f'HTTPError while fetching Quote of the day! {err}'
+ logging.error(error_info)
+ return
+ qod_data = qod.json()
+ return {
+ 'quote': qod_data['contents']['quotes'][0]['quote'],
+ 'author': qod_data['contents']['quotes'][0]['author'],
+ }
-def info(request):
- data = {
- 'Org name': get_org_name(),
- 'Owner': get_owner(),
- 'Deploy URL': get_deploy_url(),
- }
- try:
- upstream_deploy_url = get_upstream_deploy_url()
- data['Upstream deploy URL'] = upstream_deploy_url
- except RuntimeError:
- data['Upstream deploy URL'] = 'Not found'
-
- s = '\n'.join(name + ': ' + value
- for name, value in data.items())
- return HttpResponse(s)
+ def get_top_meta_review_users(self, count):
+ participants = MetaReviewer.objects.all()[:count]
+ return participants
+
+ def get_top_gamification_users(self, count):
+ return enumerate(GamificationParticipant.objects.all()[:count])
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context = get_header_and_footer(context)
+ org_name = context['org']['name']
+ context['org']['team_details'] = dict(self.get_team_details(org_name))
+ about_org = (f'{org_name} (always spelled with a lowercase c!) is one'
+ ' of the welcoming open-source organizations for'
+ f' newcomers. {org_name} stands for “COde AnaLysis'
+ ' Application” as it works well with animals and thus is'
+ ' well visualizable which makes it easy to memorize.'
+ f' {org_name} provides a unified interface for linting'
+ ' and fixing the code with a single configuration file,'
+ ' regardless of the programming languages used. You can'
+ f' use {org_name} from within your favorite editor,'
+ ' integrate it with your CI and, get the results as JSON'
+ ', or customize it to your needs with its flexible'
+ ' configuration syntax.')
+ context['org']['about'] = about_org
+ context['quote_details'] = self.get_quote_of_the_day()
+ context['top_meta_review_users'] = self.get_top_meta_review_users(
+ count=5)
+ context['top_gamification_users'] = self.get_top_gamification_users(
+ count=5)
+ return context
diff --git a/data/contrib_data.py b/data/contrib_data.py
index aacbab89..4d696b31 100644
--- a/data/contrib_data.py
+++ b/data/contrib_data.py
@@ -28,11 +28,10 @@ def get_contrib_data():
def import_data(contributor):
logger = logging.getLogger(__name__)
login = contributor.get('login', None)
- teams = contributor.get('teams')
+ teams = contributor.pop('teams')
try:
contributor['issues_opened'] = contributor.pop('issues')
contributor['num_commits'] = contributor.pop('contributions')
- contributor.pop('teams')
c, create = Contributor.objects.get_or_create(
**contributor
)
diff --git a/data/management/commands/create_org_cluster_map_and_activity_graph.py b/data/management/commands/create_org_cluster_map_and_activity_graph.py
new file mode 100644
index 00000000..c71647b1
--- /dev/null
+++ b/data/management/commands/create_org_cluster_map_and_activity_graph.py
@@ -0,0 +1,20 @@
+from django.core.management.base import BaseCommand
+
+from data.org_cluster_map_handler import handle as org_cluster_map_handler
+from activity.scraper import activity_json
+
+
+class Command(BaseCommand):
+ help = 'Create a cluster map using contributors geolocation'
+
+ def add_arguments(self, parser):
+ parser.add_argument('output_dir', nargs='?', type=str)
+
+ def handle(self, *args, **options):
+ output_dir = options.get('output_dir')
+ if not output_dir:
+ org_cluster_map_handler()
+ else:
+ org_cluster_map_handler(output_dir)
+ # Fetch & Store data for activity graph to be displayed on home-page
+ activity_json('static/activity-data.js')
diff --git a/data/migrations/0005_auto_20190801_1442.py b/data/migrations/0005_auto_20190801_1442.py
new file mode 100644
index 00000000..82fba40d
--- /dev/null
+++ b/data/migrations/0005_auto_20190801_1442.py
@@ -0,0 +1,33 @@
+# Generated by Django 2.1.7 on 2019-08-01 14:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('data', '0004_auto_20180809_2229'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='contributor',
+ name='followers',
+ field=models.IntegerField(default=None, null=True),
+ ),
+ migrations.AddField(
+ model_name='contributor',
+ name='location',
+ field=models.TextField(default=None, null=True),
+ ),
+ migrations.AddField(
+ model_name='contributor',
+ name='public_gists',
+ field=models.IntegerField(default=None, null=True),
+ ),
+ migrations.AddField(
+ model_name='contributor',
+ name='public_repos',
+ field=models.IntegerField(default=None, null=True),
+ ),
+ ]
diff --git a/data/migrations/0006_auto_20190801_1752.py b/data/migrations/0006_auto_20190801_1752.py
new file mode 100644
index 00000000..aa2d0ef6
--- /dev/null
+++ b/data/migrations/0006_auto_20190801_1752.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.7 on 2019-08-01 17:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('data', '0005_auto_20190801_1442'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='contributor',
+ name='teams',
+ field=models.ManyToManyField(related_name='contributors', to='data.Team'),
+ ),
+ ]
diff --git a/data/models.py b/data/models.py
index b6ba1a0e..02050fcb 100644
--- a/data/models.py
+++ b/data/models.py
@@ -13,9 +13,13 @@ class Contributor(models.Model):
name = models.TextField(default=None, null=True)
bio = models.TextField(default=None, null=True)
num_commits = models.IntegerField(default=None, null=True)
+ public_repos = models.IntegerField(default=None, null=True)
+ public_gists = models.IntegerField(default=None, null=True)
+ followers = models.IntegerField(default=None, null=True)
reviews = models.IntegerField(default=None, null=True)
issues_opened = models.IntegerField(default=None, null=True)
- teams = models.ManyToManyField(Team)
+ location = models.TextField(default=None, null=True)
+ teams = models.ManyToManyField(Team, related_name='contributors')
def __str__(self):
return self.login
diff --git a/data/org_cluster_map_handler.py b/data/org_cluster_map_handler.py
new file mode 100644
index 00000000..baf0969b
--- /dev/null
+++ b/data/org_cluster_map_handler.py
@@ -0,0 +1,82 @@
+import os
+import json
+
+import logging
+
+import getorg
+
+from data.models import Contributor
+
+
+def handle(output_dir='cluster_map'):
+ """
+ Creates a organization cluster map using the contributors location
+ stored in the database
+ :param output_dir: Directory where all the required CSS and JS files
+ are copied by 'getorg' package
+ """
+ logger = logging.getLogger(__name__)
+ logger.info("'cluster_map/' is the default directory for storing"
+ " organization map related files. If arg 'output_dir'"
+ ' not provided it will be used as a default directory by'
+ " 'getorg' package.")
+
+ # For creating the organization map, the 'getorg' uses a 'Nominatim' named
+ # package which geocodes the contributor location and then uses that class
+ # to create the map. Since, we're not dealing with that function which use
+ # that 'Nominatim' package because we're fetching a JSON data and storing
+ # it in our db. Therefore, defining our own simple class that can aid us
+ # to create a cluster map.
+ class Location:
+
+ def __init__(self, longitude, latitude):
+ self.longitude = longitude
+ self.latitude = latitude
+
+ org_location_dict = {}
+
+ for contrib in Contributor.objects.filter(location__isnull=False):
+ user_location = json.loads(contrib.location)
+ location = Location(user_location['longitude'],
+ user_location['latitude'])
+ org_location_dict[contrib.login] = location
+ logger.debug(f'{contrib.login} location {user_location} added on map')
+ getorg.orgmap.output_html_cluster_map(org_location_dict,
+ folder_name=output_dir)
+
+ move_and_make_changes_in_files(output_dir)
+
+
+def move_and_make_changes_in_files(output_dir):
+ """
+ Move static files from 'output_dir' to django static folder which
+ is being required by the map.html which is being auto-generated
+ by getorg.
+ :param output_dir: Directory from where the files have to be moved
+ """
+
+ move_leaflet_dist_folder(output_dir)
+
+ os.rename(
+ src=get_file_path(os.getcwd(), output_dir, 'org-locations.js'),
+ dst=get_file_path(os.getcwd(), 'static', 'org-locations.js')
+ )
+
+ os.remove(get_file_path(os.getcwd(), output_dir, 'map.html'))
+
+
+def move_leaflet_dist_folder(output_dir):
+ source_path = get_file_path(os.getcwd(), output_dir, 'leaflet_dist')
+ destination_path = get_file_path(os.getcwd(), 'static', 'leaflet_dist')
+
+ # Remove existing leaflet_dir if exists
+ for root, dirs, files in os.walk(destination_path):
+ for file in files:
+ os.remove(os.path.join(destination_path, file))
+ os.rmdir(root)
+
+ os.renames(source_path, destination_path)
+
+
+def get_file_path(*args):
+ return '/'.join(args)
diff --git a/data/tests/test_contrib_data.py b/data/tests/test_contrib_data.py
index e820e413..88a20ac8 100644
--- a/data/tests/test_contrib_data.py
+++ b/data/tests/test_contrib_data.py
@@ -2,7 +2,9 @@
from django.test import TestCase
-from data.contrib_data import get_contrib_data
+from data.contrib_data import get_contrib_data, import_data
+from gamification.tests.test_management_commands import (
+ get_false_contributors_data)
class GetContribDataTest(TestCase):
@@ -10,3 +12,7 @@ class GetContribDataTest(TestCase):
def test_get_contrib_data(self):
with requests_mock.Mocker():
get_contrib_data()
+
+ def test_false_contributor_data(self):
+ for contrib in get_false_contributors_data():
+ import_data(contrib)
diff --git a/data/tests/test_issues.py b/data/tests/test_issues.py
index f94e23e3..b75bc041 100644
--- a/data/tests/test_issues.py
+++ b/data/tests/test_issues.py
@@ -2,7 +2,9 @@
from django.test import TestCase
-from data.issues import fetch_issues
+from data.issues import fetch_issues, import_issue
+from gamification.tests.test_management_commands import (
+ get_false_issues_data)
class FetchIssueTest(TestCase):
@@ -10,3 +12,7 @@ class FetchIssueTest(TestCase):
def test_fetch_issues(self):
with requests_mock.Mocker():
fetch_issues('GitHub')
+
+ def test_false_issue_data(self):
+ for issue in get_false_issues_data():
+ import_issue('github', issue)
diff --git a/data/tests/test_management_commands.py b/data/tests/test_management_commands.py
index f1309700..866616eb 100644
--- a/data/tests/test_management_commands.py
+++ b/data/tests/test_management_commands.py
@@ -32,8 +32,7 @@ def test_command_import_issues_data(self):
if not issues:
raise unittest.SkipTest(
'No record of issues from webservices')
- self.assertIn('testuser',
- [issue.author.login for issue in issues])
+ self.assertGreater(issues.count(), 0)
class ImportMergeRequestDataTest(TestCase):
@@ -47,5 +46,4 @@ def test_command_import_issues_data(self):
if not mrs:
raise unittest.SkipTest(
'No record of mrs from webservices')
- self.assertIn('testuser',
- [mr.author.login for mr in mrs])
+ self.assertGreater(mrs.count(), 0)
diff --git a/data/tests/test_merge_requests.py b/data/tests/test_merge_requests.py
index 3d4350a8..f0efdead 100644
--- a/data/tests/test_merge_requests.py
+++ b/data/tests/test_merge_requests.py
@@ -2,7 +2,8 @@
from django.test import TestCase
-from data.merge_requests import fetch_mrs
+from data.merge_requests import fetch_mrs, import_mr
+from gamification.tests.test_management_commands import (get_false_mrs_data)
class FetchMergeRequestTest(TestCase):
@@ -10,3 +11,7 @@ class FetchMergeRequestTest(TestCase):
def test_fetch_mrs(self):
with requests_mock.Mocker():
fetch_mrs('GitHub')
+
+ def test_false_mr_data(self):
+ for mr in get_false_mrs_data():
+ import_mr('github', mr)
diff --git a/data/tests/test_org_cluster_map_handler.py b/data/tests/test_org_cluster_map_handler.py
new file mode 100644
index 00000000..8199dcc0
--- /dev/null
+++ b/data/tests/test_org_cluster_map_handler.py
@@ -0,0 +1,22 @@
+from django.test import TestCase
+
+from data.models import Contributor
+from data.org_cluster_map_handler import handle as org_cluster_map_handler
+
+
+class CreateOrgClusterMapAndActivityGraphTest(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ Contributor.objects.create(login='test',
+ name='Test User',
+ location='{"latitude": 12.9,'
+ '"longitude": 77.8}')
+ Contributor.objects.create(login='testuser',
+ name='Test User 2')
+
+ def test_with_output_dir(self):
+ org_cluster_map_handler()
+
+ def test_without_output_dir(self):
+ org_cluster_map_handler(output_dir='org_map')
diff --git a/data/urls.py b/data/urls.py
index a3780aa2..6eb2a98a 100644
--- a/data/urls.py
+++ b/data/urls.py
@@ -1,7 +1,7 @@
from django.conf.urls import url
-from . import views
+from .views import ContributorsListView
urlpatterns = [
- url(r'^$', views.index, name='index'),
+ url(r'^$', ContributorsListView.as_view(), name='index'),
]
diff --git a/data/views.py b/data/views.py
index 40436c72..53cd8a3e 100644
--- a/data/views.py
+++ b/data/views.py
@@ -1,8 +1,16 @@
+from django.views.generic import TemplateView
+
+from community.views import get_header_and_footer
from data.models import Contributor
-from django.shortcuts import render
-def index(request):
- contributors = Contributor.objects.all()
- args = {'contributors': contributors}
- return render(request, 'contributors.html', args)
+class ContributorsListView(TemplateView):
+ template_name = 'contributors.html'
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context = get_header_and_footer(context)
+ contrib_objects = Contributor.objects.all()
+ context['contributors'] = contrib_objects.order_by('-num_commits',
+ 'name')
+ return context
diff --git a/gamification/tests/test_management_commands.py b/gamification/tests/test_management_commands.py
index 7519d3ad..ba795dc8 100644
--- a/gamification/tests/test_management_commands.py
+++ b/gamification/tests/test_management_commands.py
@@ -1,14 +1,20 @@
from django.core.management import call_command
from django.test import TestCase
+from data.issues import import_issue
+from community.git import get_org_name
+from data.merge_requests import import_mr
from gamification.models import (
Level,
Badge,
Participant,
BadgeActivity,
)
+from data.contrib_data import import_data
from data.newcomers import active_newcomers
+ORG_NAME = get_org_name()
+
class CreateConfigDataTest(TestCase):
@@ -79,6 +85,18 @@ class UpdateParticipantsTest(TestCase):
@classmethod
def setUpTestData(cls):
+ for contrib in get_false_contributors_data():
+ import_data(contrib)
+
+ for issue in get_false_issues_data():
+ import_issue('github', issue)
+
+ for mr in get_false_mrs_data():
+ import_mr('github', mr)
+
+ for contrib in get_false_active_newcomers():
+ Participant.objects.create(username=contrib['username'])
+
call_command('import_issues_data')
call_command('import_merge_requests_data')
call_command('create_config_data')
@@ -98,3 +116,204 @@ def test_command_update_particiapants_data(self):
number_of_badges = participant.badges.all().count()
self.assertEquals(number_of_badges, 2)
+
+
+def get_false_contributors_data():
+ return [
+ {
+ 'bio': '',
+ 'teams': [
+ f'{ORG_NAME} newcomers'
+ ],
+ 'reviews': 0,
+ 'issues': 0,
+ 'name': '',
+ 'login': 'testuser',
+ 'contributions': 1
+ },
+ {
+ 'bio': '',
+ 'teams': [
+ f'{ORG_NAME} newcomers'
+ ],
+ 'reviews': 0,
+ 'issues': 0,
+ 'name': '',
+ 'login': 'testuser',
+ 'contributions': 1
+ },
+ {
+ 'bio': '',
+ 'teams': [
+ ],
+ 'reviews': 0,
+ 'name': '',
+ 'login': 'testuser1',
+ 'contributions': 1
+ }
+ ]
+
+
+def get_false_issues_data():
+ return [
+ {
+ 'created_at': '2016-11-21T00:46:14',
+ 'hoster': 'github',
+ 'updated_at': '2017-12-21T00:00:48',
+ 'labels': [
+ 'status/duplicate'
+ ],
+ 'number': 1,
+ 'assignees': [],
+ 'repo_id': 254525111,
+ 'title': 'Test issue',
+ 'state': 'closed',
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'url': f'https://github.com/{ORG_NAME}/corobo/issues/585',
+ 'author': 'testuser'
+ },
+ {
+ 'created_at': '2016-11-21T00:46:14',
+ 'hoster': 'github',
+ 'updated_at': '2017-12-21T00:00:48',
+ 'labels': [
+ 'difficulty/newcomer',
+ 'type/bug'
+ ],
+ 'number': 3,
+ 'assignees': [],
+ 'repo_id': 254525111,
+ 'title': 'Test issue',
+ 'state': 'closed',
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/issues/1',
+ 'author': 'testuser1'
+ },
+ {
+ 'created_at': '2016-11-21T00:46:14',
+ 'hoster': 'github',
+ 'updated_at': '2017-12-21T00:00:48',
+ 'labels': [
+ 'difficulty/newcomer',
+ 'type/bug'
+ ],
+ 'number': 2,
+ 'assignees': [],
+ 'repo_id': 254525111,
+ 'title': 'Test issue',
+ 'state': 'closed',
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/issues/2',
+ 'author': 'testuser'
+ },
+ {
+ 'created_at': '2016-11-21T00:46:14',
+ 'hoster': 'github',
+ 'updated_at': '2017-12-21T00:00:48',
+ 'labels': [
+ 'difficulty/newcomer',
+ 'type/bug'
+ ],
+ 'number': 2,
+ 'assignees': [],
+ 'title': 'Test issue',
+ 'state': 'closed',
+ 'repo': 'test/test',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/issues/3',
+ 'author': 'testuser1'
+ }
+ ]
+
+
+def get_false_mrs_data():
+ return [
+ {
+ 'created_at': '2016-02-21T05:04:25',
+ 'hoster': 'github',
+ 'ci_status': True,
+ 'labels': [
+ 'difficulty/newcomer',
+ 'type/bug'
+ ],
+ 'title': 'Test merge request-I',
+ 'number': 1625,
+ 'updated_at': '2016-04-21T12:06:19',
+ 'assignees': [],
+ 'repo_id': 254525111,
+ 'closes_issues': [
+ 2,
+ 3
+ ],
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/pull/1625',
+ 'state': 'merged',
+ 'author': 'testuser'
+ },
+ {
+ 'created_at': '2016-02-21T05:04:25',
+ 'hoster': 'github',
+ 'ci_status': True,
+ 'labels': [
+ 'status/STALE'
+ ],
+ 'title': 'Test merge request-II',
+ 'number': 1626,
+ 'updated_at': '2016-02-21T12:06:19',
+ 'assignees': [],
+ 'repo_id': 25452511,
+ 'closes_issues': [
+ ],
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'state': 'merged',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/pull/1626',
+ 'author': 'testuser'
+ },
+ {
+ 'created_at': '2016-02-21T05:04:25',
+ 'hoster': 'github',
+ 'ci_status': True,
+ 'labels': [
+ 'difficulty/low',
+ 'type/bug'
+ ],
+ 'title': 'Test merge request-III',
+ 'number': 1626,
+ 'updated_at': '2016-02-21T12:06:19',
+ 'assignees': [
+ 'testuser',
+ 'testuser1'
+ ],
+ 'repo_id': 25452511,
+ 'closes_issues': [
+ ],
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'state': 'merged',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/pull/1625',
+ 'author': 'testuser'
+ },
+ {
+ 'created_at': '2016-02-21T05:04:25',
+ 'hoster': 'github',
+ 'labels': [
+ 'difficulty/low',
+ 'type/bug'
+ ],
+ 'title': 'Test merge request-III',
+ 'number': 1626,
+ 'updated_at': '2016-02-21T12:06:19',
+ 'assignees': [],
+ 'repo_id': 25452511,
+ 'repo': f'{ORG_NAME}/{ORG_NAME}',
+ 'url': f'https://github.com/{ORG_NAME}/{ORG_NAME}/pull/1625',
+ 'closes_issues': [
+ ],
+ 'author': 'testuser1'
+ }
+ ]
+
+
+def get_false_active_newcomers():
+ return [
+ {'username': 'testuser'},
+ {'username': 'testuser1'}
+ ]
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/gci/urls.py b/gci/urls.py
index a3780aa2..10e10974 100644
--- a/gci/urls.py
+++ b/gci/urls.py
@@ -3,5 +3,5 @@
from . import views
urlpatterns = [
- url(r'^$', views.index, name='index'),
+ url(r'^$', views.GCIStudentsList.as_view(), name='index'),
]
diff --git a/gci/views.py b/gci/views.py
index e9c97589..ceed5c12 100644
--- a/gci/views.py
+++ b/gci/views.py
@@ -1,11 +1,13 @@
-from django.http import HttpResponse
from datetime import datetime
from calendar import timegm
+
import logging
-import requests
+from django.views.generic import TemplateView
+
+from community.views import get_header_and_footer
+from data.models import Contributor
from .students import get_linked_students
-from .gitorg import get_logo
from .task import get_tasks
STUDENT_URL = (
@@ -15,75 +17,78 @@
)
-def index(request):
- logger = logging.getLogger(__name__ + '.index')
- try:
- get_tasks()
- except FileNotFoundError:
- logger.info('GCI data not available')
- s = ['GCI data not available']
- else:
- s = gci_overview()
-
- return HttpResponse('\n'.join(s))
-
-
-def gci_overview():
- logger = logging.getLogger(__name__ + '.gci_overview')
- linked_students = list(get_linked_students())
- if not linked_students:
- logger.info('No GCI students are linked')
- return ['No GCI students are linked']
-
- org_id = linked_students[0]['organization_id']
- org_name = linked_students[0]['organization_name']
- s = []
- s.append('')
-
- favicon = get_logo(org_name, 16)
- with open('_site/favicon.png', 'wb') as favicon_file:
- favicon_file.write(favicon)
-
- org_logo = get_logo(org_name)
- with open('_site/org_logo.png', 'wb') as org_logo_file:
- org_logo_file.write(org_logo)
-
- s.append('')
- s.append('')
- s.append('
{{ key }}: {{ value }}
+ {% endfor %}{# for key, value in build_info.items #} +Great! Wait for build logs to get displayed.
+ {% else %} +No logs found! Please run '.ci/build.sh' on the project.
+ {% endif %}{# if logs_stored #} +login: {{ contributor.login }}
-name: {{ contributor.name }}
-bio: {{ contributor.bio }}
-num_commits: {{ contributor.num_commits }}
-reviews: {{ contributor.reviews }}
-issues_opened: {{ contributor.issues_opened }}
-teams: - {% for team in contributor.teams.all %} - {{ team.name }} - {% endfor %}{# for team in contributor.teams.all #} -
-+ Contributor's who've been putting their hard-work to make {{ org.name }} best of its + own. Thanks to all contributors to make {{ org.name }} what is it today. +
+Search Results | +
---|
+ No results found! + | +
{{ contributor.num_commits }}
+Commits
+{{ contributor.reviews }}
+Reviews
+{{ contributor.issues_opened }}
+Issues
+Note: All the datetime is in UTC
-Username: {{ participant.username }}
- - - -Score: {{ participant.score }}
-Level: {{ participant.level.name }}
-Activities Performed: - {% for activity in participant.activities.all %} -
{{ forloop.counter }}. {{ activity.name }}, performed_at: - {{ activity.performed_at }} updated_at: {{ activity.updated_at }} -
- {% endfor %}{# for activity in participant.activities.all #} -Badges Earned: - {% for badge in participant.badges.all %} -
{{ forloop.counter }}.{{ badge.name }}
- {% endfor %}{# for badge in participant.badges.all #} - +{% extends 'base.html' %} +{% load staticfiles %} +{% block title %} + Community | Gamification Leaderboard +{% endblock %} + +{% block add_css_files %} + + + +{% endblock %} + +{% block add_js_files %} + + +{% endblock %} + +{% block main-content %} + ++ 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. +
+Search Results | +
---|
+ No results found! + | +
+ Hello, World! {{ org.name }} has been participating in GCI (Google Code-In) from last few years and will + be participating in coming years too. Following are the GCI students who participated in GCI with {{ org.name }} + organization. +
+{{ student.bio }}
+ {% endif %}{# if student.bio #} +ID:
+ {{ student.id }} +Participation year: {{ student.program_year }}
+Repos:
+{{ student.public_repos }}
+Gists:
+{{ student.public_gists }}
+Followers:
+{{ student.followers }}
+{{ org.about }}
+{{ quote_details.quote }}
+Rank | +Username | +Gamification Score | +
---|---|---|
{{ index|add:"1" }} | +{{ contrib.username }} | +{{ contrib.score }} | +
login: {{ participant.login }}
-name: {{ participant.name }}
-score: {{ participant.score|floatformat:2 }}
-rank: {{ participant.rank }}
-trend: {{ participant.trend }}
-received:
-number of positive reactions: {{ participant.pos_in }}
-- weighted positive reactions: - {{ participant.weighted_pos_in|floatformat:2 }} -
-number of negative reactions: {{ participant.neg_in }}
-- weighted negative reactions: - {{ participant.weighted_neg_in|floatformat:2 }} -
-give away:
-number of positive reactions: {{ participant.pos_out }}
-number of negative reactions: {{ participant.neg_out }}
-- negative point offset: - {{ participant.offset|floatformat:2 }} -
-weight_factor: {{ participant.weight_factor|floatformat:2 }}
-+ Contributor meta-review related details are calculated based upon the + GitHub reactions made by that contributor on any issue or merge request + within an organization, for example - {{ org.name }}. +
+Search Results | +
---|
+ No results found! + | +