From 1f53ce33a83dfe1b1533a7fe871ca12c328f751f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 11 Sep 2021 07:52:56 -0700 Subject: [PATCH 1/5] Fixes user stats reporting --- bookwyrm/views/wellknown.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bookwyrm/views/wellknown.py b/bookwyrm/views/wellknown.py index 426d0cdcf..096d2ed30 100644 --- a/bookwyrm/views/wellknown.py +++ b/bookwyrm/views/wellknown.py @@ -56,17 +56,17 @@ def nodeinfo_pointer(_): @require_GET def nodeinfo(_): """basic info about the server""" - status_count = models.Status.objects.filter(user__local=True).count() - user_count = models.User.objects.filter(local=True).count() + status_count = models.Status.objects.filter(user__local=True, deleted=False).count() + user_count = models.User.objects.filter(is_active=True, local=True).count() month_ago = timezone.now() - relativedelta(months=1) last_month_count = models.User.objects.filter( - local=True, last_active_date__gt=month_ago + is_active=True, local=True, last_active_date__gt=month_ago ).count() six_months_ago = timezone.now() - relativedelta(months=6) six_month_count = models.User.objects.filter( - local=True, last_active_date__gt=six_months_ago + is_active=True, local=True, last_active_date__gt=six_months_ago ).count() site = models.SiteSettings.get() @@ -91,8 +91,8 @@ def nodeinfo(_): @require_GET def instance_info(_): """let's talk about your cool unique instance""" - user_count = models.User.objects.filter(local=True).count() - status_count = models.Status.objects.filter(user__local=True).count() + user_count = models.User.objects.filter(is_active=True, local=True).count() + status_count = models.Status.objects.filter(user__local=True, deleted=False).count() site = models.SiteSettings.get() logo_path = site.logo_small or "images/logo-small.png" @@ -111,7 +111,7 @@ def instance_info(_): "thumbnail": logo, "languages": ["en"], "registrations": site.allow_registration, - "approval_required": False, + "approval_required": site.allow_registration and site.allow_invite_requests, "email": site.admin_email, } ) @@ -120,7 +120,9 @@ def instance_info(_): @require_GET def peers(_): """list of federated servers this instance connects with""" - names = models.FederatedServer.objects.values_list("server_name", flat=True) + names = models.FederatedServer.objects.values_list( + "server_name", flat=True, status="federated" + ) return JsonResponse(list(names), safe=False) From 84b90db4bbb9c776ecdd2fc4b1082fef4760b47c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 11 Sep 2021 08:47:16 -0700 Subject: [PATCH 2/5] Adds filtering to admin panel --- bookwyrm/templates/settings/dashboard.html | 34 ++++++++++++++++ bookwyrm/views/admin/dashboard.py | 45 ++++++++++++++-------- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/bookwyrm/templates/settings/dashboard.html b/bookwyrm/templates/settings/dashboard.html index 81ed28f8a..608d32c97 100644 --- a/bookwyrm/templates/settings/dashboard.html +++ b/bookwyrm/templates/settings/dashboard.html @@ -63,13 +63,47 @@

{% trans "Instance Activity" %}

+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+

{% trans "User signup activity" %}

+

{% trans "Status activity" %}

diff --git a/bookwyrm/views/admin/dashboard.py b/bookwyrm/views/admin/dashboard.py index 2d7e8bdb4..49c690175 100644 --- a/bookwyrm/views/admin/dashboard.py +++ b/bookwyrm/views/admin/dashboard.py @@ -1,5 +1,6 @@ """ instance overview """ from datetime import timedelta +from dateutil.parser import parse from django.contrib.auth.decorators import login_required, permission_required from django.template.response import TemplateResponse @@ -21,43 +22,55 @@ class Dashboard(View): def get(self, request): """list of users""" - buckets = 6 - bucket_size = 1 # days + interval = int(request.GET.get("days", 1)) now = timezone.now() user_queryset = models.User.objects.filter(local=True, is_active=True) - user_stats = {"labels": [], "total": [], "active": []} - interval_end = now - timedelta(days=buckets * bucket_size) - while interval_end < timezone.now(): + + status_queryset = models.Status.objects.filter(user__local=True, deleted=False) + status_stats = {"labels": [], "total": []} + + start = request.GET.get("start") + if start: + start = timezone.make_aware(parse(start)) + else: + start = now - timedelta(days=6 * interval) + + end = request.GET.get("end") + end = timezone.make_aware(parse(end)) if end else now + + interval_start = start + interval_end = interval_start + timedelta(days=interval) + while interval_end <= end: user_stats["total"].append( - user_queryset.filter(created_date__day__lte=interval_end.day).count() + user_queryset.filter(created_date__lte=interval_end).count() ) user_stats["active"].append( user_queryset.filter( last_active_date__gt=interval_end - timedelta(days=31), - created_date__day__lte=interval_end.day, + created_date__lte=interval_end, ).count() ) user_stats["labels"].append(interval_end.strftime("%b %d")) - interval_end += timedelta(days=bucket_size) - status_queryset = models.Status.objects.filter(user__local=True, deleted=False) - status_stats = {"labels": [], "total": []} - interval_start = now - timedelta(days=buckets * bucket_size) - interval_end = interval_start + timedelta(days=bucket_size) - while interval_end < timezone.now(): status_stats["total"].append( status_queryset.filter( - created_date__day__gt=interval_start.day, - created_date__day__lte=interval_end.day, + created_date__gt=interval_start, + created_date__lte=interval_end, ).count() ) status_stats["labels"].append(interval_end.strftime("%b %d")) interval_start = interval_end - interval_end += timedelta(days=bucket_size) + if interval_end >= end: + break + interval_end += timedelta(days=interval) + interval_end = interval_end if interval_end < end else end data = { + "start": start.strftime("%Y-%m-%d"), + "end": end.strftime("%Y-%m-%d"), + "interval": interval, "users": user_queryset.count(), "active_users": user_queryset.filter( last_active_date__gte=now - timedelta(days=31) From 430554444fa793fe221c4296db433a8bd7c00711 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 11 Sep 2021 08:57:38 -0700 Subject: [PATCH 3/5] Adds deactivation date to user model Also fixes django admin around saved lists --- .../migrations/0094_auto_20210911_1550.py | 39 +++++++++++++++++++ bookwyrm/models/user.py | 3 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/migrations/0094_auto_20210911_1550.py diff --git a/bookwyrm/migrations/0094_auto_20210911_1550.py b/bookwyrm/migrations/0094_auto_20210911_1550.py new file mode 100644 index 000000000..8c3be9f89 --- /dev/null +++ b/bookwyrm/migrations/0094_auto_20210911_1550.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.4 on 2021-09-11 15:50 + +from django.db import migrations, models +from django.db.models import F, Value, CharField + + +def set_deactivate_date(apps, schema_editor): + """best-guess for deactivation date""" + db_alias = schema_editor.connection.alias + apps.get_model("bookwyrm", "User").objects.using(db_alias).filter( + is_active=False + ).update(deactivation_date=models.F("last_active_date")) + + +def reverse_func(apps, schema_editor): + """noop""" + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0093_alter_sitesettings_instance_short_description"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="deactivation_date", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name="user", + name="saved_lists", + field=models.ManyToManyField( + blank=True, related_name="saved_lists", to="bookwyrm.List" + ), + ), + migrations.RunPython(set_deactivate_date, reverse_func), + ] diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 7500669f7..e6bd453fb 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -105,7 +105,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): related_name="blocked_by", ) saved_lists = models.ManyToManyField( - "List", symmetrical=False, related_name="saved_lists" + "List", symmetrical=False, related_name="saved_lists", blank=True ) favorites = models.ManyToManyField( "Status", @@ -136,6 +136,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): deactivation_reason = models.CharField( max_length=255, choices=DeactivationReason.choices, null=True, blank=True ) + deactivation_date = models.DateTimeField(null=True, blank=True) confirmation_code = models.CharField(max_length=32, default=new_access_code) name_field = "username" From 3e7847e6452c254346bf1c48649d36b113b6263f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 11 Sep 2021 09:00:52 -0700 Subject: [PATCH 4/5] Set deactivation date --- bookwyrm/models/user.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index e6bd453fb..1b03f93af 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -270,6 +270,11 @@ class User(OrderedCollectionPageMixin, AbstractUser): # this user already exists, no need to populate fields if not created: + if self.is_active: + self.deactivation_date = None + elif not self.deactivation_date: + self.deactivation_date = timezone.now() + super().save(*args, **kwargs) return From e801c7d99165d1de005152bae7a50bac82bf58cd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 11 Sep 2021 09:31:11 -0700 Subject: [PATCH 5/5] Cleaner chart intervals --- bookwyrm/views/admin/dashboard.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/bookwyrm/views/admin/dashboard.py b/bookwyrm/views/admin/dashboard.py index 49c690175..161dc6da1 100644 --- a/bookwyrm/views/admin/dashboard.py +++ b/bookwyrm/views/admin/dashboard.py @@ -3,6 +3,7 @@ from datetime import timedelta from dateutil.parser import parse from django.contrib.auth.decorators import login_required, permission_required +from django.db.models import Q from django.template.response import TemplateResponse from django.utils import timezone from django.utils.decorators import method_decorator @@ -25,7 +26,7 @@ class Dashboard(View): interval = int(request.GET.get("days", 1)) now = timezone.now() - user_queryset = models.User.objects.filter(local=True, is_active=True) + user_queryset = models.User.objects.filter(local=True) user_stats = {"labels": [], "total": [], "active": []} status_queryset = models.Status.objects.filter(user__local=True, deleted=False) @@ -39,20 +40,23 @@ class Dashboard(View): end = request.GET.get("end") end = timezone.make_aware(parse(end)) if end else now + start = start.replace(hour=0, minute=0, second=0) interval_start = start interval_end = interval_start + timedelta(days=interval) - while interval_end <= end: - user_stats["total"].append( - user_queryset.filter(created_date__lte=interval_end).count() + while interval_start <= end: + print(interval_start, interval_end) + interval_queryset = user_queryset.filter( + Q(is_active=True) | Q(deactivation_date__gt=interval_end), + created_date__lte=interval_end, ) + user_stats["total"].append(interval_queryset.filter().count()) user_stats["active"].append( - user_queryset.filter( + interval_queryset.filter( last_active_date__gt=interval_end - timedelta(days=31), - created_date__lte=interval_end, ).count() ) - user_stats["labels"].append(interval_end.strftime("%b %d")) + user_stats["labels"].append(interval_start.strftime("%b %d")) status_stats["total"].append( status_queryset.filter( @@ -60,20 +64,17 @@ class Dashboard(View): created_date__lte=interval_end, ).count() ) - status_stats["labels"].append(interval_end.strftime("%b %d")) + status_stats["labels"].append(interval_start.strftime("%b %d")) interval_start = interval_end - if interval_end >= end: - break interval_end += timedelta(days=interval) - interval_end = interval_end if interval_end < end else end data = { "start": start.strftime("%Y-%m-%d"), "end": end.strftime("%Y-%m-%d"), "interval": interval, - "users": user_queryset.count(), + "users": user_queryset.filter(is_active=True).count(), "active_users": user_queryset.filter( - last_active_date__gte=now - timedelta(days=31) + is_active=True, last_active_date__gte=now - timedelta(days=31) ).count(), "statuses": status_queryset.count(), "works": models.Work.objects.count(),