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..1b03f93af 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" @@ -269,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 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..161dc6da1 100644 --- a/bookwyrm/views/admin/dashboard.py +++ b/bookwyrm/views/admin/dashboard.py @@ -1,7 +1,9 @@ """ instance overview """ 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 @@ -21,46 +23,58 @@ 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_queryset = models.User.objects.filter(local=True) user_stats = {"labels": [], "total": [], "active": []} - interval_end = now - timedelta(days=buckets * bucket_size) - while interval_end < timezone.now(): - user_stats["total"].append( - user_queryset.filter(created_date__day__lte=interval_end.day).count() - ) - user_stats["active"].append( - user_queryset.filter( - last_active_date__gt=interval_end - timedelta(days=31), - created_date__day__lte=interval_end.day, - ).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, + + 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 + start = start.replace(hour=0, minute=0, second=0) + + interval_start = start + interval_end = interval_start + timedelta(days=interval) + 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( + interval_queryset.filter( + last_active_date__gt=interval_end - timedelta(days=31), ).count() ) - status_stats["labels"].append(interval_end.strftime("%b %d")) + user_stats["labels"].append(interval_start.strftime("%b %d")) + + status_stats["total"].append( + status_queryset.filter( + created_date__gt=interval_start, + created_date__lte=interval_end, + ).count() + ) + status_stats["labels"].append(interval_start.strftime("%b %d")) interval_start = interval_end - interval_end += timedelta(days=bucket_size) + interval_end += timedelta(days=interval) data = { - "users": user_queryset.count(), + "start": start.strftime("%Y-%m-%d"), + "end": end.strftime("%Y-%m-%d"), + "interval": interval, + "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(), 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)