diff --git a/.env.dev.example b/.env.dev.example index 3cba06ab4..f42aaaaec 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -2,7 +2,8 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG=false +DEBUG=true +USE_HTTPS=false DOMAIN=your.domain.here #EMAIL=your@email.here @@ -42,6 +43,21 @@ EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false +# S3 configuration +USE_S3=false +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Commented are example values if you use a non-AWS, S3-compatible service +# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME +# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME, +# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL + +# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name" +# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud" +# AWS_S3_REGION_NAME=None # "fr-par" +# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" + # Preview image generation can be computing and storage intensive # ENABLE_PREVIEW_IMAGES=True diff --git a/.env.prod.example b/.env.prod.example index d61c46af5..5115469ca 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -3,6 +3,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" # SECURITY WARNING: don't run with debug turned on in production! DEBUG=false +USE_HTTPS=true DOMAIN=your.domain.here EMAIL=your@email.here @@ -42,6 +43,21 @@ EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false +# S3 configuration +USE_S3=false +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Commented are example values if you use a non-AWS, S3-compatible service +# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME +# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME, +# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL + +# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name" +# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud" +# AWS_S3_REGION_NAME=None # "fr-par" +# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" + # Preview image generation can be computing and storage intensive # ENABLE_PREVIEW_IMAGES=True diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index f2661a193..1f0387fe7 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -11,10 +11,7 @@ def site_settings(request): # pylint: disable=unused-argument return { "site": models.SiteSettings.objects.get(), "active_announcements": models.Announcement.active_announcements(), - "static_url": settings.STATIC_URL, - "media_url": settings.MEDIA_URL, - "static_path": settings.STATIC_PATH, - "media_path": settings.MEDIA_PATH, + "media_full_url": settings.MEDIA_FULL_URL, "preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES, "request_protocol": request_protocol, } diff --git a/bookwyrm/importers/importer.py b/bookwyrm/importers/importer.py index 203db0343..d5f1449ca 100644 --- a/bookwyrm/importers/importer.py +++ b/bookwyrm/importers/importer.py @@ -2,6 +2,8 @@ import csv import logging +from django.utils import timezone + from bookwyrm import models from bookwyrm.models import ImportJob, ImportItem from bookwyrm.tasks import app @@ -100,7 +102,10 @@ def handle_imported_book(source, user, item, include_reviews, privacy): # shelve the book if it hasn't been shelved already if item.shelf and not existing_shelf: desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user) - models.ShelfBook.objects.create(book=item.book, shelf=desired_shelf, user=user) + shelved_date = item.date_added or timezone.now() + models.ShelfBook.objects.create( + book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date + ) for read in item.reads: # check for an existing readthrough with the same dates diff --git a/bookwyrm/migrations/0078_add_shelved_date.py b/bookwyrm/migrations/0078_add_shelved_date.py new file mode 100644 index 000000000..b8a95ab17 --- /dev/null +++ b/bookwyrm/migrations/0078_add_shelved_date.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.4 on 2021-07-03 08:25 + +from django.db import migrations, models +import django.utils.timezone + + +def copy_created_date(app_registry, schema_editor): + db_alias = schema_editor.connection.alias + ShelfBook = app_registry.get_model("bookwyrm", "ShelfBook") + ShelfBook.objects.all().update(shelved_date=models.F("created_date")) + + +def do_nothing(app_registry, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0077_auto_20210623_2155"), + ] + + operations = [ + migrations.AlterModelOptions( + name="shelfbook", + options={"ordering": ("-shelved_date",)}, + ), + migrations.AddField( + model_name="shelfbook", + name="shelved_date", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.RunPython(copy_created_date, reverse_code=do_nothing), + ] diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index 4110ae8dc..c4e907d27 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -1,6 +1,7 @@ """ puttin' books on shelves """ import re from django.db import models +from django.utils import timezone from bookwyrm import activitypub from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin @@ -69,6 +70,7 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel): "Edition", on_delete=models.PROTECT, activitypub_field="book" ) shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT) + shelved_date = models.DateTimeField(default=timezone.now) user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="actor" ) @@ -86,4 +88,4 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel): you can't put a book on shelf twice""" unique_together = ("book", "shelf") - ordering = ("-created_date",) + ordering = ("-shelved_date", "-created_date", "-updated_date") diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 510625e39..29c4961c2 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -11,6 +11,7 @@ from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor from django.core.files.base import ContentFile from django.core.files.uploadedfile import InMemoryUploadedFile +from django.core.files.storage import default_storage from django.db.models import Avg from bookwyrm import models, settings @@ -319,9 +320,9 @@ def save_and_cleanup(image, instance=None): try: try: - old_path = instance.preview_image.path + old_path = instance.preview_image.name except ValueError: - old_path = "" + old_path = None # Save image.save(image_buffer, format="jpeg", quality=75) @@ -342,8 +343,8 @@ def save_and_cleanup(image, instance=None): instance.save() # Clean up old file after saving - if os.path.exists(old_path): - os.remove(old_path) + if old_path and default_storage.exists(old_path): + default_storage.delete(old_path) finally: image_buffer.close() diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 50b526bd9..17fcfabe5 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -58,6 +58,7 @@ SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env.bool("DEBUG", True) +USE_HTTPS = env.bool("USE_HTTPS", False) ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) @@ -74,6 +75,7 @@ INSTALLED_APPS = [ "django_rename_app", "bookwyrm", "celery", + "storages", ] MIDDLEWARE = [ @@ -179,19 +181,51 @@ USE_L10N = True USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ - -PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -STATIC_URL = "/static/" -STATIC_PATH = "%s/%s" % (DOMAIN, env("STATIC_ROOT", "static")) -STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) -MEDIA_URL = "/images/" -MEDIA_PATH = "%s/%s" % (DOMAIN, env("MEDIA_ROOT", "images")) -MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) - USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( requests.utils.default_user_agent(), VERSION, DOMAIN, ) + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Storage + +PROTOCOL = "http" +if USE_HTTPS: + PROTOCOL = "https" + +USE_S3 = env.bool("USE_S3", False) + +if USE_S3: + # AWS settings + AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") + AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") + AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") + AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN") + AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "") + AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL") + AWS_DEFAULT_ACL = "public-read" + AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"} + # S3 Static settings + STATIC_LOCATION = "static" + STATIC_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, STATIC_LOCATION) + STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage" + # S3 Media settings + MEDIA_LOCATION = "images" + MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIA_LOCATION) + MEDIA_FULL_URL = MEDIA_URL + DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage" + # I don't know if it's used, but the site crashes without it + STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) + MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) +else: + STATIC_URL = "/static/" + STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) + MEDIA_URL = "/images/" + MEDIA_FULL_URL = "%s://%s%s" % (PROTOCOL, DOMAIN, MEDIA_URL) + MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index 3db25d1fe..d10fb9b7e 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -72,6 +72,14 @@ body { flex-grow: 1; } +.preserve-whitespace p { + white-space: pre-wrap !important; +} + +.display-inline p { + display: inline !important; +} + /** Shelving ******************************************************************************/ diff --git a/bookwyrm/storage_backends.py b/bookwyrm/storage_backends.py new file mode 100644 index 000000000..e10dfb841 --- /dev/null +++ b/bookwyrm/storage_backends.py @@ -0,0 +1,17 @@ +"""Handles backends for storages""" +from storages.backends.s3boto3 import S3Boto3Storage + + +class StaticStorage(S3Boto3Storage): # pylint: disable=abstract-method + """Storage class for Static contents""" + + location = "static" + default_acl = "public-read" + + +class ImagesStorage(S3Boto3Storage): # pylint: disable=abstract-method + """Storage class for Image files""" + + location = "images" + default_acl = "public-read" + file_overwrite = False diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 9a75cbe94..c5dab1097 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -1,5 +1,10 @@ {% extends 'layout.html' %} -{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}{% load layout %} +{% load i18n %} +{% load bookwyrm_tags %} +{% load humanize %} +{% load utilities %} +{% load static %} +{% load layout %} {% block title %}{{ book|book_title }}{% endblock %} @@ -321,5 +326,5 @@ {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/bookwyrm/templates/directory/user_card.html b/bookwyrm/templates/directory/user_card.html index c52c1f7ae..b7941826b 100644 --- a/bookwyrm/templates/directory/user_card.html +++ b/bookwyrm/templates/directory/user_card.html @@ -18,7 +18,7 @@ -
{% trans "There are no books here right now! Try searching for a book to get started" %}
- {% else %} - {% with active_book=request.GET.book %} -- {% 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 %} -
-{% include 'snippets/book_titleby.html' with book=book %}
- {% include 'snippets/shelve_button/shelve_button.html' with book=book %} -{% trans "There are no books here right now! Try searching for a book to get started" %}
+ {% else %} + {% with active_book=request.GET.book %} ++ {% 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 %} +
+{% include 'snippets/book_titleby.html' with book=book %}
+ {% include 'snippets/shelve_button/shelve_button.html' with book=book %} +