Merge branch 'main' into list-not-loading

This commit is contained in:
Mouse Reeve 2022-01-07 10:32:17 -08:00
commit 165fdc6d2d
38 changed files with 1673 additions and 1871 deletions

View file

@ -16,6 +16,7 @@ DEFAULT_LANGUAGE="English"
MEDIA_ROOT=images/ MEDIA_ROOT=images/
# Database configuration
PGPORT=5432 PGPORT=5432
POSTGRES_PASSWORD=securedbypassword123 POSTGRES_PASSWORD=securedbypassword123
POSTGRES_USER=fedireads POSTGRES_USER=fedireads
@ -26,22 +27,31 @@ POSTGRES_HOST=db
MAX_STREAM_LENGTH=200 MAX_STREAM_LENGTH=200
REDIS_ACTIVITY_HOST=redis_activity REDIS_ACTIVITY_HOST=redis_activity
REDIS_ACTIVITY_PORT=6379 REDIS_ACTIVITY_PORT=6379
#REDIS_ACTIVITY_PASSWORD=redispassword345 REDIS_ACTIVITY_PASSWORD=redispassword345
# Redis as celery broker # Redis as celery broker
REDIS_BROKER_PORT=6379 REDIS_BROKER_PORT=6379
#REDIS_BROKER_PASSWORD=redispassword123 REDIS_BROKER_PASSWORD=redispassword123
# Monitoring for celery
FLOWER_PORT=8888 FLOWER_PORT=8888
#FLOWER_USER=mouse FLOWER_USER=mouse
#FLOWER_PASSWORD=changeme FLOWER_PASSWORD=changeme
# Email config
EMAIL_HOST=smtp.mailgun.org EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
EMAIL_SENDER_NAME=admin
# defaults to DOMAIN
EMAIL_SENDER_DOMAIN=
# Query timeouts
SEARCH_TIMEOUT=15
QUERY_TIMEOUT=5
# Thumbnails Generation # Thumbnails Generation
ENABLE_THUMBNAIL_GENERATION=false ENABLE_THUMBNAIL_GENERATION=false

View file

@ -16,6 +16,7 @@ DEFAULT_LANGUAGE="English"
MEDIA_ROOT=images/ MEDIA_ROOT=images/
# Database configuration
PGPORT=5432 PGPORT=5432
POSTGRES_PASSWORD=securedbypassword123 POSTGRES_PASSWORD=securedbypassword123
POSTGRES_USER=fedireads POSTGRES_USER=fedireads
@ -32,16 +33,25 @@ REDIS_ACTIVITY_PASSWORD=redispassword345
REDIS_BROKER_PORT=6379 REDIS_BROKER_PORT=6379
REDIS_BROKER_PASSWORD=redispassword123 REDIS_BROKER_PASSWORD=redispassword123
# Monitoring for celery
FLOWER_PORT=8888 FLOWER_PORT=8888
FLOWER_USER=mouse FLOWER_USER=mouse
FLOWER_PASSWORD=changeme FLOWER_PASSWORD=changeme
# Email config
EMAIL_HOST=smtp.mailgun.org EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
EMAIL_SENDER_NAME=admin
# defaults to DOMAIN
EMAIL_SENDER_DOMAIN=
# Query timeouts
SEARCH_TIMEOUT=15
QUERY_TIMEOUT=5
# Thumbnails Generation # Thumbnails Generation
ENABLE_THUMBNAIL_GENERATION=false ENABLE_THUMBNAIL_GENERATION=false

View file

@ -46,6 +46,8 @@ jobs:
POSTGRES_HOST: 127.0.0.1 POSTGRES_HOST: 127.0.0.1
CELERY_BROKER: "" CELERY_BROKER: ""
REDIS_BROKER_PORT: 6379 REDIS_BROKER_PORT: 6379
REDIS_BROKER_PASSWORD: beep
USE_DUMMY_CACHE: true
FLOWER_PORT: 8888 FLOWER_PORT: 8888
EMAIL_HOST: "smtp.mailgun.org" EMAIL_HOST: "smtp.mailgun.org"
EMAIL_PORT: 587 EMAIL_PORT: 587

View file

@ -35,7 +35,7 @@ class AbstractMinimalConnector(ABC):
for field in self_fields: for field in self_fields:
setattr(self, field, getattr(info, field)) 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""" """free text search"""
params = {} params = {}
if min_confidence: if min_confidence:
@ -52,12 +52,13 @@ class AbstractMinimalConnector(ABC):
results.append(self.format_search_result(doc)) results.append(self.format_search_result(doc))
return results return results
def isbn_search(self, query): def isbn_search(self, query, timeout=settings.QUERY_TIMEOUT):
"""isbn search""" """isbn search"""
params = {} params = {}
data = self.get_search_data( data = self.get_search_data(
f"{self.isbn_search_url}{query}", f"{self.isbn_search_url}{query}",
params=params, params=params,
timeout=timeout,
) )
results = [] results = []

View file

@ -11,6 +11,7 @@ from django.db.models import signals
from requests import HTTPError from requests import HTTPError
from bookwyrm import book_search, models from bookwyrm import book_search, models
from bookwyrm.settings import SEARCH_TIMEOUT
from bookwyrm.tasks import app from bookwyrm.tasks import app
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,7 +31,6 @@ def search(query, min_confidence=0.1, return_first=False):
isbn = re.sub(r"[\W_]", "", query) isbn = re.sub(r"[\W_]", "", query)
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
timeout = 15
start_time = datetime.now() start_time = datetime.now()
for connector in get_connectors(): for connector in get_connectors():
result_set = None result_set = None
@ -62,7 +62,7 @@ def search(query, min_confidence=0.1, return_first=False):
"results": result_set, "results": result_set,
} }
) )
if (datetime.now() - start_time).seconds >= timeout: if (datetime.now() - start_time).seconds >= SEARCH_TIMEOUT:
break break
if return_first: if return_first:

View file

@ -69,7 +69,7 @@ def format_email(email_name, data):
def send_email(recipient, subject, html_content, text_content): def send_email(recipient, subject, html_content, text_content):
"""use a task to send the email""" """use a task to send the email"""
email = EmailMultiAlternatives( 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.attach_alternative(html_content, "text/html")
email.send() email.send()

View file

@ -1,6 +1,8 @@
""" database schema for info about authors """ """ database schema for info about authors """
import re import re
from django.contrib.postgres.indexes import GinIndex 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 django.db import models
from bookwyrm import activitypub from bookwyrm import activitypub
@ -34,6 +36,17 @@ class Author(BookDataModel):
) )
bio = fields.HtmlField(null=True, blank=True) 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 @property
def isni_link(self): def isni_link(self):
"""generate the url from the isni id""" """generate the url from the isni id"""

View file

@ -3,6 +3,8 @@ import re
from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex 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 import models, transaction
from django.db.models import Prefetch from django.db.models import Prefetch
from django.dispatch import receiver 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""" """can't be abstract for query reasons, but you shouldn't USE it"""
if not isinstance(self, Edition) and not isinstance(self, Work): if not isinstance(self, Edition) and not isinstance(self, Work):
raise ValueError("Books should be added as Editions or Works") 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) return super().save(*args, **kwargs)
def get_remote_id(self): def get_remote_id(self):

View file

@ -1,5 +1,7 @@
""" defines relationships between users """ """ defines relationships between users """
from django.apps import apps 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 import models, transaction, IntegrityError
from django.db.models import Q from django.db.models import Q
@ -36,6 +38,20 @@ class UserRelationship(BookWyrmModel):
"""the remote user needs to recieve direct broadcasts""" """the remote user needs to recieve direct broadcasts"""
return [u for u in [self.user_subject, self.user_object] if not u.local] 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: class Meta:
"""relationships should be unique""" """relationships should be unique"""

View file

@ -82,6 +82,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
if not self.reply_parent: if not self.reply_parent:
self.thread_id = self.id self.thread_id = self.id
super().save(broadcast=False, update_fields=["thread_id"]) super().save(broadcast=False, update_fields=["thread_id"])
def delete(self, *args, **kwargs): # pylint: disable=unused-argument def delete(self, *args, **kwargs): # pylint: disable=unused-argument

View file

@ -5,7 +5,10 @@ import redis
from bookwyrm import settings from bookwyrm import settings
r = redis.Redis( 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,
) )

View file

@ -24,7 +24,9 @@ EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True) EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False) 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, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -119,6 +121,34 @@ STREAMS = [
{"key": "books", "name": _("Books Timeline"), "shortname": _("Books")}, {"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 # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/3.2/ref/settings/#databases

View file

@ -30,6 +30,7 @@ class SuggestedUsers(RedisStore):
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
"""calculate mutuals count and shared books count from rank""" """calculate mutuals count and shared books count from rank"""
# pylint: disable=c-extension-no-member
return { return {
"mutuals": math.floor(rank), "mutuals": math.floor(rank),
# "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1, # "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)) ).annotate(mutuals=Case(*annotations, output_field=IntegerField(), default=0))
if local: if local:
users = users.filter(local=True) users = users.filter(local=True)
return users[:5] return users.order_by("-mutuals")[:5]
def get_annotated_users(viewer, *args, **kwargs): def get_annotated_users(viewer, *args, **kwargs):
@ -113,6 +114,7 @@ def get_annotated_users(viewer, *args, **kwargs):
), ),
distinct=True, distinct=True,
), ),
# pylint: disable=line-too-long
# shared_books=Count( # shared_books=Count(
# "shelfbook", # "shelfbook",
# filter=Q( # filter=Q(

View file

@ -109,7 +109,7 @@
</div> </div>
{% if not books %} {% if not books %}
<p class="has-text-centered is-size-5">{% blocktrans %}Sadly {{ display_name }} didnt finish any book in {{ year }}{% endblocktrans %}</p> <p class="has-text-centered is-size-5">{% blocktrans %}Sadly {{ display_name }} didnt finish any books in {{ year }}{% endblocktrans %}</p>
{% else %} {% else %}
<div class="columns is-mobile"> <div class="columns is-mobile">

View file

@ -3,10 +3,12 @@
{% block filter %} {% block filter %}
<label class="label" for="id_sort">{% trans "Order by" %}</label> <label class="label" for="id_sort">{% trans "Order by" %}</label>
<div class="select"> <div class="control">
<div class="select">
<select name="sort" id="id_sort"> <select name="sort" id="id_sort">
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option> <option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option> <option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
</select> </select>
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -8,82 +8,7 @@
<div class="columns"> <div class="columns">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="column is-one-third"> <div class="column is-one-third">
<section class="block"> {% include "feed/suggested_books.html" %}
<h2 class="title is-4">{% trans "Your Books" %}</h2>
{% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %}
{% with active_book=request.GET.book %}
<div class="tab-group">
<div class="tabs is-small">
<ul role="tablist">
{% for shelf in suggested_books %}
{% if shelf.books %}
{% with shelf_counter=forloop.counter %}
<li>
<p>
{% 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 %}
</p>
<div class="tabs is-small is-toggle">
<ul>
{% for book in shelf.books %}
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
<a
href="{{ request.path }}?book={{ book.id }}"
id="tab_book_{{ book.id }}"
role="tab"
aria-label="{{ book.title }}"
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
aria-controls="book_{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
</a>
</li>
{% endfor %}
</ul>
</div>
</li>
{% endwith %}
{% endif %}
{% endfor %}
</ul>
</div>
{% for shelf in suggested_books %}
{% with shelf_counter=forloop.counter %}
{% for book in shelf.books %}
<div
class="suggested-tabs card"
role="tabpanel"
id="book_{{ book.id }}"
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
aria-labelledby="tab_book_{{ book.id }}">
<div class="card-header">
<div class="card-header-title">
<div>
<p class="mb-2">{% include 'snippets/book_titleby.html' with book=book %}</p>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
<div class="card-header-icon is-hidden-tablet">
{% 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 %}
</div>
</div>
<div class="card-content">
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
{% endfor %}
{% endwith %}
{% endfor %}
</div>
{% endwith %}
{% endif %}
</section>
{% if goal %} {% if goal %}
<section class="block"> <section class="block">
<div class="block"> <div class="block">

View file

@ -0,0 +1,79 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% suggested_books as suggested_books %}
<section class="block">
<h2 class="title is-4">{% trans "Your Books" %}</h2>
{% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %}
{% with active_book=request.GET.book %}
<div class="tab-group">
<div class="tabs is-small">
<ul role="tablist">
{% for shelf in suggested_books %}
{% if shelf.books %}
{% with shelf_counter=forloop.counter %}
<li>
<p>
{% 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 %}
</p>
<div class="tabs is-small is-toggle">
<ul>
{% for book in shelf.books %}
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
<a
href="{{ request.path }}?book={{ book.id }}"
id="tab_book_{{ book.id }}"
role="tab"
aria-label="{{ book.title }}"
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
aria-controls="book_{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
</a>
</li>
{% endfor %}
</ul>
</div>
</li>
{% endwith %}
{% endif %}
{% endfor %}
</ul>
</div>
{% for shelf in suggested_books %}
{% with shelf_counter=forloop.counter %}
{% for book in shelf.books %}
<div
class="suggested-tabs card"
role="tabpanel"
id="book_{{ book.id }}"
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
aria-labelledby="tab_book_{{ book.id }}">
<div class="card-header">
<div class="card-header-title">
<div>
<p class="mb-2">{% include 'snippets/book_titleby.html' with book=book %}</p>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
<div class="card-header-icon is-hidden-tablet">
{% 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 %}
</div>
</div>
<div class="card-content">
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
{% endfor %}
{% endwith %}
{% endfor %}
</div>
{% endwith %}
{% endif %}
</section>

View file

@ -1,11 +1,13 @@
{% extends 'landing/layout.html' %} {% extends 'landing/layout.html' %}
{% load i18n %} {% load i18n %}
{% load cache %}
{% block panel %} {% block panel %}
<div class="block is-hidden-tablet"> <div class="block is-hidden-tablet">
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2> <h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
</div> </div>
{% cache 60 * 60 %}
<section class="tile is-ancestor"> <section class="tile is-ancestor">
<div class="tile is-vertical is-6"> <div class="tile is-vertical is-6">
<div class="tile is-parent"> <div class="tile is-parent">
@ -46,5 +48,5 @@
</div> </div>
</div> </div>
</section> </section>
{% endcache %}
{% endblock %} {% endblock %}

View file

@ -3,5 +3,7 @@
{% block filter %} {% block filter %}
<label class="label" for="id_server">{% trans "Instance name" %}</label> <label class="label" for="id_server">{% trans "Instance name" %}</label>
<input type="text" class="input" name="server" value="{{ request.GET.server|default:'' }}" id="id_server" placeholder="example.server.com"> <div class="control">
<input type="text" class="input" name="server" value="{{ request.GET.server|default:'' }}" id="id_server" placeholder="example.server.com">
</div>
{% endblock %} {% endblock %}

View file

@ -3,6 +3,7 @@
{% block filter %} {% block filter %}
<label class="label" for="id_username">{% trans "Username" %}</label> <label class="label" for="id_username">{% trans "Username" %}</label>
<input type="text" class="input" name="username" value="{{ request.GET.username|default:'' }}" id="id_username" placeholder="user@domain.com"> <div class="control">
<input type="text" class="input" name="username" value="{{ request.GET.username|default:'' }}" id="id_username" placeholder="user@domain.com">
</div>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,11 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% load cache %}
{% spaceless %} {% spaceless %}
{# 6 month cache #}
{% cache 15552000 titleby book.id %}
{% if book.authors.exists %} {% if book.authors.exists %}
{% blocktrans trimmed with path=book.local_path title=book|book_title %} {% blocktrans trimmed with path=book.local_path title=book|book_title %}
<a href="{{ path }}">{{ title }}</a> by <a href="{{ path }}">{{ title }}</a> by
@ -10,4 +14,6 @@
{% else %} {% else %}
<a href="{{ book.local_path }}">{{ book|book_title }}</a> <a href="{{ book.local_path }}">{{ book|book_title }}</a>
{% endif %} {% endif %}
{% endcache %}
{% endspaceless %} {% endspaceless %}

View file

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% if request.user == user or not request.user.is_authenticated %} {% if request.user == user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %} {% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' with blocks=True %} {% include 'snippets/block_button.html' with blocks=True %}

View file

@ -1,3 +1,7 @@
{% load cache %}
{# Three day cache #}
{% cache 259200 generated_note_header status.id %}
{% if status.content == 'wants to read' %} {% if status.content == 'wants to read' %}
{% include 'snippets/status/headers/to_read.html' with book=status.mention_books.first %} {% include 'snippets/status/headers/to_read.html' with book=status.mention_books.first %}
{% elif status.content == 'finished reading' %} {% elif status.content == 'finished reading' %}
@ -7,3 +11,4 @@
{% else %} {% else %}
{{ status.content }} {{ status.content }}
{% endif %} {% endif %}
{% endcache %}

View file

@ -30,24 +30,25 @@
{# nothing here #} {# nothing here #}
{% elif request.user.is_authenticated %} {% elif request.user.is_authenticated %}
<div class="card-footer-item"> <div class="card-footer-item">
{% trans "Reply" as button_text %} {% trans "Reply" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %} {% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{% include 'snippets/boost_button.html' with status=status %} {% include 'snippets/boost_button.html' with status=status %}
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{% include 'snippets/fav_button.html' with status=status %} {% include 'snippets/fav_button.html' with status=status %}
</div> </div>
{% if not moderation_mode %} {% if not moderation_mode %}
<div class="card-footer-item"> <div class="card-footer-item">
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %} {% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="card-footer-item">
<div class="card-footer-item">
<a href="{% url 'login' %}"> <a href="{% url 'login' %}">
<span class="icon icon-comment is-small" title="{% trans 'Reply' %}"> <span class="icon icon-comment is-small" title="{% trans 'Reply' %}">
<span class="is-sr-only">{% trans "Reply" %}</span> <span class="is-sr-only">{% trans "Reply" %}</span>
@ -61,7 +62,7 @@
<span class="is-sr-only">{% trans "Like status" %}</span> <span class="is-sr-only">{% trans "Like status" %}</span>
</span> </span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -20,17 +20,21 @@
</li> </li>
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %} {% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
<span class="control">
<a href="{% url 'edit-status' status.id %}" class="button is-radiusless is-fullwidth is-small" type="submit"> <a href="{% url 'edit-status' status.id %}" class="button is-radiusless is-fullwidth is-small" type="submit">
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
</span>
</li> </li>
{% endif %} {% endif %}
{% else %} {% else %}
{# things you can do to other people's statuses #} {# things you can do to other people's statuses #}
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
<span class="control">
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-white is-radiusless is-fullwidth"> <a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-white is-radiusless is-fullwidth">
{% trans "Send direct message" %} {% trans "Send direct message" %}
</a> </a>
</span>
</li> </li>
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
{% include 'snippets/report_button.html' with user=status.user status=status %} {% include 'snippets/report_button.html' with user=status.user status=status %}

View file

@ -3,6 +3,7 @@ from django import template
from django.db.models import Avg from django.db.models import Avg
from bookwyrm import models from bookwyrm import models
from bookwyrm.views.feed import get_suggested_books
register = template.Library() register = template.Library()
@ -62,6 +63,8 @@ def load_subclass(status):
return status.review return status.review
if hasattr(status, "comment"): if hasattr(status, "comment"):
return status.comment return status.comment
if hasattr(status, "generatednote"):
return status.generatednote
return status return status
@ -115,3 +118,11 @@ def mutuals_count(context, user):
if not viewer.is_authenticated: if not viewer.is_authenticated:
return None return None
return user.followers.filter(followers=viewer).count() return user.followers.filter(followers=viewer).count()
@register.simple_tag(takes_context=True)
def suggested_books(context):
"""get books for suggested books panel"""
# this happens here instead of in the view so that the template snippet can
# be cached in the template
return get_suggested_books(context["request"].user)

View file

@ -1,5 +1,7 @@
""" template filters for status interaction buttons """ """ template filters for status interaction buttons """
from django import template from django import template
from django.core.cache import cache
from bookwyrm import models from bookwyrm import models
@ -9,13 +11,21 @@ register = template.Library()
@register.filter(name="liked") @register.filter(name="liked")
def get_user_liked(user, status): def get_user_liked(user, status):
"""did the given user fav a status?""" """did the given user fav a status?"""
return models.Favorite.objects.filter(user=user, status=status).exists() return cache.get_or_set(
f"fav-{user.id}-{status.id}",
models.Favorite.objects.filter(user=user, status=status).exists(),
259200,
)
@register.filter(name="boosted") @register.filter(name="boosted")
def get_user_boosted(user, status): def get_user_boosted(user, status):
"""did the given user fav a status?""" """did the given user fav a status?"""
return status.boosters.filter(user=user).exists() return cache.get_or_set(
f"boost-{user.id}-{status.id}",
status.boosters.filter(user=user).exists(),
259200,
)
@register.filter(name="saved") @register.filter(name="saved")

View file

@ -25,6 +25,13 @@ class GetStartedViews(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
self.local_user = models.User.objects.create_user(
"rat@local.com",
"rat@rat.rat",
"password",
local=True,
localname="rat",
)
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
parent_work=models.Work.objects.create(title="hi"), parent_work=models.Work.objects.create(title="hi"),
title="Example Edition", title="Example Edition",
@ -121,13 +128,14 @@ class GetStartedViews(TestCase):
validate_html(result.render()) validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.SuggestedUsers.get_suggestions")
def test_users_view_with_query(self, *_): def test_users_view_with_query(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.GetStartedUsers.as_view() view = views.GetStartedUsers.as_view()
request = self.factory.get("?query=rat") request = self.factory.get("?query=rat")
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.suggested_users.SuggestedUsers.get_suggestions") as mock:
mock.return_value = models.User.objects.all()
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)

View file

@ -225,7 +225,6 @@ def feed_page_data(user):
goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first() goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first()
return { return {
"suggested_books": get_suggested_books(user),
"goal": goal, "goal": goal,
"goal_form": forms.GoalForm(), "goal_form": forms.GoalForm(),
} }

View file

@ -113,13 +113,16 @@ class GetStartedUsers(View):
.filter( .filter(
similarity__gt=0.5, similarity__gt=0.5,
) )
.exclude(
id=request.user.id,
)
.order_by("-similarity")[:5] .order_by("-similarity")[:5]
) )
data = {"no_results": not user_results} data = {"no_results": not user_results}
if user_results.count() < 5: if user_results.count() < 5:
user_results = list(user_results) + suggested_users.get_suggestions( user_results = list(user_results) + list(
request.user suggested_users.get_suggestions(request.user)
) )
data["suggested_users"] = user_results data["suggested_users"] = user_results

View file

@ -1,6 +1,7 @@
""" boosts and favs """ """ boosts and favs """
from django.db import IntegrityError
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -17,6 +18,7 @@ class Favorite(View):
def post(self, request, status_id): def post(self, request, status_id):
"""create a like""" """create a like"""
cache.delete(f"fav-{request.user.id}-{status_id}")
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
try: try:
models.Favorite.objects.create(status=status, user=request.user) models.Favorite.objects.create(status=status, user=request.user)
@ -35,6 +37,7 @@ class Unfavorite(View):
def post(self, request, status_id): def post(self, request, status_id):
"""unlike a status""" """unlike a status"""
cache.delete(f"fav-{request.user.id}-{status_id}")
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
try: try:
favorite = models.Favorite.objects.get(status=status, user=request.user) favorite = models.Favorite.objects.get(status=status, user=request.user)
@ -54,6 +57,7 @@ class Boost(View):
def post(self, request, status_id): def post(self, request, status_id):
"""boost a status""" """boost a status"""
cache.delete(f"boost-{request.user.id}-{status_id}")
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
# is it boostable? # is it boostable?
if not status.boostable: if not status.boostable:
@ -81,6 +85,7 @@ class Unboost(View):
def post(self, request, status_id): def post(self, request, status_id):
"""boost a status""" """boost a status"""
cache.delete(f"boost-{request.user.id}-{status_id}")
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
boost = models.Boost.objects.filter( boost = models.Boost.objects.filter(
boosted_status=status, user=request.user boosted_status=status, user=request.user

View file

@ -1,5 +1,7 @@
""" the good stuff! the books! """ """ the good stuff! the books! """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db import transaction from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@ -44,6 +46,13 @@ class ReadingStatus(View):
if not identifier: if not identifier:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# invalidate the template cache
cache_keys = [
make_template_fragment_key("shelve_button", [request.user.id, book_id]),
make_template_fragment_key("suggested_books", [request.user.id]),
]
cache.delete_many(cache_keys)
desired_shelf = get_object_or_404( desired_shelf = get_object_or_404(
models.Shelf, identifier=identifier, user=request.user models.Shelf, identifier=identifier, user=request.user
) )

View file

@ -3,11 +3,15 @@
# pylint: disable=unused-wildcard-import # pylint: disable=unused-wildcard-import
from bookwyrm.settings import * from bookwyrm.settings import *
CELERY_BROKER_URL = "redis://:{}@redis_broker:{}/0".format( REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", None))
requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")), env("REDIS_BROKER_PORT") REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker")
REDIS_BROKER_PORT = env("REDIS_BROKER_PORT", 6379)
CELERY_BROKER_URL = (
f"redis://:{REDIS_BROKER_PASSWORD}@{REDIS_BROKER_HOST}:{REDIS_BROKER_PORT}/0"
) )
CELERY_RESULT_BACKEND = "redis://:{}@redis_broker:{}/0".format( CELERY_RESULT_BACKEND = (
requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")), env("REDIS_BROKER_PORT") f"redis://:{REDIS_BROKER_PASSWORD}@{REDIS_BROKER_HOST}:{REDIS_BROKER_PORT}/0"
) )
CELERY_DEFAULT_QUEUE = "low_priority" CELERY_DEFAULT_QUEUE = "low_priority"

View file

@ -38,16 +38,17 @@ services:
- 8000:8000 - 8000:8000
redis_activity: redis_activity:
image: redis image: redis
command: ["redis-server", "--appendonly", "yes"] command: redis-server --requirepass ${REDIS_ACTIVITY_PASSWORD} --appendonly yes --port ${REDIS_ACTIVITY_PORT}
env_file: .env env_file: .env
networks: networks:
- main - main
restart: on-failure restart: on-failure
volumes: volumes:
- ./redis.conf:/etc/redis/redis.conf
- redis_activity_data:/data - redis_activity_data:/data
redis_broker: redis_broker:
image: redis image: redis
command: ["redis-server", "--appendonly", "yes"] command: redis-server --requirepass ${REDIS_BROKER_PASSWORD} --appendonly yes --port ${REDIS_BROKER_PORT}
env_file: .env env_file: .env
ports: ports:
- 6379:6379 - 6379:6379
@ -55,6 +56,7 @@ services:
- main - main
restart: on-failure restart: on-failure
volumes: volumes:
- ./redis.conf:/etc/redis/redis.conf
- redis_broker_data:/data - redis_broker_data:/data
celery_worker: celery_worker:
env_file: .env env_file: .env
@ -72,8 +74,10 @@ services:
restart: on-failure restart: on-failure
flower: flower:
build: . build: .
command: flower -A celerywyrm command: celery -A celerywyrm flower
env_file: .env env_file: .env
ports:
- ${FLOWER_PORT}:${FLOWER_PORT}
volumes: volumes:
- .:/app - .:/app
networks: networks:
@ -82,8 +86,6 @@ services:
- db - db
- redis_broker - redis_broker
restart: on-failure restart: on-failure
ports:
- 8888:8888
volumes: volumes:
pgdata: pgdata:
static_volume: static_volume:

View file

@ -12,6 +12,6 @@
}, },
"dependencies": { "dependencies": {
"merge": "2.1.1", "merge": "2.1.1",
"postcss": "8.2.10" "postcss": "8.2.13"
} }
} }

9
redis.conf Normal file
View file

@ -0,0 +1,9 @@
bind 127.0.0.1 ::1
protected-mode yes
port 6379
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command DEBUG ""
rename-command CONFIG ""
rename-command SHUTDOWN ""

View file

@ -1,10 +1,10 @@
celery==4.4.2 celery==5.2.2
colorthief==0.2.1 colorthief==0.2.1
Django==3.2.10 Django==3.2.10
django-imagekit==4.1.0 django-imagekit==4.1.0
django-model-utils==4.0.0 django-model-utils==4.0.0
environs==9.3.4 environs==9.3.4
flower==0.9.4 flower==1.0.0
Markdown==3.3.3 Markdown==3.3.3
Pillow>=8.2.0 Pillow>=8.2.0
psycopg2==2.8.4 psycopg2==2.8.4
@ -17,6 +17,7 @@ django-rename-app==0.1.2
pytz>=2021.1 pytz>=2021.1
boto3==1.17.88 boto3==1.17.88
django-storages==1.11.1 django-storages==1.11.1
django-redis==5.2.0
# Dev # Dev
black==21.4b0 black==21.4b0

3036
yarn.lock

File diff suppressed because it is too large Load diff