Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit achievements #437

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4c58a85
edit_achievements
AleksandrKosmylev Jun 15, 2024
1dd4735
edit_achievements_addition
AleksandrKosmylev Jun 15, 2024
be4c7d1
Added common statistics in footer
AleksandrKosmylev Jun 18, 2024
df93d1a
Merge branch 'Hexlet:main' into edit_achievements
AleksandrKosmylev Jun 18, 2024
a68db2e
add test for contributors:contributor_achievements and minor linter fix
AleksandrKosmylev Jun 19, 2024
3b4a8fa
Merge remote-tracking branch 'origin/edit_achievements' into edit_ach…
AleksandrKosmylev Jun 20, 2024
883c439
Add helper function to calc achivements percentage
AleksandrKosmylev Jun 30, 2024
c43a264
fix inline styles
AleksandrKosmylev Jun 30, 2024
8ddf4bb
update common and personal stat
AleksandrKosmylev Aug 18, 2024
23abc8c
resolve conflict
AleksandrKosmylev Oct 6, 2024
eb11834
resolve wps
AleksandrKosmylev Oct 6, 2024
3849707
resolve wps editon 1
AleksandrKosmylev Oct 6, 2024
f9c26ed
resolve wps editon 2
AleksandrKosmylev Oct 6, 2024
fa49f24
resolve wps editon 3
AleksandrKosmylev Oct 6, 2024
a068fe8
resolve wps editon 4
AleksandrKosmylev Oct 6, 2024
439944b
resolve wps editon 6
AleksandrKosmylev Oct 6, 2024
be1689c
resolve wps editon 7
AleksandrKosmylev Oct 6, 2024
023b76d
Merge branch 'Hexlet:main' into edit_achievements
AleksandrKosmylev Oct 6, 2024
91289fb
fix linter notifications
AleksandrKosmylev Oct 27, 2024
130b868
fix linter notifications
AleksandrKosmylev Oct 27, 2024
4b3067c
refactor contributor_achievements.py
AleksandrKosmylev Nov 17, 2024
8c312f8
delete sum brackets
AleksandrKosmylev Nov 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions auth/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ def setUp(self):
"""Create a test client."""
self.client: Client = Client()

@patch('contributors.utils.github_lib.get_access_token', lambda *args: None) # noqa: E501
@patch('contributors.utils.github_lib.get_data_of_token_holder', lambda *args: None) # noqa: E501
@patch('contributors.utils.github_lib.get_access_token', lambda *args: None)
@patch('contributors.utils.github_lib.get_data_of_token_holder', lambda *args: None)
@patch('auth.backends.GitHubBackend.authenticate', lambda *args: None)
def test_github_auth_view(self):
"""Send a request without authentication and check the response."""
Expand Down
13 changes: 13 additions & 0 deletions contributors/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@
from contributors.models.organization import Organization
from contributors.models.project import Project
from contributors.models.repository import Repository

__all__ = [
'CommonFields',
'CommitStats',
'Contribution',
'ContributionLabel',
'Contributor',
'IssueInfo',
'Label',
'Organization',
'Project',
'Repository',
]
10 changes: 10 additions & 0 deletions contributors/templatetags/contrib_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,13 @@ def get_canonical_url(context):
if request:
return request.build_absolute_uri(request.path)
return ''


@register.simple_tag
def calc_percent_achievement(numerator, denominator):
"""Get contributor statistics and required quantity."""
if numerator is not None:
if numerator / denominator > 1:
return 100.0
return numerator / denominator * 100
return 0
10 changes: 10 additions & 0 deletions contributors/tests/test_contributors_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
EXPECTED_CONTRIBUTORS_ISSUE_COUNT = 2
EXPECTED_CONTRIBUTORS_PR_COUNT = 2

TEST_CONTRIBUTORS = ["mintough57", "kinganduld", "indecing", "pilly1964", "suir1948"]


class TestContributorDetailView(TestCase):
"""Test the methods for the contributor's details view."""
Expand Down Expand Up @@ -149,3 +151,11 @@ def test_get_context_data(self):
self.assertEqual(response.context['contributors_issues_gte_1'], 2)
self.assertEqual(response.context['contributors_comments_gte_1'], 0)
self.assertEqual(response.context['contributors_editions_gte_1'], 0)

def test_get_context_data_contributor(self):
for contributor in TEST_CONTRIBUTORS:
response = self.client.get(reverse(
'contributors:contributor_achievements',
args=[contributor]),
)
self.assertEqual(response.status_code, HTTPStatus.OK)
5 changes: 5 additions & 0 deletions contributors/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
views.achievements.AchievementListView.as_view(),
name='achievements',
),
path(
'contributor_achievements/<slug:slug>',
views.contributor_achievements.ContributorAchievementListView.as_view(),
name='contributor_achievements',
),
path(
'landing/',
views.landing.LandingView.as_view(),
Expand Down
2 changes: 1 addition & 1 deletion contributors/utils/github_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.conf import settings

GITHUB_API_URL = 'https://api.github.com'
GITHUB_TOKEN_PROVIDER_URL = 'https://github.com/login/oauth/access_token' # noqa: E501,S105
GITHUB_TOKEN_PROVIDER_URL = 'https://github.com/login/oauth/access_token' # noqa: S105


def merge_dicts(*dicts):
Expand Down
1 change: 1 addition & 0 deletions contributors/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
about,
achievements,
config,
contributor_achievements,
contributor_compare,
filters,
home,
Expand Down
292 changes: 292 additions & 0 deletions contributors/views/contributor_achievements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
from django.db import models
from django.db.models.functions import Coalesce
from django.views import generic

from contributors.models import Contributor, Repository

ID = 'id'


class ContributorAchievementListView(generic.ListView):
"""Achievement list."""

template_name = 'contributor/contributor_achievements_list.html'
model = Contributor
contributors = Contributor.objects.with_contributions()

pull_request_ranges_for_achievements = [1, 10, 25, 50, 100]
commit_ranges_for_achievements = [1, 25, 50, 100, 200]
issue_ranges_for_achievements = [1, 5, 10, 25, 50]
comment_ranges_for_achievements = [1, 25, 50, 100, 200]
edition_ranges_for_achievements = [1, 100, 250, 500, 1000]

def get_context_data(self, **kwargs):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я бы разделил метод на несколько. Сейчас это огромная стена кода. Выделите подметоды, сделайте их приватными (_method_name), разделите логику.

"""Add context data for achievement list."""
self.contributors_amount = Contributor.objects.count()
context = super().get_context_data(**kwargs)
contributors = Contributor.objects.with_contributions()
current_contributor = self._get_cur_contributor()
repositories = self._get_repositories(current_contributor)
contributions = self._aggregate_contributions(repositories)

context.update(self._calculate_achievements(contributors, contributions))

context['current_contributor'] = current_contributor
context['contributors_amount'] = self.contributors_amount
context['contributors_with_any_contribution'] = (
self._contributors_with_any_contribution(contributors))
return context

def _get_cur_contributor(self):
return Contributor.objects.get(login=self.kwargs['slug'])

def _get_repositories(self, current_contributor):
"""Get repositories where the current contributor made contributions."""
return Repository.objects.select_related(
'organization',
).filter(
is_visible=True,
contribution__contributor=current_contributor,
).annotate(
commits=models.Count('id', filter=models.Q(contribution__type='cit')),
additions=Coalesce(models.Sum('contribution__stats__additions'), 0),
deletions=Coalesce(models.Sum('contribution__stats__deletions'), 0),
pull_requests=models.Count(
'contribution', filter=models.Q(contribution__type='pr'),
),
issues=models.Count(
'contribution',
filter=models.Q(contribution__type='iss')),
comments=models.Count(
'contribution',
filter=models.Q(contribution__type='cnt')),
).order_by('organization', 'name')

def _aggregate_contributions(self, repositories):
"""Aggregate all the contributions for the contributor."""
return repositories.values().aggregate(
contributor_deletions=models.Sum('deletions'),
contributor_additions=models.Sum('additions'),
contributor_commits=models.Sum('commits'),
contributor_pull_requests=models.Sum('pull_requests'),
contributor_issues=models.Sum('issues'),
contributor_comments=models.Sum('comments'),
)

def _calculate_achievements(self, contributors, contributions):
"""Calculate achievements for various contribution types."""
finished = []
unfinished = []

context = {
'commits': contributions['contributor_commits'],
'pull_requests': contributions['contributor_pull_requests'],
'issues': contributions['contributor_issues'],
'comments': contributions['contributor_comments'],
'total_editions': self._calculate_editions(contributions),
'total_actions': self._calculate_total_actions(contributions),
'pull_request_ranges_for_achievements':
self.pull_request_ranges_for_achievements,
}

# Process each type of achievement
finished, unfinished = self._process_achievements(
finished, unfinished, context, contributors, contributions
)
context['finished'] = finished
context['unfinished'] = unfinished
context['closed'] = len(finished)
context['all_achievements'] = len(finished) + len(unfinished)
return context

def _process_achievements(
self, finished, unfinished, context, contributors, contributions
):
"""Process all achievements types (pull request, commit, etc.)."""
# Pull request achievements:
finished, unfinished = self._process_pull_request_achievements(
finished, unfinished, context, contributors, contributions
)

# Commit achievements:
finished, unfinished = self._process_commit_achievements(
finished, unfinished, context, contributors, contributions
)

# Issue achievements:
finished, unfinished = self._process_issue_achievements(
finished, unfinished, context, contributors, contributions
)

# Comment achievements:
finished, unfinished = self._process_comment_achievements(
finished, unfinished, context, contributors, contributions
)

# Edition achievements:
finished, unfinished = self._process_edition_achievements(
finished, unfinished, context, contributors, contributions
)

return finished, unfinished

def _process_pull_request_achievements(
self, finished, unfinished, context, contributors, contributions
):
"""Process achievements related to pull requests."""
for pr_num in self.pull_request_ranges_for_achievements:
context[f'contributors_pull_requests_gte_{pr_num}'] = {
'stat': contributors.filter(pull_requests__gte=pr_num).count(),
'acomplished':
self._get_cur_contributor() in contributors.filter(
pull_requests__gte=pr_num
),
}
a_data = self._create_achievement_data(
pr_num, 'pull_requests', 'Pull requests'
)
finished, unfinished = self._update_achievement_status(
pr_num,
contributions['contributor_pull_requests'],
a_data,
finished,
unfinished
)
return finished, unfinished

def _process_commit_achievements(
self, finished, unfinished, context, contributors, contributions
):
"""Process achievements related to commits."""
for commit_num in self.commit_ranges_for_achievements:
context[f'contributors_commits_gte_{commit_num}'] = {
'stat': contributors.filter(commits__gte=commit_num).count(),
'acomplished':
self._get_cur_contributor() in contributors.filter(
commits__gte=commit_num
),
}
a_data = self._create_achievement_data(
commit_num, 'commits', 'Commits'
)
finished, unfinished = self._update_achievement_status(
commit_num,
contributions['contributor_commits'],
a_data,
finished,
unfinished
)
return finished, unfinished

def _process_issue_achievements(
self, finished, unfinished, context, contributors, contributions
):
"""Process achievements related to issues."""
for issue_num in self.issue_ranges_for_achievements:
context[f'contributors_issues_gte_{issue_num}'] = {
'stat': contributors.filter(issues__gte=issue_num).count(),
'acomplished':
self._get_cur_contributor() in contributors.filter(
issues__gte=issue_num),
}
a_data = self._create_achievement_data(
issue_num, 'issues', 'Issues'
)
finished, unfinished = self._update_achievement_status(
issue_num,
contributions['contributor_issues'],
a_data,
finished,
unfinished
)
return finished, unfinished

def _process_comment_achievements(
self, finished, unfinished, context, contributors, contributions
):
"""Process achievements related to comments."""
for comment_num in self.comment_ranges_for_achievements:
context[f'contributors_comments_gte_{comment_num}'] = {
'stat': contributors.filter(comments__gte=comment_num).count(),
'acomplished':
self._get_cur_contributor() in contributors.filter(
comments__gte=comment_num),
}
a_data = self._create_achievement_data(
comment_num, 'comments', 'Comments'
)
finished, unfinished = self._update_achievement_status(
comment_num,
contributions['contributor_comments'],
a_data,
finished,
unfinished
)
return finished, unfinished

def _process_edition_achievements(
self, finished, unfinished, context, contributors, contributions
):
"""Process achievements related to code editions (additions + deletions)."""
editions = self._calculate_editions(contributions)
for ed_num in self.edition_ranges_for_achievements:
context[f'contributors_editions_gte_{ed_num}'] = {
'stat': contributors.filter(editions__gte=ed_num).count(),
'acomplished':
self._get_cur_contributor() in contributors.filter(
editions__gte=ed_num),
}
a_data = self._create_achievement_data(
ed_num, 'code_editions', 'Additions and deletions'
)
finished, unfinished = self._update_achievement_status(
ed_num, editions, a_data, finished, unfinished
)
return finished, unfinished

def _create_achievement_data(self, num, type_key, type_name):
"""Create achievement data dictionary."""
return {
'img': f'images/achievments_icons/{type_key}-{num}.svg',
'name': f'{type_name} (equal to or more than {num})',
'description':
f"Make {type_name.lower()} in amount of equal to or more than {num}",
}

def _update_achievement_status(self, num, cur_value, a_data, finished, unfinished):
"""Update the achievement status."""
if num > (0 if cur_value is None else cur_value):
unfinished.append(a_data)
a_data['acomplished'] = False
else:
finished.append(a_data)
return finished, unfinished

def _calculate_editions(self, contributions):
"""Calculate total editions (additions + deletions)."""
return sum(
0 if edit is None else edit
for edit in [
contributions['contributor_additions'],
contributions['contributor_deletions'],
])

def _calculate_total_actions(self, contributions):
"""Calculate total actions"""
return sum(
0 if action is None else action
for action in [
contributions['contributor_commits'],
contributions['contributor_pull_requests'],
contributions['contributor_issues'],
contributions['contributor_comments'],
contributions['contributor_additions'],
contributions['contributor_deletions'],
])

def _contributors_with_any_contribution(self, contributors):
"""Get the contributors with any contributions."""
return {
'stat': contributors.filter(contribution_amount__gte=1).count(),
'acomplished': True,
}
Loading
Loading