diff --git a/.env.dev.example b/.env.dev.example index 22e12de1..0a9865cd 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -16,6 +16,7 @@ DEFAULT_LANGUAGE="English" MEDIA_ROOT=images/ +# Database configuration PGPORT=5432 POSTGRES_PASSWORD=securedbypassword123 POSTGRES_USER=fedireads @@ -26,22 +27,31 @@ POSTGRES_HOST=db MAX_STREAM_LENGTH=200 REDIS_ACTIVITY_HOST=redis_activity REDIS_ACTIVITY_PORT=6379 -#REDIS_ACTIVITY_PASSWORD=redispassword345 +REDIS_ACTIVITY_PASSWORD=redispassword345 # Redis as celery broker REDIS_BROKER_PORT=6379 -#REDIS_BROKER_PASSWORD=redispassword123 +REDIS_BROKER_PASSWORD=redispassword123 +# Monitoring for celery FLOWER_PORT=8888 -#FLOWER_USER=mouse -#FLOWER_PASSWORD=changeme +FLOWER_USER=mouse +FLOWER_PASSWORD=changeme +# Email config EMAIL_HOST=smtp.mailgun.org EMAIL_PORT=587 EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false +EMAIL_SENDER_NAME=admin +# defaults to DOMAIN +EMAIL_SENDER_DOMAIN= + +# Query timeouts +SEARCH_TIMEOUT=15 +QUERY_TIMEOUT=5 # Thumbnails Generation ENABLE_THUMBNAIL_GENERATION=false diff --git a/.env.prod.example b/.env.prod.example index 2e0ced5e..3c935287 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -16,6 +16,7 @@ DEFAULT_LANGUAGE="English" MEDIA_ROOT=images/ +# Database configuration PGPORT=5432 POSTGRES_PASSWORD=securedbypassword123 POSTGRES_USER=fedireads @@ -32,16 +33,25 @@ REDIS_ACTIVITY_PASSWORD=redispassword345 REDIS_BROKER_PORT=6379 REDIS_BROKER_PASSWORD=redispassword123 +# Monitoring for celery FLOWER_PORT=8888 FLOWER_USER=mouse FLOWER_PASSWORD=changeme +# Email config EMAIL_HOST=smtp.mailgun.org EMAIL_PORT=587 EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false +EMAIL_SENDER_NAME=admin +# defaults to DOMAIN +EMAIL_SENDER_DOMAIN= + +# Query timeouts +SEARCH_TIMEOUT=15 +QUERY_TIMEOUT=5 # Thumbnails Generation ENABLE_THUMBNAIL_GENERATION=false diff --git a/.github/workflows/django-tests.yml b/.github/workflows/django-tests.yml index 03875193..00e08dad 100644 --- a/.github/workflows/django-tests.yml +++ b/.github/workflows/django-tests.yml @@ -46,6 +46,8 @@ jobs: POSTGRES_HOST: 127.0.0.1 CELERY_BROKER: "" REDIS_BROKER_PORT: 6379 + REDIS_BROKER_PASSWORD: beep + USE_DUMMY_CACHE: true FLOWER_PORT: 8888 EMAIL_HOST: "smtp.mailgun.org" EMAIL_PORT: 587 diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 20a17526..5ed57df1 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -35,7 +35,7 @@ class AbstractMinimalConnector(ABC): for field in self_fields: setattr(self, field, getattr(info, field)) - def search(self, query, min_confidence=None, timeout=5): + def search(self, query, min_confidence=None, timeout=settings.QUERY_TIMEOUT): """free text search""" params = {} if min_confidence: @@ -52,12 +52,13 @@ class AbstractMinimalConnector(ABC): results.append(self.format_search_result(doc)) return results - def isbn_search(self, query): + def isbn_search(self, query, timeout=settings.QUERY_TIMEOUT): """isbn search""" params = {} data = self.get_search_data( f"{self.isbn_search_url}{query}", params=params, + timeout=timeout, ) results = [] diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index 45530cd6..3bdd5cb4 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -11,6 +11,7 @@ from django.db.models import signals from requests import HTTPError from bookwyrm import book_search, models +from bookwyrm.settings import SEARCH_TIMEOUT from bookwyrm.tasks import app logger = logging.getLogger(__name__) @@ -30,7 +31,6 @@ def search(query, min_confidence=0.1, return_first=False): isbn = re.sub(r"[\W_]", "", query) maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 - timeout = 15 start_time = datetime.now() for connector in get_connectors(): result_set = None @@ -62,7 +62,7 @@ def search(query, min_confidence=0.1, return_first=False): "results": result_set, } ) - if (datetime.now() - start_time).seconds >= timeout: + if (datetime.now() - start_time).seconds >= SEARCH_TIMEOUT: break if return_first: diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 08fd9ef8..efef1263 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -69,7 +69,7 @@ def format_email(email_name, data): def send_email(recipient, subject, html_content, text_content): """use a task to send the email""" email = EmailMultiAlternatives( - subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient] + subject, text_content, settings.EMAIL_SENDER, [recipient] ) email.attach_alternative(html_content, "text/html") email.send() diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index 5cc11afd..5edac57d 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -1,6 +1,8 @@ """ database schema for info about authors """ import re from django.contrib.postgres.indexes import GinIndex +from django.core.cache import cache +from django.core.cache.utils import make_template_fragment_key from django.db import models from bookwyrm import activitypub @@ -34,6 +36,17 @@ class Author(BookDataModel): ) bio = fields.HtmlField(null=True, blank=True) + def save(self, *args, **kwargs): + """clear related template caches""" + # clear template caches + if self.id: + cache_keys = [ + make_template_fragment_key("titleby", [book]) + for book in self.book_set.values_list("id", flat=True) + ] + cache.delete_many(cache_keys) + return super().save(*args, **kwargs) + @property def isni_link(self): """generate the url from the isni id""" diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 0a551bf2..a9dd9508 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -3,6 +3,8 @@ import re from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex +from django.core.cache import cache +from django.core.cache.utils import make_template_fragment_key from django.db import models, transaction from django.db.models import Prefetch from django.dispatch import receiver @@ -185,6 +187,11 @@ class Book(BookDataModel): """can't be abstract for query reasons, but you shouldn't USE it""" if not isinstance(self, Edition) and not isinstance(self, Work): raise ValueError("Books should be added as Editions or Works") + + # clear template caches + cache_key = make_template_fragment_key("titleby", [self.id]) + cache.delete(cache_key) + return super().save(*args, **kwargs) def get_remote_id(self): diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index fc7a9df8..03417454 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,5 +1,7 @@ """ defines relationships between users """ from django.apps import apps +from django.core.cache import cache +from django.core.cache.utils import make_template_fragment_key from django.db import models, transaction, IntegrityError from django.db.models import Q @@ -36,6 +38,20 @@ class UserRelationship(BookWyrmModel): """the remote user needs to recieve direct broadcasts""" return [u for u in [self.user_subject, self.user_object] if not u.local] + def save(self, *args, **kwargs): + """clear the template cache""" + # invalidate the template cache + cache_keys = [ + make_template_fragment_key( + "follow_button", [self.user_subject.id, self.user_object.id] + ), + make_template_fragment_key( + "follow_button", [self.user_object.id, self.user_subject.id] + ), + ] + cache.delete_many(cache_keys) + super().save(*args, **kwargs) + class Meta: """relationships should be unique""" diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index c7c0a425..ee138d97 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -82,6 +82,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): if not self.reply_parent: self.thread_id = self.id + super().save(broadcast=False, update_fields=["thread_id"]) def delete(self, *args, **kwargs): # pylint: disable=unused-argument diff --git a/bookwyrm/redis_store.py b/bookwyrm/redis_store.py index 3b1b54a4..263d7fa2 100644 --- a/bookwyrm/redis_store.py +++ b/bookwyrm/redis_store.py @@ -5,7 +5,10 @@ import redis from bookwyrm import settings r = redis.Redis( - host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0 + host=settings.REDIS_ACTIVITY_HOST, + port=settings.REDIS_ACTIVITY_PORT, + password=settings.REDIS_ACTIVITY_PASSWORD, + db=0, ) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index f2068a16..5920ed80 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -24,7 +24,9 @@ EMAIL_HOST_USER = env("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True) EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False) -DEFAULT_FROM_EMAIL = f"admin@{DOMAIN}" +EMAIL_SENDER_NAME = env("EMAIL_SENDER_NAME", "admin") +EMAIL_SENDER_DOMAIN = env("EMAIL_SENDER_NAME", DOMAIN) +EMAIL_SENDER = f"{EMAIL_SENDER_NAME}@{EMAIL_SENDER_DOMAIN}" # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -119,6 +121,34 @@ STREAMS = [ {"key": "books", "name": _("Books Timeline"), "shortname": _("Books")}, ] +# Search configuration +# total time in seconds that the instance will spend searching connectors +SEARCH_TIMEOUT = int(env("SEARCH_TIMEOUT", 15)) +# timeout for a query to an individual connector +QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5)) + +# Redis cache backend +if env("USE_DUMMY_CACHE", False): + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + } +else: + # pylint: disable=line-too-long + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": f"redis://:{REDIS_ACTIVITY_PASSWORD}@{REDIS_ACTIVITY_HOST}:{REDIS_ACTIVITY_PORT}/0", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + } + } + + SESSION_ENGINE = "django.contrib.sessions.backends.cache" + SESSION_CACHE_ALIAS = "default" + # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases diff --git a/bookwyrm/suggested_users.py b/bookwyrm/suggested_users.py index eb03ceae..ea6bc886 100644 --- a/bookwyrm/suggested_users.py +++ b/bookwyrm/suggested_users.py @@ -30,6 +30,7 @@ class SuggestedUsers(RedisStore): def get_counts_from_rank(self, rank): # pylint: disable=no-self-use """calculate mutuals count and shared books count from rank""" + # pylint: disable=c-extension-no-member return { "mutuals": math.floor(rank), # "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1, @@ -95,7 +96,7 @@ class SuggestedUsers(RedisStore): ).annotate(mutuals=Case(*annotations, output_field=IntegerField(), default=0)) if local: users = users.filter(local=True) - return users[:5] + return users.order_by("-mutuals")[:5] def get_annotated_users(viewer, *args, **kwargs): @@ -113,16 +114,17 @@ 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, - # ), + # pylint: disable=line-too-long + # 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, + # ), ) ) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index ab25458c..c333c26d 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -109,7 +109,7 @@ {% if not books %} -

{% blocktrans %}Sadly {{ display_name }} didn’t finish any book in {{ year }}{% endblocktrans %}

+

{% blocktrans %}Sadly {{ display_name }} didn’t finish any books in {{ year }}{% endblocktrans %}

{% else %}
diff --git a/bookwyrm/templates/directory/sort_filter.html b/bookwyrm/templates/directory/sort_filter.html index c7c19f6f..34436601 100644 --- a/bookwyrm/templates/directory/sort_filter.html +++ b/bookwyrm/templates/directory/sort_filter.html @@ -3,10 +3,12 @@ {% block filter %} -
- +
+
+ +
{% endblock %} diff --git a/bookwyrm/templates/feed/layout.html b/bookwyrm/templates/feed/layout.html index 6e7ec849..5697f266 100644 --- a/bookwyrm/templates/feed/layout.html +++ b/bookwyrm/templates/feed/layout.html @@ -8,82 +8,7 @@
{% if user.is_authenticated %}
-
-

{% trans "Your Books" %}

- {% if not suggested_books %} -

{% trans "There are no books here right now! Try searching for a book to get started" %}

- {% else %} - {% with active_book=request.GET.book %} -
-
-
    - {% for shelf in suggested_books %} - {% if shelf.books %} - {% with shelf_counter=forloop.counter %} -
  • -

    - {% if shelf.identifier == 'to-read' %}{% trans "To Read" %} - {% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %} - {% elif shelf.identifier == 'read' %}{% trans "Read" %} - {% else %}{{ shelf.name }}{% endif %} -

    -
    - -
    -
  • - {% endwith %} - {% endif %} - {% endfor %} -
-
- {% for shelf in suggested_books %} - {% with shelf_counter=forloop.counter %} - {% for book in shelf.books %} -
- -
-
-
-

{% include 'snippets/book_titleby.html' with book=book %}

- {% include 'snippets/shelve_button/shelve_button.html' with book=book %} -
-
-
- {% trans "Close" as button_text %} - {% include 'snippets/toggle/toggle_button.html' with label=button_text controls_text="book" controls_uid=book.id class="delete" nonbutton=True pressed=True %} -
-
-
- {% include 'snippets/create_status.html' with book=book %} -
-
- {% endfor %} - {% endwith %} - {% endfor %} -
- {% endwith %} - {% endif %} -
- + {% include "feed/suggested_books.html" %} {% if goal %}
diff --git a/bookwyrm/templates/feed/suggested_books.html b/bookwyrm/templates/feed/suggested_books.html new file mode 100644 index 00000000..899767d1 --- /dev/null +++ b/bookwyrm/templates/feed/suggested_books.html @@ -0,0 +1,79 @@ +{% load i18n %} +{% load bookwyrm_tags %} + +{% suggested_books as suggested_books %} +
+

{% trans "Your Books" %}

+ {% if not suggested_books %} +

{% trans "There are no books here right now! Try searching for a book to get started" %}

+ {% else %} + {% with active_book=request.GET.book %} +
+
+
    + {% for shelf in suggested_books %} + {% if shelf.books %} + {% with shelf_counter=forloop.counter %} +
  • +

    + {% if shelf.identifier == 'to-read' %}{% trans "To Read" %} + {% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %} + {% elif shelf.identifier == 'read' %}{% trans "Read" %} + {% else %}{{ shelf.name }}{% endif %} +

    +
    + +
    +
  • + {% endwith %} + {% endif %} + {% endfor %} +
+
+ {% for shelf in suggested_books %} + {% with shelf_counter=forloop.counter %} + {% for book in shelf.books %} +
+ +
+
+
+

{% include 'snippets/book_titleby.html' with book=book %}

+ {% include 'snippets/shelve_button/shelve_button.html' with book=book %} +
+
+
+ {% trans "Close" as button_text %} + {% include 'snippets/toggle/toggle_button.html' with label=button_text controls_text="book" controls_uid=book.id class="delete" nonbutton=True pressed=True %} +
+
+
+ {% include 'snippets/create_status.html' with book=book %} +
+
+ {% endfor %} + {% endwith %} + {% endfor %} +
+ {% endwith %} + {% endif %} +
diff --git a/bookwyrm/templates/landing/landing.html b/bookwyrm/templates/landing/landing.html index d13cd582..759e8c61 100644 --- a/bookwyrm/templates/landing/landing.html +++ b/bookwyrm/templates/landing/landing.html @@ -1,11 +1,13 @@ {% extends 'landing/layout.html' %} {% load i18n %} +{% load cache %} {% block panel %}

{% trans "Recent Books" %}

+{% cache 60 * 60 %}
@@ -46,5 +48,5 @@
- +{% endcache %} {% endblock %} diff --git a/bookwyrm/templates/settings/users/server_filter.html b/bookwyrm/templates/settings/users/server_filter.html index 2a4b89fd..5879f748 100644 --- a/bookwyrm/templates/settings/users/server_filter.html +++ b/bookwyrm/templates/settings/users/server_filter.html @@ -3,5 +3,7 @@ {% block filter %} - +
+ +
{% endblock %} diff --git a/bookwyrm/templates/settings/users/username_filter.html b/bookwyrm/templates/settings/users/username_filter.html index d7da033a..343e61c8 100644 --- a/bookwyrm/templates/settings/users/username_filter.html +++ b/bookwyrm/templates/settings/users/username_filter.html @@ -3,6 +3,7 @@ {% block filter %} - +
+ +
{% endblock %} - diff --git a/bookwyrm/templates/snippets/book_titleby.html b/bookwyrm/templates/snippets/book_titleby.html index 6dbaeb26..5e35e36a 100644 --- a/bookwyrm/templates/snippets/book_titleby.html +++ b/bookwyrm/templates/snippets/book_titleby.html @@ -1,7 +1,11 @@ {% load i18n %} {% load utilities %} +{% load cache %} {% spaceless %} +{# 6 month cache #} +{% cache 15552000 titleby book.id %} + {% if book.authors.exists %} {% blocktrans trimmed with path=book.local_path title=book|book_title %} {{ title }} by @@ -10,4 +14,6 @@ {% else %} {{ book|book_title }} {% endif %} + +{% endcache %} {% endspaceless %} diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index 530322b9..f7025bba 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -1,4 +1,5 @@ {% load i18n %} + {% if request.user == user or not request.user.is_authenticated %} {% elif user in request.user.blocks.all %} {% include 'snippets/block_button.html' with blocks=True %} diff --git a/bookwyrm/templates/snippets/status/headers/generatednote.html b/bookwyrm/templates/snippets/status/headers/generatednote.html index cc684a5f..7fc635ab 100644 --- a/bookwyrm/templates/snippets/status/headers/generatednote.html +++ b/bookwyrm/templates/snippets/status/headers/generatednote.html @@ -1,3 +1,7 @@ +{% load cache %} + +{# Three day cache #} +{% cache 259200 generated_note_header status.id %} {% if status.content == 'wants to read' %} {% include 'snippets/status/headers/to_read.html' with book=status.mention_books.first %} {% elif status.content == 'finished reading' %} @@ -7,3 +11,4 @@ {% else %} {{ status.content }} {% endif %} +{% endcache %} diff --git a/bookwyrm/templates/snippets/status/layout.html b/bookwyrm/templates/snippets/status/layout.html index 93620a08..5cbcef20 100644 --- a/bookwyrm/templates/snippets/status/layout.html +++ b/bookwyrm/templates/snippets/status/layout.html @@ -30,38 +30,39 @@ {# nothing here #} {% elif request.user.is_authenticated %} - - - -{% if not moderation_mode %} - -{% endif %} + + + + {% if not moderation_mode %} + + {% endif %} {% else %} - {% endif %} {% endblock %} diff --git a/bookwyrm/templates/snippets/status/status_options.html b/bookwyrm/templates/snippets/status/status_options.html index 854d4779..fdf8ac14 100644 --- a/bookwyrm/templates/snippets/status/status_options.html +++ b/bookwyrm/templates/snippets/status/status_options.html @@ -20,17 +20,21 @@ {% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %} {% endif %} {% else %} {# things you can do to other people's statuses #}