diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 05e7d8a05..aa4b5b687 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -396,7 +396,7 @@ def resolve_remote_id( def get_representative(): """Get or create an actor representing the instance - to sign requests to 'secure mastodon' servers""" + to sign outgoing HTTP GET requests""" username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}" email = "bookwyrm@localhost" try: diff --git a/bookwyrm/migrations/0186_invite_request_notification.py b/bookwyrm/migrations/0186_invite_request_notification.py new file mode 100644 index 000000000..3680b1de7 --- /dev/null +++ b/bookwyrm/migrations/0186_invite_request_notification.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.20 on 2023-11-14 10:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0185_alter_notification_notification_type"), + ] + + operations = [ + migrations.AddField( + model_name="notification", + name="related_invite_requests", + field=models.ManyToManyField(to="bookwyrm.InviteRequest"), + ), + migrations.AlterField( + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("BOOST", "Boost"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ("REPORT", "Report"), + ("LINK_DOMAIN", "Link Domain"), + ("INVITE_REQUEST", "Invite Request"), + ("INVITE", "Invite"), + ("ACCEPT", "Accept"), + ("JOIN", "Join"), + ("LEAVE", "Leave"), + ("REMOVE", "Remove"), + ("GROUP_PRIVACY", "Group Privacy"), + ("GROUP_NAME", "Group Name"), + ("GROUP_DESCRIPTION", "Group Description"), + ("MOVE", "Move"), + ], + max_length=255, + ), + ), + ] diff --git a/bookwyrm/migrations/0187_partial_publication_dates.py b/bookwyrm/migrations/0187_partial_publication_dates.py new file mode 100644 index 000000000..10ef599a7 --- /dev/null +++ b/bookwyrm/migrations/0187_partial_publication_dates.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.20 on 2023-11-09 16:57 + +import bookwyrm.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0186_invite_request_notification"), + ] + + operations = [ + migrations.AddField( + model_name="book", + name="first_published_date_precision", + field=models.CharField( + blank=True, + choices=[ + ("DAY", "Day prec."), + ("MONTH", "Month prec."), + ("YEAR", "Year prec."), + ], + editable=False, + max_length=10, + null=True, + ), + ), + migrations.AddField( + model_name="book", + name="published_date_precision", + field=models.CharField( + blank=True, + choices=[ + ("DAY", "Day prec."), + ("MONTH", "Month prec."), + ("YEAR", "Year prec."), + ], + editable=False, + max_length=10, + null=True, + ), + ), + migrations.AlterField( + model_name="book", + name="first_published_date", + field=bookwyrm.models.fields.PartialDateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="book", + name="published_date", + field=bookwyrm.models.fields.PartialDateField(blank=True, null=True), + ), + ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index e5941136f..6893b9da1 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -135,8 +135,8 @@ class Book(BookDataModel): preview_image = models.ImageField( upload_to="previews/covers/", blank=True, null=True ) - first_published_date = fields.DateTimeField(blank=True, null=True) - published_date = fields.DateTimeField(blank=True, null=True) + first_published_date = fields.PartialDateField(blank=True, null=True) + published_date = fields.PartialDateField(blank=True, null=True) objects = InheritanceManager() field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"]) diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 1e458c815..4bd580705 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -20,6 +20,11 @@ from markdown import markdown from bookwyrm import activitypub from bookwyrm.connectors import get_image from bookwyrm.utils.sanitizer import clean +from bookwyrm.utils.partial_date import ( + PartialDate, + PartialDateModel, + from_partial_isoformat, +) from bookwyrm.settings import MEDIA_FULL_URL @@ -539,7 +544,6 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): def field_from_activity(self, value, allow_external_connections=True): missing_fields = datetime(1970, 1, 1) # "2022-10" => "2022-10-01" try: - # TODO(dato): investigate `ignoretz=True` wrt bookwyrm#3028. date_value = dateutil.parser.parse(value, default=missing_fields) try: return timezone.make_aware(date_value) @@ -549,6 +553,37 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): return None +class PartialDateField(ActivitypubFieldMixin, PartialDateModel): + """activitypub-aware partial date field""" + + def field_to_activity(self, value) -> str: + return value.partial_isoformat() if value else None + + def field_from_activity(self, value, allow_external_connections=True): + # pylint: disable=no-else-return + try: + return from_partial_isoformat(value) + except ValueError: + pass + + # fallback to full ISO-8601 parsing + try: + parsed = dateutil.parser.isoparse(value) + except (ValueError, ParserError): + return None + + if timezone.is_aware(parsed): + return PartialDate.from_datetime(parsed) + else: + # Should not happen on the wire, but truncate down to date parts. + return PartialDate.from_date_parts(parsed.year, parsed.month, parsed.day) + + # FIXME: decide whether to fix timestamps like "2023-09-30T21:00:00-03": + # clearly Oct 1st, not Sep 30th (an unwanted side-effect of USE_TZ). It's + # basically the remnants of #3028; there is a data migration pending (see …) + # but over the wire we might get these for an indeterminate amount of time. + + class HtmlField(ActivitypubFieldMixin, models.TextField): """a text field for storing html""" diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 70aeec82a..515346031 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -12,6 +12,7 @@ from . import ( LinkDomain, ) from . import ListItem, Report, Status, User, UserFollowRequest +from .site import InviteRequest class NotificationType(models.TextChoices): @@ -39,6 +40,7 @@ class NotificationType(models.TextChoices): # Admin REPORT = "REPORT" LINK_DOMAIN = "LINK_DOMAIN" + INVITE_REQUEST = "INVITE_REQUEST" # Groups INVITE = "INVITE" @@ -77,8 +79,9 @@ class Notification(BookWyrmModel): related_list_items = models.ManyToManyField( "ListItem", symmetrical=False, related_name="notifications" ) - related_reports = models.ManyToManyField("Report", symmetrical=False) - related_link_domains = models.ManyToManyField("LinkDomain", symmetrical=False) + related_reports = models.ManyToManyField("Report") + related_link_domains = models.ManyToManyField("LinkDomain") + related_invite_requests = models.ManyToManyField("InviteRequest") @classmethod @transaction.atomic @@ -276,8 +279,7 @@ def notify_admins_on_report(sender, instance, created, *args, **kwargs): return # moderators and superusers should be notified - admins = User.admins() - for admin in admins: + for admin in User.admins(): notification, _ = Notification.objects.get_or_create( user=admin, notification_type=NotificationType.REPORT, @@ -296,8 +298,7 @@ def notify_admins_on_link_domain(sender, instance, created, *args, **kwargs): return # moderators and superusers should be notified - admins = User.admins() - for admin in admins: + for admin in User.admins(): notification, _ = Notification.objects.get_or_create( user=admin, notification_type=NotificationType.LINK_DOMAIN, @@ -306,6 +307,24 @@ def notify_admins_on_link_domain(sender, instance, created, *args, **kwargs): notification.related_link_domains.add(instance) +@receiver(models.signals.post_save, sender=InviteRequest) +@transaction.atomic +# pylint: disable=unused-argument +def notify_admins_on_invite_request(sender, instance, created, *args, **kwargs): + """need to handle a new invite request""" + if not created: + return + + # moderators and superusers should be notified + for admin in User.admins(): + notification, _ = Notification.objects.get_or_create( + user=admin, + notification_type=NotificationType.INVITE_REQUEST, + read=False, + ) + notification.related_invite_requests.add(instance) + + @receiver(models.signals.post_save, sender=GroupMemberInvitation) # pylint: disable=unused-argument def notify_user_on_group_invite(sender, instance, *args, **kwargs): diff --git a/bookwyrm/suggested_users.py b/bookwyrm/suggested_users.py index 3e9bef9c4..a13ee97fd 100644 --- a/bookwyrm/suggested_users.py +++ b/bookwyrm/suggested_users.py @@ -8,6 +8,7 @@ from opentelemetry import trace from bookwyrm import models from bookwyrm.redis_store import RedisStore, r +from bookwyrm.settings import INSTANCE_ACTOR_USERNAME from bookwyrm.tasks import app, SUGGESTED_USERS from bookwyrm.telemetry import open_telemetry @@ -98,9 +99,15 @@ class SuggestedUsers(RedisStore): for (pk, score) in values ] # annotate users with mutuals and shared book counts - users = models.User.objects.filter( - is_active=True, bookwyrm_user=True, id__in=[pk for (pk, _) in values] - ).annotate(mutuals=Case(*annotations, output_field=IntegerField(), default=0)) + users = ( + models.User.objects.filter( + is_active=True, bookwyrm_user=True, id__in=[pk for (pk, _) in values] + ) + .annotate( + mutuals=Case(*annotations, output_field=IntegerField(), default=0) + ) + .exclude(localname=INSTANCE_ACTOR_USERNAME) + ) if local: users = users.filter(local=True) return users.order_by("-mutuals")[:5] diff --git a/bookwyrm/templates/book/publisher_info.html b/bookwyrm/templates/book/publisher_info.html index e3ffedca8..a69b7d86f 100644 --- a/bookwyrm/templates/book/publisher_info.html +++ b/bookwyrm/templates/book/publisher_info.html @@ -1,7 +1,7 @@ {% spaceless %} {% load i18n %} -{% load humanize %} +{% load date_ext %} {% firstof book.physical_format_detail book.get_physical_format_display as format %} {% firstof book.physical_format book.physical_format_detail as format_property %} @@ -57,7 +57,7 @@ {% endfor %} {% endif %} - {% with date=book.published_date|default:book.first_published_date|naturalday publisher=book.publishers|join:', ' %} + {% with date=book.published_date|default:book.first_published_date|naturalday_partial publisher=book.publishers|join:', ' %} {% if book.published_date and publisher %} {% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %} {% elif publisher %} diff --git a/bookwyrm/templates/notifications/item.html b/bookwyrm/templates/notifications/item.html index 1f7adbf17..a1329d31e 100644 --- a/bookwyrm/templates/notifications/item.html +++ b/bookwyrm/templates/notifications/item.html @@ -25,6 +25,8 @@ {% include 'notifications/items/report.html' %} {% elif notification.notification_type == 'LINK_DOMAIN' %} {% include 'notifications/items/link_domain.html' %} +{% elif notification.notification_type == 'INVITE_REQUEST' %} + {% include 'notifications/items/invite_request.html' %} {% elif notification.notification_type == 'INVITE' %} {% include 'notifications/items/invite.html' %} {% elif notification.notification_type == 'ACCEPT' %} diff --git a/bookwyrm/templates/notifications/items/invite_request.html b/bookwyrm/templates/notifications/items/invite_request.html new file mode 100644 index 000000000..acc08d5d0 --- /dev/null +++ b/bookwyrm/templates/notifications/items/invite_request.html @@ -0,0 +1,20 @@ +{% extends 'notifications/items/layout.html' %} +{% load humanize %} +{% load i18n %} + +{% block primary_link %}{% spaceless %} +{% url 'settings-invite-requests' %} +{% endspaceless %}{% endblock %} + +{% block icon %} + +{% endblock %} + +{% block description %} + {% url 'settings-invite-requests' as path %} + {% blocktrans trimmed count counter=notification.related_invite_requests.count with display_count=notification.related_invite_requests.count|intcomma %} + New invite request awaiting response + {% plural %} + {{ display_count }} new invite requests awaiting response + {% endblocktrans %} +{% endblock %} diff --git a/bookwyrm/templates/settings/users/user_info.html b/bookwyrm/templates/settings/users/user_info.html index f35c60db9..e07a7e439 100644 --- a/bookwyrm/templates/settings/users/user_info.html +++ b/bookwyrm/templates/settings/users/user_info.html @@ -1,6 +1,7 @@ {% load i18n %} {% load markdown %} {% load humanize %} +{% load utilities %}
{% trans "View user profile" %}
+ {% endif %} + + {% url 'settings-user' user.id as url %} {% if not request.path == url %}{% trans "Go to user admin" %}
diff --git a/bookwyrm/templates/settings/users/user_moderation_actions.html b/bookwyrm/templates/settings/users/user_moderation_actions.html index 4a624a5e4..fd3e66aa8 100644 --- a/bookwyrm/templates/settings/users/user_moderation_actions.html +++ b/bookwyrm/templates/settings/users/user_moderation_actions.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load utilities %}- {% trans "Send direct message" %} -
- {% endif %} + {% if user.localname|is_instance_admin %} ++ {% trans "Send direct message" %} +
+ {% endif %} - {% if not user.is_active and user.deactivation_reason == "pending" %} - - {% endif %} - {% if user.is_active or user.deactivation_reason == "pending" %} - - {% else %} - + {% if not user.is_active and user.deactivation_reason == "pending" %} + + {% endif %} + {% if user.is_active or user.deactivation_reason == "pending" %} + + {% else %} + + {% endif %} + + {% if user.local %} +