forked from mirrors/bookwyrm
Merge branch 'main' into about-page
This commit is contained in:
commit
392dbfce01
40 changed files with 535 additions and 1035 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
.github/workflows/django-tests.yml
vendored
2
.github/workflows/django-tests.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -12,6 +12,9 @@ module.exports = {
|
||||||
"custom-properties",
|
"custom-properties",
|
||||||
"declarations"
|
"declarations"
|
||||||
],
|
],
|
||||||
"indentation": 4
|
"indentation": 4,
|
||||||
|
"property-no-vendor-prefix": null,
|
||||||
|
"color-function-notation": null,
|
||||||
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -183,7 +183,7 @@ button .button-invisible-overlay {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(0, 0, 0, 0.66);
|
background: rgba(0, 0, 0, 66%);
|
||||||
color: white;
|
color: white;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
@ -198,7 +198,7 @@ button:focus-visible .button-invisible-overlay {
|
||||||
/** File input styles
|
/** File input styles
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
input[type=file]::file-selector-button {
|
input[type="file"]::file-selector-button {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
@ -219,7 +219,7 @@ input[type=file]::file-selector-button {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=file]::file-selector-button:hover {
|
input[type="file"]::file-selector-button:hover {
|
||||||
border-color: #b5b5b5;
|
border-color: #b5b5b5;
|
||||||
color: #363636;
|
color: #363636;
|
||||||
}
|
}
|
||||||
|
@ -279,7 +279,7 @@ details.dropdown .dropdown-menu a:focus-visible {
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
details.dropdown[open] summary.dropdown-trigger::before {
|
details.dropdown[open] summary.dropdown-trigger::before {
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 50%);
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,14 +305,14 @@ details.dropdown .dropdown-menu a:focus-visible {
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
details.details-panel {
|
details.details-panel {
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 10%);
|
||||||
transition: box-shadow 0.2s ease;
|
transition: box-shadow 0.2s ease;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open].details-panel,
|
details[open].details-panel,
|
||||||
details.details-panel:hover {
|
details.details-panel:hover {
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
details.details-panel summary {
|
details.details-panel summary {
|
||||||
|
@ -333,7 +333,7 @@ details[open].details-panel summary .details-close {
|
||||||
|
|
||||||
@media only screen and (min-width: 769px) {
|
@media only screen and (min-width: 769px) {
|
||||||
.details-panel .filters-field:not(:last-child) {
|
.details-panel .filters-field:not(:last-child) {
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
border-right: 1px solid rgba(0, 0, 0, 10%);
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
|
@ -347,7 +347,7 @@ details[open].details-panel summary .details-close {
|
||||||
/** @todo Replace icons with SVG symbols.
|
/** @todo Replace icons with SVG symbols.
|
||||||
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
||||||
.shelf-option:disabled > *::after {
|
.shelf-option:disabled > *::after {
|
||||||
font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||||
content: "\e919"; /* icon-check */
|
content: "\e919"; /* icon-check */
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -355,14 +355,14 @@ details[open].details-panel summary .details-close {
|
||||||
/** Toggles
|
/** Toggles
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
.toggle-button[aria-pressed=true],
|
.toggle-button[aria-pressed="true"],
|
||||||
.toggle-button[aria-pressed=true]:hover {
|
.toggle-button[aria-pressed="true"]:hover {
|
||||||
background-color: hsl(171, 100%, 41%);
|
background-color: hsl(171deg, 100%, 41%);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-active[aria-pressed=true],
|
.hide-active[aria-pressed="true"],
|
||||||
.hide-inactive[aria-pressed=false] {
|
.hide-inactive[aria-pressed="false"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,36 +419,36 @@ details[open].details-panel summary .details-close {
|
||||||
|
|
||||||
/* All stars are visually filled by default. */
|
/* All stars are visually filled by default. */
|
||||||
.form-rate-stars .icon::before {
|
.form-rate-stars .icon::before {
|
||||||
content: '\e9d9'; /* icon-star-full */
|
content: "\e9d9"; /* icon-star-full */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following half star inputs are marked as half */
|
/* Icons directly following half star inputs are marked as half */
|
||||||
.form-rate-stars input.half:checked ~ .icon::before {
|
.form-rate-stars input.half:checked ~ .icon::before {
|
||||||
content: '\e9d8'; /* icon-star-half */
|
content: "\e9d8"; /* icon-star-half */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stylelint-disable no-descending-specificity */
|
/* stylelint-disable no-descending-specificity */
|
||||||
.form-rate-stars input.half:checked + input + .icon:hover::before {
|
.form-rate-stars input.half:checked + input + .icon:hover::before {
|
||||||
content: '\e9d8' !important; /* icon-star-half */
|
content: "\e9d8" !important; /* icon-star-half */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following half check inputs that follow the checked input are emptied. */
|
/* Icons directly following half check inputs that follow the checked input are emptied. */
|
||||||
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
|
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
|
||||||
content: '\e9d7'; /* icon-star-empty */
|
content: "\e9d7"; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following inputs that follow the checked input are emptied. */
|
/* Icons directly following inputs that follow the checked input are emptied. */
|
||||||
.form-rate-stars input:checked ~ input + .icon::before {
|
.form-rate-stars input:checked ~ input + .icon::before {
|
||||||
content: '\e9d7'; /* icon-star-empty */
|
content: "\e9d7"; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
||||||
.form-rate-stars:hover .icon.icon::before {
|
.form-rate-stars:hover .icon.icon::before {
|
||||||
content: '\e9d9' !important; /* icon-star-full */
|
content: "\e9d9" !important; /* icon-star-full */
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-rate-stars .icon:hover ~ .icon::before {
|
.form-rate-stars .icon:hover ~ .icon::before {
|
||||||
content: '\e9d7' !important; /* icon-star-empty */
|
content: "\e9d7" !important; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Book covers
|
/** Book covers
|
||||||
|
@ -556,7 +556,7 @@ details[open].details-panel summary .details-close {
|
||||||
|
|
||||||
.quote > blockquote::before,
|
.quote > blockquote::before,
|
||||||
.quote > blockquote::after {
|
.quote > blockquote::after {
|
||||||
font-family: 'icomoon';
|
font-family: icomoon;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,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,
|
||||||
|
@ -94,7 +95,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):
|
||||||
|
@ -112,16 +113,17 @@ def get_annotated_users(viewer, *args, **kwargs):
|
||||||
),
|
),
|
||||||
distinct=True,
|
distinct=True,
|
||||||
),
|
),
|
||||||
# shared_books=Count(
|
# pylint: disable=line-too-long
|
||||||
# "shelfbook",
|
# shared_books=Count(
|
||||||
# filter=Q(
|
# "shelfbook",
|
||||||
# ~Q(id=viewer.id),
|
# filter=Q(
|
||||||
# shelfbook__book__parent_work__in=[
|
# ~Q(id=viewer.id),
|
||||||
# s.book.parent_work for s in viewer.shelfbook_set.all()
|
# shelfbook__book__parent_work__in=[
|
||||||
# ],
|
# s.book.parent_work for s in viewer.shelfbook_set.all()
|
||||||
# ),
|
# ],
|
||||||
# distinct=True,
|
# ),
|
||||||
# ),
|
# distinct=True,
|
||||||
|
# ),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not books %}
|
{% if not books %}
|
||||||
<p class="has-text-centered is-size-5">{% blocktrans %}Sadly {{ display_name }} didn’t finish any book in {{ year }}{% endblocktrans %}</p>
|
<p class="has-text-centered is-size-5">{% blocktrans %}Sadly {{ display_name }} didn’t finish any books in {{ year }}{% endblocktrans %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
|
|
|
@ -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">
|
||||||
<select name="sort" id="id_sort">
|
<div class="select">
|
||||||
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
|
<select name="sort" id="id_sort">
|
||||||
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
|
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
|
||||||
</select>
|
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
79
bookwyrm/templates/feed/suggested_books.html
Normal file
79
bookwyrm/templates/feed/suggested_books.html
Normal 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>
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -30,38 +30,39 @@
|
||||||
{# 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">
|
|
||||||
<a href="{% url 'login' %}">
|
|
||||||
<span class="icon icon-comment is-small" title="{% trans 'Reply' %}">
|
|
||||||
<span class="is-sr-only">{% trans "Reply" %}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="icon icon-boost is-small ml-4" title="{% trans 'Boost status' %}">
|
<div class="card-footer-item">
|
||||||
<span class="is-sr-only">{% trans "Boost status" %}</span>
|
<a href="{% url 'login' %}">
|
||||||
</span>
|
<span class="icon icon-comment is-small" title="{% trans 'Reply' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Reply" %}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="icon icon-heart is-small ml-4" title="{% trans 'Like status' %}">
|
<span class="icon icon-boost is-small ml-4" title="{% trans 'Boost status' %}">
|
||||||
<span class="is-sr-only">{% trans "Like status" %}</span>
|
<span class="is-sr-only">{% trans "Boost status" %}</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
|
||||||
</div>
|
<span class="icon icon-heart is-small ml-4" title="{% trans 'Like status' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Like status" %}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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">
|
||||||
<a href="{% url 'edit-status' status.id %}" class="button is-radiusless is-fullwidth is-small" type="submit">
|
<span class="control">
|
||||||
{% trans "Edit" %}
|
<a href="{% url 'edit-status' status.id %}" class="button is-radiusless is-fullwidth is-small" type="submit">
|
||||||
</a>
|
{% trans "Edit" %}
|
||||||
|
</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">
|
||||||
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-white is-radiusless is-fullwidth">
|
<span class="control">
|
||||||
{% trans "Send direct message" %}
|
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-white is-radiusless is-fullwidth">
|
||||||
</a>
|
{% trans "Send direct message" %}
|
||||||
|
</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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,14 +128,15 @@ 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
|
||||||
|
|
||||||
result = view(request)
|
with patch("bookwyrm.suggested_users.SuggestedUsers.get_suggestions") as mock:
|
||||||
|
mock.return_value = models.User.objects.all()
|
||||||
|
result = view(request)
|
||||||
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
validate_html(result.render())
|
validate_html(result.render())
|
||||||
|
|
|
@ -223,7 +223,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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^7.23.0",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
"stylelint": "^13.12.0",
|
"stylelint": "^14.2.0",
|
||||||
"stylelint-config-standard": "^21.0.0",
|
"stylelint-config-standard": "^24.0.0",
|
||||||
"stylelint-order": "^4.1.0",
|
"stylelint-order": "^5.0.0",
|
||||||
"watch": "^0.13.0"
|
"watch": "^0.13.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"postcss": "8.2.10"
|
"postcss": "8.2.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
redis.conf
Normal file
9
redis.conf
Normal 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 ""
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue