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

Replace DataTables with backend solutions #100

Merged
merged 2 commits into from Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors/forms/__init__.py
@@ -1 +1,2 @@
from contributors.forms.admin_forms import OrgNamesForm, RepoNamesForm
from contributors.forms.forms import ListSortAndSearchForm
28 changes: 28 additions & 0 deletions contributors/forms/forms.py
@@ -0,0 +1,28 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from django.utils.translation import gettext_lazy as _

from contributors.utils.misc import prepare_choices


class ListSortAndSearchForm(forms.Form):
"""A form for sort and search."""

sort = forms.ChoiceField(label=_("Sort by"), required=False)
descending = forms.BooleanField(
label=_("Descending"), initial=False, required=False,
)
search = forms.CharField(label=_("Search"), required=False)
page = forms.IntegerField(
widget=forms.HiddenInput(), initial=1, required=False,
)

def __init__(self, sortable_fields, *args, **kwargs):
"""Initialize the form."""
super().__init__(*args, **kwargs)
self.fields['sort'].choices = prepare_choices(sortable_fields)
self.helper = FormHelper()
self.helper.form_method = 'get'
self.helper.form_class = 'form-inline my-3'
self.helper.add_input(Submit('', _("Apply")))
9 changes: 6 additions & 3 deletions contributors/models/contributor.py
Expand Up @@ -9,11 +9,14 @@


class ContributorQuerySet(models.QuerySet):
"""Custom contributor QuerySet."""
"""A custom contributor QuerySet."""

def visible(self):
"""Return only visible contributors."""
return self.filter(is_visible=True)
return self.filter(
is_visible=True,
contribution__repository__is_visible=True,
)

def with_contributions(self):
"""Return a list of contributors annotated with contributions."""
Expand All @@ -36,7 +39,7 @@ def for_month(self):


class Contributor(CommonFields):
"""Model representing a contributor."""
"""A model representing a contributor."""

login = models.CharField(_("login"), max_length=NAME_LENGTH)
avatar_url = models.URLField(_("avatar URL"))
Expand Down
4 changes: 2 additions & 2 deletions contributors/urls.py
Expand Up @@ -12,7 +12,7 @@
),
path(
'organizations/<int:pk>',
views.organization.DetailView.as_view(),
views.organization.OrgRepositoryList.as_view(),
name='organization_details',
),
path(
Expand All @@ -22,7 +22,7 @@
),
path(
'repositories/<int:pk>',
views.repository.DetailView.as_view(),
views.repository.RepoContributorList.as_view(),
name='repository_details',
),
path(
Expand Down
16 changes: 16 additions & 0 deletions contributors/utils/misc.py
Expand Up @@ -7,6 +7,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as trans

NUM_OF_MONTHS_IN_A_YEAR = 12

Expand Down Expand Up @@ -129,3 +130,18 @@ def datetime_month_ago():
"""Return datetime 1 month ago from now."""
dt_now = timezone.now()
return dt_now - relativedelta.relativedelta(months=1)


def prepare_choices(collection):
"""Return a collection of 2-tuples to use as choices."""
normalized_items = []
for col_item in collection:
if isinstance(col_item, str):
normalized_items.append(
(col_item, trans(col_item.replace('_', ' ').capitalize())),
)
elif isinstance(col_item, tuple):
normalized_items.append(col_item)
else:
raise TypeError("Unknown item type")
return normalized_items
82 changes: 82 additions & 0 deletions contributors/utils/mixins.py
@@ -0,0 +1,82 @@
from contextlib import suppress
from functools import reduce
from operator import __or__

from django.core.paginator import Paginator
from django.db.models import Q # noqa: WPS347
from django.views.generic.list import MultipleObjectMixin

from contributors.forms import ListSortAndSearchForm


def get_page_range(page_obj):
"""
Return a range of page numbers to display.

The first and last 5 pages are visible when the current page is among them.
3 page numbers are displayed in other cases.
"""
index = page_obj.number - 1
max_index = page_obj.paginator.num_pages
if index < 4:
start_index = 0
end_index = 5
elif index <= max_index - 5:
start_index = index - 1
end_index = index + 2
else:
start_index = max_index - 5
end_index = max_index
return page_obj.paginator.page_range[start_index:end_index]


class PaginationMixin(MultipleObjectMixin):
"""A mixin for pagination."""

paginate_by = 25

def get_context_data(self, **kwargs):
"""Add context."""
context = super().get_context_data(**kwargs)

paginator = Paginator(self.get_adjusted_queryset(), self.paginate_by)
page_obj = paginator.get_page(self.request.GET.get('page'))

context['page_obj'] = page_obj
context['page_range'] = get_page_range(page_obj)
return context


class TableControlsMixin(object):
"""A mixin for table controls."""

def get_adjusted_queryset(self):
"""Return a sorted and filtered QuerySet."""
self.ordering = self.request.GET.get('sort', self.get_ordering())
filter_value = self.request.GET.get('search', '').strip()
lookups = {}
for field in self.searchable_fields:
key = '{0}{1}'.format(field, '__icontains')
lookups[key] = filter_value
expressions = [Q(**{key: value}) for key, value in lookups.items()] # noqa: WPS110,E501
direction = '-' if self.request.GET.get('descending', False) else ''
return self.get_queryset().filter(
reduce(__or__, expressions),
).order_by('{0}{1}'.format(direction, self.get_ordering()))

def get_context_data(self, **kwargs):
"""Add context."""
context = super().get_context_data(**kwargs)

form = ListSortAndSearchForm(self.sortable_fields, self.request.GET)
get_params = self.request.GET.copy()
with suppress(KeyError):
get_params.pop('page')

context['form'] = form
context['get_params'] = get_params.urlencode()
return context


class TableControlsAndPaginationMixin(TableControlsMixin, PaginationMixin):
"""Combine mixins for table controls and pagination."""
18 changes: 15 additions & 3 deletions contributors/views/contributors.py
@@ -1,11 +1,23 @@
from django.views import generic

from contributors.models.contributor import Contributor
from contributors.models import Contributor
from contributors.utils.mixins import TableControlsAndPaginationMixin


class ListView(generic.ListView):
class ListView(TableControlsAndPaginationMixin, generic.ListView):
"""A list of contributors with contributions."""

queryset = Contributor.objects.visible().with_contributions()
template_name = 'contributors_list.html'
context_object_name = 'contributors_list'
sortable_fields = (
'login',
'name',
'commits',
'additions',
'deletions',
'pull_requests',
'issues',
'comments',
)
searchable_fields = ('login', 'name')
ordering = sortable_fields[0]
7 changes: 3 additions & 4 deletions contributors/views/contributors_for_month.py
@@ -1,10 +1,9 @@
from django.views import generic

from contributors.models.contributor import Contributor
from contributors.models import Contributor
from contributors.utils import misc
from contributors.views import contributors


class ListView(generic.ListView):
class ListView(contributors.ListView):
"""A list of contributors with monthly contributions."""

template_name = 'contributors_for_month.html'
Expand Down
43 changes: 18 additions & 25 deletions contributors/views/organization.py
@@ -1,35 +1,28 @@
from django.db.models import Count, Q # noqa: WPS347
from django.views import generic
from django.utils.translation import gettext_lazy as _

from contributors.models import Organization
from contributors.views import repositories


class DetailView(generic.DetailView):
"""Organization's details."""
class OrgRepositoryList(repositories.ListView):
"""An organization's details."""

model = Organization
template_name = 'organization_details.html'
sortable_fields = ( # noqa: WPS317
'name',
('project__name', _("Project")),
'pull_requests',
'issues',
('contributors_count', _("Contributors")),
)

def get_queryset(self):
"""Get a dataset."""
self.organization = Organization.objects.get(pk=self.kwargs['pk'])
return super().get_queryset().filter(organization=self.organization)

def get_context_data(self, **kwargs):
"""Add additional context for the organization."""
"""Add context."""
context = super().get_context_data(**kwargs)

repositories = (
self.object.repository_set.filter(is_visible=True).filter(
Q(contribution__contributor__is_visible=True)
| Q(contributors__isnull=True),
).annotate(
pull_requests=Count(
'contribution', filter=Q(contribution__type='pr'),
),
issues=Count(
'contribution', filter=Q(contribution__type='iss'),
),
contributors_count=Count(
'contribution__contributor', distinct=True,
),
)
)

context['repositories'] = repositories
context['organization'] = self.organization
return context
14 changes: 10 additions & 4 deletions contributors/views/organizations.py
@@ -1,15 +1,21 @@
from django.db.models import Count
from django.utils.translation import gettext_lazy as _
from django.views import generic

from contributors.models import Organization
from contributors.utils.mixins import TableControlsAndPaginationMixin


class ListView(generic.ListView):
"""A view for a list of organizations."""
class ListView(TableControlsAndPaginationMixin, generic.ListView):
"""A list of organizations."""

queryset = Organization.objects.filter(
repository__is_visible=True,
).annotate(repository_count=Count('repository'))

template_name = 'organizations_list.html'
context_object_name = 'organizations_list'
sortable_fields = (
'name',
('repository_count', _("Repositories")),
)
searchable_fields = ('name',)
ordering = sortable_fields[0]
20 changes: 16 additions & 4 deletions contributors/views/repositories.py
@@ -1,16 +1,19 @@
from django.db.models import Count, Q # noqa: WPS347
from django.utils.translation import gettext_lazy as _
from django.views import generic

from contributors.models import Repository
from contributors.utils.mixins import TableControlsAndPaginationMixin


class ListView(generic.ListView):
"""A view for a list of repositories."""
class ListView(TableControlsAndPaginationMixin, generic.ListView):
"""A list of repositories."""

queryset = (
Repository.objects.select_related('organization').filter(
Q(contribution__contributor__is_visible=True)
| Q(contributors__isnull=True),
is_visible=True,
contribution__contributor__is_visible=True,
).annotate(
pull_requests=Count(
'contribution', filter=Q(contribution__type='pr'),
Expand All @@ -24,4 +27,13 @@ class ListView(generic.ListView):
)
)
template_name = 'repositories_list.html'
context_object_name = 'repositories_list'
sortable_fields = ( # noqa: WPS317
'name',
('organization__name', _("Organization")),
('project__name', _("Project")),
'pull_requests',
'issues',
('contributors_count', _("Contributors")),
)
searchable_fields = ('name', 'organization__name', 'project__name')
ordering = sortable_fields[0]
39 changes: 10 additions & 29 deletions contributors/views/repository.py
@@ -1,38 +1,19 @@
from django.db.models import Count, Q, Sum # noqa: WPS347
from django.db.models.functions import Coalesce
from django.views import generic

from contributors.models import Repository
from contributors.views import contributors


class DetailView(generic.DetailView):
"""Repository's details."""
class RepoContributorList(contributors.ListView):
"""A repository's details."""

model = Repository
template_name = 'repository_details.html'

def get_queryset(self):
"""Get a dataset."""
self.repository = Repository.objects.get(pk=self.kwargs['pk'])
return self.repository.contributors.visible().with_contributions()

def get_context_data(self, **kwargs):
"""Add additional context for the repository."""
"""Add context."""
context = super().get_context_data(**kwargs)

contributors = self.object.contributors.filter(
is_visible=True,
).annotate(
pull_requests=Count(
'contribution', filter=Q(contribution__type='pr'),
),
issues=Count(
'contribution', filter=Q(contribution__type='iss'),
),
comments=Count(
'contribution', filter=Q(contribution__type='cnt'),
),
commits=Count(
'contribution', filter=Q(contribution__type='cit'),
),
additions=Coalesce(Sum('contribution__stats__additions'), 0),
deletions=Coalesce(Sum('contribution__stats__deletions'), 0),
)

context['contributors'] = contributors
context['repository'] = self.repository
return context
Binary file modified locale/ru/LC_MESSAGES/django.mo
Binary file not shown.