1
0
Fork 0

Merge branch 'main' into tour

Also fixes conflict
This commit is contained in:
Hugh Rundle 2022-07-09 20:54:48 +10:00
commit ab5e4128e6
129 changed files with 6239 additions and 2325 deletions

View file

@ -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()

View file

@ -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"""

View file

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

View file

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

View file

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

View file

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

View file

@ -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:

View file

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

View file

@ -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":

View file

@ -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

View file

@ -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:

View file

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

View file

@ -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",