|
4 | 4 | import re
|
5 | 5 | import socket
|
6 | 6 | import time
|
| 7 | +from datetime import datetime, timedelta |
7 | 8 | from io import BytesIO
|
8 | 9 | from pathlib import Path
|
9 | 10 | from urllib.parse import urlparse
|
|
23 | 24 | from django.shortcuts import get_object_or_404, redirect, render
|
24 | 25 | from django.utils.dateparse import parse_datetime
|
25 | 26 | from django.utils.text import slugify
|
26 |
| -from django.utils.timezone import localtime |
| 27 | +from django.utils.timezone import localtime, now |
27 | 28 | from django.views.decorators.http import require_http_methods
|
28 |
| -from django.views.generic import DetailView |
| 29 | +from django.views.generic import DetailView, ListView |
29 | 30 | from django_filters.views import FilterView
|
30 | 31 | from rest_framework.views import APIView
|
31 | 32 |
|
32 | 33 | from website.bitcoin_utils import create_bacon_token
|
| 34 | +from website.forms import GitHubURLForm |
33 | 35 | from website.models import IP, BaconToken, Contribution, Organization, Project, Repo
|
34 | 36 | from website.utils import admin_required
|
35 | 37 |
|
@@ -83,6 +85,105 @@ def distribute_bacon(request, contribution_id):
|
83 | 85 | return render(request, "select_contribution.html", {"contributions": contributions})
|
84 | 86 |
|
85 | 87 |
|
| 88 | +class ProjectDetailView(DetailView): |
| 89 | + model = Project |
| 90 | + period = None |
| 91 | + selected_year = None |
| 92 | + |
| 93 | + def post(self, request, *args, **kwargs): |
| 94 | + from django.core.management import call_command |
| 95 | + |
| 96 | + project = self.get_object() |
| 97 | + |
| 98 | + if "refresh_stats" in request.POST: |
| 99 | + call_command("update_projects", "--project_id", project.pk) |
| 100 | + messages.success(request, f"Refreshing stats for {project.name}") |
| 101 | + |
| 102 | + elif "refresh_contributor_stats" in request.POST: |
| 103 | + owner_repo = project.github_url.rstrip("/").split("/")[-2:] |
| 104 | + repo = f"{owner_repo[0]}/{owner_repo[1]}" |
| 105 | + call_command("fetch_contributor_stats", "--repo", repo) |
| 106 | + messages.success(request, f"Refreshing contributor stats for {project.name}") |
| 107 | + |
| 108 | + elif "refresh_contributors" in request.POST: |
| 109 | + call_command("fetch_contributors", "--project_id", project.pk) |
| 110 | + return redirect("project_view", slug=project.slug) |
| 111 | + |
| 112 | + def get(self, request, *args, **kwargs): |
| 113 | + project = self.get_object() |
| 114 | + project.project_visit_count += 1 |
| 115 | + project.save() |
| 116 | + return super().get(request, *args, **kwargs) |
| 117 | + |
| 118 | + def get_context_data(self, **kwargs): |
| 119 | + context = super().get_context_data(**kwargs) |
| 120 | + end_date = now() |
| 121 | + display_end_date = end_date.date() |
| 122 | + selected_year = self.request.GET.get("year", None) |
| 123 | + if selected_year: |
| 124 | + start_date = datetime(int(selected_year), 1, 1) |
| 125 | + display_end_date = datetime(int(selected_year), 12, 31) |
| 126 | + else: |
| 127 | + self.period = self.request.GET.get("period", "30") |
| 128 | + days = int(self.period) |
| 129 | + start_date = end_date - timedelta(days=days) |
| 130 | + start_date = start_date.date() |
| 131 | + |
| 132 | + contributions = Contribution.objects.filter( |
| 133 | + created__date__gte=start_date, |
| 134 | + created__date__lte=display_end_date, |
| 135 | + repository=self.get_object(), |
| 136 | + ) |
| 137 | + |
| 138 | + user_stats = {} |
| 139 | + for contribution in contributions: |
| 140 | + username = contribution.github_username |
| 141 | + if username not in user_stats: |
| 142 | + user_stats[username] = { |
| 143 | + "commits": 0, |
| 144 | + "issues_opened": 0, |
| 145 | + "issues_closed": 0, |
| 146 | + "prs": 0, |
| 147 | + "comments": 0, |
| 148 | + "total": 0, |
| 149 | + } |
| 150 | + if contribution.contribution_type == "commit": |
| 151 | + user_stats[username]["commits"] += 1 |
| 152 | + elif contribution.contribution_type == "issue_opened": |
| 153 | + user_stats[username]["issues_opened"] += 1 |
| 154 | + elif contribution.contribution_type == "issue_closed": |
| 155 | + user_stats[username]["issues_closed"] += 1 |
| 156 | + elif contribution.contribution_type == "pull_request": |
| 157 | + user_stats[username]["prs"] += 1 |
| 158 | + elif contribution.contribution_type == "comment": |
| 159 | + user_stats[username]["comments"] += 1 |
| 160 | + total = ( |
| 161 | + user_stats[username]["commits"] * 5 |
| 162 | + + user_stats[username]["prs"] * 3 |
| 163 | + + user_stats[username]["issues_opened"] * 2 |
| 164 | + + user_stats[username]["issues_closed"] * 2 |
| 165 | + + user_stats[username]["comments"] |
| 166 | + ) |
| 167 | + user_stats[username]["total"] = total |
| 168 | + |
| 169 | + user_stats = dict(sorted(user_stats.items(), key=lambda x: x[1]["total"], reverse=True)) |
| 170 | + |
| 171 | + current_year = now().year |
| 172 | + year_list = list(range(current_year, current_year - 10, -1)) |
| 173 | + |
| 174 | + context.update( |
| 175 | + { |
| 176 | + "user_stats": user_stats, |
| 177 | + "period": self.period, |
| 178 | + "start_date": start_date.strftime("%Y-%m-%d"), |
| 179 | + "end_date": display_end_date.strftime("%Y-%m-%d"), |
| 180 | + "year_list": year_list, |
| 181 | + "selected_year": selected_year, |
| 182 | + } |
| 183 | + ) |
| 184 | + return context |
| 185 | + |
| 186 | + |
86 | 187 | class ProjectBadgeView(APIView):
|
87 | 188 | def get(self, request, slug):
|
88 | 189 | # Retrieve the project or return 404
|
@@ -138,6 +239,94 @@ def get(self, request, slug):
|
138 | 239 | return response
|
139 | 240 |
|
140 | 241 |
|
| 242 | +class ProjectListView(ListView): |
| 243 | + model = Project |
| 244 | + context_object_name = "projects" |
| 245 | + |
| 246 | + def get_context_data(self, **kwargs): |
| 247 | + context = super().get_context_data(**kwargs) |
| 248 | + context["form"] = GitHubURLForm() |
| 249 | + context["sort_by"] = self.request.GET.get("sort_by", "-created") |
| 250 | + context["order"] = self.request.GET.get("order", "desc") |
| 251 | + return context |
| 252 | + |
| 253 | + def post(self, request, *args, **kwargs): |
| 254 | + if "refresh_stats" in request.POST: |
| 255 | + from django.core.management import call_command |
| 256 | + |
| 257 | + call_command("update_projects") |
| 258 | + messages.success(request, "Refreshing project statistics...") |
| 259 | + return redirect("project_list") |
| 260 | + |
| 261 | + if "refresh_contributors" in request.POST: |
| 262 | + from django.core.management import call_command |
| 263 | + |
| 264 | + projects = Project.objects.all() |
| 265 | + for project in projects: |
| 266 | + owner_repo = project.github_url.rstrip("/").split("/")[-2:] |
| 267 | + repo = f"{owner_repo[0]}/{owner_repo[1]}" |
| 268 | + call_command("fetch_contributor_stats", "--repo", repo) |
| 269 | + messages.success(request, "Refreshing contributor data...") |
| 270 | + return redirect("project_list") |
| 271 | + |
| 272 | + form = GitHubURLForm(request.POST) |
| 273 | + if form.is_valid(): |
| 274 | + github_url = form.cleaned_data["github_url"] |
| 275 | + # Extract the repository part of the URL |
| 276 | + match = re.match(r"https://github.com/([^/]+/[^/]+)", github_url) |
| 277 | + if match: |
| 278 | + repo_path = match.group(1) |
| 279 | + api_url = f"https://api.github.com/repos/{repo_path}" |
| 280 | + response = requests.get(api_url) |
| 281 | + if response.status_code == 200: |
| 282 | + data = response.json() |
| 283 | + # if the description is empty, use the name as the description |
| 284 | + if not data["description"]: |
| 285 | + data["description"] = data["name"] |
| 286 | + |
| 287 | + # Check if a project with the same slug already exists |
| 288 | + slug = data["name"].lower() |
| 289 | + if Project.objects.filter(slug=slug).exists(): |
| 290 | + messages.error(request, "A project with this slug already exists.") |
| 291 | + return redirect("project_list") |
| 292 | + |
| 293 | + project, created = Project.objects.get_or_create( |
| 294 | + github_url=github_url, |
| 295 | + defaults={ |
| 296 | + "name": data["name"], |
| 297 | + "slug": slug, |
| 298 | + "description": data["description"], |
| 299 | + "wiki_url": data["html_url"], |
| 300 | + "homepage_url": data.get("homepage", ""), |
| 301 | + "logo_url": data["owner"]["avatar_url"], |
| 302 | + }, |
| 303 | + ) |
| 304 | + if created: |
| 305 | + messages.success(request, "Project added successfully.") |
| 306 | + else: |
| 307 | + messages.info(request, "Project already exists.") |
| 308 | + else: |
| 309 | + messages.error(request, "Failed to fetch project from GitHub.") |
| 310 | + else: |
| 311 | + messages.error(request, "Invalid GitHub URL.") |
| 312 | + return redirect("project_list") |
| 313 | + context = self.get_context_data() |
| 314 | + context["form"] = form |
| 315 | + return self.render_to_response(context) |
| 316 | + |
| 317 | + def get_queryset(self): |
| 318 | + queryset = super().get_queryset() |
| 319 | + sort_by = self.request.GET.get("sort_by", "-created") |
| 320 | + order = self.request.GET.get("order", "desc") |
| 321 | + |
| 322 | + if order == "asc" and sort_by.startswith("-"): |
| 323 | + sort_by = sort_by[1:] |
| 324 | + elif order == "desc" and not sort_by.startswith("-"): |
| 325 | + sort_by = f"-{sort_by}" |
| 326 | + |
| 327 | + return queryset.order_by(sort_by) |
| 328 | + |
| 329 | + |
141 | 330 | class ProjectRepoFilter(django_filters.FilterSet):
|
142 | 331 | search = django_filters.CharFilter(method="filter_search", label="Search")
|
143 | 332 | repo_type = django_filters.ChoiceFilter(
|
@@ -810,18 +999,11 @@ def get_issue_count(full_name, query, headers):
|
810 | 999 | )
|
811 | 1000 |
|
812 | 1001 | except requests.RequestException as e:
|
813 |
| - return JsonResponse( |
814 |
| - {"status": "error", "message": f"Network error: {str(e)}"}, status=503 |
815 |
| - ) |
| 1002 | + return JsonResponse({"status": "error", "message": f"Network error: {str(e)}"}, status=503) |
816 | 1003 | except requests.HTTPError as e:
|
817 |
| - return JsonResponse( |
818 |
| - {"status": "error", "message": f"GitHub API error: {str(e)}"}, |
819 |
| - status=e.response.status_code, |
820 |
| - ) |
| 1004 | + return JsonResponse({"status": "error", "message": f"GitHub API error: {str(e)}"}, status=e.response.status_code) |
821 | 1005 | except ValueError as e:
|
822 |
| - return JsonResponse( |
823 |
| - {"status": "error", "message": f"Data parsing error: {str(e)}"}, status=400 |
824 |
| - ) |
| 1006 | + return JsonResponse({"status": "error", "message": f"Data parsing error: {str(e)}"}, status=400) |
825 | 1007 |
|
826 | 1008 | elif section == "metrics":
|
827 | 1009 | try:
|
@@ -929,17 +1111,10 @@ def get_issue_count(full_name, query, headers):
|
929 | 1111 | )
|
930 | 1112 |
|
931 | 1113 | except requests.RequestException as e:
|
932 |
| - return JsonResponse( |
933 |
| - {"status": "error", "message": f"Network error: {str(e)}"}, status=503 |
934 |
| - ) |
| 1114 | + return JsonResponse({"status": "error", "message": f"Network error: {str(e)}"}, status=503) |
935 | 1115 | except requests.HTTPError as e:
|
936 |
| - return JsonResponse( |
937 |
| - {"status": "error", "message": f"GitHub API error: {str(e)}"}, |
938 |
| - status=e.response.status_code, |
939 |
| - ) |
| 1116 | + return JsonResponse({"status": "error", "message": f"GitHub API error: {str(e)}"}, status=e.response.status_code) |
940 | 1117 | except ValueError as e:
|
941 |
| - return JsonResponse( |
942 |
| - {"status": "error", "message": f"Data parsing error: {str(e)}"}, status=400 |
943 |
| - ) |
| 1118 | + return JsonResponse({"status": "error", "message": f"Data parsing error: {str(e)}"}, status=400) |
944 | 1119 |
|
945 | 1120 | return super().post(request, *args, **kwargs)
|
0 commit comments