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..f01d1c3f 100644 --- a/community/urls.py +++ b/community/urls.py @@ -5,15 +5,12 @@ 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 ci_build.view_log import BuildLogsView +from data.views import ContributorsListView from gamification.views import index as gamification_index from meta_review.views import index as meta_review_index from inactive_issues.inactive_issues_scraper import inactive_issues_json @@ -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,25 +85,19 @@ 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', 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/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 #} ++ 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
++ 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 }} | +