Skip to content

Commit dc00615

Browse files
authored
Merge pull request #3 from tarxemo/dev
intellilgence ranking for users
2 parents a4c9a68 + 99fde6d commit dc00615

6 files changed

Lines changed: 156 additions & 3 deletions

File tree

github_management/models.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.urls import reverse
99
from .managers import GitHubUserManager
1010
from users.abstract_models import BaseUser
11+
import math
1112

1213
class Country(models.Model):
1314
"""Model to store available countries from committers.top"""
@@ -34,6 +35,7 @@ class GitHubUser(BaseUser):
3435
"""Model to store GitHub user information and their statistics."""
3536
country = models.ForeignKey('Country', on_delete=models.CASCADE, related_name='users')
3637
rank = models.PositiveIntegerField(default=0)
38+
intelligence_score = models.FloatField(default=0)
3739

3840
objects = GitHubUserManager()
3941

@@ -62,6 +64,42 @@ def is_followed_by(self, user):
6264
to_user__github_username__iexact=self.github_username
6365
).exists()
6466

67+
def compute_intelligence_score(self):
68+
followers = self.followers or 0
69+
following = self.following or 0
70+
contributions = self.contributions_last_year or 0
71+
public_repos = self.public_repos or 0
72+
public_gists = self.public_gists or 0
73+
74+
followers_score = math.log10(followers + 1)
75+
following_score = math.log10(following + 1) * 0.2
76+
contributions_score = math.log10(contributions + 1) * 2.0
77+
repos_score = math.log10(public_repos + 1) * 1.5
78+
gists_score = math.log10(public_gists + 1) * 0.5
79+
80+
account_age_years = 0.0
81+
if self.github_created_at:
82+
delta = timezone.now() - self.github_created_at
83+
account_age_years = max(delta.days / 365.0, 0.0)
84+
account_age_score = min(account_age_years, 10.0) * 0.1
85+
86+
activity_balance_score = 0.0
87+
if following > 0:
88+
ratio = followers / float(following)
89+
activity_balance_score = min(math.log10(ratio + 1), 2.0) * 0.5
90+
91+
score = (
92+
followers_score
93+
+ following_score
94+
+ contributions_score
95+
+ repos_score
96+
+ gists_score
97+
+ account_age_score
98+
+ activity_balance_score
99+
)
100+
101+
return round(score, 4)
102+
65103

66104
class GitHubFollowAction(models.Model):
67105
"""Tracks follow actions to GitHub users and their status."""

github_management/tasks.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,47 @@ def fetch_users_for_country(self, country_id):
9595
# Make sure to mark as not fetching even if there was an error
9696
Country.objects.filter(id=country_id).update(is_fetching=False)
9797

98+
99+
@shared_task(bind=True)
100+
def recompute_country_intelligence_ranking(self, country_id):
101+
"""Recompute intelligence_score and rank for all users in a country."""
102+
try:
103+
country = Country.objects.get(id=country_id)
104+
except Country.DoesNotExist:
105+
logger.warning(f"Country with id={country_id} does not exist")
106+
return
107+
108+
users = list(GitHubUser.objects.filter(country=country))
109+
if not users:
110+
logger.info(f"No users found for country {country.name} to rank")
111+
return
112+
113+
for user in users:
114+
user.intelligence_score = user.compute_intelligence_score()
115+
116+
# Sort by intelligence score descending, then by contributions and followers as tie-breakers
117+
users.sort(
118+
key=lambda u: (
119+
-(u.intelligence_score or 0),
120+
-(u.contributions_last_year or 0),
121+
-(u.followers or 0),
122+
)
123+
)
124+
125+
# Assign rank per country based on sorted order (1-based)
126+
for idx, user in enumerate(users, start=1):
127+
user.rank = idx
128+
129+
GitHubUser.objects.bulk_update(users, ["intelligence_score", "rank"])
130+
logger.info(f"Recomputed intelligence ranking for {len(users)} users in {country.name}")
131+
132+
133+
@shared_task(bind=True)
134+
def recompute_all_countries_intelligence_ranking(self):
135+
"""Recompute intelligence-based rankings for all countries."""
136+
for country in Country.objects.all().only("id"):
137+
recompute_country_intelligence_ranking.delay(country.id)
138+
98139
@shared_task
99140
def update_users_stats_batch(user_ids, model_name):
100141
"""

github_management/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
path('countries/', views.CountryListView.as_view(), name='country_list'),
1414
path('countries/<slug:slug>/', views.CountryDetailView.as_view(), name='country_detail'),
1515
path('countries/<slug:slug>/update-stats/', views.UpdateCountryUsersStatsView.as_view(), name='country_update_stats'),
16+
path('countries/<slug:slug>/recompute-ranking/', views.RecomputeCountryRankingView.as_view(), name='country_recompute_ranking'),
1617
path('countries/<slug:slug>/fetch/', views.FetchUsersView.as_view(), name='fetch_users'),
1718
path('api/countries/<slug:slug>/status/', views.FetchStatusView.as_view(), name='country_status'),
1819

@@ -38,4 +39,7 @@
3839
path('fetch-all-countries/',
3940
login_required(views.FetchAllCountriesView.as_view()),
4041
name='fetch_all_countries'),
42+
path('recompute-all-countries-ranking/',
43+
login_required(views.RecomputeAllCountriesRankingView.as_view()),
44+
name='recompute_all_countries_ranking'),
4145
]

github_management/views.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,27 @@ def get(self, request, *args, **kwargs):
6262
messages.error(request, "You do not have permission to perform this action.")
6363
return redirect('github_management:country_list')
6464

65+
66+
class RecomputeAllCountriesRankingView(View):
67+
"""View to trigger recomputing intelligence-based ranking for all countries (superuser only)."""
68+
def get(self, request, *args, **kwargs):
69+
if not request.user.is_superuser:
70+
messages.error(request, "You do not have permission to perform this action.")
71+
return redirect('github_management:country_list')
72+
73+
try:
74+
from .tasks import recompute_all_countries_intelligence_ranking
75+
task = recompute_all_countries_intelligence_ranking.delay()
76+
messages.success(
77+
request,
78+
f"Started recomputing intelligence ranking for all countries. Task ID: {task.id}"
79+
)
80+
except Exception as e:
81+
logger.error(f"Error enqueuing global ranking recomputation: {e}")
82+
messages.error(request, f"Failed to start global ranking recomputation: {str(e)}")
83+
84+
return redirect('github_management:country_list')
85+
6586
class UpdateCountryUsersStatsView(LoginRequiredMixin, UserPassesTestMixin, View):
6687
"""Trigger batch update of all GitHub users' stats for a given country (superuser only)."""
6788
def test_func(self):
@@ -82,6 +103,35 @@ def post(self, request, slug):
82103
messages.info(request, f"No users found for {country.name} to update.")
83104
return redirect('github_management:country_detail', slug=slug)
84105

106+
107+
class RecomputeCountryRankingView(LoginRequiredMixin, UserPassesTestMixin, View):
108+
"""Trigger recomputation of intelligence-based ranking for a given country (superuser only)."""
109+
def test_func(self):
110+
return self.request.user.is_superuser
111+
112+
def post(self, request, slug):
113+
country = get_object_or_404(Country, slug=slug)
114+
try:
115+
from .tasks import recompute_country_intelligence_ranking
116+
task = recompute_country_intelligence_ranking.delay(country.id)
117+
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
118+
return JsonResponse({
119+
'success': True,
120+
'message': f"Started recomputing intelligence ranking for users in {country.name}.",
121+
'task_id': str(task.id),
122+
})
123+
messages.success(request, f"Started recomputing intelligence ranking for users in {country.name}. Task ID: {task.id}")
124+
except Exception as e:
125+
logger.error(f"Error enqueuing ranking recomputation for {country.name}: {e}")
126+
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
127+
return JsonResponse({
128+
'success': False,
129+
'message': f"Failed to start ranking recomputation: {str(e)}",
130+
}, status=400)
131+
messages.error(request, f"Failed to start ranking recomputation: {str(e)}")
132+
133+
return redirect('github_management:country_detail', slug=slug)
134+
85135
try:
86136
from .tasks import update_users_stats_batch
87137
task = update_users_stats_batch.delay(user_ids, "GitHubUser")
@@ -110,7 +160,7 @@ def get(self, request, slug):
110160
country = get_object_or_404(Country, slug=slug)
111161

112162
# Get users for this country
113-
users = GitHubUser.objects.filter(country=country).order_by('-contributions_last_year')
163+
users = GitHubUser.objects.filter(country=country).order_by('rank', '-intelligence_score', '-contributions_last_year')
114164

115165
# Pagination
116166
paginator = Paginator(users, 25) # Show 25 users per page

templates/github_management/country_detail.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,30 @@ <h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ country.name }}<
8282
});
8383
});
8484
</script>
85-
<div class="mt-4 flex md:mt-0 md:ml-4">
85+
<div class="mt-4 flex md:mt-0 md:ml-4 space-x-3">
8686
{% if request.user.is_superuser %}
8787
<form method="post" action="{% url 'github_management:fetch_users' slug=country.slug %}" class="inline-flex">
8888
{% csrf_token %}
8989
<button type="submit"
9090
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
9191
{% if country.is_fetching %}disabled{% endif %}>
9292
<svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
93-
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 110 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
93+
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 011.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 110 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
9494
</svg>
9595
{% if country.is_fetching %}Fetching...{% else %}Refresh Users{% endif %}
9696
</button>
9797
</form>
98+
99+
<form method="post" action="{% url 'github_management:country_recompute_ranking' slug=country.slug %}" class="inline-flex">
100+
{% csrf_token %}
101+
<button type="submit"
102+
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
103+
<svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
104+
<path fill-rule="evenodd" d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 5a1 1 0 012 0v4a1 1 0 01-.293.707l-2 2a1 1 0 11-1.414-1.414L9 8.586V5z" clip-rule="evenodd" />
105+
</svg>
106+
Recompute Intelligence Ranking
107+
</button>
108+
</form>
98109
{% endif %}
99110
</div>
100111
</div>

templates/github_management/country_list.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ <h1 class="text-3xl font-bold text-gray-900 dark:text-white">Countries</h1>
4545
</svg>
4646
Fetch All Countries
4747
</a>
48+
49+
<a href="{% url 'github_management:recompute_all_countries_ranking' %}"
50+
onclick="return confirm('This will recompute intelligence ranking for all countries. Continue?')"
51+
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
52+
<svg class="-ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
53+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
54+
</svg>
55+
Recompute All Rankings
56+
</a>
4857
{% endif %}
4958
</div>
5059
</div>

0 commit comments

Comments
 (0)