Files
website-v2/libraries/admin.py

412 lines
13 KiB
Python

from django.contrib import admin
from django.db import transaction
from django.db.models import F, Count, OuterRef, Window
from django.db.models.functions import RowNumber
from django.http import HttpResponse, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.safestring import mark_safe
from django.shortcuts import redirect
from django.views.generic import TemplateView
from libraries.forms import CreateReportForm, CreateReportFullForm
from versions.models import Version
from versions.tasks import import_all_library_versions
from .models import (
Category,
Commit,
CommitAuthor,
CommitAuthorEmail,
Issue,
Library,
LibraryVersion,
PullRequest,
)
from .tasks import (
generate_library_report,
update_authors_and_maintainers,
update_commit_author_github_data,
update_commits,
update_issues,
update_libraries,
update_library_version_documentation_urls_all_versions,
generate_release_report,
)
@admin.register(Commit)
class CommitAdmin(admin.ModelAdmin):
list_display = ["library_version", "sha", "author"]
autocomplete_fields = ["author", "library_version"]
list_filter = ["library_version__library"]
search_fields = ["sha", "author__name"]
change_list_template = "admin/commit_change_list.html"
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(
"update_commits/",
self.admin_site.admin_view(self.update_commits),
name="update_commits",
),
]
return my_urls + urls
def update_commits(self, request):
update_commits.delay(clean=True)
self.message_user(
request,
"""
Commits for all libraries are being imported.
""",
)
return HttpResponseRedirect("../")
class CommitAuthorEmailInline(admin.TabularInline):
model = CommitAuthorEmail
extra = 0
@admin.register(CommitAuthor)
class CommitAuthorAdmin(admin.ModelAdmin):
list_display = ["name", "emails"]
search_fields = ["name", "commitauthoremail__email"]
actions = ["merge_authors"]
inlines = [CommitAuthorEmailInline]
change_list_template = "admin/commit_author_change_list.html"
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related("commitauthoremail_set")
def emails(self, obj):
return ", ".join(x.email for x in obj.commitauthoremail_set.all())
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(
"update_github_data/",
self.admin_site.admin_view(self.update_github_data),
name="commit_author_update_github_data",
),
]
return my_urls + urls
def update_github_data(self, request):
update_commit_author_github_data.delay(clean=True)
self.message_user(
request,
"""
Updating CommitAuthor Github data.
""",
)
return HttpResponseRedirect("../")
@admin.action(
description="Combine 2 or more authors into one. References will be updated."
)
def merge_authors(self, request, queryset):
objects = list(queryset)
if len(objects) < 2:
return
author = objects[0]
with transaction.atomic():
for other in objects[1:]:
author.merge_author(other)
message = "Merged authors -- " + ", ".join([x.name for x in objects])
self.message_user(request, message)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ["name"]
ordering = ["name"]
search_fields = ["name"]
class LibraryVersionInline(admin.TabularInline):
model = LibraryVersion
extra = 0
ordering = ["-version__name"]
fields = ["version", "documentation_url"]
class ReleaseReportView(TemplateView):
polling_template = "admin/report_polling.html"
form_template = "admin/library_report_form.html"
form_class = CreateReportForm
report_type = "release report"
def get_template_names(self):
if not self.request.GET.get("submit", None):
return [self.form_template]
form = self.get_form()
if not form.is_valid():
return [self.form_template]
if form.cleaned_data["no_cache"]:
return [self.form_template]
return [self.polling_template]
def get_form(self):
data = None
if self.request.GET.get("submit", None):
data = self.request.GET
return self.form_class(data)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["report_type"] = self.report_type
context["form"] = self.get_form()
return context
def generate_report(self):
generate_release_report.delay(self.request.GET)
def get(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
if form.cleaned_data["no_cache"]:
params = request.GET.copy()
form.cache_clear()
del params["no_cache"]
return redirect(request.path + f"?{params.urlencode()}")
content = form.cache_get()
if not content:
# Ensure a RenderedContent exists so the task is not re-queued
form.cache_set("")
self.generate_report()
elif content.content_html:
return HttpResponse(content.content_html)
return TemplateResponse(
request,
self.get_template_names(),
self.get_context_data(),
)
class LibraryReportView(ReleaseReportView):
form_class = CreateReportFullForm
report_type = "library report"
def generate_report(self):
generate_library_report.delay(self.request.GET)
@admin.register(Library)
class LibraryAdmin(admin.ModelAdmin):
list_display = ["name", "key", "github_url", "view_stats"]
search_fields = ["name", "description"]
list_filter = ["categories"]
ordering = ["name"]
change_list_template = "admin/library_change_list.html"
inlines = [LibraryVersionInline]
def get_urls(self):
urls = super().get_urls()
my_urls = [
path("update_libraries/", self.update_libraries, name="update_libraries"),
path(
"update_authors_and_maintainers/",
self.update_authors_and_maintainers,
name="update_authors_and_maintainers",
),
path(
"<int:pk>/stats/",
self.admin_site.admin_view(self.library_stat_detail),
name="library_stat_detail",
),
path(
"release-report/",
self.admin_site.admin_view(ReleaseReportView.as_view()),
name="release_report",
),
path(
"library-report/",
self.admin_site.admin_view(LibraryReportView.as_view()),
name="library_report_full",
),
]
return my_urls + urls
def view_stats(self, instance):
url = reverse("admin:library_stat_detail", kwargs={"pk": instance.pk})
return mark_safe(f"<a href='{url}'>View Stats</a>")
def update_authors_and_maintainers(self, request):
update_authors_and_maintainers.delay()
self.message_user(request, "Authors and Maintainers are being updated.")
return HttpResponseRedirect("../")
def update_libraries(self, request):
"""Run the task to refresh the library data from GitHub"""
update_libraries.delay()
import_all_library_versions.delay()
self.message_user(
request,
"""
Library data is being refreshed.
""",
)
return HttpResponseRedirect("../")
def library_stat_detail(self, request, pk):
context = {
"object": self.get_object(request, pk),
"commits_per_release": self.get_commits_per_release(pk),
"commits_per_author": self.get_commits_per_author(pk),
"commits_per_author_release": self.get_commits_per_author_release(pk),
"new_contributor_counts": self.get_new_contributor_counts(pk),
}
return TemplateResponse(request, "admin/library_stat_detail.html", context)
def get_commits_per_release(self, pk):
return (
LibraryVersion.objects.filter(
library_id=pk, version__in=Version.objects.minor_versions()
)
.annotate(count=Count("commit"), version_name=F("version__name"))
.order_by("-version__name")
.filter(count__gt=0)
)[:10]
def get_commits_per_author(self, pk):
return (
CommitAuthor.objects.filter(commit__library_version__library_id=pk)
.annotate(count=Count("commit"))
.order_by("-count")[:20]
)
def get_commits_per_author_release(self, pk):
return (
LibraryVersion.objects.filter(library_id=pk)
.filter(commit__author__isnull=False)
.annotate(
count=Count("commit"),
row_number=Window(
expression=RowNumber(), partition_by=["id"], order_by=["-count"]
),
)
.values(
"count",
"commit__author",
"commit__author__name",
"version__name",
"commit__author__avatar_url",
)
.order_by("-version__name", "-count")
.filter(row_number__lte=3)
)
def get_new_contributor_counts(self, pk):
return (
LibraryVersion.objects.filter(
library_id=pk, version__in=Version.objects.minor_versions()
)
.annotate(
up_to_count=CommitAuthor.objects.filter(
commit__library_version__version__name__lte=OuterRef(
"version__name"
),
commit__library_version__library_id=pk,
)
.values("commit__library_version__library")
.annotate(count=Count("id", distinct=True))
.values("count")[:1],
before_count=CommitAuthor.objects.filter(
commit__library_version__version__name__lt=OuterRef(
"version__name"
),
commit__library_version__library_id=pk,
)
.values("commit__library_version__library")
.annotate(count=Count("id", distinct=True))
.values("count")[:1],
count=F("up_to_count") - F("before_count"),
)
.order_by("-version__name")
.select_related("version")
)
@admin.register(LibraryVersion)
class LibraryVersionAdmin(admin.ModelAdmin):
list_display = ["library", "version", "missing_docs", "documentation_url"]
list_filter = ["library", "version", "missing_docs"]
ordering = ["library__name", "-version__name"]
search_fields = ["library__name", "version__name"]
change_list_template = "admin/libraryversion_change_list.html"
autocomplete_fields = ["authors", "maintainers", "dependencies"]
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(
"update_docs_urls/",
self.admin_site.admin_view(self.update_docs_urls),
name="update_docs_urls",
),
]
return my_urls + urls
def update_docs_urls(self, request):
"""Run the task to refresh the documentation URLS from S3"""
update_library_version_documentation_urls_all_versions.delay()
self.message_user(
request,
"""
Documentation links are being refreshed.
""",
)
return HttpResponseRedirect("../")
@admin.register(Issue)
class IssueAdmin(admin.ModelAdmin):
list_display = ["title", "number", "is_open", "closed"]
search_fields = ["title"]
list_filter = ["is_open", "library"]
change_list_template = "admin/issue_change_list.html"
readonly_fields = [
"title",
"number",
"github_id",
"created",
"modified",
"closed",
]
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(
"update_issues/",
self.admin_site.admin_view(self.update_issues),
name="update_issues",
),
]
return my_urls + urls
def update_issues(self, request):
update_issues.delay(clean=True)
self.message_user(request, "Issues are being updated.")
return HttpResponseRedirect("../")
@admin.register(PullRequest)
class PullRequestAdmin(admin.ModelAdmin):
list_display = ["title", "number", "is_open", "closed"]
search_fields = ["title"]
list_filter = ["is_open", "library"]
readonly_fields = [
"title",
"number",
"github_id",
"created",
"modified",
"closed",
]