Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-08-02 07:32:22 -07:00
commit 9bf79bf9b9
36 changed files with 708 additions and 447 deletions

View file

@ -2,7 +2,8 @@
SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG=false DEBUG=true
USE_HTTPS=false
DOMAIN=your.domain.here DOMAIN=your.domain.here
#EMAIL=your@email.here #EMAIL=your@email.here
@ -42,6 +43,21 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false 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 # Preview image generation can be computing and storage intensive
# ENABLE_PREVIEW_IMAGES=True # ENABLE_PREVIEW_IMAGES=True

View file

@ -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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG=false DEBUG=false
USE_HTTPS=true
DOMAIN=your.domain.here DOMAIN=your.domain.here
EMAIL=your@email.here EMAIL=your@email.here
@ -42,6 +43,21 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false 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 # Preview image generation can be computing and storage intensive
# ENABLE_PREVIEW_IMAGES=True # ENABLE_PREVIEW_IMAGES=True

View file

@ -11,10 +11,7 @@ def site_settings(request): # pylint: disable=unused-argument
return { return {
"site": models.SiteSettings.objects.get(), "site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(), "active_announcements": models.Announcement.active_announcements(),
"static_url": settings.STATIC_URL, "media_full_url": settings.MEDIA_FULL_URL,
"media_url": settings.MEDIA_URL,
"static_path": settings.STATIC_PATH,
"media_path": settings.MEDIA_PATH,
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES, "preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
"request_protocol": request_protocol, "request_protocol": request_protocol,
} }

View file

@ -2,6 +2,8 @@
import csv import csv
import logging import logging
from django.utils import timezone
from bookwyrm import models from bookwyrm import models
from bookwyrm.models import ImportJob, ImportItem from bookwyrm.models import ImportJob, ImportItem
from bookwyrm.tasks import app 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 # shelve the book if it hasn't been shelved already
if item.shelf and not existing_shelf: if item.shelf and not existing_shelf:
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user) 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: for read in item.reads:
# check for an existing readthrough with the same dates # check for an existing readthrough with the same dates

View file

@ -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),
]

View file

@ -1,6 +1,7 @@
""" puttin' books on shelves """ """ puttin' books on shelves """
import re import re
from django.db import models from django.db import models
from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
@ -69,6 +70,7 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
"Edition", on_delete=models.PROTECT, activitypub_field="book" "Edition", on_delete=models.PROTECT, activitypub_field="book"
) )
shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT) shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT)
shelved_date = models.DateTimeField(default=timezone.now)
user = fields.ForeignKey( user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor" "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""" you can't put a book on shelf twice"""
unique_together = ("book", "shelf") unique_together = ("book", "shelf")
ordering = ("-created_date",) ordering = ("-shelved_date", "-created_date", "-updated_date")

View file

@ -11,6 +11,7 @@ from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.storage import default_storage
from django.db.models import Avg from django.db.models import Avg
from bookwyrm import models, settings from bookwyrm import models, settings
@ -319,9 +320,9 @@ def save_and_cleanup(image, instance=None):
try: try:
try: try:
old_path = instance.preview_image.path old_path = instance.preview_image.name
except ValueError: except ValueError:
old_path = "" old_path = None
# Save # Save
image.save(image_buffer, format="jpeg", quality=75) image.save(image_buffer, format="jpeg", quality=75)
@ -342,8 +343,8 @@ def save_and_cleanup(image, instance=None):
instance.save() instance.save()
# Clean up old file after saving # Clean up old file after saving
if os.path.exists(old_path): if old_path and default_storage.exists(old_path):
os.remove(old_path) default_storage.delete(old_path)
finally: finally:
image_buffer.close() image_buffer.close()

View file

@ -58,6 +58,7 @@ SECRET_KEY = env("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DEBUG", True) DEBUG = env.bool("DEBUG", True)
USE_HTTPS = env.bool("USE_HTTPS", False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
@ -74,6 +75,7 @@ INSTALLED_APPS = [
"django_rename_app", "django_rename_app",
"bookwyrm", "bookwyrm",
"celery", "celery",
"storages",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -179,19 +181,51 @@ USE_L10N = True
USE_TZ = 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/)" % ( USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
requests.utils.default_user_agent(), requests.utils.default_user_agent(),
VERSION, VERSION,
DOMAIN, 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"))

View file

@ -72,6 +72,14 @@ body {
flex-grow: 1; flex-grow: 1;
} }
.preserve-whitespace p {
white-space: pre-wrap !important;
}
.display-inline p {
display: inline !important;
}
/** Shelving /** Shelving
******************************************************************************/ ******************************************************************************/

View file

@ -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

View file

@ -1,5 +1,10 @@
{% extends 'layout.html' %} {% 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 %} {% block title %}{{ book|book_title }}{% endblock %}
@ -321,5 +326,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/vendor/tabs.js"></script> <script src="{% static "js/vendor/tabs.js" %}"></script>
{% endblock %} {% endblock %}

View file

@ -18,7 +18,7 @@
</div> </div>
</div> </div>
<div> <div class="display-inline">
{% if user.summary %} {% if user.summary %}
{{ user.summary|to_markdown|safe|truncatechars_html:40 }} {{ user.summary|to_markdown|safe|truncatechars_html:40 }}
{% else %}&nbsp;{% endif %} {% else %}&nbsp;{% endif %}

View file

@ -1,4 +1,4 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -1,4 +1,4 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -1,107 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}{% trans "Updates" %}{% endblock %}
{% block content %}
<div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third">
<h2 class="title is-5">{% 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 %}
{% if goal %}
<section class="section">
<div class="block">
<h3 class="title is-4">{% blocktrans with yar=goal.year %}{{ year }} Reading Goal{% endblocktrans %}</h3>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
</section>
{% endif %}
</div>
{% endif %}
<div class="column is-two-thirds" id="feed">
{% block panel %}{% endblock %}
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %}
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/vendor/tabs.js"></script>
{% endblock %}

View file

@ -0,0 +1,110 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Updates" %}{% endblock %}
{% block content %}
<div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third">
<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>
{% if goal %}
<section class="block">
<div class="block">
<h3 class="title is-4">{% blocktrans with yar=goal.year %}{{ year }} Reading Goal{% endblocktrans %}</h3>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
</section>
{% endif %}
</div>
{% endif %}
<div class="column is-two-thirds" id="feed">
{% block panel %}{% endblock %}
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %}
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}"></script>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -1,5 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title %}{% trans "Welcome" %}{% endblock %} {% block title %}{% trans "Welcome" %}{% endblock %}
@ -9,7 +10,7 @@
<div class="modal-background"></div> <div class="modal-background"></div>
<div class="modal-card is-fullwidth"> <div class="modal-card is-fullwidth">
<header class="modal-card-head"> <header class="modal-card-head">
<img class="image logo mr-2" src="{% if site.logo_small %}/images/{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" aria-hidden="true"> <img class="image logo mr-2" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" aria-hidden="true">
<h1 class="modal-card-title" id="get-started-header"> <h1 class="modal-card-title" id="get-started-header">
{% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %} {% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %}
<span class="subtitle is-block"> <span class="subtitle is-block">

View file

@ -1,6 +1,7 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load static %}
{% block title %}{% trans "Import Status" %}{% endblock %} {% block title %}{% trans "Import Status" %}{% endblock %}
@ -156,5 +157,5 @@
{% endspaceless %}{% endblock %} {% endspaceless %}{% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/check_all.js"></script> <script src="{% static "js/check_all.js" %}"></script>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,16 @@
{% load layout %}{% load i18n %} {% load layout %}
{% load i18n %}
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% get_lang %}"> <html lang="{% get_lang %}">
<head> <head>
<title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title> <title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/vendor/bulma.min.css"> <link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
<link rel="stylesheet" href="/static/css/vendor/icons.css"> <link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
<link rel="stylesheet" href="/static/css/bookwyrm.css"> <link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{{ media_url }}{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
{% if preview_images_enabled is True %} {% if preview_images_enabled is True %}
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
@ -30,7 +32,7 @@
<div class="container"> <div class="container">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}{{ media_url }}{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page"> <img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
</a> </a>
<form class="navbar-item column" action="/search/"> <form class="navbar-item column" action="/search/">
<div class="field has-addons"> <div class="field has-addons">
@ -242,8 +244,8 @@
<script> <script>
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
</script> </script>
<script src="/static/js/bookwyrm.js"></script> <script src="{% static "js/bookwyrm.js" %}"></script>
<script src="/static/js/localstorage.js"></script> <script src="{% static "js/localstorage.js" %}"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -1,7 +1,9 @@
{% load static %}
<div class="columns"> <div class="columns">
<div class="column is-narrow is-hidden-mobile"> <div class="column is-narrow is-hidden-mobile">
<figure class="block is-w-xl"> <figure class="block is-w-xl">
<img src="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}" alt="BookWyrm logo"> <img src="{% if site.logo %}/images/{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}" alt="BookWyrm logo">
</figure> </figure>
</div> </div>
<div class="content"> <div class="content">

View file

@ -1,2 +1,4 @@
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}"> {% load static %}
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}{% get_media_prefix %}{{ user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">

View file

@ -1,6 +1,7 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% load static %}
<figure <figure
class=" class="
@ -20,14 +21,14 @@
class="book-cover" class="book-cover"
{% if book.cover %} {% if book.cover %}
src="{% if img_path is None %}/images/{% else %}{{ img_path }}{% endif %}{{ book.cover }}" src="{% if img_path is None %}{% get_media_prefix %}{% else %}{{ img_path }}{% endif %}{{ book.cover }}"
itemprop="thumbnailUrl" itemprop="thumbnailUrl"
{% if book.alt_text %} {% if book.alt_text %}
alt="{{ book.alt_text }}" alt="{{ book.alt_text }}"
{% endif %} {% endif %}
{% else %} {% else %}
src="/static/images/no_cover.jpg" src="{% static "images/no_cover.jpg" %}"
alt="{% trans "No cover" %}" alt="{% trans "No cover" %}"
{% endif %} {% endif %}
> >

View file

@ -1,12 +1,14 @@
{% load static %}
{% if preview_images_enabled is True %} {% if preview_images_enabled is True %}
{% if image %} {% if image %}
<meta name="twitter:image" content="{{ request.scheme }}://{{ media_path }}{{ image }}"> <meta name="twitter:image" content="{{ media_full_url }}{{ image }}">
<meta name="og:image" content="{{ request.scheme }}://{{ media_path }}{{ image }}"> <meta name="og:image" content="{{ media_full_url }}{{ image }}">
{% else %} {% else %}
<meta name="twitter:image" content="{{ request.scheme }}://{{ media_path }}{{ site.preview_image }}"> <meta name="twitter:image" content="{{ media_full_url }}{{ site.preview_image }}">
<meta name="og:image" content="{{ request.scheme }}://{{ media_path }}{{ site.preview_image }}"> <meta name="og:image" content="{{ media_full_url }}{{ site.preview_image }}">
{% endif %} {% endif %}
{% else %} {% else %}
<meta name="twitter:image" content="{{ request.scheme }}://{% if site.logo %}{{ media_path }}{{ site.logo }}{% else %}{{ static_path }}/images/logo.png{% endif %}"> <meta name="twitter:image" content="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}">
<meta name="og:image" content="{{ request.scheme }}://{% if site.logo %}{{ media_path }}{{ site.logo }}{% else %}{{ static_path }}/images/logo.png{% endif %}"> <meta name="og:image" content="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}">
{% endif %} {% endif %}

View file

@ -1,6 +1,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %} {% load markdown %}
{% load i18n %} {% load i18n %}
{% load static %}
{% with status_type=status.status_type %} {% with status_type=status.status_type %}
<div <div
@ -111,12 +112,12 @@
<div class="column is-narrow"> <div class="column is-narrow">
<figure class="image is-128x128"> <figure class="image is-128x128">
<a <a
href="/images/{{ attachment.image }}" href="{% get_media_prefix %}{{ attachment.image }}"
target="_blank" target="_blank"
aria-label="{% trans 'Open image in new window' %}" aria-label="{% trans 'Open image in new window' %}"
> >
<img <img
src="/images/{{ attachment.image }}" src="{% get_media_prefix %}{{ attachment.image }}"
{% if attachment.caption %} {% if attachment.caption %}
alt="{{ attachment.caption }}" alt="{{ attachment.caption }}"

View file

@ -2,6 +2,7 @@
{% load status_display %} {% load status_display %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load static %}
<div class="media"> <div class="media">
<figure class="media-left" aria-hidden="true"> <figure class="media-left" aria-hidden="true">
@ -18,7 +19,7 @@
itemtype="https://schema.org/Person" itemtype="https://schema.org/Person"
> >
{% if status.user.avatar %} {% if status.user.avatar %}
<meta itemprop="image" content="/images/{{ status.user.avatar }}"> <meta itemprop="image" content="{% get_media_prefix %}{{ status.user.avatar }}">
{% endif %} {% endif %}
<a <a

View file

@ -28,8 +28,10 @@
</div> </div>
{% if user.summary %} {% if user.summary %}
<div class="column box has-background-white-bis content"> {% spaceless %}
<div class="column box has-background-white-bis content preserve-whitespace">
{{ user.summary|to_markdown|safe }} {{ user.summary|to_markdown|safe }}
{% endspaceless %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -103,7 +103,7 @@
{% include 'snippets/authors.html' %} {% include 'snippets/authors.html' %}
</td> </td>
<td data-title="{% trans "Shelved" %}"> <td data-title="{% trans "Shelved" %}">
{{ book.created_date|naturalday }} {{ book.shelved_date|naturalday }}
</td> </td>
{% latest_read_through book user as read_through %} {% latest_read_through book user as read_through %}
<td data-title="{% trans "Started" %}"> <td data-title="{% trans "Started" %}">

View file

@ -3,6 +3,8 @@ from collections import namedtuple
import csv import csv
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
import datetime
import pytz
from django.test import TestCase from django.test import TestCase
import responses import responses
@ -13,6 +15,10 @@ from bookwyrm.importers.importer import import_data, handle_imported_book
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
def make_date(*args):
return datetime.datetime(*args, tzinfo=pytz.UTC)
class GoodreadsImport(TestCase): class GoodreadsImport(TestCase):
"""importing from goodreads csv""" """importing from goodreads csv"""
@ -130,22 +136,25 @@ class GoodreadsImport(TestCase):
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2020, 10, 21)
)
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.start_date.year, 2020) self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_imported_book_already_shelved(self): def test_handle_imported_book_already_shelved(self):
"""goodreads import added a book, this adds related connections""" """goodreads import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first() shelf = self.user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) models.ShelfBook.objects.create(
shelf=shelf,
user=self.user,
book=self.book,
shelved_date=make_date(2020, 2, 2),
)
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
@ -164,15 +173,15 @@ class GoodreadsImport(TestCase):
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2)
)
self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first()) self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first())
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date.year, 2020) self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.start_date.month, 10) self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_import_twice(self): def test_handle_import_twice(self):
"""re-importing books""" """re-importing books"""
@ -198,16 +207,14 @@ class GoodreadsImport(TestCase):
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2020, 10, 21)
)
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.start_date.year, 2020) self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _): def test_handle_imported_book_review(self, _):
@ -229,9 +236,7 @@ class GoodreadsImport(TestCase):
review = models.Review.objects.get(book=self.book, user=self.user) review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, "mixed feelings") self.assertEqual(review.content, "mixed feelings")
self.assertEqual(review.rating, 2) self.assertEqual(review.rating, 2)
self.assertEqual(review.published_date.year, 2019) self.assertEqual(review.published_date, make_date(2019, 7, 8))
self.assertEqual(review.published_date.month, 7)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@ -256,9 +261,7 @@ class GoodreadsImport(TestCase):
review = models.ReviewRating.objects.get(book=self.book, user=self.user) review = models.ReviewRating.objects.get(book=self.book, user=self.user)
self.assertIsInstance(review, models.ReviewRating) self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 2) self.assertEqual(review.rating, 2)
self.assertEqual(review.published_date.year, 2019) self.assertEqual(review.published_date, make_date(2019, 7, 8))
self.assertEqual(review.published_date.month, 7)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self): def test_handle_imported_book_reviews_disabled(self):

View file

@ -2,6 +2,8 @@
import csv import csv
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
import datetime
import pytz
from django.test import TestCase from django.test import TestCase
import responses import responses
@ -12,6 +14,10 @@ from bookwyrm.importers.importer import import_data, handle_imported_book
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
def make_date(*args):
return datetime.datetime(*args, tzinfo=pytz.UTC)
class LibrarythingImport(TestCase): class LibrarythingImport(TestCase):
"""importing from librarything tsv""" """importing from librarything tsv"""
@ -125,13 +131,8 @@ class LibrarythingImport(TestCase):
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.start_date.year, 2007) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_imported_book_already_shelved(self): def test_handle_imported_book_already_shelved(self):
"""librarything import added a book, this adds related connections""" """librarything import added a book, this adds related connections"""
@ -160,14 +161,11 @@ class LibrarythingImport(TestCase):
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first()) self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first())
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date.year, 2007) self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.start_date.month, 4) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_import_twice(self): def test_handle_import_twice(self):
"""re-importing books""" """re-importing books"""
@ -198,13 +196,8 @@ class LibrarythingImport(TestCase):
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.start_date.year, 2007) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _): def test_handle_imported_book_review(self, _):
@ -226,9 +219,7 @@ class LibrarythingImport(TestCase):
review = models.Review.objects.get(book=self.book, user=self.user) review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, "chef d'oeuvre") self.assertEqual(review.content, "chef d'oeuvre")
self.assertEqual(review.rating, 5) self.assertEqual(review.rating, 5)
self.assertEqual(review.published_date.year, 2007) self.assertEqual(review.published_date, make_date(2007, 5, 8))
self.assertEqual(review.published_date.month, 5)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self): def test_handle_imported_book_reviews_disabled(self):

View file

@ -2,7 +2,7 @@
from collections import namedtuple from collections import namedtuple
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import OuterRef, Subquery from django.db.models import OuterRef, Subquery, F
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
@ -69,7 +69,8 @@ class Shelf(View):
reviews = privacy_filter(request.user, reviews) reviews = privacy_filter(request.user, reviews)
books = books.annotate( books = books.annotate(
rating=Subquery(reviews.values("rating")[:1]) rating=Subquery(reviews.values("rating")[:1]),
shelved_date=F("shelfbook__shelved_date"),
).prefetch_related("authors") ).prefetch_related("authors")
paginated = Paginator( paginated = Paginator(

44
bw-dev
View file

@ -31,6 +31,17 @@ function initdb {
execweb python manage.py initdb execweb python manage.py initdb
} }
function awscommand {
# expose env vars
export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
export AWS_DEFAULT_REGION=${AWS_S3_REGION_NAME}
# first arg is mountpoint, second is the whole aws command
docker run --rm -it -v $1\
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION\
amazon/aws-cli $2
}
CMD=$1 CMD=$1
if [ -n "$CMD" ]; then if [ -n "$CMD" ]; then
shift shift
@ -83,6 +94,19 @@ case "$CMD" in
generate_preview_images) generate_preview_images)
runweb python manage.py generate_preview_images $@ runweb python manage.py generate_preview_images $@
;; ;;
copy_media_to_s3)
awscommand "bookwyrm_media_volume:/images"\
"s3 cp /images s3://${AWS_STORAGE_BUCKET_NAME}/images\
--endpoint-url ${AWS_S3_ENDPOINT_URL}\
--recursive --acl public-read"
;;
set_cors_to_s3)
awscommand "$(pwd):/bw"\
"s3api put-bucket-cors\
--bucket ${AWS_STORAGE_BUCKET_NAME}\
--endpoint-url ${AWS_S3_ENDPOINT_URL}\
--cors-configuration file:///bw/$@"
;;
runweb) runweb)
runweb "$@" runweb "$@"
;; ;;
@ -91,6 +115,24 @@ case "$CMD" in
;; ;;
*) *)
set +x # No need to echo echo set +x # No need to echo echo
echo "Unrecognised command. Try: build, up, initdb, migrate, bash, shell, dbshell, restart_celery, update, populate_streams, generate_preview_images" echo "Unrecognised command. Try:"
echo " up [container]"
echo " run"
echo " initdb"
echo " makemigrations [migration]"
echo " migrate [migration]"
echo " bash"
echo " shell"
echo " dbshell"
echo " restart_celery"
echo " collectstatic"
echo " build"
echo " clean"
echo " populate_streams"
echo " generate_preview_images [--all]"
echo " copy_media_to_s3"
echo " set_cors_to_s3 [cors file]"
echo " runweb [command]"
echo " rundb [command]"
;; ;;
esac esac

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -15,3 +15,5 @@ requests==2.22.0
responses==0.10.14 responses==0.10.14
django-rename-app==0.1.2 django-rename-app==0.1.2
pytz>=2021.1 pytz>=2021.1
boto3==1.17.88
django-storages==1.11.1