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..d2942cee 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 @@ -58,7 +59,7 @@ shell = bash # for use by other organizations. files = ** # .coverage crashes AnnotationBear -ignore += .coafile, *requirements.txt, .travis.yml, LICENSE, .nocover.yaml, .moban.yaml, .moban.dt/community-*.jj2, public/**, _site/**, .ci/check_moban.sh, .coverage +ignore += .coafile, *requirements.txt, .travis.yml, LICENSE, .nocover.yaml, .moban.yaml, .moban.dt/community-*.jj2, public/**, _site/**, .ci/check_moban.sh, .coverage, static/js/main.js bears = KeywordBear language = python 3 keywords = coala 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/forms.py b/community/forms.py new file mode 100644 index 00000000..f4d95dcf --- /dev/null +++ b/community/forms.py @@ -0,0 +1,106 @@ +from django import forms + + +class JoinCommunityForm(forms.Form): + + github_username = forms.CharField( + max_length=50, label='GitHub Username', + widget=forms.TextInput( + attrs={ + 'placeholder': 'Make sure to NOT enter your github profile url', + 'autocomplete': 'off' + } + ) + ) + gh_first_repo = forms.URLField( + required=False, label='GitHub Personal Repository', + widget=forms.URLInput( + attrs={ + 'placeholder': 'A valid github url of your personal repository', + 'autocomplete': 'off' + } + ) + ) + gh_git_training_exercise = forms.URLField( + required=False, label='From which GitHub repository you have done git' + ' training?', + widget=forms.URLInput( + attrs={ + 'placeholder': 'A valid github url of git training repository', + 'autocomplete': 'off' + } + ) + ) + gh_most_contributed_repo = forms.URLField( + required=False, + label="GitHub Repository to which you've contributed most!", + widget=forms.URLInput( + attrs={ + 'placeholder': 'A valid github public repository url', + 'autocomplete': 'off' + } + ) + ) + + gitlab_user_id = forms.IntegerField( + label='GitLab User ID', + widget=forms.NumberInput( + attrs={ + 'placeholder': 'Make sure to NOT enter your gitlab profile url', + 'autocomplete': 'off' + } + ) + ) + gl_first_repo_id = forms.IntegerField( + required=False, label='GitLab Personal Project ID', + widget=forms.NumberInput( + attrs={ + 'placeholder': 'Your personal gitlab project ID', + 'autocomplete': 'off' + } + ) + ) + gl_git_training_exercise = forms.IntegerField( + required=False, label='From which GitLab project you have done git' + ' training?', + widget=forms.NumberInput( + attrs={ + 'placeholder': 'A valid project ID of Git training project', + 'autocomplete': 'off' + } + ) + ) + gl_most_contributed_repo_id = forms.IntegerField( + required=False, + label="GitLab Project to which you've contributed most!", + widget=forms.NumberInput( + attrs={ + 'placeholder': 'A valid ID of gitlab public project', + 'autocomplete': 'off', + } + ) + ) + + +class CommunityGoogleForm(forms.Form): + user = forms.CharField( + max_length=50, label='GitHub Username', + widget=forms.TextInput(attrs={'autocomplete': 'off'}) + ) + title = forms.CharField( + max_length=100, label='Title', + widget=forms.TextInput(attrs={'autocomplete': 'off'}) + ) + description = forms.CharField( + max_length=1000, label='Form Description', required=False, + widget=forms.Textarea(attrs={'autocomplete': 'off'}) + ) + url = forms.URLField( + label='Google Form URL', + widget=forms.TextInput(attrs={'autocomplete': 'off'}) + ) + expiry_date = forms.DateTimeField( + label='Expiry date and time', required=False, + help_text='DateTime Format should be YYYY-MM-DD HH:MM:SS', + widget=forms.TextInput(attrs={'autocomplete': 'off'}) + ) 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..a42078b6 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, JoinCommunityView +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 @@ -82,22 +79,10 @@ def get_organization(): 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', + r'^join/', JoinCommunityView.as_view(), + name='join-community', distill_func=get_index, - distill_file='activity/index.html', + distill_file='join/index.html', ), distill_url( r'gci/tasks/rss.xml', gci_tasks_rss(), @@ -106,31 +91,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 +199,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..e2f0fd60 100644 --- a/community/views.py +++ b/community/views.py @@ -1,43 +1,143 @@ -from django.http import HttpResponse -from django.views.generic.base import TemplateView +import os + +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 .forms import JoinCommunityForm, CommunityGoogleForm +from data.models import Team +from gamification.models import Participant as GamificationParticipant +from meta_review.models import Participant as MetaReviewer + +GL_NEWCOMERS_GRP = 'https://gitlab.com/{}/roles/newcomers'.format( + get_org_name() ) +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_community_google_form_variables(context): + context['community_google_form'] = CommunityGoogleForm() + context['community_google_form_name'] = os.environ.get( + 'OSFORMS_NETLIFY_FORM_NAME', None + ) + return context + + +def get_header_and_footer(context): + context['isTravis'] = Travis.TRAVIS + context['travisLink'] = Travis.TRAVIS_BUILD_WEB_URL + context['org'] = initialize_org_context_details() + context = get_community_google_form_variables(context) + print('Running on Travis: {}, build link: {}'.format(context['isTravis'], + context['travisLink'] + )) + return context + + class HomePageView(TemplateView): template_name = 'index.html' + 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 + + def get_quote_of_the_day(self): + + 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 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['isTravis'] = Travis.TRAVIS - context['travisLink'] = Travis.TRAVIS_BUILD_WEB_URL + 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 - print('Running on Travis: {}, build link: {}'.format( - context['isTravis'], - context['travisLink'])) - return context +class JoinCommunityView(TemplateView): + template_name = 'join_community.html' -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_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context = get_header_and_footer(context) + context['join_community_form'] = JoinCommunityForm() + context['gitlab_newcomers_group_url'] = GL_NEWCOMERS_GRP + context['join_community_form_name'] = os.environ.get( + 'JOIN_COMMUNITY_FORM_NAME', None + ) + 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/migrations/0007_auto_20190802_2015.py b/data/migrations/0007_auto_20190802_2015.py new file mode 100644 index 00000000..b0cd314b --- /dev/null +++ b/data/migrations/0007_auto_20190802_2015.py @@ -0,0 +1,43 @@ +# Generated by Django 2.1.7 on 2019-08-02 20:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0006_auto_20190801_1752'), + ] + + operations = [ + migrations.AddField( + model_name='contributor', + name='is_gci_participant', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='contributor', + name='oauth_completed', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='contributor', + name='statistics', + field=models.TextField(default=None, null=True), + ), + migrations.AddField( + model_name='contributor', + name='type_of_issues_worked_on', + field=models.TextField(default=None, null=True), + ), + migrations.AddField( + model_name='contributor', + name='updated_at', + field=models.TextField(default=None, null=True), + ), + migrations.AddField( + model_name='contributor', + name='working_on_issues_count', + field=models.TextField(default=None, null=True), + ), + ] diff --git a/data/models.py b/data/models.py index b6ba1a0e..c794f00e 100644 --- a/data/models.py +++ b/data/models.py @@ -13,9 +13,19 @@ 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') + statistics = models.TextField(default=None, null=True) + type_of_issues_worked_on = models.TextField(default=None, null=True) + is_gci_participant = models.BooleanField(default=False) + working_on_issues_count = models.TextField(default=None, null=True) + updated_at = models.TextField(default=None, null=True) + oauth_completed = models.BooleanField(default=False) 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..d006b231 100644 --- a/gamification/views.py +++ b/gamification/views.py @@ -1,10 +1,14 @@ -from django.shortcuts import render +from django.views.generic import TemplateView +from community.views import get_header_and_footer from gamification.models import Participant -def index(request): - Participant.objects.filter(username__startswith='testuser').delete() - participants = Participant.objects.all() - args = {'participants': participants} - return render(request, 'gamification.html', args) +class GamificationResults(TemplateView): + template_name = 'gamification.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context = get_header_and_footer(context) + context['gamification_results'] = Participant.objects.all() + 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('
').text( + 'GitHub: ' + gh_date.toUTCString()); + var last_fetched_gl_data_el = $('
').text( + 'GitLab: ' + gl_date.toUTCString()); + user_updated_el.append(last_fetched_gh_data_el); + user_updated_el.append(last_fetched_gl_data_el); + user_updated_el.css('justify-content', 'space-evenly'); + } + + function displayWorkingOnIssuesCount(data) { + var on_hoster_counts = data.working_on_issues_count; + var count_list_el = $('.count-list'); + count_list_el.empty(); + var github_issue_count_el = $('').text('GitHub: ' + + on_hoster_counts.github); + var gitlab_issue_count_el = $('').text('GitLab: ' + + on_hoster_counts.gitlab); + count_list_el.append(github_issue_count_el); + count_list_el.append(gitlab_issue_count_el); + } + + function setLabelCSSProperties(label_element, bg_color){ + label_element.css('background-color', bg_color); + label_element.css('color', 'white'); + label_element.css('border-radius', '10px'); + label_element.css('margin', '0 3px'); + label_element.css('padding', '5px'); + return label_element; + } + + function displayWorkedOnIssueLabels(data) { + var issue_labels = data.type_of_issues_worked_on; + var gh_issue_labels_el = $('.github-issue-labels'); + var gl_issue_labels_el = $('.gitlab-issue-labels'); + gh_issue_labels_el.empty(); + gl_issue_labels_el.empty(); + jQuery.each(issue_labels.github, function (label_name, color) { + var label_el = $('').text(label_name); + label_el = setLabelCSSProperties(label_el, '#'+color); + gh_issue_labels_el.append(label_el); + }); + jQuery.each(issue_labels.gitlab, function (label_name, color) { + var label_el = $('').text(label_name); + label_el = setLabelCSSProperties(label_el, color); + gl_issue_labels_el.append(label_el); + }); + } + + function get_options(title) { + return { + responsive: true, + maintainAspectRatio: false, + fill: true, + borderWidth: 3, + title: { + display: true, + text: title + }, + tooltips: { + mode: 'index', + intersect: false, + }, + hover: { + mode: 'nearest', + intersect: true + }, + scales: { + xAxes: [{ + stacked: true, + display: true, + scaleLabel: { + display: true, + } + }], + yAxes: [{ + stacked: true, + display: true, + scaleLabel: { + display: true, + labelString: 'Number' + } + }] + } + }; + } + + function get_dataset_properties(label, backgroundColor, data){ + return { + label: label, + backgroundColor: backgroundColor, + data: data, + }; + } + + function setRepositoryCanvasChart(stats) { + new Chart(repository_stats_canvas, { + type: 'bar', + data: { + labels: stats.repositories, + datasets: [ + get_dataset_properties("Commits", "RGBA(236,255,52,0.7)", + stats.commits), + get_dataset_properties("Reviews", "RGBA(236,151,52,0.7)", + stats.reviews), + get_dataset_properties("Issues Opened", "RGBA(178, 191, 0, 0.7)", + stats.issues_opened), + get_dataset_properties("Issues Assigned", "RGBA(178, 52, 237, 0.7)", + stats.assigned_issues), + get_dataset_properties("Issues Closed", "RGBA(255, 52, 61, 0.7)", + stats.issues_closed), + get_dataset_properties("Merge Requests Opened", "RGBA(255, 190," + + " 217, 0.7)", stats.merge_requests_opened), + get_dataset_properties("Unmerged Merge Requests", "RGBA(87, 190," + + " 138, 0.7)", stats.unmerged_merge_requests), + ] + }, + options: get_options('Repository-Wise Statistics') + }); + } + + function setCommitsAndReviewsChart(commitLabels, commitData, + reviewLabels, reviewData) { + + commitsChart = new Chart(commits_canvas, { + type: 'bar', + data: { + labels: commitLabels, + datasets: [ + get_dataset_properties("Commits Activity", "RGBA(87, 190, 217," + + " 0.7)", commitData) + ] + }, + options: get_options('Commits Activity') + }); + + reviewsChart = new Chart(reviews_canvas, { + type: 'bar', + data: { + labels: reviewLabels, + datasets: [ + get_dataset_properties("Reviews Activity", "RGBA(87, 190, 138," + + " 0.7)", reviewData) + ] + }, + options: get_options('Review Activity'), + }); + } + + function setIssuesCanvasChart(data){ + var data_labels = new Set(); + jQuery.each(data, function (subtype, display_filters){ + jQuery.each(display_filters, function (filter, value) { + data_labels.add(filter); + }); + }); + data_labels.forEach(function (data_label) { + jQuery.each(data, function (subtype){ + if(isNaN(data[subtype][data_label])){ + data[subtype][data_label] = 0; + } + }); + }); + data_labels = Array.from(data_labels); + issuesChart = new Chart(issues_canvas, { + type: 'bar', + data: { + labels: data_labels, + datasets: [ + get_dataset_properties("Closed Issues Activity","RGBA(87, 190," + + " 138, 0.7)", Object.values(data.closed)), + get_dataset_properties("Assigned Issues Activity","RGBA(255, 224," + + " 217, 0.8)", Object.values(data.assigned)), + get_dataset_properties("Opened Issues Activity", "RGBA(236, 151," + + " 52, 0.6)", data.open !== undefined?Object.values(data.open): + Object.values(data.opened)) + ] + }, + options: get_options('Issues Activity') + }); + } + + function setMergeRequestsCanvasChart(data){ + var data_labels = new Set(); + jQuery.each(data, function (subtype, display_filters){ + jQuery.each(display_filters, function (filter, value) { + data_labels.add(filter); + }); + }); + data_labels.forEach(function (data_label) { + jQuery.each(data, function (subtype){ + if(isNaN(data[subtype][data_label])){ + data[subtype][data_label] = 0; + } + }); + }); + data_labels = Array.from(data_labels); + mergeRequestsChart = new Chart(merge_requests_canvas, { + type: 'line', + data: { + labels: data_labels, + datasets: [ + get_dataset_properties("Merged PRs Activity","RGBA(255, 224, 217," + + " 0.5)",data.merged !==undefined?Object.values(data.merged): + new Array(data_labels.size)), + get_dataset_properties("Unmerged PRs Activity","RGBA(178, 52, 237," + + " 0.7)", data.unmerged !== undefined?Object.values(data.unmerged): + new Array(data_labels.size)), + get_dataset_properties("Opened PRs Activity", "RGBA(236, 151, 52," + + " 0.9)", data.open !== undefined?Object.values(data.open): + (data.opened !== undefined?Object.values(data.opened): + new Array(data_labels.size))) + ] + }, + options: get_options('Merge Requests Activity') + }); + } + + function setCanvasCharts(data){ + setCommitsAndReviewsChart( + Object.keys(data.commits), Object.values(data.commits), + Object.keys(data.reviews), Object.values(data.reviews) + ); + setIssuesCanvasChart(data.issues); + setMergeRequestsCanvasChart(data.merge_requests); + } + + function toggleCanvasDisplays(data){ + if (Object.values(data.reviews).length === 0) { + $('.bar-reviews-canvas').css('display', 'none'); + } + else { + $('.bar-reviews-canvas').css('display', 'block'); + } + if (Object.values(data.issues).length === 0) { + $('.bar-issues-canvas').css('display', 'none'); + } + else { + $('.bar-issues-canvas').css('display', 'block'); + } + if (Object.values(data.commits).length === 0) { + $('.bar-commits-canvas').css('display', 'none'); + } + else { + $('.bar-commits-canvas').css('display', 'block'); + } + if (Object.values(data.merge_requests).length === 0) { + $('.line-merge-requests-canvas').css('display', 'none'); + } + else { + $('.line-merge-requests-canvas').css('display', 'block'); + } + } + + function getWeekNumber(date) { + var d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), + date.getDate())); + var dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + } + + function getMonthNameFromWeekNumber(year, week_number) { + var total_ms_count = ((week_number * 7) - 1) * 86400000; + var current_date = new Date(); + var d = new Date(Date.UTC(year, current_date.getMonth(), + current_date.getDate())); + var dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + var date = new Date(total_ms_count + yearStart.getTime()); + return month_names[date.getMonth()]; + } + + function get_last_twelve_months_begin_end_weeks() { + var current_date = new Date(); + var last_year_date = new Date( + current_date.getFullYear() - 1, current_date.getMonth(), + current_date.getDate() + ); + return [current_date.getFullYear(), getWeekNumber(current_date), + getWeekNumber(last_year_date)]; + } + + function get_last_twelve_weeks_begin_end() { + var current_date = new Date(); + var current_week = getWeekNumber(current_date); + var last_twelfth_week = 1; + if (current_week > 12) { + last_twelfth_week = current_week - 12; + } else { + var week_difference = 12 - current_week; + var month = Math.trunc((11 - week_difference) / 4); + var last_year_date = new Date( + current_date.getFullYear() - 1, month, + current_date.getDate()); + last_twelfth_week = getWeekNumber(last_year_date); + } + return [current_date.getFullYear(), current_week, last_twelfth_week]; + } + + function updateCharts(data, hoster_type, display_type) { + if(commitsChart){ + commitsChart.destroy(); + reviewsChart.destroy(); + issuesChart.destroy(); + mergeRequestsChart.destroy(); + } + var hoster_stats = data.statistics[hoster_type]; + var charts_data = { + issues: new Map(), + commits: new Map(), + merge_requests: new Map(), + reviews: new Map(), + }; + var issue_stats, commits_stats, prs_stats, reviews_stats, current_year, + current_week; + if (display_type === "yearly") { + + jQuery.each(hoster_stats, function (repo_name, repo_stats) { + issue_stats = repo_stats.issues; + jQuery.each(issue_stats, function (issue_type, years) { + if (charts_data.issues[issue_type] === undefined) { + charts_data.issues[issue_type] = new Map(); + } + jQuery.each(years, function (year, week_numbers) { + jQuery.each(week_numbers, function ( + week_number, weekdays) { + jQuery.each(weekdays, function ( + weekday, issues) { + if (isNaN(charts_data.issues[issue_type][year])) { + charts_data.issues[issue_type][year] = 0; + } + charts_data.issues[issue_type][year] += + Object.keys(issues).length; + }); + + }); + }); + }); + + prs_stats = repo_stats.prs || repo_stats.merge_requests; + jQuery.each(prs_stats, function (pr_type, years) { + if (charts_data.merge_requests[pr_type] === undefined) { + charts_data.merge_requests[pr_type] = new Map(); + } + jQuery.each(years, function (year, week_numbers) { + jQuery.each(week_numbers, function ( + week_number, weekdays) { + jQuery.each(weekdays, function (weekday, mrs) { + if (isNaN(charts_data.merge_requests[pr_type][year])) { + charts_data.merge_requests[pr_type][year] = 0; + } + charts_data.merge_requests[pr_type][year] += + Object.keys(mrs).length; + }); + + }); + }); + }); + + commits_stats = repo_stats.commits; + jQuery.each(commits_stats, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays){ + jQuery.each(weekdays, function (weekday, commits_done){ + if (isNaN(charts_data.commits[year])) { + charts_data.commits[year] = 0; + } + charts_data.commits[year] += + Object.keys(commits_done).length; + }); + }); + }); + + reviews_stats = repo_stats.reviews; + jQuery.each(reviews_stats, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays){ + jQuery.each(weekdays, function (weekday, reviews_done){ + if (isNaN(charts_data.reviews[year])) { + charts_data.reviews[year] = 0; + } + charts_data.reviews[year] += + Object.keys(reviews_done).length; + }); + }); + }); + }); + } + + else if (display_type === "monthly") { + var last_twelve_months_weeks = get_last_twelve_months_begin_end_weeks(); + current_year = last_twelve_months_weeks[0]; + current_week = last_twelve_months_weeks[1]; + var last_year_week = last_twelve_months_weeks[2]; + + jQuery.each(hoster_stats, function (repo_name, repo_stats) { + + issue_stats = repo_stats.issues; + jQuery.each(issue_stats, function (issue_type, years) { + if (charts_data.issues[issue_type] === undefined) { + charts_data.issues[issue_type] = new Map(); + } + jQuery.each(years, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + year = parseInt(year); + week_number = parseInt(week_number); + if ( + (current_year === year && + week_number <= current_week) || + (year === (current_year - 1) && + week_number >= last_year_week)) { + jQuery.each(weekdays, function (weekday, + issues) { + var month_name + = getMonthNameFromWeekNumber(year, week_number); + var key = month_name + '\'' + year%100; + if (isNaN(charts_data.issues[issue_type][key])) { + charts_data.issues[issue_type][key] = 0; + } + charts_data.issues[issue_type][key] += + Object.keys(issues).length; + }); + } + }); + }); + }); + + prs_stats = repo_stats.prs || repo_stats.merge_requests; + jQuery.each(prs_stats, function (pr_type, years) { + if (charts_data.merge_requests[pr_type] === undefined) { + charts_data.merge_requests[pr_type] = new Map(); + } + jQuery.each(years, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + year = parseInt(year); + week_number = parseInt(week_number); + if ((current_year === year && week_number <= current_week) || + (year === (current_year - 1) && + week_number >= last_year_week)) { + jQuery.each(weekdays, function (weekday, mrs) { + var month_name + = getMonthNameFromWeekNumber(year, week_number); + var key = month_name + '\'' + year%100; + if (isNaN(charts_data.merge_requests[pr_type][key])) { + charts_data.merge_requests[pr_type][key] = 0; + } + charts_data.merge_requests[pr_type][key] += + Object.keys(mrs).length; + }); + } + }); + }); + }); + + commits_stats = repo_stats.commits; + jQuery.each(commits_stats, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + year = parseInt(year); + week_number = parseInt(week_number); + if ((current_year === year && week_number <= current_week) || + (year === (current_year - 1) && week_number >= last_year_week)) { + jQuery.each(weekdays, function (weekday, commits_done) { + var month_name + = getMonthNameFromWeekNumber(year, week_number); + var key = month_name + '\'' + year%100; + if (isNaN(charts_data.commits[key])) { + charts_data.commits[key] = 0; + } + charts_data.commits[key] += Object.keys(commits_done).length; + }); + } + }); + }); + + reviews_stats = repo_stats.reviews; + jQuery.each(reviews_stats, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + year = parseInt(year); + week_number = parseInt(week_number); + if ((current_year === year && week_number <= current_week) || + (year === (current_year - 1) && week_number >= last_year_week)) { + jQuery.each(weekdays, function (weekday, reviews_done) { + var month_name = getMonthNameFromWeekNumber(year, week_number); + var key = month_name + '\'' + year%100; + if (isNaN(charts_data.reviews[key])) { + charts_data.reviews[key] = 0; + } + charts_data.reviews[key] += Object.keys(reviews_done).length; + }); + } + }); + }); + }); + } + + else { + + var last_twelve_weeks = get_last_twelve_weeks_begin_end(); + current_year = last_twelve_weeks[0]; + current_week = last_twelve_weeks[1]; + var last_twelfth_week = last_twelve_weeks[2]; + + jQuery.each(hoster_stats, function (repo_name, repo_stats) { + + issue_stats = repo_stats.issues; + jQuery.each(issue_stats, function (issue_type, years) { + if (charts_data.issues[issue_type] === undefined) { + charts_data.issues[issue_type] = new Map(); + } + jQuery.each(years, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + if ((current_year === parseInt(year) && + last_twelfth_week <= parseInt(week_number) && + parseInt(week_number) <= current_week && + current_week >= 12) || + (parseInt(year) === (current_year - 1) && + parseInt(week_number) >= last_twelfth_week && + current_week < 12)) { + jQuery.each(weekdays, function (weekday, issues) { + var key = 'Week-' + week_number + ',' + year; + if (isNaN(charts_data.issues[issue_type][key])) { + charts_data.issues[issue_type][key] = 0; + } + charts_data.issues[issue_type][key] += + Object.keys(issues).length; + }); + } + }); + }); + }); + + prs_stats = repo_stats.prs || repo_stats.merge_requests; + jQuery.each(prs_stats, function (pr_type, years) { + if (charts_data.merge_requests[pr_type] === undefined) { + charts_data.merge_requests[pr_type] = new Map(); + } + jQuery.each(years, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + if ((current_year === parseInt(year) && + last_twelfth_week <= parseInt(week_number) && + parseInt(week_number) <= current_week && + current_week >= 12) || + (parseInt(year) === (current_year - 1) && + parseInt(week_number) >= last_twelfth_week && + current_week < 12)) { + jQuery.each(weekdays, function (weekday, mrs) { + var key = 'Week-' + week_number + ',' + year; + if (isNaN(charts_data.merge_requests[pr_type][key])) { + charts_data.merge_requests[pr_type][key] = 0; + } + charts_data.merge_requests[pr_type][key] += + Object.keys(mrs).length; + }); + } + }); + }); + }); + + commits_stats = repo_stats.commits; + jQuery.each(commits_stats, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + if ((current_year === parseInt(year) && + last_twelfth_week <= parseInt(week_number) && + parseInt(week_number) <= current_week && + current_week >= 12) || + (parseInt(year) === (current_year - 1) && + parseInt(week_number) >= last_twelfth_week && + current_week < 12)) { + jQuery.each(weekdays, function (weekday, commits_done) { + var key = 'Week-' + week_number + ',' + year; + if (isNaN(charts_data.commits[key])) { + charts_data.commits[key] = 0; + } + charts_data.commits[key] += Object.keys(commits_done).length; + }); + } + }); + }); + + reviews_stats = repo_stats.reviews; + jQuery.each(reviews_stats, function (year, week_numbers) { + jQuery.each(week_numbers, function (week_number, weekdays) { + if ((current_year === parseInt(year) && + last_twelfth_week <= parseInt(week_number) && + parseInt(week_number) <= current_week && + current_week >= 12) || + (parseInt(year) === (current_year - 1) && + parseInt(week_number) >= last_twelfth_week && + current_week < 12)) { + jQuery.each(weekdays, function (weekday, reviews_done) { + var key = 'Week-' + week_number + ',' + year; + if (isNaN(charts_data.reviews[key])) { + charts_data.reviews[key] = 0; + } + charts_data.reviews[key] += Object.keys(reviews_done).length; + }); + } + }); + }); + }); + } + + toggleCanvasDisplays(charts_data); + setCanvasCharts(charts_data); + } + + function addEventListenerToStatisticsSelector(contrib_data){ + hoster_selector.on('change', function () { + updateCharts(contrib_data, hoster_selector.val(), stats_divider.val()); + }); + stats_divider.on('change', function () { + updateCharts(contrib_data, hoster_selector.val(), stats_divider.val()); + }); + updateCharts(contrib_data, hoster_selector.val(), stats_divider.val()); + } + + function getContributionsCount(data){ + var contributions_count = 0; + jQuery.each(data, function (years, contributions) { + jQuery.each(contributions, function (weeknumbers, weekdays) { + contributions_count += Object.keys(weekdays).length; + }); + }); + return contributions_count; + } + + function createRepositoryCanvasChart(data){ + var repositories_stats = { + repositories: [], commits: [], reviews: [], issues_opened: [], + assigned_issues: [], issues_closed: [], merge_requests_opened: [], + unmerged_merge_requests: [] + }; + var github_data = data.statistics.github, + gitlab_data = data.statistics.gitlab; + jQuery.each(github_data, function (repository, stats) { + repositories_stats.repositories.push(repository); + repositories_stats.commits.push(getContributionsCount(stats.commits)); + repositories_stats.reviews.push(getContributionsCount(stats.reviews)); + repositories_stats.issues_opened.push(getContributionsCount( + stats.issues === undefined? new Map(): stats.issues.open + )); + repositories_stats.assigned_issues.push(getContributionsCount( + stats.issues === undefined? new Map(): stats.issues.assigned + )); + repositories_stats.issues_closed.push(getContributionsCount( + stats.issues === undefined? new Map(): stats.issues.closed + )); + repositories_stats.merge_requests_opened.push(getContributionsCount( + stats.prs === undefined? new Map(): stats.prs.open + )); + repositories_stats.unmerged_merge_requests.push(getContributionsCount( + stats.prs === undefined? new Map(): stats.prs.unmerged + )); + }); + + jQuery.each(gitlab_data, function (repository, stats) { + repositories_stats.repositories.push(repository); + repositories_stats.commits.push(getContributionsCount(stats.commits)); + repositories_stats.reviews.push(getContributionsCount(stats.reviews)); + repositories_stats.issues_opened.push(getContributionsCount( + stats.issues === undefined? new Map(): stats.issues.opened + )); + repositories_stats.assigned_issues.push(getContributionsCount( + stats.issues === undefined? new Map(): stats.issues.assigned + )); + repositories_stats.issues_closed.push(getContributionsCount( + stats.issues === undefined? new Map(): stats.issues.closed + )); + repositories_stats.merge_requests_opened.push(getContributionsCount( + stats.merge_requests === undefined? new Map(): + stats.merge_requests.opened + )); + repositories_stats.unmerged_merge_requests.push(getContributionsCount( + stats.merge_requests === undefined? new Map(): + stats.merge_requests.unmerged + )); + }); + setRepositoryCanvasChart(repositories_stats); + } + + $('select').formSelect(); + $('.user-statistics-option').on('click', function () { + var username = $(this).attr('username'); + user_statistics_display.css('display', 'block'); + $.getJSON("/static/contributors-data.json", function (data) { + var contrib_data = data[username]; + contrib_data.statistics = $.parseJSON(contrib_data.statistics); + contrib_data.type_of_issues_worked_on = $.parseJSON( + contrib_data.type_of_issues_worked_on + ); + contrib_data.working_on_issues_count = $.parseJSON( + contrib_data.working_on_issues_count + ); + contrib_data.updated_at = $.parseJSON(contrib_data.updated_at); + createRepositoryCanvasChart(contrib_data); + addEventListenerToStatisticsSelector(contrib_data); + displayWorkedOnIssueLabels(contrib_data); + displayWorkingOnIssuesCount(contrib_data); + displayDataUpdatedDates(contrib_data); + }).fail(function (data, textStatus, error) { + console.error("Request Failed: " + textStatus + ", " + error); + }); + $('.close-statistics').on('click', function () { + user_statistics_display.css('display', 'none'); + }); + }); +}); diff --git a/static/js/contributors.js b/static/js/contributors.js new file mode 100644 index 00000000..beaa9d64 --- /dev/null +++ b/static/js/contributors.js @@ -0,0 +1,77 @@ +$(document).ready(function(){ + var search_input = $('#search'); + var close_icon = $('.contributors-section .fa-close'); + var results_body = $('.search-results-tbody'); + var searched_keyword = null; + + function appendChildren(element, username, el_result_value, + hide_all_contributors){ + var result_td = $('
{{ 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
+Note: All the datetime is in UTC
-