From 3dbedeb1bb25ed5dce3059a786569906e7e73286 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 22 Jan 2025 18:58:05 -0800 Subject: [PATCH 1/2] Add initial project overview page --- readthedocs/projects/urls/private.py | 6 +++ readthedocs/projects/views/private.py | 77 +++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py index 73947d2b11c..a8e2b3ee7a0 100644 --- a/readthedocs/projects/urls/private.py +++ b/readthedocs/projects/urls/private.py @@ -8,6 +8,7 @@ from readthedocs.core.views import PageNotFoundView from readthedocs.projects.backends.views import ImportWizardView from readthedocs.projects.views import private +from readthedocs.projects.views.private import ProjectOverview # Add this import from readthedocs.projects.views.private import ( AddonsConfigUpdate, AutomationRuleDelete, @@ -80,6 +81,11 @@ ), name="projects_manage", ), + re_path( + r"^(?P[-\w]+)/overview/$", + ProjectOverview.as_view(), + name="projects_overview", + ), re_path( r"^(?P[-\w]+)/edit/$", ProjectUpdate.as_view(), diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 9ff05e07cf6..1ca3b488c9f 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -6,7 +6,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.db.models import Count, Q +from django.db.models import Count, Q, Sum from django.http import ( Http404, HttpResponse, @@ -617,8 +617,9 @@ def post(self, request, *args, **kwargs): username=username, ) if self._is_last_user(): - # NOTE: don't include user input in the message, since it's a security risk. - return HttpResponseBadRequest(_("User is the last owner, can't be removed")) + return HttpResponseBadRequest( + _(f"{username} is the last owner, can't be removed") + ) project = self.get_project() project.users.remove(user) @@ -1360,3 +1361,73 @@ def get_queryset(self): def get_success_url(self): return reverse("projects_pull_requests", args=[self.object.slug]) + + +class ProjectOverview(ProjectAdminMixin, PrivateViewMixin, TemplateView): + template_name = "projects/project_overview.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + project = self.get_project() + thirty_days_ago = timezone.now() - timezone.timedelta(days=30) + + # Analytics data + context["total_pageviews"] = ( + PageView.objects.filter( + project=project, + date__gte=thirty_days_ago, + ).aggregate(total=Sum("view_count"))["total"] + or 0 + ) + + context["total_searches"] = SearchQuery.objects.filter( + project=project, + created__gte=thirty_days_ago, + ).count() + + # Project statistics - removed the generic stats section + # and moved counts into their relevant sections + + # Setup section stats + context.update( + { + "maintainers_count": project.users.count(), + "automation_rules_count": project.automation_rules.count(), + "environment_variables_count": project.environmentvariable_set.count(), + } + ) + + # Building section stats + builds = project.builds.filter(date__gte=thirty_days_ago) + context.update( + { + "integrations_count": project.integrations.count(), + "pr_build_count": builds.filter(version__type="external").count(), + "successful_builds": builds.filter(success=True).count(), + "monthly_build_time": builds.aggregate(total_time=Sum("length"))[ + "total_time" + ] + or 0, + "total_builds": builds.count(), + } + ) + + # Hosting section stats + context.update( + { + "domains_count": project.domains.count(), + "subprojects_count": project.subprojects.count(), + "translation_count": project.translations.count(), + "active_versions_count": project.versions.filter(active=True).count(), + } + ) + + # Maintaining section stats + context.update( + { + "redirect_count": project.redirects.count(), + "webhook_count": project.webhook_notifications.count(), + } + ) + + return context From f940be4fad0d5c83a5f87b4d4d142882a0cd04b6 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 25 Feb 2025 16:01:23 -0800 Subject: [PATCH 2/2] Update view for latest UI --- readthedocs/projects/views/private.py | 65 +++++---------------------- 1 file changed, 12 insertions(+), 53 deletions(-) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 1ca3b488c9f..af2258d13a5 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -34,6 +34,7 @@ from readthedocs.builds.forms import RegexAutomationRuleForm, VersionForm from readthedocs.builds.models import ( AutomationRuleMatch, + Build, RegexAutomationRule, Version, VersionAutomationRule, @@ -1369,65 +1370,23 @@ class ProjectOverview(ProjectAdminMixin, PrivateViewMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) project = self.get_project() - thirty_days_ago = timezone.now() - timezone.timedelta(days=30) + limit_reached, concurrent, max_concurrent = Build.objects.concurrent(project) - # Analytics data - context["total_pageviews"] = ( - PageView.objects.filter( - project=project, - date__gte=thirty_days_ago, - ).aggregate(total=Sum("view_count"))["total"] - or 0 - ) - - context["total_searches"] = SearchQuery.objects.filter( - project=project, - created__gte=thirty_days_ago, - ).count() - - # Project statistics - removed the generic stats section - # and moved counts into their relevant sections - - # Setup section stats - context.update( - { - "maintainers_count": project.users.count(), - "automation_rules_count": project.automation_rules.count(), - "environment_variables_count": project.environmentvariable_set.count(), - } - ) - - # Building section stats - builds = project.builds.filter(date__gte=thirty_days_ago) context.update( { - "integrations_count": project.integrations.count(), - "pr_build_count": builds.filter(version__type="external").count(), - "successful_builds": builds.filter(success=True).count(), - "monthly_build_time": builds.aggregate(total_time=Sum("length"))[ - "total_time" - ] + "project": project, + "concurrent_builds": { + "limit_reached": limit_reached, + "current": concurrent, + "max": max_concurrent, + }, + "successful_builds": project.builds.filter(success=True).count(), + "monthly_build_time": project.builds.aggregate( + total_time=Sum("length") + )["total_time"] or 0, - "total_builds": builds.count(), - } - ) - - # Hosting section stats - context.update( - { - "domains_count": project.domains.count(), - "subprojects_count": project.subprojects.count(), - "translation_count": project.translations.count(), "active_versions_count": project.versions.filter(active=True).count(), } ) - # Maintaining section stats - context.update( - { - "redirect_count": project.redirects.count(), - "webhook_count": project.webhook_notifications.count(), - } - ) - return context