Merge tag 'v0.4.0' into nix
This commit is contained in:
commit
629f78a4f3
160 changed files with 10654 additions and 3418 deletions
2
.github/workflows/pylint.yml
vendored
2
.github/workflows/pylint.yml
vendored
|
@ -24,5 +24,5 @@ jobs:
|
||||||
pip install pylint
|
pip install pylint
|
||||||
- name: Analysing the code with pylint
|
- name: Analysing the code with pylint
|
||||||
run: |
|
run: |
|
||||||
pylint bookwyrm/ --ignore=migrations,tests --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801
|
pylint bookwyrm/ --ignore=migrations --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801
|
||||||
|
|
||||||
|
|
18
README.md
18
README.md
|
@ -9,21 +9,18 @@ Social reading and reviewing, decentralized with ActivityPub. This branch is a f
|
||||||
- [What it is and isn't](#what-it-is-and-isnt)
|
- [What it is and isn't](#what-it-is-and-isnt)
|
||||||
- [The role of federation](#the-role-of-federation)
|
- [The role of federation](#the-role-of-federation)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Book data](#book-data)
|
- [Set up BookWyrm](#set-up-bookwyrm)
|
||||||
- [Set up Bookwyrm](#set-up-bookwyrm)
|
|
||||||
|
|
||||||
## Joining BookWyrm
|
## Joining BookWyrm
|
||||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
||||||
|
|
||||||
You can request an invite by entering your email address at https://bookwyrm.social.
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions.
|
See [contributing](https://docs.joinbookwyrm.com/contributing.html) for code, translation or monetary contributions.
|
||||||
|
|
||||||
## About BookWyrm
|
## About BookWyrm
|
||||||
### What it is and isn't
|
### What it is and isn't
|
||||||
BookWyrm is a platform for social reading! You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
|
BookWyrm is a platform for social reading. You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
|
||||||
|
|
||||||
### The role of federation
|
### The role of federation
|
||||||
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
|
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
|
||||||
|
@ -78,8 +75,5 @@ Deployment
|
||||||
- [Nginx](https://nginx.org/en/) HTTP server
|
- [Nginx](https://nginx.org/en/) HTTP server
|
||||||
|
|
||||||
|
|
||||||
## Book data
|
## Set up BookWyrm
|
||||||
The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written.
|
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/install-dev.html) or [production](https://docs.joinbookwyrm.com/install-prod.html).
|
||||||
|
|
||||||
## Set up Bookwyrm
|
|
||||||
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up Bookwyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html).
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" basics for an activitypub serializer """
|
""" basics for an activitypub serializer """
|
||||||
from dataclasses import dataclass, fields, MISSING
|
from dataclasses import dataclass, fields, MISSING
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
|
@ -8,6 +9,8 @@ from django.db import IntegrityError, transaction
|
||||||
from bookwyrm.connectors import ConnectorException, get_data
|
from bookwyrm.connectors import ConnectorException, get_data
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ActivitySerializerError(ValueError):
|
class ActivitySerializerError(ValueError):
|
||||||
"""routine problems serializing activitypub json"""
|
"""routine problems serializing activitypub json"""
|
||||||
|
@ -39,12 +42,12 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
||||||
activity_json["type"] = "PublicKey"
|
activity_json["type"] = "PublicKey"
|
||||||
|
|
||||||
activity_type = activity_json.get("type")
|
activity_type = activity_json.get("type")
|
||||||
|
if activity_type in ["Question", "Article"]:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
serializer = activity_objects[activity_type]
|
serializer = activity_objects[activity_type]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
# we know this exists and that we can't handle it
|
# we know this exists and that we can't handle it
|
||||||
if activity_type in ["Question"]:
|
|
||||||
return None
|
|
||||||
raise ActivitySerializerError(err)
|
raise ActivitySerializerError(err)
|
||||||
|
|
||||||
return serializer(activity_objects=activity_objects, **activity_json)
|
return serializer(activity_objects=activity_objects, **activity_json)
|
||||||
|
@ -65,7 +68,7 @@ class ActivityObject:
|
||||||
try:
|
try:
|
||||||
value = kwargs[field.name]
|
value = kwargs[field.name]
|
||||||
if value in (None, MISSING, {}):
|
if value in (None, MISSING, {}):
|
||||||
raise KeyError()
|
raise KeyError("Missing required field", field.name)
|
||||||
try:
|
try:
|
||||||
is_subclass = issubclass(field.type, ActivityObject)
|
is_subclass = issubclass(field.type, ActivityObject)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -268,9 +271,9 @@ def resolve_remote_id(
|
||||||
try:
|
try:
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
raise ActivitySerializerError(
|
logger.exception("Could not connect to host for remote_id: %s", remote_id)
|
||||||
f"Could not connect to host for remote_id: {remote_id}"
|
return None
|
||||||
)
|
|
||||||
# determine the model implicitly, if not provided
|
# determine the model implicitly, if not provided
|
||||||
# or if it's a model with subclasses like Status, check again
|
# or if it's a model with subclasses like Status, check again
|
||||||
if not model or hasattr(model.objects, "select_subclasses"):
|
if not model or hasattr(model.objects, "select_subclasses"):
|
||||||
|
|
|
@ -298,8 +298,9 @@ def add_status_on_create_command(sender, instance, created):
|
||||||
priority = HIGH
|
priority = HIGH
|
||||||
# check if this is an old status, de-prioritize if so
|
# check if this is an old status, de-prioritize if so
|
||||||
# (this will happen if federation is very slow, or, more expectedly, on csv import)
|
# (this will happen if federation is very slow, or, more expectedly, on csv import)
|
||||||
one_day = 60 * 60 * 24
|
if instance.published_date < timezone.now() - timedelta(
|
||||||
if (instance.created_date - instance.published_date).seconds > one_day:
|
days=1
|
||||||
|
) or instance.created_date < instance.published_date - timedelta(days=1):
|
||||||
priority = LOW
|
priority = LOW
|
||||||
|
|
||||||
add_status_task.apply_async(
|
add_status_task.apply_async(
|
||||||
|
|
|
@ -53,7 +53,12 @@ class ReadThroughForm(CustomForm):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"finish_date", _("Reading finish date cannot be before start date.")
|
"finish_date", _("Reading finish date cannot be before start date.")
|
||||||
)
|
)
|
||||||
|
stopped_date = cleaned_data.get("stopped_date")
|
||||||
|
if start_date and stopped_date and start_date > stopped_date:
|
||||||
|
self.add_error(
|
||||||
|
"stopped_date", _("Reading stopped date cannot be before start date.")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ReadThrough
|
model = models.ReadThrough
|
||||||
fields = ["user", "book", "start_date", "finish_date"]
|
fields = ["user", "book", "start_date", "finish_date", "stopped_date"]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" import classes """
|
""" import classes """
|
||||||
|
|
||||||
from .importer import Importer
|
from .importer import Importer
|
||||||
|
from .calibre_import import CalibreImporter
|
||||||
from .goodreads_import import GoodreadsImporter
|
from .goodreads_import import GoodreadsImporter
|
||||||
from .librarything_import import LibrarythingImporter
|
from .librarything_import import LibrarythingImporter
|
||||||
from .openlibrary_import import OpenLibraryImporter
|
from .openlibrary_import import OpenLibraryImporter
|
||||||
|
|
28
bookwyrm/importers/calibre_import.py
Normal file
28
bookwyrm/importers/calibre_import.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
""" handle reading a csv from calibre """
|
||||||
|
from bookwyrm.models import Shelf
|
||||||
|
|
||||||
|
from . import Importer
|
||||||
|
|
||||||
|
|
||||||
|
class CalibreImporter(Importer):
|
||||||
|
"""csv downloads from Calibre"""
|
||||||
|
|
||||||
|
service = "Calibre"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Add timestamp to row_mappings_guesses for date_added to avoid
|
||||||
|
# integrity error
|
||||||
|
row_mappings_guesses = []
|
||||||
|
|
||||||
|
for field, mapping in self.row_mappings_guesses:
|
||||||
|
if field in ("date_added",):
|
||||||
|
row_mappings_guesses.append((field, mapping + ["timestamp"]))
|
||||||
|
else:
|
||||||
|
row_mappings_guesses.append((field, mapping))
|
||||||
|
|
||||||
|
self.row_mappings_guesses = row_mappings_guesses
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_shelf(self, normalized_row):
|
||||||
|
# Calibre export does not indicate which shelf to use. Go with a default one for now
|
||||||
|
return Shelf.TO_READ
|
|
@ -1,5 +1,8 @@
|
||||||
""" handle reading a tsv from librarything """
|
""" handle reading a tsv from librarything """
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from bookwyrm.models import Shelf
|
||||||
|
|
||||||
from . import Importer
|
from . import Importer
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +24,7 @@ class LibrarythingImporter(Importer):
|
||||||
|
|
||||||
def get_shelf(self, normalized_row):
|
def get_shelf(self, normalized_row):
|
||||||
if normalized_row["date_finished"]:
|
if normalized_row["date_finished"]:
|
||||||
return "read"
|
return Shelf.READ_FINISHED
|
||||||
if normalized_row["date_started"]:
|
if normalized_row["date_started"]:
|
||||||
return "reading"
|
return Shelf.READING
|
||||||
return "to-read"
|
return Shelf.TO_READ
|
||||||
|
|
|
@ -105,16 +105,6 @@ def init_connectors():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init_federated_servers():
|
|
||||||
"""big no to nazis"""
|
|
||||||
built_in_blocks = ["gab.ai", "gab.com"]
|
|
||||||
for server in built_in_blocks:
|
|
||||||
models.FederatedServer.objects.create(
|
|
||||||
server_name=server,
|
|
||||||
status="blocked",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def init_settings():
|
def init_settings():
|
||||||
"""info about the instance"""
|
"""info about the instance"""
|
||||||
models.SiteSettings.objects.create(
|
models.SiteSettings.objects.create(
|
||||||
|
@ -163,7 +153,6 @@ class Command(BaseCommand):
|
||||||
"group",
|
"group",
|
||||||
"permission",
|
"permission",
|
||||||
"connector",
|
"connector",
|
||||||
"federatedserver",
|
|
||||||
"settings",
|
"settings",
|
||||||
"linkdomain",
|
"linkdomain",
|
||||||
]
|
]
|
||||||
|
@ -176,8 +165,6 @@ class Command(BaseCommand):
|
||||||
init_permissions()
|
init_permissions()
|
||||||
if not limit or limit == "connector":
|
if not limit or limit == "connector":
|
||||||
init_connectors()
|
init_connectors()
|
||||||
if not limit or limit == "federatedserver":
|
|
||||||
init_federated_servers()
|
|
||||||
if not limit or limit == "settings":
|
if not limit or limit == "settings":
|
||||||
init_settings()
|
init_settings()
|
||||||
if not limit or limit == "linkdomain":
|
if not limit or limit == "linkdomain":
|
||||||
|
|
80
bookwyrm/migrations/0146_auto_20220316_2320.py
Normal file
80
bookwyrm/migrations/0146_auto_20220316_2320.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-16 23:20
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
from bookwyrm.models import Shelf
|
||||||
|
|
||||||
|
|
||||||
|
def add_shelves(apps, schema_editor):
|
||||||
|
"""add any superusers to the "admin" group"""
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
shelf_model = apps.get_model("bookwyrm", "Shelf")
|
||||||
|
|
||||||
|
users = apps.get_model("bookwyrm", "User")
|
||||||
|
local_users = users.objects.using(db_alias).filter(local=True)
|
||||||
|
for user in local_users:
|
||||||
|
remote_id = f"{user.remote_id}/books/stopped"
|
||||||
|
shelf_model.objects.using(db_alias).create(
|
||||||
|
name="Stopped reading",
|
||||||
|
identifier=Shelf.STOPPED_READING,
|
||||||
|
user=user,
|
||||||
|
editable=False,
|
||||||
|
remote_id=remote_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0145_sitesettings_version"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="comment",
|
||||||
|
name="reading_status",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("to-read", "To-Read"),
|
||||||
|
("reading", "Reading"),
|
||||||
|
("read", "Read"),
|
||||||
|
("stopped-reading", "Stopped-Reading"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="quotation",
|
||||||
|
name="reading_status",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("to-read", "To-Read"),
|
||||||
|
("reading", "Reading"),
|
||||||
|
("read", "Read"),
|
||||||
|
("stopped-reading", "Stopped-Reading"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="review",
|
||||||
|
name="reading_status",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("to-read", "To-Read"),
|
||||||
|
("reading", "Reading"),
|
||||||
|
("read", "Read"),
|
||||||
|
("stopped-reading", "Stopped-Reading"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(add_shelves, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-31 14:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0147_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="preferred_language",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("en-us", "English"),
|
||||||
|
("de-de", "Deutsch (German)"),
|
||||||
|
("es-es", "Español (Spanish)"),
|
||||||
|
("gl-es", "Galego (Galician)"),
|
||||||
|
("it-it", "Italiano (Italian)"),
|
||||||
|
("fi-fi", "Suomi (Finnish)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("no-no", "Norsk (Norwegian)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("ro-ro", "Română (Romanian)"),
|
||||||
|
("sv-se", "Svenska (Swedish)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0148_merge_20220326_2006.py
Normal file
13
bookwyrm/migrations/0148_merge_20220326_2006.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-26 20:06
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0146_auto_20220316_2320"),
|
||||||
|
("bookwyrm", "0147_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
13
bookwyrm/migrations/0149_merge_20220526_1716.py
Normal file
13
bookwyrm/migrations/0149_merge_20220526_1716.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-05-26 17:16
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0148_alter_user_preferred_language"),
|
||||||
|
("bookwyrm", "0148_merge_20220326_2006"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
18
bookwyrm/migrations/0150_readthrough_stopped_date.py
Normal file
18
bookwyrm/migrations/0150_readthrough_stopped_date.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-05-26 18:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0149_merge_20220526_1716"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="readthrough",
|
||||||
|
name="stopped_date",
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,6 +8,7 @@ from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .fields import RemoteIdField
|
from .fields import RemoteIdField
|
||||||
|
@ -35,10 +36,11 @@ class BookWyrmModel(models.Model):
|
||||||
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""generate a url that resolves to the local object"""
|
"""generate the url that resolves to the local object, without a slug"""
|
||||||
base_path = f"https://{DOMAIN}"
|
base_path = f"https://{DOMAIN}"
|
||||||
if hasattr(self, "user"):
|
if hasattr(self, "user"):
|
||||||
base_path = f"{base_path}{self.user.local_path}"
|
base_path = f"{base_path}{self.user.local_path}"
|
||||||
|
|
||||||
model_name = type(self).__name__.lower()
|
model_name = type(self).__name__.lower()
|
||||||
return f"{base_path}/{model_name}/{self.id}"
|
return f"{base_path}/{model_name}/{self.id}"
|
||||||
|
|
||||||
|
@ -49,8 +51,20 @@ class BookWyrmModel(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
"""how to link to this object in the local app"""
|
"""how to link to this object in the local app, with a slug"""
|
||||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||||
|
|
||||||
|
name = None
|
||||||
|
if hasattr(self, "name_field"):
|
||||||
|
name = getattr(self, self.name_field)
|
||||||
|
elif hasattr(self, "name"):
|
||||||
|
name = self.name
|
||||||
|
|
||||||
|
if name:
|
||||||
|
slug = slugify(name)
|
||||||
|
local = f"{local}/s/{slug}"
|
||||||
|
|
||||||
|
return local
|
||||||
|
|
||||||
def raise_visible_to_user(self, viewer):
|
def raise_visible_to_user(self, viewer):
|
||||||
"""is a user authorized to view an object?"""
|
"""is a user authorized to view an object?"""
|
||||||
|
|
|
@ -176,8 +176,8 @@ class Book(BookDataModel):
|
||||||
"""properties of this edition, as a string"""
|
"""properties of this edition, as a string"""
|
||||||
items = [
|
items = [
|
||||||
self.physical_format if hasattr(self, "physical_format") else None,
|
self.physical_format if hasattr(self, "physical_format") else None,
|
||||||
self.languages[0] + " language"
|
f"{self.languages[0]} language"
|
||||||
if self.languages and self.languages[0] != "English"
|
if self.languages and self.languages[0] and self.languages[0] != "English"
|
||||||
else None,
|
else None,
|
||||||
str(self.published_date.year) if self.published_date else None,
|
str(self.published_date.year) if self.published_date else None,
|
||||||
", ".join(self.publishers) if hasattr(self, "publishers") else None,
|
", ".join(self.publishers) if hasattr(self, "publishers") else None,
|
||||||
|
|
|
@ -125,7 +125,7 @@ class ActivitypubFieldMixin:
|
||||||
"""model_field_name to activitypubFieldName"""
|
"""model_field_name to activitypubFieldName"""
|
||||||
if self.activitypub_field:
|
if self.activitypub_field:
|
||||||
return self.activitypub_field
|
return self.activitypub_field
|
||||||
name = self.name.split(".")[-1]
|
name = self.name.rsplit(".", maxsplit=1)[-1]
|
||||||
components = name.split("_")
|
components = name.split("_")
|
||||||
return components[0] + "".join(x.title() for x in components[1:])
|
return components[0] + "".join(x.title() for x in components[1:])
|
||||||
|
|
||||||
|
|
|
@ -175,9 +175,15 @@ class ImportItem(models.Model):
|
||||||
def date_added(self):
|
def date_added(self):
|
||||||
"""when the book was added to this dataset"""
|
"""when the book was added to this dataset"""
|
||||||
if self.normalized_data.get("date_added"):
|
if self.normalized_data.get("date_added"):
|
||||||
return timezone.make_aware(
|
parsed_date_added = dateutil.parser.parse(
|
||||||
dateutil.parser.parse(self.normalized_data.get("date_added"))
|
self.normalized_data.get("date_added")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if timezone.is_aware(parsed_date_added):
|
||||||
|
# Keep timezone if import already had one
|
||||||
|
return parsed_date_added
|
||||||
|
|
||||||
|
return timezone.make_aware(parsed_date_added)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -27,6 +27,7 @@ class ReadThrough(BookWyrmModel):
|
||||||
)
|
)
|
||||||
start_date = models.DateTimeField(blank=True, null=True)
|
start_date = models.DateTimeField(blank=True, null=True)
|
||||||
finish_date = models.DateTimeField(blank=True, null=True)
|
finish_date = models.DateTimeField(blank=True, null=True)
|
||||||
|
stopped_date = models.DateTimeField(blank=True, null=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -34,7 +35,7 @@ class ReadThrough(BookWyrmModel):
|
||||||
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
|
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
|
||||||
self.user.update_active_date()
|
self.user.update_active_date()
|
||||||
# an active readthrough must have an unset finish date
|
# an active readthrough must have an unset finish date
|
||||||
if self.finish_date:
|
if self.finish_date or self.stopped_date:
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
@ -17,8 +18,9 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
TO_READ = "to-read"
|
TO_READ = "to-read"
|
||||||
READING = "reading"
|
READING = "reading"
|
||||||
READ_FINISHED = "read"
|
READ_FINISHED = "read"
|
||||||
|
STOPPED_READING = "stopped-reading"
|
||||||
|
|
||||||
READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED)
|
READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED, STOPPED_READING)
|
||||||
|
|
||||||
name = fields.CharField(max_length=100)
|
name = fields.CharField(max_length=100)
|
||||||
identifier = models.CharField(max_length=100)
|
identifier = models.CharField(max_length=100)
|
||||||
|
@ -65,6 +67,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
identifier = self.identifier or self.get_identifier()
|
identifier = self.identifier or self.get_identifier()
|
||||||
return f"{base_path}/books/{identifier}"
|
return f"{base_path}/books/{identifier}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def local_path(self):
|
||||||
|
"""No slugs"""
|
||||||
|
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||||
|
|
||||||
def raise_not_deletable(self, viewer):
|
def raise_not_deletable(self, viewer):
|
||||||
"""don't let anyone delete a default shelf"""
|
"""don't let anyone delete a default shelf"""
|
||||||
super().raise_not_deletable(viewer)
|
super().raise_not_deletable(viewer)
|
||||||
|
|
|
@ -116,11 +116,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
|
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
|
||||||
"""keep notes if they are replies to existing statuses"""
|
"""keep notes if they are replies to existing statuses"""
|
||||||
if activity.type == "Announce":
|
if activity.type == "Announce":
|
||||||
try:
|
boosted = activitypub.resolve_remote_id(activity.object, get_activity=True)
|
||||||
boosted = activitypub.resolve_remote_id(
|
if not boosted:
|
||||||
activity.object, get_activity=True
|
|
||||||
)
|
|
||||||
except activitypub.ActivitySerializerError:
|
|
||||||
# if we can't load the status, definitely ignore it
|
# if we can't load the status, definitely ignore it
|
||||||
return True
|
return True
|
||||||
# keep the boost if we would keep the status
|
# keep the boost if we would keep the status
|
||||||
|
@ -265,7 +262,7 @@ class GeneratedNote(Status):
|
||||||
|
|
||||||
|
|
||||||
ReadingStatusChoices = models.TextChoices(
|
ReadingStatusChoices = models.TextChoices(
|
||||||
"ReadingStatusChoices", ["to-read", "reading", "read"]
|
"ReadingStatusChoices", ["to-read", "reading", "read", "stopped-reading"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -374,6 +374,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
"name": "Read",
|
"name": "Read",
|
||||||
"identifier": "read",
|
"identifier": "read",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Stopped Reading",
|
||||||
|
"identifier": "stopped-reading",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for shelf in shelves:
|
for shelf in shelves:
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.3.4"
|
VERSION = "0.4.0"
|
||||||
|
|
||||||
RELEASE_API = env(
|
RELEASE_API = env(
|
||||||
"RELEASE_API",
|
"RELEASE_API",
|
||||||
|
@ -21,7 +21,7 @@ RELEASE_API = env(
|
||||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||||
|
|
||||||
JS_CACHE = "bc93172a"
|
JS_CACHE = "e678183b"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
@ -293,6 +293,7 @@ LANGUAGES = [
|
||||||
("es-es", _("Español (Spanish)")),
|
("es-es", _("Español (Spanish)")),
|
||||||
("gl-es", _("Galego (Galician)")),
|
("gl-es", _("Galego (Galician)")),
|
||||||
("it-it", _("Italiano (Italian)")),
|
("it-it", _("Italiano (Italian)")),
|
||||||
|
("fi-fi", _("Suomi (Finnish)")),
|
||||||
("fr-fr", _("Français (French)")),
|
("fr-fr", _("Français (French)")),
|
||||||
("lt-lt", _("Lietuvių (Lithuanian)")),
|
("lt-lt", _("Lietuvių (Lithuanian)")),
|
||||||
("no-no", _("Norsk (Norwegian)")),
|
("no-no", _("Norsk (Norwegian)")),
|
||||||
|
|
|
@ -114,3 +114,17 @@ details[open] summary .details-close {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Navbar details
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#navbar-dropdown .navbar-item {
|
||||||
|
color: $text;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.375rem 3rem 0.375rem 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar-dropdown .navbar-item:hover {
|
||||||
|
background-color: $background-secondary;
|
||||||
|
}
|
||||||
|
|
|
@ -23,3 +23,8 @@
|
||||||
.has-background-tertiary {
|
.has-background-tertiary {
|
||||||
background-color: $background-tertiary !important;
|
background-color: $background-tertiary !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Workaround for dark theme as .has-text-black doesn't give desired effect. */
|
||||||
|
.has-text-default {
|
||||||
|
color: $text !important;
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ $link-hover: $white-bis;
|
||||||
$link-hover-border: #51595d;
|
$link-hover-border: #51595d;
|
||||||
$link-focus: $white-bis;
|
$link-focus: $white-bis;
|
||||||
$link-active: $white-bis;
|
$link-active: $white-bis;
|
||||||
|
$link-light: #0d1c26;
|
||||||
|
|
||||||
/* bulma overrides */
|
/* bulma overrides */
|
||||||
$background: $background-secondary;
|
$background: $background-secondary;
|
||||||
|
@ -83,6 +84,13 @@ $progress-value-background-color: $border-light;
|
||||||
$family-primary: $family-sans-serif;
|
$family-primary: $family-sans-serif;
|
||||||
$family-secondary: $family-sans-serif;
|
$family-secondary: $family-sans-serif;
|
||||||
|
|
||||||
|
.has-text-muted {
|
||||||
|
color: $grey-lighter !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-text-more-muted {
|
||||||
|
color: $grey-light !important;
|
||||||
|
}
|
||||||
|
|
||||||
@import "../bookwyrm.scss";
|
@import "../bookwyrm.scss";
|
||||||
@import "../vendor/icons.css";
|
@import "../vendor/icons.css";
|
||||||
|
|
|
@ -57,5 +57,13 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
||||||
$family-primary: $family-sans-serif;
|
$family-primary: $family-sans-serif;
|
||||||
$family-secondary: $family-sans-serif;
|
$family-secondary: $family-sans-serif;
|
||||||
|
|
||||||
|
.has-text-muted {
|
||||||
|
color: $grey-dark !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-text-more-muted {
|
||||||
|
color: $grey !important;
|
||||||
|
}
|
||||||
|
|
||||||
@import "../bookwyrm.scss";
|
@import "../bookwyrm.scss";
|
||||||
@import "../vendor/icons.css";
|
@import "../vendor/icons.css";
|
||||||
|
|
|
@ -203,6 +203,8 @@ let StatusCache = new (class {
|
||||||
.forEach((item) => (item.disabled = false));
|
.forEach((item) => (item.disabled = false));
|
||||||
|
|
||||||
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
|
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
|
||||||
|
next_identifier =
|
||||||
|
next_identifier == "stopped-reading-complete" ? "stopped-reading" : next_identifier;
|
||||||
|
|
||||||
// Disable the current state
|
// Disable the current state
|
||||||
button.querySelector(
|
button.querySelector(
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="block" name="edit-author" action="{{ author.local_path }}/edit" method="post">
|
<form class="block" name="edit-author" action="{% url 'edit-author' author.id %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
|
|
||||||
|
|
|
@ -284,7 +284,7 @@
|
||||||
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
|
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
{% url 'book' book.id as tab_url %}
|
{% url 'book' book.id book.name|slugify as tab_url %}
|
||||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
|
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -41,10 +41,18 @@
|
||||||
class="block"
|
class="block"
|
||||||
{% if book.id %}
|
{% if book.id %}
|
||||||
name="edit-book"
|
name="edit-book"
|
||||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
{% if confirm_mode %}
|
||||||
|
action="{% url 'edit-book-confirm' book.id %}"
|
||||||
|
{% else %}
|
||||||
|
action="{% url 'edit-book' book.id %}"
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
name="create-book"
|
name="create-book"
|
||||||
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
|
{% if confirm_mode %}
|
||||||
|
action="{% url 'create-book-confirm' %}"
|
||||||
|
{% else %}
|
||||||
|
action="{% url 'create-book' %}"
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
method="post"
|
method="post"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
||||||
<h2 class="title is-5 mb-1">
|
<h2 class="title is-5 mb-1">
|
||||||
<a href="{{ book.local_path }}" class="has-text-black">
|
<a href="{{ book.local_path }}" class="has-text-default">
|
||||||
{{ book|book_title }}
|
{{ book|book_title }}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
|
<li><a href="{% url 'book' book.id book.name|slugify %}">{{ book|book_title }}</a></li>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
<a href="#" aria-current="page">
|
<a href="#" aria-current="page">
|
||||||
{% trans "Edit links" %}
|
{% trans "Edit links" %}
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<a href="https://joinbookwyrm.com/">
|
<a href="https://joinbookwyrm.com/">
|
||||||
{% trans "Join Bookwyrm" %}
|
{% trans "Join BookWyrm" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
|
||||||
{% else %}{{ shelf.name }}{% endif %}
|
{% else %}{{ shelf.name }}{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
{% for membership in group.memberships.all %}
|
{% for membership in group.memberships.all %}
|
||||||
{% with member=membership.user %}
|
{% with member=membership.user %}
|
||||||
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
|
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
|
||||||
<a href="{{ member.local_path }}" class="has-text-black">
|
<a href="{{ member.local_path }}" class="has-text-default">
|
||||||
{% include 'snippets/avatar.html' with user=member large=True %}
|
{% include 'snippets/avatar.html' with user=member large=True %}
|
||||||
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
|
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
|
||||||
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>
|
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="column is-flex is-flex-grow-0">
|
<div class="column is-flex is-flex-grow-0">
|
||||||
{% for user in suggested_users %}
|
{% for user in suggested_users %}
|
||||||
<div class="box has-text-centered is-shadowless has-background-tertiary m-2">
|
<div class="box has-text-centered is-shadowless has-background-tertiary m-2">
|
||||||
<a href="{{ user.local_path }}" class="has-text-black">
|
<a href="{{ user.local_path }}" class="has-text-default">
|
||||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
||||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||||
|
|
|
@ -32,6 +32,9 @@
|
||||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||||
OpenLibrary (CSV)
|
OpenLibrary (CSV)
|
||||||
</option>
|
</option>
|
||||||
|
<option value="Calibre" {% if current == 'Calibre' %}selected{% endif %}>
|
||||||
|
Calibre (CSV)
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -90,64 +90,8 @@
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
|
<div class="navbar-item mt-3 py-0">
|
||||||
<a
|
{% include 'user_menu.html' %}
|
||||||
href="{{ request.user.local_path }}"
|
|
||||||
class="navbar-link pulldown-menu"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
tabindex="0"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-controls="navbar-dropdown"
|
|
||||||
>
|
|
||||||
{% include 'snippets/avatar.html' with user=request.user %}
|
|
||||||
<span class="ml-2">{{ request.user.display_name }}</span>
|
|
||||||
</a>
|
|
||||||
<ul class="navbar-dropdown" id="navbar_dropdown">
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'directory' %}" class="navbar-item">
|
|
||||||
{% trans "Directory" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
|
||||||
{% trans 'Your Books' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
|
||||||
{% trans "Direct Messages" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
|
||||||
{% trans 'Settings' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
|
||||||
<li class="navbar-divider" role="presentation"> </li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
|
||||||
{% trans 'Invites' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.bookwyrm.moderate_user %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
|
||||||
{% trans 'Admin' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="navbar-divider" role="presentation"> </li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'logout' %}" class="navbar-item">
|
|
||||||
{% trans 'Log out' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item mt-3 py-0">
|
<div class="navbar-item mt-3 py-0">
|
||||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
||||||
<li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li>
|
<li><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{{ list.name|truncatechars:30 }}</a></li>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
<a href="#" aria-current="page">
|
<a href="#" aria-current="page">
|
||||||
{% trans "Curate" %}
|
{% trans "Curate" %}
|
||||||
|
|
|
@ -180,7 +180,7 @@
|
||||||
<h2 class="title is-5">
|
<h2 class="title is-5">
|
||||||
{% trans "Sort List" %}
|
{% trans "Sort List" %}
|
||||||
</h2>
|
</h2>
|
||||||
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
|
<form name="sort" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
@ -207,7 +207,7 @@
|
||||||
{% trans "Suggest Books" %}
|
{% trans "Suggest Books" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
|
<form name="search" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
|
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
|
||||||
|
@ -221,7 +221,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if query %}
|
{% if query %}
|
||||||
<p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p>
|
<p class="help"><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{% trans "Clear search" %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if not suggested_books %}
|
{% if not suggested_books %}
|
||||||
|
|
|
@ -47,12 +47,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-grey-dark">
|
<div class="column is-narrow has-text-muted">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,12 +47,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-grey-dark">
|
<div class="column is-narrow has-text-muted">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load notification_page_tags %}
|
{% load notification_page_tags %}
|
||||||
{% related_status notification as related_status %}
|
{% related_status notification as related_status %}
|
||||||
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
|
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
|
||||||
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
|
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-more-muted{% endif %}">
|
||||||
<div class="column is-narrow is-size-3">
|
<div class="column is-narrow is-size-3">
|
||||||
<a class="icon" href="{% block primary_link %}{% endblock %}">
|
<a class="icon" href="{% block primary_link %}{% endblock %}">
|
||||||
{% block icon %}{% endblock %}
|
{% block icon %}{% endblock %}
|
||||||
|
|
|
@ -48,12 +48,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-text-black">
|
<div class="column is-narrow has-text-default">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,12 +51,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-text-black">
|
<div class="column is-narrow has-text-default">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
22
bookwyrm/templates/preferences/export.html
Normal file
22
bookwyrm/templates/preferences/export.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'preferences/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "CSV Export" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% trans "CSV Export" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<div class="block content">
|
||||||
|
<p class="notification">
|
||||||
|
{% trans "Your export will include all the books on your shelves, books you have reviewed, and books with reading activity." %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'prefs-export-file' %}" class="button">
|
||||||
|
<span class="icon icon-download" aria-hidden="true"></span>
|
||||||
|
<span>Download file</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -24,6 +24,17 @@
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<h2 class="menu-label">{% trans "Data" %}</h2>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
{% url 'import' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Import" %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'prefs-export' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "CSV export" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
|
|
14
bookwyrm/templates/reading_progress/stop.html
Normal file
14
bookwyrm/templates/reading_progress/stop.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% blocktrans trimmed with book_title=book.title %}
|
||||||
|
Stop Reading "{{ book_title }}"
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "snippets/reading_modals/stop_reading_modal.html" with book=book active=True static=True %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -19,6 +19,7 @@
|
||||||
</label>
|
</label>
|
||||||
{% include "snippets/progress_field.html" with id=field_id %}
|
{% include "snippets/progress_field.html" with id=field_id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_finish_date_{{ readthrough.id }}">
|
<label class="label" for="id_finish_date_{{ readthrough.id }}">
|
||||||
{% trans "Finished reading" %}
|
{% trans "Finished reading" %}
|
||||||
|
|
|
@ -8,10 +8,12 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{% trans "Progress Updates:" %}
|
{% trans "Progress Updates:" %}
|
||||||
<ul>
|
<ul>
|
||||||
{% if readthrough.finish_date or readthrough.progress %}
|
{% if readthrough.finish_date or readthrough.stopped_date or readthrough.progress %}
|
||||||
<li>
|
<li>
|
||||||
{% if readthrough.finish_date %}
|
{% if readthrough.finish_date %}
|
||||||
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
||||||
|
{% elif readthrough.stopped_date %}
|
||||||
|
{{ readthrough.stopped_date | localtime | naturalday }}: {% trans "stopped" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% if readthrough.progress_mode == 'PG' %}
|
{% if readthrough.progress_mode == 'PG' %}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
|
{% block filter_fields %}
|
||||||
|
{% include 'settings/federation/software_filter.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
{% include 'settings/federation/instance_filters.html' %}
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
{% url 'settings-federation' status='federated' as url %}
|
{% url 'settings-federation' status='federated' as url %}
|
||||||
|
@ -36,6 +39,10 @@
|
||||||
{% trans "Date added" as text %}
|
{% trans "Date added" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Last updated" as text %}
|
||||||
|
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Software" as text %}
|
{% trans "Software" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||||
|
@ -43,12 +50,12 @@
|
||||||
<th>
|
<th>
|
||||||
{% trans "Users" %}
|
{% trans "Users" %}
|
||||||
</th>
|
</th>
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% for server in servers %}
|
{% for server in servers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||||
<td>{{ server.created_date }}</td>
|
<td>{{ server.created_date|date:'Y-m-d' }}</td>
|
||||||
|
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if server.application_type %}
|
{% if server.application_type %}
|
||||||
{{ server.application_type }}
|
{{ server.application_type }}
|
||||||
|
@ -56,7 +63,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ server.user_set.count }}</td>
|
<td>{{ server.user_set.count }}</td>
|
||||||
<td>{{ server.get_status_display }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not servers %}
|
{% if not servers %}
|
||||||
|
|
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block filter %}
|
||||||
|
<label class="label" for="id_server">{% trans "Software" %}</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select name="application_type">
|
||||||
|
<option value="">-----</option>
|
||||||
|
{% for option in software_options %}
|
||||||
|
{% if option %}
|
||||||
|
<option value="{{ option }}">{{ option }}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
|
||||||
{% else %}{{ shelf.name }}{% endif %}
|
{% else %}{{ shelf.name }}{% endif %}
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
{% include 'snippets/privacy-icons.html' with item=shelf %}
|
{% include 'snippets/privacy-icons.html' with item=shelf %}
|
||||||
|
@ -150,7 +151,7 @@
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th>
|
<th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th>
|
||||||
<th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th>
|
<th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th>
|
||||||
<th>{% trans "Finished" as text %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th>
|
<th>{% if shelf.identifier == 'read' %}{% trans "Finished" as text %}{% else %}{% trans "Until" as text %}{% endif %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
|
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -180,7 +181,7 @@
|
||||||
<td data-title="{% trans "Started" %}">
|
<td data-title="{% trans "Started" %}">
|
||||||
{{ book.start_date|naturalday|default_if_none:""}}
|
{{ book.start_date|naturalday|default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
<td data-title="{% trans "Finished" %}">
|
<td data-title="{% if shelf.identifier == 'read' %}{% trans "Finished" as text %}{% else %}{% trans "Until" as text %}{% endif %}">
|
||||||
{{ book.finish_date|naturalday|default_if_none:""}}
|
{{ book.finish_date|naturalday|default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends 'snippets/reading_modals/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block modal-title %}
|
||||||
|
{% blocktrans trimmed with book_title=book|book_title %}
|
||||||
|
Stop Reading "<em>{{ book_title }}</em>"
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-open %}
|
||||||
|
<form name="stop-reading-{{ uuid }}" action="{% url 'reading-status' 'stop' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
|
<input type="hidden" name="reading_status" value="stopped-reading">
|
||||||
|
<input type="hidden" name="shelf" value="{{ move_from }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block reading-dates %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-half">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="stop_id_start_date_{{ uuid }}">
|
||||||
|
{% trans "Started reading" %}
|
||||||
|
</label>
|
||||||
|
<input type="date" name="start_date" class="input" id="stop_id_start_date_{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_read_until_date_{{ uuid }}">
|
||||||
|
{% trans "Stopped reading" %}
|
||||||
|
</label>
|
||||||
|
<input type="date" name="stopped_date" class="input" id="id_read_until_date_{{ uuid }}" value="{% now "Y-m-d" %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
{% include "snippets/reading_modals/form.html" with optional=True type="stop_modal" %}
|
||||||
|
{% endblock %}
|
|
@ -49,6 +49,13 @@
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
|
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
|
||||||
|
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}
|
||||||
|
|
||||||
|
{% trans "Stopped reading" as button_text %}
|
||||||
|
{% url 'reading-status' 'stop' book.id as fallback_url %}
|
||||||
|
{% join "stop_reading" uuid as modal_id %}
|
||||||
|
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
|
||||||
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{% elif shelf.identifier == 'to-read' %}
|
||||||
|
|
||||||
{% trans "Want to read" as button_text %}
|
{% trans "Want to read" as button_text %}
|
||||||
|
@ -99,5 +106,8 @@
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
|
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
|
||||||
|
|
||||||
|
{% join "stop_reading" uuid as modal_id %}
|
||||||
|
{% include 'snippets/reading_modals/stop_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
|
{% join "stop_reading" uuid as modal_id %}
|
||||||
|
{% include 'snippets/reading_modals/stop_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
{% join "progress_update" uuid as modal_id %}
|
{% join "progress_update" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
||||||
<li role="menuitem" class="dropdown-item p-0">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<div
|
<div
|
||||||
class="{% if next_shelf_identifier == shelf.identifier %}is-hidden{% endif %}"
|
class="{% if is_current or next_shelf_identifier == shelf.identifier %}is-hidden{% elif shelf.identifier == 'stopped-reading' and active_shelf.shelf.identifier != "reading" %}is-hidden{% endif %}"
|
||||||
data-shelf-dropdown-identifier="{{ shelf.identifier }}"
|
data-shelf-dropdown-identifier="{{ shelf.identifier }}"
|
||||||
data-shelf-next="{{ shelf.identifier|next_shelf }}"
|
data-shelf-next="{{ shelf.identifier|next_shelf }}"
|
||||||
>
|
>
|
||||||
|
@ -26,6 +26,13 @@
|
||||||
{% join "finish_reading" button_uuid as modal_id %}
|
{% join "finish_reading" button_uuid as modal_id %}
|
||||||
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}
|
||||||
|
|
||||||
|
{% trans "Stop reading" as button_text %}
|
||||||
|
{% url 'reading-status' 'stop' book.id as fallback_url %}
|
||||||
|
{% join "stop_reading" button_uuid as modal_id %}
|
||||||
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{% elif shelf.identifier == 'to-read' %}
|
||||||
|
|
||||||
{% trans "Want to read" as button_text %}
|
{% trans "Want to read" as button_text %}
|
||||||
|
|
|
@ -13,6 +13,15 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="{% if next_shelf_identifier != 'stopped-reading-complete' %}is-hidden{% endif %}"
|
||||||
|
data-shelf-identifier="stopped-reading-complete"
|
||||||
|
>
|
||||||
|
<button type="button" class="button {{ class }}" disabled>
|
||||||
|
<span>{% trans "Stopped reading" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for shelf in shelves %}
|
{% for shelf in shelves %}
|
||||||
<div
|
<div
|
||||||
class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
|
class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
|
||||||
|
@ -33,6 +42,14 @@
|
||||||
{% join "finish_reading" button_uuid as modal_id %}
|
{% join "finish_reading" button_uuid as modal_id %}
|
||||||
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
|
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}
|
||||||
|
|
||||||
|
{% trans "Stop reading" as button_text %}
|
||||||
|
{% url 'reading-status' 'finish' book.id as fallback_url %}
|
||||||
|
{% join "stop_reading" button_uuid as modal_id %}
|
||||||
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{% elif shelf.identifier == 'to-read' %}
|
||||||
|
|
||||||
{% trans "Want to read" as button_text %}
|
{% trans "Want to read" as button_text %}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% spaceless %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
{% load status_display %}
|
||||||
|
|
||||||
|
{% load_book status as book %}
|
||||||
|
{% if book.authors.exists %}
|
||||||
|
|
||||||
|
{% with author=book.authors.first %}
|
||||||
|
{% blocktrans trimmed with book_path=book.local_path book=book|book_title author_name=author.name author_path=author.local_path %}
|
||||||
|
stopped reading <a href="{{ book_path }}">{{ book }}</a> by <a href="{{ author_path }}">{{ author_name }}</a>
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% blocktrans trimmed with book_path=book.local_path book=book|book_title %}
|
||||||
|
stopped reading <a href="{{ book_path }}">{{ book }}</a>
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endspaceless %}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{% for user in suggested_users %}
|
{% for user in suggested_users %}
|
||||||
<div class="column is-flex is-flex-grow-0">
|
<div class="column is-flex is-flex-grow-0">
|
||||||
<div class="box has-text-centered is-shadowless has-background-tertiary m-0">
|
<div class="box has-text-centered is-shadowless has-background-tertiary m-0">
|
||||||
<a href="{{ user.local_path }}" class="has-text-black">
|
<a href="{{ user.local_path }}" class="has-text-default">
|
||||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
||||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||||
|
|
|
@ -33,8 +33,9 @@
|
||||||
{% if shelf.name == 'To Read' %}{% trans "To Read" %}
|
{% if shelf.name == 'To Read' %}{% trans "To Read" %}
|
||||||
{% elif shelf.name == 'Currently Reading' %}{% trans "Currently Reading" %}
|
{% elif shelf.name == 'Currently Reading' %}{% trans "Currently Reading" %}
|
||||||
{% elif shelf.name == 'Read' %}{% trans "Read" %}
|
{% elif shelf.name == 'Read' %}{% trans "Read" %}
|
||||||
|
{% elif shelf.name == 'Stopped Reading' %}{% trans "Stopped Reading" %}
|
||||||
{% else %}{{ shelf.name }}{% endif %}
|
{% else %}{{ shelf.name }}{% endif %}
|
||||||
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}
|
{% if shelf.size > 4 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="is-mobile field is-grouped">
|
<div class="is-mobile field is-grouped">
|
||||||
{% for book in shelf.books %}
|
{% for book in shelf.books %}
|
||||||
|
|
77
bookwyrm/templates/user_menu.html
Normal file
77
bookwyrm/templates/user_menu.html
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
{% load utilities %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<details class="dropdown" id="navbar-dropdown">
|
||||||
|
<summary
|
||||||
|
class="is-relative pulldown-menu dropdown-trigger"
|
||||||
|
aria-label="{% trans 'View profile and more' %}"
|
||||||
|
role="button"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
>
|
||||||
|
<span class="">
|
||||||
|
{% include 'snippets/avatar.html' with user=request.user %}
|
||||||
|
<span class="ml-2">{{ request.user.display_name }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="icon icon-arrow-down is-hidden-mobile" aria-hidden="true"></span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<ul
|
||||||
|
class="dropdown-content"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'user-feed' request.user.localname %}" class="navbar-item">
|
||||||
|
{% trans "Profile" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'directory' %}" class="navbar-item">
|
||||||
|
{% trans "Directory" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
||||||
|
{% trans 'Your Books' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
||||||
|
{% trans "Direct Messages" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
||||||
|
{% trans 'Settings' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||||
|
<li class="navbar-divider" role="presentation" aria-hidden="true"> </li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
||||||
|
{% trans 'Invites' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.bookwyrm.moderate_user %}
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
||||||
|
{% trans 'Admin' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="navbar-divider" role="presentation" aria-hidden="true"> </li>
|
||||||
|
|
||||||
|
<li role="menuitem">
|
||||||
|
<a href="{% url 'logout' %}" class="navbar-item">
|
||||||
|
{% trans 'Log out' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
|
@ -30,6 +30,8 @@ def get_next_shelf(current_shelf):
|
||||||
return "read"
|
return "read"
|
||||||
if current_shelf == "read":
|
if current_shelf == "read":
|
||||||
return "complete"
|
return "complete"
|
||||||
|
if current_shelf == "stopped-reading":
|
||||||
|
return "stopped-reading-complete"
|
||||||
return "to-read"
|
return "to-read"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
""" import ALL the tests """
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -4,7 +4,10 @@ from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
class Author(TestCase):
|
class Author(TestCase):
|
||||||
|
"""serialize author tests"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
"""initial data"""
|
||||||
self.book = models.Edition.objects.create(
|
self.book = models.Edition.objects.create(
|
||||||
title="Example Edition",
|
title="Example Edition",
|
||||||
remote_id="https://example.com/book/1",
|
remote_id="https://example.com/book/1",
|
||||||
|
@ -16,6 +19,7 @@ class Author(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_serialize_model(self):
|
def test_serialize_model(self):
|
||||||
|
"""check presense of author fields"""
|
||||||
activity = self.author.to_activity()
|
activity = self.author.to_activity()
|
||||||
self.assertEqual(activity["id"], self.author.remote_id)
|
self.assertEqual(activity["id"], self.author.remote_id)
|
||||||
self.assertIsInstance(activity["aliases"], list)
|
self.assertIsInstance(activity["aliases"], list)
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
""" testing activitystreams """
|
""" testing activitystreams """
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitystreams, models
|
from bookwyrm import activitystreams, models
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +66,39 @@ class ActivitystreamsSignals(TestCase):
|
||||||
self.assertEqual(args["args"][0], status.id)
|
self.assertEqual(args["args"][0], status.id)
|
||||||
self.assertEqual(args["queue"], "high_priority")
|
self.assertEqual(args["queue"], "high_priority")
|
||||||
|
|
||||||
|
def test_add_status_on_create_created_low_priority(self, *_):
|
||||||
|
"""a new statuses has entered"""
|
||||||
|
# created later than publication
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.remote_user,
|
||||||
|
content="hi",
|
||||||
|
privacy="public",
|
||||||
|
created_date=datetime(2022, 5, 16, tzinfo=timezone.utc),
|
||||||
|
published_date=datetime(2022, 5, 14, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
|
||||||
|
activitystreams.add_status_on_create_command(models.Status, status, False)
|
||||||
|
|
||||||
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
args = mock.call_args[1]
|
||||||
|
self.assertEqual(args["args"][0], status.id)
|
||||||
|
self.assertEqual(args["queue"], "low_priority")
|
||||||
|
|
||||||
|
# published later than yesterday
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.remote_user,
|
||||||
|
content="hi",
|
||||||
|
privacy="public",
|
||||||
|
published_date=timezone.now() - timedelta(days=1),
|
||||||
|
)
|
||||||
|
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
|
||||||
|
activitystreams.add_status_on_create_command(models.Status, status, False)
|
||||||
|
|
||||||
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
args = mock.call_args[1]
|
||||||
|
self.assertEqual(args["args"][0], status.id)
|
||||||
|
self.assertEqual(args["queue"], "low_priority")
|
||||||
|
|
||||||
def test_populate_streams_on_account_create_command(self, *_):
|
def test_populate_streams_on_account_create_command(self, *_):
|
||||||
"""create streams for a user"""
|
"""create streams for a user"""
|
||||||
with patch("bookwyrm.activitystreams.populate_stream_task.delay") as mock:
|
with patch("bookwyrm.activitystreams.populate_stream_task.delay") as mock:
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -101,6 +101,7 @@ class AbstractConnector(TestCase):
|
||||||
result = self.connector.get_or_create_book(
|
result = self.connector.get_or_create_book(
|
||||||
f"https://{DOMAIN}/book/{self.book.id}"
|
f"https://{DOMAIN}/book/{self.book.id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(models.Book.objects.count(), 1)
|
self.assertEqual(models.Book.objects.count(), 1)
|
||||||
self.assertEqual(result, self.book)
|
self.assertEqual(result, self.book)
|
||||||
|
|
||||||
|
|
2
bookwyrm/tests/data/calibre.csv
Normal file
2
bookwyrm/tests/data/calibre.csv
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
authors,author_sort,rating,library_name,timestamp,formats,size,isbn,identifiers,comments,tags,series,series_index,languages,title,cover,title_sort,publisher,pubdate,id,uuid
|
||||||
|
"Seanan McGuire","McGuire, Seanan","5","Bücher","2021-01-19T22:41:16+01:00","epub, original_epub","1433809","9780756411800","goodreads:39077187,isbn:9780756411800","REPLACED COMMENTS (BOOK DESCRIPTION) BECAUSE IT IS REALLY LONG.","Cryptids, Fantasy, Romance, Magic","InCryptid","8.0","eng","That Ain't Witchcraft","/home/tastytea/Bücher/Seanan McGuire/That Ain't Witchcraft (864)/cover.jpg","That Ain't Witchcraft","Daw Books","2019-03-05T01:00:00+01:00","864","3051ed45-8943-4900-a22a-d2704e3583df"
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
71
bookwyrm/tests/importers/test_calibre_import.py
Normal file
71
bookwyrm/tests/importers/test_calibre_import.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
""" testing import """
|
||||||
|
import pathlib
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.importers import CalibreImporter
|
||||||
|
from bookwyrm.importers.importer import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=consider-using-with
|
||||||
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||||
|
class CalibreImport(TestCase):
|
||||||
|
"""importing from Calibre csv"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""use a test csv"""
|
||||||
|
self.importer = CalibreImporter()
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath("../data/calibre.csv")
|
||||||
|
self.csv = open(datafile, "r", encoding=self.importer.encoding)
|
||||||
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"mouse", "mouse@mouse.mouse", "password", local=True
|
||||||
|
)
|
||||||
|
|
||||||
|
work = models.Work.objects.create(title="Test Work")
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title="Example Edition",
|
||||||
|
remote_id="https://example.com/book/1",
|
||||||
|
parent_work=work,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_job(self, *_):
|
||||||
|
"""creates the import job entry and checks csv"""
|
||||||
|
import_job = self.importer.create_job(
|
||||||
|
self.local_user, self.csv, False, "public"
|
||||||
|
)
|
||||||
|
|
||||||
|
import_items = (
|
||||||
|
models.ImportItem.objects.filter(job=import_job).order_by("index").all()
|
||||||
|
)
|
||||||
|
self.assertEqual(len(import_items), 1)
|
||||||
|
self.assertEqual(import_items[0].index, 0)
|
||||||
|
self.assertEqual(
|
||||||
|
import_items[0].normalized_data["title"], "That Ain't Witchcraft"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_handle_imported_book(self, *_):
|
||||||
|
"""calibre import added a book, this adds related connections"""
|
||||||
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.TO_READ
|
||||||
|
).first()
|
||||||
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
|
import_job = self.importer.create_job(
|
||||||
|
self.local_user, self.csv, False, "public"
|
||||||
|
)
|
||||||
|
import_item = import_job.items.first()
|
||||||
|
import_item.book = self.book
|
||||||
|
import_item.save()
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
|
handle_imported_book(import_item)
|
||||||
|
|
||||||
|
shelf.refresh_from_db()
|
||||||
|
self.assertEqual(shelf.books.first(), self.book)
|
|
@ -84,7 +84,9 @@ class GoodreadsImport(TestCase):
|
||||||
|
|
||||||
def test_handle_imported_book(self, *_):
|
def test_handle_imported_book(self, *_):
|
||||||
"""goodreads import added a book, this adds related connections"""
|
"""goodreads import added a book, this adds related connections"""
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.READ_FINISHED
|
||||||
|
).first()
|
||||||
self.assertIsNone(shelf.books.first())
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
|
|
|
@ -174,7 +174,9 @@ class GenericImporter(TestCase):
|
||||||
|
|
||||||
def test_handle_imported_book(self, *_):
|
def test_handle_imported_book(self, *_):
|
||||||
"""import added a book, this adds related connections"""
|
"""import added a book, this adds related connections"""
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.READ_FINISHED
|
||||||
|
).first()
|
||||||
self.assertIsNone(shelf.books.first())
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
|
@ -193,7 +195,9 @@ class GenericImporter(TestCase):
|
||||||
def test_handle_imported_book_already_shelved(self, *_):
|
def test_handle_imported_book_already_shelved(self, *_):
|
||||||
"""import added a book, this adds related connections"""
|
"""import added a book, this adds related connections"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.TO_READ
|
||||||
|
).first()
|
||||||
models.ShelfBook.objects.create(
|
models.ShelfBook.objects.create(
|
||||||
shelf=shelf,
|
shelf=shelf,
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
|
@ -217,12 +221,16 @@ class GenericImporter(TestCase):
|
||||||
shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2)
|
shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2)
|
||||||
)
|
)
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
self.local_user.shelf_set.get(identifier="read").books.first()
|
self.local_user.shelf_set.get(
|
||||||
|
identifier=models.Shelf.READ_FINISHED
|
||||||
|
).books.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_handle_import_twice(self, *_):
|
def test_handle_import_twice(self, *_):
|
||||||
"""re-importing books"""
|
"""re-importing books"""
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.READ_FINISHED
|
||||||
|
).first()
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
self.local_user, self.csv, False, "public"
|
self.local_user, self.csv, False, "public"
|
||||||
)
|
)
|
||||||
|
|
|
@ -93,7 +93,9 @@ class LibrarythingImport(TestCase):
|
||||||
|
|
||||||
def test_handle_imported_book(self, *_):
|
def test_handle_imported_book(self, *_):
|
||||||
"""librarything import added a book, this adds related connections"""
|
"""librarything import added a book, this adds related connections"""
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.READ_FINISHED
|
||||||
|
).first()
|
||||||
self.assertIsNone(shelf.books.first())
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
|
@ -117,7 +119,9 @@ class LibrarythingImport(TestCase):
|
||||||
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"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.TO_READ
|
||||||
|
).first()
|
||||||
models.ShelfBook.objects.create(
|
models.ShelfBook.objects.create(
|
||||||
shelf=shelf, user=self.local_user, book=self.book
|
shelf=shelf, user=self.local_user, book=self.book
|
||||||
)
|
)
|
||||||
|
@ -135,7 +139,9 @@ 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.assertIsNone(
|
||||||
self.local_user.shelf_set.get(identifier="read").books.first()
|
self.local_user.shelf_set.get(
|
||||||
|
identifier=models.Shelf.READ_FINISHED
|
||||||
|
).books.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
||||||
|
|
|
@ -70,7 +70,9 @@ class OpenLibraryImport(TestCase):
|
||||||
|
|
||||||
def test_handle_imported_book(self, *_):
|
def test_handle_imported_book(self, *_):
|
||||||
"""openlibrary import added a book, this adds related connections"""
|
"""openlibrary import added a book, this adds related connections"""
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="reading").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.READING
|
||||||
|
).first()
|
||||||
self.assertIsNone(shelf.books.first())
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
|
|
|
@ -62,7 +62,9 @@ class StorygraphImport(TestCase):
|
||||||
|
|
||||||
def test_handle_imported_book(self, *_):
|
def test_handle_imported_book(self, *_):
|
||||||
"""storygraph import added a book, this adds related connections"""
|
"""storygraph import added a book, this adds related connections"""
|
||||||
shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
|
shelf = self.local_user.shelf_set.filter(
|
||||||
|
identifier=models.Shelf.TO_READ
|
||||||
|
).first()
|
||||||
self.assertIsNone(shelf.books.first())
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -90,7 +90,6 @@ class InitDB(TestCase):
|
||||||
self.assertEqual(Group.objects.count(), 3)
|
self.assertEqual(Group.objects.count(), 3)
|
||||||
self.assertTrue(Permission.objects.exists())
|
self.assertTrue(Permission.objects.exists())
|
||||||
self.assertEqual(models.Connector.objects.count(), 3)
|
self.assertEqual(models.Connector.objects.count(), 3)
|
||||||
self.assertEqual(models.FederatedServer.objects.count(), 2)
|
|
||||||
self.assertEqual(models.SiteSettings.objects.count(), 1)
|
self.assertEqual(models.SiteSettings.objects.count(), 1)
|
||||||
self.assertEqual(models.LinkDomain.objects.count(), 5)
|
self.assertEqual(models.LinkDomain.objects.count(), 5)
|
||||||
|
|
||||||
|
@ -102,7 +101,6 @@ class InitDB(TestCase):
|
||||||
# everything should have been called
|
# everything should have been called
|
||||||
self.assertEqual(Group.objects.count(), 3)
|
self.assertEqual(Group.objects.count(), 3)
|
||||||
self.assertEqual(models.Connector.objects.count(), 0)
|
self.assertEqual(models.Connector.objects.count(), 0)
|
||||||
self.assertEqual(models.FederatedServer.objects.count(), 0)
|
|
||||||
self.assertEqual(models.SiteSettings.objects.count(), 0)
|
self.assertEqual(models.SiteSettings.objects.count(), 0)
|
||||||
self.assertEqual(models.LinkDomain.objects.count(), 0)
|
self.assertEqual(models.LinkDomain.objects.count(), 0)
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -38,7 +38,7 @@ class BaseModel(TestCase):
|
||||||
|
|
||||||
def test_remote_id(self):
|
def test_remote_id(self):
|
||||||
"""these should be generated"""
|
"""these should be generated"""
|
||||||
self.test_model.id = 1
|
self.test_model.id = 1 # pylint: disable=invalid-name
|
||||||
expected = self.test_model.get_remote_id()
|
expected = self.test_model.get_remote_id()
|
||||||
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
|
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
|
||||||
|
|
||||||
|
|
|
@ -162,6 +162,7 @@ class ModelFields(TestCase):
|
||||||
class TestActivity(ActivityObject):
|
class TestActivity(ActivityObject):
|
||||||
"""real simple mock"""
|
"""real simple mock"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
to: List[str]
|
to: List[str]
|
||||||
cc: List[str]
|
cc: List[str]
|
||||||
id: str = "http://hi.com"
|
id: str = "http://hi.com"
|
||||||
|
|
|
@ -462,6 +462,8 @@ class Status(TestCase):
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_ignore_activity_boost(self, *_):
|
def test_ignore_activity_boost(self, *_):
|
||||||
"""don't bother with most remote statuses"""
|
"""don't bother with most remote statuses"""
|
||||||
|
responses.add(responses.GET, "http://fish.com/nothing")
|
||||||
|
|
||||||
activity = activitypub.Announce(
|
activity = activitypub.Announce(
|
||||||
id="http://www.faraway.com/boost/12",
|
id="http://www.faraway.com/boost/12",
|
||||||
actor=self.remote_user.remote_id,
|
actor=self.remote_user.remote_id,
|
||||||
|
|
|
@ -17,7 +17,7 @@ class User(TestCase):
|
||||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||||
self.user = models.User.objects.create_user(
|
self.user = models.User.objects.create_user(
|
||||||
"mouse@%s" % DOMAIN,
|
f"mouse@{DOMAIN}",
|
||||||
"mouse@mouse.mouse",
|
"mouse@mouse.mouse",
|
||||||
"mouseword",
|
"mouseword",
|
||||||
local=True,
|
local=True,
|
||||||
|
@ -53,15 +53,17 @@ class User(TestCase):
|
||||||
|
|
||||||
def test_user_shelves(self):
|
def test_user_shelves(self):
|
||||||
shelves = models.Shelf.objects.filter(user=self.user).all()
|
shelves = models.Shelf.objects.filter(user=self.user).all()
|
||||||
self.assertEqual(len(shelves), 3)
|
self.assertEqual(len(shelves), 4)
|
||||||
names = [s.name for s in shelves]
|
names = [s.name for s in shelves]
|
||||||
self.assertTrue("To Read" in names)
|
self.assertTrue("To Read" in names)
|
||||||
self.assertTrue("Currently Reading" in names)
|
self.assertTrue("Currently Reading" in names)
|
||||||
self.assertTrue("Read" in names)
|
self.assertTrue("Read" in names)
|
||||||
|
self.assertTrue("Stopped Reading" in names)
|
||||||
ids = [s.identifier for s in shelves]
|
ids = [s.identifier for s in shelves]
|
||||||
self.assertTrue("to-read" in ids)
|
self.assertTrue("to-read" in ids)
|
||||||
self.assertTrue("reading" in ids)
|
self.assertTrue("reading" in ids)
|
||||||
self.assertTrue("read" in ids)
|
self.assertTrue("read" in ids)
|
||||||
|
self.assertTrue("stopped-reading" in ids)
|
||||||
|
|
||||||
def test_activitypub_serialize(self):
|
def test_activitypub_serialize(self):
|
||||||
activity = self.user.to_activity()
|
activity = self.user.to_activity()
|
||||||
|
@ -107,7 +109,7 @@ class User(TestCase):
|
||||||
def test_get_or_create_remote_server(self):
|
def test_get_or_create_remote_server(self):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.GET,
|
responses.GET,
|
||||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||||
)
|
)
|
||||||
responses.add(
|
responses.add(
|
||||||
|
@ -124,7 +126,7 @@ class User(TestCase):
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_get_or_create_remote_server_no_wellknown(self):
|
def test_get_or_create_remote_server_no_wellknown(self):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.GET, "https://%s/.well-known/nodeinfo" % DOMAIN, status=404
|
responses.GET, f"https://{DOMAIN}/.well-known/nodeinfo", status=404
|
||||||
)
|
)
|
||||||
|
|
||||||
server = models.user.get_or_create_remote_server(DOMAIN)
|
server = models.user.get_or_create_remote_server(DOMAIN)
|
||||||
|
@ -136,7 +138,7 @@ class User(TestCase):
|
||||||
def test_get_or_create_remote_server_no_links(self):
|
def test_get_or_create_remote_server_no_links(self):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.GET,
|
responses.GET,
|
||||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||||
)
|
)
|
||||||
responses.add(responses.GET, "http://www.example.com", status=404)
|
responses.add(responses.GET, "http://www.example.com", status=404)
|
||||||
|
@ -150,7 +152,7 @@ class User(TestCase):
|
||||||
def test_get_or_create_remote_server_unknown_format(self):
|
def test_get_or_create_remote_server_unknown_format(self):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.GET,
|
responses.GET,
|
||||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||||
)
|
)
|
||||||
responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"})
|
responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"})
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -64,8 +64,8 @@ class Signature(TestCase):
|
||||||
|
|
||||||
def send(self, signature, now, data, digest):
|
def send(self, signature, now, data, digest):
|
||||||
"""test request"""
|
"""test request"""
|
||||||
c = Client()
|
client = Client()
|
||||||
return c.post(
|
return client.post(
|
||||||
urlsplit(self.rat.inbox).path,
|
urlsplit(self.rat.inbox).path,
|
||||||
data=data,
|
data=data,
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import forms, models, views
|
from bookwyrm import forms, models, views
|
||||||
|
from bookwyrm.views.books.edit_book import add_authors
|
||||||
from bookwyrm.tests.validate_html import validate_html
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
from bookwyrm.tests.views.books.test_book import _setup_cover_url
|
from bookwyrm.tests.views.books.test_book import _setup_cover_url
|
||||||
|
|
||||||
|
@ -214,3 +215,22 @@ class EditBookViews(TestCase):
|
||||||
|
|
||||||
self.book.refresh_from_db()
|
self.book.refresh_from_db()
|
||||||
self.assertTrue(self.book.cover)
|
self.assertTrue(self.book.cover)
|
||||||
|
|
||||||
|
def test_add_authors_helper(self):
|
||||||
|
"""converts form input into author matches"""
|
||||||
|
form = forms.EditionForm(instance=self.book)
|
||||||
|
form.data["title"] = "New Title"
|
||||||
|
form.data["last_edited_by"] = self.local_user.id
|
||||||
|
form.data["add_author"] = ["Sappho", "Some Guy"]
|
||||||
|
request = self.factory.post("", form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch("bookwyrm.utils.isni.find_authors_by_name") as mock:
|
||||||
|
mock.return_value = []
|
||||||
|
result = add_authors(request, form.data)
|
||||||
|
|
||||||
|
self.assertTrue(result["confirm_mode"])
|
||||||
|
self.assertEqual(result["add_author"], ["Sappho", "Some Guy"])
|
||||||
|
self.assertEqual(len(result["author_matches"]), 2)
|
||||||
|
self.assertEqual(result["author_matches"][0]["name"], "Sappho")
|
||||||
|
self.assertEqual(result["author_matches"][1]["name"], "Some Guy")
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -61,7 +61,7 @@ class InboxActivities(TestCase):
|
||||||
self.assertEqual(models.Notification.objects.count(), 0)
|
self.assertEqual(models.Notification.objects.count(), 0)
|
||||||
activity = {
|
activity = {
|
||||||
"type": "Announce",
|
"type": "Announce",
|
||||||
"id": "%s/boost" % self.status.remote_id,
|
"id": f"{self.status.remote_id}/boost",
|
||||||
"actor": self.remote_user.remote_id,
|
"actor": self.remote_user.remote_id,
|
||||||
"object": self.status.remote_id,
|
"object": self.status.remote_id,
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||||
|
@ -94,7 +94,7 @@ class InboxActivities(TestCase):
|
||||||
self.assertEqual(models.Notification.objects.count(), 0)
|
self.assertEqual(models.Notification.objects.count(), 0)
|
||||||
activity = {
|
activity = {
|
||||||
"type": "Announce",
|
"type": "Announce",
|
||||||
"id": "%s/boost" % self.status.remote_id,
|
"id": f"{self.status.remote_id}/boost",
|
||||||
"actor": self.remote_user.remote_id,
|
"actor": self.remote_user.remote_id,
|
||||||
"object": "https://remote.com/status/1",
|
"object": "https://remote.com/status/1",
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||||
|
|
|
@ -208,16 +208,44 @@ class InboxCreate(TestCase):
|
||||||
self.assertEqual(book_list.description, "summary text")
|
self.assertEqual(book_list.description, "summary text")
|
||||||
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
|
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
|
||||||
|
|
||||||
def test_create_unsupported_type(self, *_):
|
def test_create_unsupported_type_question(self, *_):
|
||||||
"""ignore activities we know we can't handle"""
|
"""ignore activities we know we can't handle"""
|
||||||
activity = self.create_json
|
activity = self.create_json
|
||||||
activity["object"] = {
|
activity["object"] = {
|
||||||
"id": "https://example.com/status/887",
|
"id": "https://example.com/status/887",
|
||||||
"type": "Question",
|
"type": "Question",
|
||||||
}
|
}
|
||||||
# just observer how it doesn't throw an error
|
# just observe how it doesn't throw an error
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
|
|
||||||
|
def test_create_unsupported_type_article(self, *_):
|
||||||
|
"""special case in unsupported type because we do know what it is"""
|
||||||
|
activity = self.create_json
|
||||||
|
activity["object"] = {
|
||||||
|
"id": "https://example.com/status/887",
|
||||||
|
"type": "Article",
|
||||||
|
"name": "hello",
|
||||||
|
"published": "2021-04-29T21:27:30.014235+00:00",
|
||||||
|
"attributedTo": "https://example.com/user/mouse",
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc": ["https://example.com/user/mouse/followers"],
|
||||||
|
"sensitive": False,
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
}
|
||||||
|
# just observe how it doesn't throw an error
|
||||||
|
views.inbox.activity_task(activity)
|
||||||
|
|
||||||
|
def test_create_unsupported_type_unknown(self, *_):
|
||||||
|
"""Something truly unexpected should throw an error"""
|
||||||
|
activity = self.create_json
|
||||||
|
activity["object"] = {
|
||||||
|
"id": "https://example.com/status/887",
|
||||||
|
"type": "Blaaaah",
|
||||||
|
}
|
||||||
|
# error this time
|
||||||
|
with self.assertRaises(ActivitySerializerError):
|
||||||
|
views.inbox.activity_task(activity)
|
||||||
|
|
||||||
def test_create_unknown_type(self, *_):
|
def test_create_unknown_type(self, *_):
|
||||||
"""ignore activities we know we've never heard of"""
|
"""ignore activities we know we've never heard of"""
|
||||||
activity = self.create_json
|
activity = self.create_json
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from . import *
|
# pylint: disable=missing-module-docstring
|
||||||
|
from . import * # pylint: disable=import-self
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue