diff --git a/.github/workflows/django-tests.yml b/.github/workflows/django-tests.yml index 03147744..03875193 100644 --- a/.github/workflows/django-tests.yml +++ b/.github/workflows/django-tests.yml @@ -36,6 +36,7 @@ jobs: env: SECRET_KEY: beepbeep DEBUG: false + USE_HTTPS: true DOMAIN: your.domain.here BOOKWYRM_DATABASE_BACKEND: postgres MEDIA_ROOT: images/ diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index da32fbaf..ddd45426 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -48,7 +48,7 @@ class Signature: def naive_parse(activity_objects, activity_json, serializer=None): - """this navigates circular import issues""" + """this navigates circular import issues by looking up models' serializers""" if not serializer: if activity_json.get("publicKeyPem"): # ugh diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index ebe2cbd9..0755314b 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -6,7 +6,6 @@ from django.db.models import signals, Q from bookwyrm import models from bookwyrm.redis_store import RedisStore, r from bookwyrm.tasks import app -from bookwyrm.settings import STREAMS from bookwyrm.views.helpers import privacy_filter @@ -58,7 +57,13 @@ class ActivityStream(RedisStore): return ( models.Status.objects.select_subclasses() .filter(id__in=statuses) - .select_related("user", "reply_parent") + .select_related( + "user", + "reply_parent", + "comment__book", + "review__book", + "quotation__book", + ) .prefetch_related("mention_books", "mention_users") .order_by("-published_date") ) @@ -237,15 +242,10 @@ class BooksStream(ActivityStream): # determine which streams are enabled in settings.py -available_streams = [s["key"] for s in STREAMS] streams = { - k: v - for (k, v) in { - "home": HomeStream(), - "local": LocalStream(), - "books": BooksStream(), - }.items() - if k in available_streams + "home": HomeStream(), + "local": LocalStream(), + "books": BooksStream(), } @@ -261,8 +261,6 @@ def add_status_on_create(sender, instance, created, *args, **kwargs): remove_status_task.delay(instance.id) return - if not created: - return # when creating new things, gotta wait on the transaction transaction.on_commit(lambda: add_status_on_create_command(sender, instance)) @@ -395,8 +393,53 @@ def remove_statuses_on_shelve(sender, instance, *args, **kwargs): BooksStream().remove_book_statuses(instance.user, instance.book) +@receiver(signals.pre_save, sender=models.ShelfBook) +# pylint: disable=unused-argument +def add_statuses_on_shelve(sender, instance, *args, **kwargs): + """update books stream when user shelves a book""" + if not instance.user.local: + return + book = None + if hasattr(instance, "book"): + book = instance.book + elif instance.mention_books.exists(): + book = instance.mention_books.first() + if not book: + return + + # check if the book is already on the user's shelves + editions = book.parent_work.editions.all() + if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists(): + return + + BooksStream().add_book_statuses(instance.user, book) + + +@receiver(signals.post_delete, sender=models.ShelfBook) +# pylint: disable=unused-argument +def remove_statuses_on_unshelve(sender, instance, *args, **kwargs): + """update books stream when user unshelves a book""" + if not instance.user.local: + return + + book = None + if hasattr(instance, "book"): + book = instance.book + elif instance.mention_books.exists(): + book = instance.mention_books.first() + if not book: + return + # check if the book is actually unshelved, not just moved + editions = book.parent_work.editions.all() + if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists(): + return + + BooksStream().remove_book_statuses(instance.user, instance.book) + + # ---- TASKS +# TODO: merge conflict: reconcile these tasks @app.task def populate_streams_task(user_id): @@ -406,6 +449,14 @@ def populate_streams_task(user_id): stream.populate_streams(user) +@app.task +def populate_stream_task(stream, user_id): + """background task for populating an empty activitystream""" + user = models.User.objects.get(id=user_id) + stream = streams[stream] + stream.populate_streams(user) + + @app.task def remove_status_task(status_ids): """remove a status from any stream it might be in""" @@ -444,4 +495,4 @@ def add_user_statuses_task(viewer_id, user_id, stream_list=None): viewer = models.User.objects.get(id=viewer_id) user = models.User.objects.get(id=user_id) for stream in stream_list: - stream.add_user_statuses(viewer, user) + stream.add_user_statuses(viewer, user) \ No newline at end of file diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py index 116aa5c1..842d0997 100644 --- a/bookwyrm/connectors/inventaire.py +++ b/bookwyrm/connectors/inventaire.py @@ -71,7 +71,7 @@ class Connector(AbstractConnector): # flatten the data so that images, uri, and claims are on the same level return { **data.get("claims", {}), - **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]}, + **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]}, } def search(self, query, min_confidence=None): # pylint: disable=arguments-differ diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 657310b0..fff3985e 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -23,6 +23,14 @@ def email_data(): } +def email_confirmation_email(user): + """newly registered users confirm email address""" + data = email_data() + data["confirmation_code"] = user.confirmation_code + data["confirmation_link"] = user.confirmation_link + send_email.delay(user.email, *format_email("confirm", data)) + + def invite_email(invite_request): """send out an invite code""" data = email_data() diff --git a/bookwyrm/management/commands/populate_streams.py b/bookwyrm/management/commands/populate_streams.py index f8aa21a5..a04d7f5a 100644 --- a/bookwyrm/management/commands/populate_streams.py +++ b/bookwyrm/management/commands/populate_streams.py @@ -3,22 +3,35 @@ from django.core.management.base import BaseCommand from bookwyrm import activitystreams, models -def populate_streams(): +def populate_streams(stream=None): """build all the streams for all the users""" + streams = [stream] if stream else activitystreams.streams.keys() + print("Populations streams", streams) users = models.User.objects.filter( local=True, is_active=True, - ) + ).order_by("-last_active_date") + print("This may take a long time! Please be patient.") for user in users: - for stream in activitystreams.streams.values(): - stream.populate_streams(user) + for stream_key in streams: + print(".", end="") + activitystreams.populate_stream_task.delay(stream_key, user.id) class Command(BaseCommand): """start all over with user streams""" help = "Populate streams for all users" + + def add_arguments(self, parser): + parser.add_argument( + "--stream", + default=None, + help="Specifies which time of stream to populate", + ) + # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): """run feed builder""" - populate_streams() + stream = options.get("stream") + populate_streams(stream=stream) diff --git a/bookwyrm/migrations/0081_alter_user_last_active_date.py b/bookwyrm/migrations/0081_alter_user_last_active_date.py new file mode 100644 index 00000000..dc6b640f --- /dev/null +++ b/bookwyrm/migrations/0081_alter_user_last_active_date.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.4 on 2021-08-06 02:51 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0080_alter_shelfbook_options"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="last_active_date", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/bookwyrm/migrations/0082_auto_20210806_2324.py b/bookwyrm/migrations/0082_auto_20210806_2324.py new file mode 100644 index 00000000..ab0aa158 --- /dev/null +++ b/bookwyrm/migrations/0082_auto_20210806_2324.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.4 on 2021-08-06 23:24 + +import bookwyrm.models.base_model +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0081_alter_user_last_active_date"), + ] + + operations = [ + migrations.AddField( + model_name="sitesettings", + name="require_confirm_email", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="user", + name="confirmation_code", + field=models.CharField( + default=bookwyrm.models.base_model.new_access_code, max_length=32 + ), + ), + migrations.AlterField( + model_name="connector", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending"), + ("self_deletion", "Self Deletion"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending"), + ("self_deletion", "Self Deletion"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 729d9cba..4e313723 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -7,7 +7,7 @@ import operator import logging from uuid import uuid4 import requests -from requests.exceptions import HTTPError, SSLError +from requests.exceptions import RequestException from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 @@ -43,7 +43,7 @@ class ActivitypubMixin: reverse_unfurl = False def __init__(self, *args, **kwargs): - """collect some info on model fields""" + """collect some info on model fields for later use""" self.image_fields = [] self.many_to_many_fields = [] self.simple_fields = [] # "simple" @@ -503,7 +503,7 @@ def broadcast_task(sender_id, activity, recipients): for recipient in recipients: try: sign_and_send(sender, activity, recipient) - except (HTTPError, SSLError, requests.exceptions.ConnectionError): + except RequestException: pass diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 2cb7c036..5b55ea50 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,4 +1,6 @@ """ base model with default fields """ +import base64 +from Crypto import Random from django.db import models from django.dispatch import receiver @@ -9,6 +11,7 @@ from .fields import RemoteIdField DeactivationReason = models.TextChoices( "DeactivationReason", [ + "pending", "self_deletion", "moderator_deletion", "domain_block", @@ -16,6 +19,11 @@ DeactivationReason = models.TextChoices( ) +def new_access_code(): + """the identifier for a user invite""" + return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") + + class BookWyrmModel(models.Model): """shared fields""" diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index f2993846..05aada16 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -80,7 +80,7 @@ class ImportItem(models.Model): else: # don't fall back on title/author search is isbn is present. # you're too likely to mismatch - self.get_book_from_title_author() + self.book = self.get_book_from_title_author() def get_book_from_isbn(self): """search by isbn""" diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 872f6b45..ef3f7c3c 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -1,8 +1,6 @@ """ the particulars for this instance of BookWyrm """ -import base64 import datetime -from Crypto import Random from django.db import models, IntegrityError from django.dispatch import receiver from django.utils import timezone @@ -10,7 +8,7 @@ from model_utils import FieldTracker from bookwyrm.preview_images import generate_site_preview_image_task from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES -from .base_model import BookWyrmModel +from .base_model import BookWyrmModel, new_access_code from .user import User @@ -33,6 +31,7 @@ class SiteSettings(models.Model): # registration allow_registration = models.BooleanField(default=True) allow_invite_requests = models.BooleanField(default=True) + require_confirm_email = models.BooleanField(default=True) # images logo = models.ImageField(upload_to="logos/", null=True, blank=True) @@ -61,11 +60,6 @@ class SiteSettings(models.Model): return default_settings -def new_access_code(): - """the identifier for a user invite""" - return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") - - class SiteInvite(models.Model): """gives someone access to create an account on the instance""" diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 21b6bbaa..e10bcd29 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -17,16 +17,22 @@ from bookwyrm.connectors import get_data, ConnectorException from bookwyrm.models.shelf import Shelf from bookwyrm.models.status import Status, Review from bookwyrm.preview_images import generate_user_preview_image_task -from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES +from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, USE_HTTPS from bookwyrm.signatures import create_key_pair from bookwyrm.tasks import app from bookwyrm.utils import regex from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin -from .base_model import BookWyrmModel, DeactivationReason +from .base_model import BookWyrmModel, DeactivationReason, new_access_code from .federated_server import FederatedServer from . import fields, Review +def site_link(): + """helper for generating links to the site""" + protocol = "https" if USE_HTTPS else "http" + return f"{protocol}://{DOMAIN}" + + class User(OrderedCollectionPageMixin, AbstractUser): """a user who wants to read books""" @@ -111,7 +117,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) - last_active_date = models.DateTimeField(auto_now=True) + last_active_date = models.DateTimeField(default=timezone.now) manually_approves_followers = fields.BooleanField(default=False) show_goal = models.BooleanField(default=True) discoverable = fields.BooleanField(default=False) @@ -123,11 +129,18 @@ class User(OrderedCollectionPageMixin, AbstractUser): deactivation_reason = models.CharField( max_length=255, choices=DeactivationReason.choices, null=True, blank=True ) + confirmation_code = models.CharField(max_length=32, default=new_access_code) name_field = "username" property_fields = [("following_link", "following")] field_tracker = FieldTracker(fields=["name", "avatar"]) + @property + def confirmation_link(self): + """helper for generating confirmation links""" + link = site_link() + return f"{link}/confirm-email/{self.confirmation_code}" + @property def following_link(self): """just how to find out the following info""" @@ -207,7 +220,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): self.following.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, - **kwargs + **kwargs, ) def to_followers_activity(self, **kwargs): @@ -217,7 +230,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): self.followers.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, - **kwargs + **kwargs, ) def to_activity(self, **kwargs): @@ -259,9 +272,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): return # populate fields for local users - self.remote_id = "https://%s/user/%s" % (DOMAIN, self.localname) + self.remote_id = "%s/user/%s" % (site_link(), self.localname) self.inbox = "%s/inbox" % self.remote_id - self.shared_inbox = "https://%s/inbox" % DOMAIN + self.shared_inbox = "%s/inbox" % site_link() self.outbox = "%s/outbox" % self.remote_id # an id needs to be set before we can proceed with related models diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index d10fb9b7..8fbdcfc6 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -29,6 +29,11 @@ body { min-width: 75% !important; } +.clip-text { + max-height: 35em; + overflow: hidden; +} + /** Utilities not covered by Bulma ******************************************************************************/ diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index e43ed134..a4002c2d 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -164,7 +164,7 @@ let BookWyrm = new class { } // Show/hide container. - let container = document.getElementById('hide-' + targetId); + let container = document.getElementById('hide_' + targetId); if (container) { this.toggleContainer(container, pressed); @@ -219,7 +219,7 @@ let BookWyrm = new class { /** * Check or uncheck a checbox. * - * @param {object} checkbox - DOM node + * @param {string} checkbox - id of the checkbox * @param {boolean} pressed - Is the trigger pressed? * @return {undefined} */ diff --git a/bookwyrm/suggested_users.py b/bookwyrm/suggested_users.py index 3a95ef7f..9c42d79d 100644 --- a/bookwyrm/suggested_users.py +++ b/bookwyrm/suggested_users.py @@ -19,7 +19,7 @@ class SuggestedUsers(RedisStore): def get_rank(self, obj): """get computed rank""" - return obj.mutuals + (1.0 - (1.0 / (obj.shared_books + 1))) + return obj.mutuals # + (1.0 - (1.0 / (obj.shared_books + 1))) def store_id(self, user): # pylint: disable=no-self-use """the key used to store this user's recs""" @@ -31,7 +31,7 @@ class SuggestedUsers(RedisStore): """calculate mutuals count and shared books count from rank""" return { "mutuals": math.floor(rank), - "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1, + # "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1, } def get_objects_for_store(self, store): @@ -95,7 +95,7 @@ class SuggestedUsers(RedisStore): logger.exception(err) continue user.mutuals = counts["mutuals"] - user.shared_books = counts["shared_books"] + # user.shared_books = counts["shared_books"] results.append(user) return results @@ -115,16 +115,16 @@ def get_annotated_users(viewer, *args, **kwargs): ), distinct=True, ), - shared_books=Count( - "shelfbook", - filter=Q( - ~Q(id=viewer.id), - shelfbook__book__parent_work__in=[ - s.book.parent_work for s in viewer.shelfbook_set.all() - ], - ), - distinct=True, - ), + # shared_books=Count( + # "shelfbook", + # filter=Q( + # ~Q(id=viewer.id), + # shelfbook__book__parent_work__in=[ + # s.book.parent_work for s in viewer.shelfbook_set.all() + # ], + # ), + # distinct=True, + # ), ) ) @@ -162,18 +162,18 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs): rerank_user_task.delay(instance.user_object.id, update_only=False) -@receiver(signals.post_save, sender=models.ShelfBook) -@receiver(signals.post_delete, sender=models.ShelfBook) -# pylint: disable=unused-argument -def update_rank_on_shelving(sender, instance, *args, **kwargs): - """when a user shelves or unshelves a book, re-compute their rank""" - # if it's a local user, re-calculate who is rec'ed to them - if instance.user.local: - rerank_suggestions_task.delay(instance.user.id) - - # if the user is discoverable, update their rankings - if instance.user.discoverable: - rerank_user_task.delay(instance.user.id) +# @receiver(signals.post_save, sender=models.ShelfBook) +# @receiver(signals.post_delete, sender=models.ShelfBook) +# # pylint: disable=unused-argument +# def update_rank_on_shelving(sender, instance, *args, **kwargs): +# """when a user shelves or unshelves a book, re-compute their rank""" +# # if it's a local user, re-calculate who is rec'ed to them +# if instance.user.local: +# rerank_suggestions_task.delay(instance.user.id) +# +# # if the user is discoverable, update their rankings +# if instance.user.discoverable: +# rerank_user_task.delay(instance.user.id) @receiver(signals.post_save, sender=models.User) diff --git a/bookwyrm/templates/author/author.html b/bookwyrm/templates/author/author.html index 0bc42775..1c409d06 100644 --- a/bookwyrm/templates/author/author.html +++ b/bookwyrm/templates/author/author.html @@ -70,7 +70,7 @@

{% endif %} - + {% if author.inventaire_id %}

@@ -86,7 +86,7 @@

{% endif %} - + {% if author.goodreads_key %}

@@ -109,7 +109,7 @@

{% for book in books %}
- {% include 'discover/small-book.html' with book=book %} + {% include 'landing/small-book.html' with book=book %}
{% endfor %}
diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index c5dab109..2e8ff0d0 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -72,8 +72,8 @@ {% if user_authenticated and not book.cover %}
{% trans "Add cover" as button_text %} - {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add-cover" controls_uid=book.id focus="modal-title-add-cover" class="is-small" %} - {% include 'book/cover_modal.html' with book=book controls_text="add-cover" controls_uid=book.id %} + {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add_cover" controls_uid=book.id focus="modal_title_add_cover" class="is-small" %} + {% include 'book/cover_modal.html' with book=book controls_text="add_cover" controls_uid=book.id %} {% if request.GET.cover_error %}

{% trans "Failed to load cover" %}

{% endif %} @@ -128,19 +128,19 @@ {% if user_authenticated and can_edit_book and not book|book_description %} {% trans 'Add Description' as button_text %} - {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add-description" controls_uid=book.id focus="id_description" hide_active=True id="hide-description" %} + {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %} -
{% trans "Add read dates" as button_text %} - {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add-readthrough" focus="add-readthrough-focus" %} + {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add_readthrough" focus="add_readthrough_focus_" %}
-
diff --git a/bookwyrm/templates/book/readthrough.html b/bookwyrm/templates/book/readthrough.html index 75140746..05ed3c63 100644 --- a/bookwyrm/templates/book/readthrough.html +++ b/bookwyrm/templates/book/readthrough.html @@ -2,7 +2,7 @@ {% load humanize %} {% load tz %}
-
+
{% trans "Progress Updates:" %} @@ -24,7 +24,7 @@ {% if readthrough.progress %} {% trans "Show all updates" as button_text %} {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="updates" controls_uid=readthrough.id class="is-small" %} -
@@ -69,15 +69,15 @@
-