Merge branch 'main' into tour
Also fixes conflict
This commit is contained in:
commit
ab5e4128e6
129 changed files with 6239 additions and 2325 deletions
|
@ -33,8 +33,7 @@ class AutoMod(View):
|
|||
def post(self, request):
|
||||
"""add rule"""
|
||||
form = forms.AutoModRuleForm(request.POST)
|
||||
success = form.is_valid()
|
||||
if success:
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
form = forms.AutoModRuleForm()
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
""" instance overview """
|
||||
from datetime import timedelta
|
||||
import re
|
||||
|
||||
from dateutil.parser import parse
|
||||
from packaging import version
|
||||
|
||||
|
@ -13,6 +15,7 @@ from django.views import View
|
|||
from bookwyrm import models, settings
|
||||
from bookwyrm.connectors.abstract_connector import get_data
|
||||
from bookwyrm.connectors.connector_manager import ConnectorException
|
||||
from bookwyrm.utils import regex
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -26,91 +29,32 @@ class Dashboard(View):
|
|||
|
||||
def get(self, request):
|
||||
"""list of users"""
|
||||
interval = int(request.GET.get("days", 1))
|
||||
now = timezone.now()
|
||||
start = request.GET.get("start")
|
||||
if start:
|
||||
start = timezone.make_aware(parse(start))
|
||||
else:
|
||||
start = now - timedelta(days=6 * interval)
|
||||
data = get_charts_and_stats(request)
|
||||
|
||||
end = request.GET.get("end")
|
||||
end = timezone.make_aware(parse(end)) if end else now
|
||||
start = start.replace(hour=0, minute=0, second=0)
|
||||
# Make sure email looks properly configured
|
||||
email_config_error = re.findall(
|
||||
r"[\s\@]", settings.EMAIL_SENDER_DOMAIN
|
||||
) or not re.match(regex.DOMAIN, settings.EMAIL_SENDER_DOMAIN)
|
||||
|
||||
user_queryset = models.User.objects.filter(local=True)
|
||||
user_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
).count(),
|
||||
"active": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
)
|
||||
.filter(
|
||||
last_active_date__gt=e - timedelta(days=31),
|
||||
)
|
||||
.count(),
|
||||
},
|
||||
data["email_config_error"] = email_config_error
|
||||
# pylint: disable=line-too-long
|
||||
data[
|
||||
"email_sender"
|
||||
] = f"{settings.EMAIL_SENDER_NAME}@{settings.EMAIL_SENDER_DOMAIN}"
|
||||
|
||||
site = models.SiteSettings.objects.get()
|
||||
# pylint: disable=protected-access
|
||||
data["missing_conduct"] = (
|
||||
not site.code_of_conduct
|
||||
or site.code_of_conduct
|
||||
== site._meta.get_field("code_of_conduct").get_default()
|
||||
)
|
||||
|
||||
status_queryset = models.Status.objects.filter(user__local=True, deleted=False)
|
||||
status_chart = Chart(
|
||||
queryset=status_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
data["missing_privacy"] = (
|
||||
not site.privacy_policy
|
||||
or site.privacy_policy
|
||||
== site._meta.get_field("privacy_policy").get_default()
|
||||
)
|
||||
|
||||
register_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
works_chart = Chart(
|
||||
queryset=models.Work.objects,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
data = {
|
||||
"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(
|
||||
is_active=True, last_active_date__gte=now - timedelta(days=31)
|
||||
).count(),
|
||||
"statuses": status_queryset.count(),
|
||||
"works": models.Work.objects.count(),
|
||||
"reports": models.Report.objects.filter(resolved=False).count(),
|
||||
"pending_domains": models.LinkDomain.objects.filter(
|
||||
status="pending"
|
||||
).count(),
|
||||
"invite_requests": models.InviteRequest.objects.filter(
|
||||
ignored=False, invite__isnull=True
|
||||
).count(),
|
||||
"user_stats": user_chart.get_chart(start, end, interval),
|
||||
"status_stats": status_chart.get_chart(start, end, interval),
|
||||
"register_stats": register_chart.get_chart(start, end, interval),
|
||||
"works_stats": works_chart.get_chart(start, end, interval),
|
||||
}
|
||||
|
||||
# check version
|
||||
try:
|
||||
release = get_data(settings.RELEASE_API, timeout=3)
|
||||
|
@ -126,6 +70,91 @@ class Dashboard(View):
|
|||
return TemplateResponse(request, "settings/dashboard/dashboard.html", data)
|
||||
|
||||
|
||||
def get_charts_and_stats(request):
|
||||
"""Defines the dashbaord charts"""
|
||||
interval = int(request.GET.get("days", 1))
|
||||
now = timezone.now()
|
||||
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)
|
||||
|
||||
user_queryset = models.User.objects.filter(local=True)
|
||||
user_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
).count(),
|
||||
"active": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
)
|
||||
.filter(
|
||||
last_active_date__gt=e - timedelta(days=31),
|
||||
)
|
||||
.count(),
|
||||
},
|
||||
)
|
||||
|
||||
status_queryset = models.Status.objects.filter(user__local=True, deleted=False)
|
||||
status_chart = Chart(
|
||||
queryset=status_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
register_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
works_chart = Chart(
|
||||
queryset=models.Work.objects,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
return {
|
||||
"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(
|
||||
is_active=True, last_active_date__gte=now - timedelta(days=31)
|
||||
).count(),
|
||||
"statuses": status_queryset.count(),
|
||||
"works": models.Work.objects.count(),
|
||||
"reports": models.Report.objects.filter(resolved=False).count(),
|
||||
"pending_domains": models.LinkDomain.objects.filter(status="pending").count(),
|
||||
"invite_requests": models.InviteRequest.objects.filter(
|
||||
ignored=False, invite__isnull=True
|
||||
).count(),
|
||||
"user_stats": user_chart.get_chart(start, end, interval),
|
||||
"status_stats": status_chart.get_chart(start, end, interval),
|
||||
"register_stats": register_chart.get_chart(start, end, interval),
|
||||
"works_stats": works_chart.get_chart(start, end, interval),
|
||||
}
|
||||
|
||||
|
||||
class Chart:
|
||||
"""Data for a chart"""
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ class LinkDomain(View):
|
|||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def update_domain_status(request, domain_id, status):
|
||||
"""This domain seems fine"""
|
||||
domain = get_object_or_404(models.LinkDomain, id=domain_id)
|
||||
|
|
|
@ -83,7 +83,7 @@ class ReportAdmin(View):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def suspend_user(_, user_id):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
@ -95,7 +95,7 @@ def suspend_user(_, user_id):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def unsuspend_user(_, user_id):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
@ -107,7 +107,7 @@ def unsuspend_user(_, user_id):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def moderator_delete_user(request, user_id):
|
||||
"""permanently delete a user"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
@ -132,7 +132,7 @@ def moderator_delete_user(request, user_id):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_post")
|
||||
@permission_required("bookwyrm.moderate_post")
|
||||
def resolve_report(_, report_id):
|
||||
"""mark a report as (un)resolved"""
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
|
|
|
@ -22,21 +22,25 @@ class UserAdminList(View):
|
|||
def get(self, request, status="local"):
|
||||
"""list of users"""
|
||||
filters = {}
|
||||
exclusions = {}
|
||||
if server := request.GET.get("server"):
|
||||
server = models.FederatedServer.objects.filter(server_name=server).first()
|
||||
filters["federated_server"] = server
|
||||
filters["federated_server__isnull"] = False
|
||||
|
||||
if username := request.GET.get("username"):
|
||||
filters["username__icontains"] = username
|
||||
scope = request.GET.get("scope")
|
||||
if scope and scope == "local":
|
||||
filters["local"] = True
|
||||
|
||||
if email := request.GET.get("email"):
|
||||
filters["email__endswith"] = email
|
||||
|
||||
filters["local"] = status == "local"
|
||||
filters["local"] = status in ["local", "deleted"]
|
||||
if status == "deleted":
|
||||
filters["deactivation_reason__icontains"] = "deletion"
|
||||
else:
|
||||
exclusions["deactivation_reason__icontains"] = "deletion"
|
||||
|
||||
users = models.User.objects.filter(**filters)
|
||||
users = models.User.objects.filter(**filters).exclude(**exclusions)
|
||||
|
||||
sort = request.GET.get("sort", "-created_date")
|
||||
sort_fields = [
|
||||
|
@ -62,7 +66,7 @@ class UserAdminList(View):
|
|||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||
permission_required("bookwyrm.moderate_user", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class UserAdmin(View):
|
||||
|
@ -77,8 +81,13 @@ class UserAdmin(View):
|
|||
def post(self, request, user):
|
||||
"""update user group"""
|
||||
user = get_object_or_404(models.User, id=user)
|
||||
form = forms.UserGroupForm(request.POST, instance=user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
if request.POST.get("groups") == "":
|
||||
user.groups.set([])
|
||||
form = forms.UserGroupForm(instance=user)
|
||||
else:
|
||||
form = forms.UserGroupForm(request.POST, instance=user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
data = {"user": user, "group_form": form}
|
||||
return TemplateResponse(request, "settings/users/user.html", data)
|
||||
|
|
|
@ -59,11 +59,11 @@ class Group(View):
|
|||
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
for field in form.changed_data:
|
||||
notification_type = (
|
||||
"GROUP_PRIVACY"
|
||||
model.GROUP_PRIVACY
|
||||
if field == "privacy"
|
||||
else "GROUP_NAME"
|
||||
else model.GROUP_NAME
|
||||
if field == "name"
|
||||
else "GROUP_DESCRIPTION"
|
||||
else model.GROUP_DESCRIPTION
|
||||
if field == "description"
|
||||
else None
|
||||
)
|
||||
|
@ -71,9 +71,9 @@ class Group(View):
|
|||
for membership in memberships:
|
||||
member = membership.user
|
||||
if member != request.user:
|
||||
model.objects.create(
|
||||
user=member,
|
||||
related_user=request.user,
|
||||
model.notify(
|
||||
member,
|
||||
request.user,
|
||||
related_group=user_group,
|
||||
notification_type=notification_type,
|
||||
)
|
||||
|
@ -244,24 +244,22 @@ def remove_member(request):
|
|||
|
||||
memberships = models.GroupMember.objects.filter(group=group)
|
||||
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
notification_type = "LEAVE" if user == request.user else "REMOVE"
|
||||
notification_type = model.LEAVE if user == request.user else model.REMOVE
|
||||
# let the other members know about it
|
||||
for membership in memberships:
|
||||
member = membership.user
|
||||
if member != request.user:
|
||||
model.objects.create(
|
||||
user=member,
|
||||
related_user=user,
|
||||
model.notify(
|
||||
member,
|
||||
user,
|
||||
related_group=group,
|
||||
notification_type=notification_type,
|
||||
)
|
||||
|
||||
# let the user (now ex-member) know as well, if they were removed
|
||||
if notification_type == "REMOVE":
|
||||
model.objects.create(
|
||||
user=user,
|
||||
related_group=group,
|
||||
notification_type=notification_type,
|
||||
if notification_type == model.REMOVE:
|
||||
model.notify(
|
||||
user, None, related_group=group, notification_type=notification_type
|
||||
)
|
||||
|
||||
return redirect(group.local_path)
|
||||
|
|
|
@ -148,13 +148,6 @@ def handle_reading_status(user, shelf, book, privacy):
|
|||
status.save()
|
||||
|
||||
|
||||
def is_blocked(viewer, user):
|
||||
"""is this viewer blocked by the user?"""
|
||||
if viewer.is_authenticated and viewer in user.blocks.all():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
||||
"""ensures that data is stored consistently in the UTC timezone"""
|
||||
if not date_str:
|
||||
|
|
|
@ -3,7 +3,6 @@ from django.contrib.auth import login
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import models
|
||||
|
@ -24,12 +23,13 @@ class PasswordResetRequest(View):
|
|||
def post(self, request):
|
||||
"""create a password reset token"""
|
||||
email = request.POST.get("email")
|
||||
data = {"sent_message": True, "email": email}
|
||||
try:
|
||||
user = models.User.viewer_aware_objects(request.user).get(
|
||||
email=email, email__isnull=False
|
||||
)
|
||||
except models.User.DoesNotExist:
|
||||
data = {"error": _("No user with that email address was found.")}
|
||||
# Showing an error message would leak whether or not this email is in use
|
||||
return TemplateResponse(
|
||||
request, "landing/password_reset_request.html", data
|
||||
)
|
||||
|
@ -40,7 +40,6 @@ class PasswordResetRequest(View):
|
|||
# create a new reset code
|
||||
code = models.PasswordReset.objects.create(user=user)
|
||||
password_reset_email(code)
|
||||
data = {"message": _(f"A password reset link was sent to {email}")}
|
||||
return TemplateResponse(request, "landing/password_reset_request.html", data)
|
||||
|
||||
|
||||
|
|
|
@ -15,16 +15,17 @@ class Notifications(View):
|
|||
"""people are interacting with you, get hyped"""
|
||||
notifications = (
|
||||
request.user.notification_set.all()
|
||||
.order_by("-created_date")
|
||||
.order_by("-updated_date")
|
||||
.select_related(
|
||||
"related_status",
|
||||
"related_status__reply_parent",
|
||||
"related_group",
|
||||
"related_import",
|
||||
"related_report",
|
||||
"related_user",
|
||||
"related_book",
|
||||
"related_list_item",
|
||||
"related_list_item__book",
|
||||
)
|
||||
.prefetch_related(
|
||||
"related_reports",
|
||||
"related_users",
|
||||
"related_list_items",
|
||||
)
|
||||
)
|
||||
if notification_type == "mentions":
|
||||
|
|
|
@ -52,9 +52,6 @@ class ReadingStatus(View):
|
|||
logger.exception("Invalid reading status type: %s", status)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# invalidate related caches
|
||||
cache.delete(f"active_shelf-{request.user.id}-{book_id}")
|
||||
|
||||
desired_shelf = get_object_or_404(
|
||||
models.Shelf, identifier=identifier, user=request.user
|
||||
)
|
||||
|
@ -65,6 +62,14 @@ class ReadingStatus(View):
|
|||
.get(id=book_id)
|
||||
)
|
||||
|
||||
# invalidate related caches
|
||||
cache.delete_many(
|
||||
[
|
||||
f"active_shelf-{request.user.id}-{ed}"
|
||||
for ed in book.parent_work.editions.values_list("id", flat=True)
|
||||
]
|
||||
)
|
||||
|
||||
# gets the first shelf that indicates a reading status, or None
|
||||
shelves = [
|
||||
s
|
||||
|
|
|
@ -13,9 +13,13 @@ from bookwyrm import emailing, forms, models
|
|||
class Report(View):
|
||||
"""Make reports"""
|
||||
|
||||
def get(self, request, user_id, status_id=None, link_id=None):
|
||||
def get(self, request, user_id=None, status_id=None, link_id=None):
|
||||
"""static view of report modal"""
|
||||
data = {"user": get_object_or_404(models.User, id=user_id)}
|
||||
data = {"user": None}
|
||||
if user_id:
|
||||
# but normally we should have an error if the user is not found
|
||||
data["user"] = get_object_or_404(models.User, id=user_id)
|
||||
|
||||
if status_id:
|
||||
data["status"] = status_id
|
||||
if link_id:
|
||||
|
|
|
@ -16,9 +16,8 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from markdown import markdown
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.sanitize_html import InputHtmlParser
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.utils import regex
|
||||
from bookwyrm.utils import regex, sanitizer
|
||||
from .helpers import handle_remote_webfinger, is_api_request
|
||||
from .helpers import load_date_in_user_tz_as_utc
|
||||
|
||||
|
@ -268,6 +267,4 @@ def to_markdown(content):
|
|||
content = format_links(content)
|
||||
content = markdown(content)
|
||||
# sanitize resulting html
|
||||
sanitizer = InputHtmlParser()
|
||||
sanitizer.feed(content)
|
||||
return sanitizer.get_output()
|
||||
return sanitizer.clean(content)
|
||||
|
|
|
@ -60,6 +60,12 @@ class User(View):
|
|||
request.user,
|
||||
)
|
||||
.filter(user=user)
|
||||
.exclude(
|
||||
privacy="direct",
|
||||
review__isnull=True,
|
||||
comment__isnull=True,
|
||||
quotation__isnull=True,
|
||||
)
|
||||
.select_related(
|
||||
"user",
|
||||
"reply_parent",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue