Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
09ed6fa
Add github_username to CustomUser for stats tracking (#615)
Moueen-Togarvi Dec 9, 2025
fe7e86a
feat: Add Djangonaut GitHub stats collection to Admin (Issue #615)
Moueen-Togarvi Dec 10, 2025
d8b71b6
update
Moueen-Togarvi Dec 10, 2025
9998ca3
Merge branch 'develop' into djangonaut-stats
FarhanAliRaza Jan 17, 2026
c26dd6f
Refactor GitHub stats collection in admin and views
FarhanAliRaza Jan 17, 2026
d73e913
Merge branch 'djangonaut-stats' of github.com:Moueen-Togarvi/wagtail-…
Moueen-Togarvi Jan 17, 2026
d25de52
Simplify GitHub stats collection per PR feedback
Moueen-Togarvi Jan 18, 2026
5b0cad3
Add unit tests for GitHubStatsCollector and related classes
Moueen-Togarvi Jan 18, 2026
4bd5d33
track closed vs merged PRs separately
Moueen-Togarvi Jan 24, 2026
0e98d28
add the test
Moueen-Togarvi Jan 24, 2026
8f8c325
--no-input is not a parameter
FarhanAliRaza Feb 1, 2026
9a0a649
Merge upstream/develop into djangonaut-stats
FarhanAliRaza Feb 1, 2026
c9b0345
Fetch merged_at status for PRs in GitHubStatsCollector
FarhanAliRaza Feb 1, 2026
cff81b0
Refactor HTML and CSS for admin report display with dark mode support
FarhanAliRaza Feb 1, 2026
fb48a92
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 1, 2026
b2f07a7
Utilize more of Django's functionality.
tim-schilling Feb 5, 2026
68fa29c
Refactor GitHub stats, testimonials, and view organization
FarhanAliRaza Mar 14, 2026
cdf1395
Merge branch 'develop' into djangonaut-stats
FarhanAliRaza Mar 14, 2026
e86e71f
Derive monitored repos from session projects instead of settings
FarhanAliRaza Mar 14, 2026
9c6b185
Merge branch 'djangonaut-stats' of github.com:Moueen-Togarvi/wagtail-…
FarhanAliRaza Mar 14, 2026
06f1125
fixed migration
FarhanAliRaza Mar 14, 2026
bc237ed
Removed Stale test
FarhanAliRaza Mar 14, 2026
a7c8f72
Simplify github_stats service and fix Cancel button styling
FarhanAliRaza Mar 16, 2026
7cf10ee
Add tests to improve Codecov patch coverage
FarhanAliRaza Mar 16, 2026
ee23b70
Normalize future dates in stats form, always regenerate testimonial s…
FarhanAliRaza Apr 1, 2026
8356f29
refactor: group stats queries by team, capture merged-not-created PRs…
FarhanAliRaza Apr 20, 2026
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: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ RECAPTCHA_PRIVATE_KEY='dummy_value'
# The toolbar should not be used in a remote environment
ENABLE_TOOLBAR=""
DJANGO_RUNSERVER_HIDE_WARNING="True"
# GitHub API Configuration (for Djangonaut stats - Issue #615)
# Get token from: https://github.com/settings/tokens
# Required scopes: public_repo
GITHUB_TOKEN= <Github token>
4 changes: 4 additions & 0 deletions .env.template.local
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ ENABLE_TOOLBAR=""
PLAYWRIGHT_TEST_USERNAME="playwright_test"
PLAYWRIGHT_TEST_PASSWORD="hunter2"
DJANGO_RUNSERVER_HIDE_WARNING="True"
# GitHub API Configuration (for Djangonaut stats - Issue #615)
# Get token from: https://github.com/settings/tokens
# Required scopes: public_repo
GITHUB_TOKEN=<Github token>
25 changes: 22 additions & 3 deletions home/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
send_session_results_view,
send_team_welcome_emails_view,
)
from .views.sessions import collect_stats_view

User = get_user_model()

Expand All @@ -50,8 +51,9 @@ class EventAdmin(DescriptiveSearchMixin, admin.ModelAdmin):

@admin.register(Project)
class ProjectAdmin(DescriptiveSearchMixin, admin.ModelAdmin):
list_display = ("name", "url")
search_fields = ("name",)
list_display = ("name", "url", "monitor_all_organization_repos")
list_filter = ("monitor_all_organization_repos",)
search_fields = ("name", "url")
ordering = ("name",)


Expand Down Expand Up @@ -370,13 +372,25 @@ class SessionAdmin(DescriptiveSearchMixin, admin.ModelAdmin):
preview_email.waitlist_email_action,
preview_email.team_welcome_email_action,
]
list_display = ("title", "start_date", "end_date", "form_teams", "email_actions")
list_display = (
"title",
"start_date",
"end_date",
"form_teams",
"collect_stats",
"email_actions",
)

@admin.display(description="Form Teams")
def form_teams(self, obj):
href = reverse("admin:session_form_teams", kwargs={"session_id": obj.id})
return mark_safe(f'<a href="{href}">Form Teams</a>')

@admin.display(description="GitHub Stats")
def collect_stats(self, obj):
href = reverse("admin:session_collect_stats", args=[obj.id])
return mark_safe(f'<a href="{href}">Collect Stats</a>')

@admin.display(description="Email Actions")
def email_actions(self, obj):
actions = [
Expand Down Expand Up @@ -416,6 +430,11 @@ def get_urls(self):
self.admin_site.admin_view(calculate_overlap_ajax),
name="session_calculate_overlap",
),
path(
"<int:session_id>/collect-stats/",
self.admin_site.admin_view(collect_stats_view),
name="session_collect_stats",
),
path(
"<int:session_id>/send-session-results/",
self.admin_site.admin_view(send_session_results_view),
Expand Down
2 changes: 1 addition & 1 deletion home/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class Meta:
model = Project

name = factory.Sequence(lambda n: "Project %d" % n)
url = factory.Sequence(lambda n: "https://github.com/project-%d" % n)
url = factory.Sequence(lambda n: "https://github.com/test-org/project-%d" % n)


class TeamFactory(factory.django.DjangoModelFactory):
Expand Down
133 changes: 133 additions & 0 deletions home/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import csv
import io
from datetime import date

from django import forms
from django.core import validators
Expand Down Expand Up @@ -1204,6 +1205,138 @@ def save(self) -> bool:
return is_accepted


class CompareAvailabilityForm(forms.Form):
"""
Form for handling compare availability querystring parameters.

Validates session_id, user selection, and offset parameters.
Also determines which users the current user can select for comparison.
"""

session = forms.ModelChoiceField(
queryset=Session.objects.all(),
required=False,
)
users = forms.CharField(required=False)
offset = forms.FloatField(required=False, initial=0)

def __init__(self, *args, user: CustomUser, **kwargs):
"""
Initialize form with the requesting user.

Args:
user: The currently logged-in user making the request
"""
super().__init__(*args, **kwargs)
self.user = user
self._session = None
self._session_membership = None

def clean_offset(self) -> float:
"""Return offset value, defaulting to 0 if not provided or invalid."""
offset = self.cleaned_data.get("offset")
return offset if offset is not None else 0.0

def clean_users(self) -> set[int]:
"""Parse user IDs from form data, handling both comma-separated and multiple params."""
result = set()
# Handle multiple params (from select multiple) and comma-separated values
values = (
self.data.getlist("users")
if hasattr(self.data, "getlist")
else [self.data.get("users", "")]
)
for value in values:
for uid in str(value).split(","):
if uid.strip().isdigit():
result.add(int(uid.strip()))
return result

def clean_session(self) -> Session | None:
if session := self.cleaned_data.get("session"):
self._session_membership = (
SessionMembership.objects.for_session(session)
.for_user(self.user)
.first()
)
self._session = session
return self._session

def get_selectable_users(self) -> list[CustomUser]:
"""
Get users that the current user can select for comparison.

Returns:
List of CustomUser objects the user can compare
"""
return list(
CustomUser.objects.for_comparing_availability(
user=self.user,
session=self._session,
session_membership=self._session_membership,
)
)

def get_selected_users(
self, selectable_users: list[CustomUser]
) -> list[CustomUser]:
"""
Get the users that are currently selected from the selectable users.

Args:
selectable_users: List of users that can be selected

Returns:
List of selected CustomUser objects
"""
if not self.cleaned_data["users"]:
return []
return [u for u in selectable_users if u.id in self.cleaned_data["users"]]

def get_offset_hours(self) -> float:
"""Return the validated offset hours value."""
return self.cleaned_data.get("offset", 0)


class CollectStatsForm(forms.Form):
"""Form for collecting GitHub stats with date range selection."""

start_date = forms.DateField(
label=_("Start Date"),
widget=forms.DateInput(attrs={"type": "date"}),
)
end_date = forms.DateField(
label=_("End Date"),
widget=forms.DateInput(attrs={"type": "date"}),
)

def clean_start_date(self) -> date:
start_date = self.cleaned_data["start_date"]
today = date.today()
if start_date > today:
return today
return start_date

def clean_end_date(self) -> date:
end_date = self.cleaned_data["end_date"]
today = date.today()
if end_date > today:
return today
return end_date

def clean(self) -> dict:
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")

if start_date and end_date and start_date > end_date:
raise forms.ValidationError(
_("Start date must be before or equal to end date.")
)

return cleaned_data


class TestimonialFormRenderer(DjangoTemplates):
field_template_name = "home/testimonials/field.html"

Expand Down
1 change: 1 addition & 0 deletions home/management/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Management commands package."""
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Migration(migrations.Migration):
name="djangonauts_have_access",
field=models.BooleanField(
default=False,
help_text="Whether team detail pages are visible to Djangonauts. Automatically set to True when team welcome emails are sent. This will be ignored once session start date is in the past.",
help_text="Whether Djangonauts can access their team detail pages. Automatically set to True when team welcome emails are sent. This will be ignored once session start date is in the past.",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.2.12 on 2026-03-14 20:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("home", "0050_add_djangonauts_have_access_to_session"),
]

operations = [
migrations.AddField(
model_name="project",
name="monitor_all_organization_repos",
field=models.BooleanField(
default=False,
help_text="When enabled, GitHub stats collection searches all source repositories in this GitHub organization instead of only this repository.",
),
),
]
22 changes: 22 additions & 0 deletions home/models/session.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
from urllib.parse import urlparse

from django.conf import settings
from django.db import models
Expand Down Expand Up @@ -34,10 +35,31 @@ class Project(models.Model):
url = models.URLField(
help_text=_("The URL for the project repository or website"),
)
monitor_all_organization_repos = models.BooleanField(
default=False,
help_text=_(
"When enabled, GitHub stats collection searches all source "
"repositories in this GitHub organization instead of only this "
"repository."
),
)

class Meta:
ordering = ["name"]

@property
def github_repo(self) -> tuple[str, str] | None:
"""Return the GitHub org, repo pair from the configured project URL."""
parsed_url = urlparse(self.url)
if parsed_url.netloc != "github.com":
return None

path_parts = [part for part in parsed_url.path.split("/") if part]
if len(path_parts) < 2:
return None

return path_parts[0], path_parts[1]

def __str__(self) -> str:
return self.name

Expand Down
1 change: 1 addition & 0 deletions home/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Services package for home app."""
Loading
Loading