Merge branch 'main' into csv-import-failures

This commit is contained in:
Mouse Reeve 2021-09-11 14:42:59 -07:00
commit d972ad2541
7 changed files with 144 additions and 37 deletions

View file

@ -1,7 +1,9 @@
""" access the activity streams stored in redis """ """ access the activity streams stored in redis """
from datetime import timedelta
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from django.db.models import signals, Q from django.db.models import signals, Q
from django.utils import timezone
from bookwyrm import models from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r from bookwyrm.redis_store import RedisStore, r
@ -436,6 +438,10 @@ def remove_status_task(status_ids):
def add_status_task(status_id, increment_unread=False): def add_status_task(status_id, increment_unread=False):
"""add a status to any stream it should be in""" """add a status to any stream it should be in"""
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
# we don't want to tick the unread count for csv import statuses, idk how better
# to check than just to see if the states is more than a few days old
if status.created_date < timezone.now() - timedelta(days=2):
increment_unread = False
for stream in streams.values(): for stream in streams.values():
stream.add_status(status, increment_unread=increment_unread) stream.add_status(status, increment_unread=increment_unread)

View file

@ -125,6 +125,12 @@ class StatusForm(CustomForm):
fields = ["user", "content", "content_warning", "sensitive", "privacy"] fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class DirectForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class EditUserForm(CustomForm): class EditUserForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User

View file

@ -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),
]

View file

@ -105,7 +105,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
related_name="blocked_by", related_name="blocked_by",
) )
saved_lists = models.ManyToManyField( saved_lists = models.ManyToManyField(
"List", symmetrical=False, related_name="saved_lists" "List", symmetrical=False, related_name="saved_lists", blank=True
) )
favorites = models.ManyToManyField( favorites = models.ManyToManyField(
"Status", "Status",
@ -136,6 +136,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
deactivation_reason = models.CharField( deactivation_reason = models.CharField(
max_length=255, choices=DeactivationReason.choices, null=True, blank=True 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) confirmation_code = models.CharField(max_length=32, default=new_access_code)
name_field = "username" name_field = "username"
@ -269,6 +270,11 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# this user already exists, no need to populate fields # this user already exists, no need to populate fields
if not created: 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) super().save(*args, **kwargs)
return return

View file

@ -63,13 +63,47 @@
<div class="block content"> <div class="block content">
<h2>{% trans "Instance Activity" %}</h2> <h2>{% trans "Instance Activity" %}</h2>
<form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis">
<div class="is-flex is-align-items-flex-end">
<div class="ml-1 mr-1">
<label class="label">
{% trans "Start date:" %}
<input class="input" type="date" name="start" value="{{ start }}">
</label>
</div>
<div class="ml-1 mr-1">
<label class="label">
{% trans "End date:" %}
<input class="input" type="date" name="end" value="{{ end }}">
</label>
</div>
<div class="ml-1 mr-1">
<label class="label">
{% trans "Interval:" %}
<div class="select">
<select name="days">
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
</select>
</div>
</label>
</div>
<div class="ml-1 mr-1">
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
</div>
</div>
</form>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h3>{% trans "User signup activity" %}</h3>
<div class="box"> <div class="box">
<canvas id="user_stats"></canvas> <canvas id="user_stats"></canvas>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<h3>{% trans "Status activity" %}</h3>
<div class="box"> <div class="box">
<canvas id="status_stats"></canvas> <canvas id="status_stats"></canvas>
</div> </div>

View file

@ -1,7 +1,9 @@
""" instance overview """ """ instance overview """
from datetime import timedelta from datetime import timedelta
from dateutil.parser import parse
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import Q
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -21,46 +23,58 @@ class Dashboard(View):
def get(self, request): def get(self, request):
"""list of users""" """list of users"""
buckets = 6 interval = int(request.GET.get("days", 1))
bucket_size = 1 # days
now = timezone.now() 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": []} 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_queryset = models.Status.objects.filter(user__local=True, deleted=False)
status_stats = {"labels": [], "total": []} status_stats = {"labels": [], "total": []}
interval_start = now - timedelta(days=buckets * bucket_size)
interval_end = interval_start + timedelta(days=bucket_size) start = request.GET.get("start")
while interval_end < timezone.now(): if start:
status_stats["total"].append( start = timezone.make_aware(parse(start))
status_queryset.filter( else:
created_date__day__gt=interval_start.day, start = now - timedelta(days=6 * interval)
created_date__day__lte=interval_end.day,
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() ).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_start = interval_end
interval_end += timedelta(days=bucket_size) interval_end += timedelta(days=interval)
data = { 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( "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(), ).count(),
"statuses": status_queryset.count(), "statuses": status_queryset.count(),
"works": models.Work.objects.count(), "works": models.Work.objects.count(),

View file

@ -56,17 +56,17 @@ def nodeinfo_pointer(_):
@require_GET @require_GET
def nodeinfo(_): def nodeinfo(_):
"""basic info about the server""" """basic info about the server"""
status_count = models.Status.objects.filter(user__local=True).count() status_count = models.Status.objects.filter(user__local=True, deleted=False).count()
user_count = models.User.objects.filter(local=True).count() user_count = models.User.objects.filter(is_active=True, local=True).count()
month_ago = timezone.now() - relativedelta(months=1) month_ago = timezone.now() - relativedelta(months=1)
last_month_count = models.User.objects.filter( 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() ).count()
six_months_ago = timezone.now() - relativedelta(months=6) six_months_ago = timezone.now() - relativedelta(months=6)
six_month_count = models.User.objects.filter( 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() ).count()
site = models.SiteSettings.get() site = models.SiteSettings.get()
@ -91,8 +91,8 @@ def nodeinfo(_):
@require_GET @require_GET
def instance_info(_): def instance_info(_):
"""let's talk about your cool unique instance""" """let's talk about your cool unique instance"""
user_count = models.User.objects.filter(local=True).count() user_count = models.User.objects.filter(is_active=True, local=True).count()
status_count = models.Status.objects.filter(user__local=True).count() status_count = models.Status.objects.filter(user__local=True, deleted=False).count()
site = models.SiteSettings.get() site = models.SiteSettings.get()
logo_path = site.logo_small or "images/logo-small.png" logo_path = site.logo_small or "images/logo-small.png"
@ -111,7 +111,7 @@ def instance_info(_):
"thumbnail": logo, "thumbnail": logo,
"languages": ["en"], "languages": ["en"],
"registrations": site.allow_registration, "registrations": site.allow_registration,
"approval_required": False, "approval_required": site.allow_registration and site.allow_invite_requests,
"email": site.admin_email, "email": site.admin_email,
} }
) )
@ -120,7 +120,9 @@ def instance_info(_):
@require_GET @require_GET
def peers(_): def peers(_):
"""list of federated servers this instance connects with""" """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) return JsonResponse(list(names), safe=False)