mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-03-13 23:12:48 +00:00
Compare commits
108 commits
Author | SHA1 | Date | |
---|---|---|---|
|
ba1f180c83 | ||
|
924f377e4e | ||
|
eef66b5fc4 | ||
|
4138a2b6aa | ||
|
03bab92ee6 | ||
|
d9d614b3bc | ||
|
609b7f58c8 | ||
|
3916666897 | ||
|
305ef9195b | ||
|
de8ba3cf86 | ||
|
023e62294e | ||
|
61845678c4 | ||
|
0a48c7c81b | ||
|
d16faac6da | ||
|
750cedd078 | ||
|
14dba48415 | ||
|
09bbaf0671 | ||
|
f2eb3fdccb | ||
|
20026b968d | ||
|
7ecf3b65d7 | ||
|
0a3fc58669 | ||
|
0ccbf8d739 | ||
|
8594c8c0f6 | ||
|
f1b0e4b9d6 | ||
|
1923db31b1 | ||
|
f8650bbb1c | ||
|
6143aa66e3 | ||
|
999829dc8b | ||
|
514c54f30a | ||
|
6745a9a3f5 | ||
|
bee9dafc9d | ||
|
dd45823790 | ||
|
a61ca162b0 | ||
|
cfe10fa874 | ||
|
13381b9b4d | ||
|
c0ce22a607 | ||
|
9c89e0fbaf | ||
|
1a93ba0cac | ||
|
ed55e052f6 | ||
|
cbdb59d3cf | ||
|
80248d3c1d | ||
|
904aa6c49a | ||
|
4123478058 | ||
|
3555ef9d2e | ||
|
23dfe3924d | ||
|
abb97e6044 | ||
|
a6912dc0c2 | ||
|
df607a0e45 | ||
|
bd773f41c7 | ||
|
9f5ca7ae60 | ||
|
698a0b113c | ||
|
c9155bdd78 | ||
|
23471f698c | ||
|
612098475a | ||
|
f53bb62b2b | ||
|
142ed70d4d | ||
|
413c26bc5e | ||
|
7ff1ab974e | ||
|
cf61279e18 | ||
|
6ec3783736 | ||
|
cf753afaa9 | ||
|
f68e33ffc6 | ||
|
2bb77d9bf8 | ||
|
7fc54c509c | ||
|
95c2798fc7 | ||
|
66dd39e6d8 | ||
|
96a2fa5afc | ||
|
9e73ab048b | ||
|
fc33edb749 | ||
|
b45435803a | ||
|
69962bb7c9 | ||
|
8aba8caae9 | ||
|
3d28019146 | ||
|
f4133e0236 | ||
|
041f2fc413 | ||
|
812221456b | ||
|
43577f3ca0 | ||
|
7f773b3dbd | ||
|
7311526f2e | ||
|
355a6071ff | ||
|
e41b27e5f4 | ||
|
eef055159e | ||
|
08876512eb | ||
|
afd44e109c | ||
|
f0e5c334e7 | ||
|
300eeac0e1 | ||
|
dc54f28e17 | ||
|
8ce29849a9 | ||
|
0b87aacfce | ||
|
81ee5e945f | ||
|
1a0fbac76c | ||
|
2cdbddca09 | ||
|
1608ca6401 | ||
|
93c6b76dab | ||
|
94dfbbcc05 | ||
|
f6eb4f4f27 | ||
|
acc68147dc | ||
|
ab307388f4 | ||
|
8a235bcda1 | ||
|
06d6360082 | ||
|
e5b260e3ee | ||
|
3236003107 | ||
|
1a2f434514 | ||
|
1d4119e853 | ||
|
261e794c1c | ||
|
2e675474a9 | ||
|
3f08d6d8c4 | ||
|
539a9fa212 |
153 changed files with 9768 additions and 4977 deletions
58
.github/pull_request_template.md
vendored
58
.github/pull_request_template.md
vendored
|
@ -1,10 +1,5 @@
|
|||
<!--
|
||||
Thanks for contributing!
|
||||
|
||||
Please ensure the name of your PR is written in imperative present tense. For example:
|
||||
|
||||
- "fix color contrast on submit buttons"
|
||||
- "add 'favourite food' value to Author model"
|
||||
Thanks for contributing! This template has some checkboxes that help keep track of what changes go into a release.
|
||||
|
||||
To check (tick) a list item, replace the space between square brackets with an x, like this:
|
||||
|
||||
|
@ -12,24 +7,23 @@ To check (tick) a list item, replace the space between square brackets with an x
|
|||
|
||||
You can find more information and tips for BookWyrm contributors at https://docs.joinbookwyrm.com/contributing.html
|
||||
-->
|
||||
|
||||
## Are you finished?
|
||||
|
||||
### Linters
|
||||
## Description
|
||||
<!--
|
||||
Please run linters on your code before submitting your PR.
|
||||
If you miss this step it is likely that the GitHub task runners will fail.
|
||||
Describe what your pull request does here
|
||||
-->
|
||||
|
||||
- [ ] I have checked my code with `black`, `pylint`, and `mypy`, or `./bw-dev formatters`
|
||||
|
||||
### Tests
|
||||
<!-- Check one -->
|
||||
<!--
|
||||
For pull requests that relate or close an issue, please include them
|
||||
below. We like to follow [Github's guidance on linking issues to pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||
|
||||
- [ ] My changes do not need new tests
|
||||
- [ ] All tests I have added are passing
|
||||
- [ ] I have written tests but need help to make them pass
|
||||
- [ ] I have not written tests and need help to write them
|
||||
For example having the text: "closes #1234" would connect the current pull
|
||||
request to issue 1234. And when we merge the pull request, Github will
|
||||
automatically close the issue.
|
||||
-->
|
||||
|
||||
- Related Issue #
|
||||
- Closes #
|
||||
|
||||
## What type of Pull Request is this?
|
||||
<!-- Check all that apply -->
|
||||
|
@ -48,21 +42,6 @@ If you miss this step it is likely that the GitHub task runners will fail.
|
|||
|
||||
### Details of breaking or configuration changes (if any of above checked)
|
||||
|
||||
## Description
|
||||
|
||||
<!--
|
||||
Describe what your pull request does here.
|
||||
|
||||
For pull requests that relate or close an issue, please include them
|
||||
below. We like to follow [Github's guidance on linking issues to pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||
|
||||
For example having the text: "closes #1234" would connect the current pull
|
||||
request to issue 1234. And when we merge the pull request, Github will
|
||||
automatically close the issue.
|
||||
-->
|
||||
|
||||
- Related Issue #
|
||||
- Closes #
|
||||
|
||||
## Documentation
|
||||
<!--
|
||||
|
@ -76,3 +55,14 @@ Our documentation is maintained in a separate repository at https://github.com/b
|
|||
- [ ] I have created a matching pull request in the Documentation repository
|
||||
- [ ] I intend to create a matching pull request in the Documentation repository after this PR is merged
|
||||
|
||||
<!-- Amazing! Thanks for filling that out. Your PR will need to have passing tests and happy linters before we can merge
|
||||
You will need to check your code with `black`, `pylint`, and `mypy`, or `./bw-dev formatters`
|
||||
-->
|
||||
|
||||
### Tests
|
||||
<!-- Check one -->
|
||||
|
||||
- [ ] My changes do not need new tests
|
||||
- [ ] All tests I have added are passing
|
||||
- [ ] I have written tests but need help to make them pass
|
||||
- [ ] I have not written tests and need help to write them
|
||||
|
|
2
.github/workflows/lint-frontend.yaml
vendored
2
.github/workflows/lint-frontend.yaml
vendored
|
@ -15,7 +15,7 @@ on:
|
|||
jobs:
|
||||
lint:
|
||||
name: Lint with stylelint and ESLint.
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
|
||||
|
|
2
.github/workflows/prettier.yaml
vendored
2
.github/workflows/prettier.yaml
vendored
|
@ -10,7 +10,7 @@ on:
|
|||
jobs:
|
||||
lint:
|
||||
name: Lint with Prettier
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
|
||||
|
|
2
.github/workflows/python.yml
vendored
2
.github/workflows/python.yml
vendored
|
@ -48,7 +48,7 @@ jobs:
|
|||
- name: Set up .env
|
||||
run: cp .env.example .env
|
||||
- name: Check migrations up-to-date
|
||||
run: python ./manage.py makemigrations --check
|
||||
run: python ./manage.py makemigrations --check -v 3
|
||||
- name: Run Tests
|
||||
run: pytest -n 3
|
||||
|
||||
|
|
14
.pylintrc
14
.pylintrc
|
@ -3,7 +3,19 @@ ignore=migrations
|
|||
load-plugins=pylint.extensions.no_self_use
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801,C3001,import-error
|
||||
disable =
|
||||
cyclic-import,
|
||||
duplicate-code,
|
||||
fixme,
|
||||
no-member,
|
||||
raise-missing-from,
|
||||
too-few-public-methods,
|
||||
too-many-ancestors,
|
||||
too-many-instance-attributes,
|
||||
unnecessary-lambda-assignment,
|
||||
unsubscriptable-object,
|
||||
enable =
|
||||
useless-suppression
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=88
|
||||
|
|
|
@ -369,17 +369,13 @@ def resolve_remote_id(
|
|||
|
||||
# load the data and create the object
|
||||
try:
|
||||
data = get_data(remote_id)
|
||||
data = get_activitypub_data(remote_id)
|
||||
except ConnectionError:
|
||||
logger.info("Could not connect to host for remote_id: %s", remote_id)
|
||||
return None
|
||||
except requests.HTTPError as e:
|
||||
if (e.response is not None) and e.response.status_code == 401:
|
||||
# This most likely means it's a mastodon with secure fetch enabled.
|
||||
data = get_activitypub_data(remote_id)
|
||||
else:
|
||||
logger.info("Could not connect to host for remote_id: %s", remote_id)
|
||||
return None
|
||||
logger.exception("HTTP error - remote_id: %s - error: %s", remote_id, e)
|
||||
return None
|
||||
# determine the model implicitly, if not provided
|
||||
# or if it's a model with subclasses like Status, check again
|
||||
if not model or hasattr(model.objects, "select_subclasses"):
|
||||
|
|
|
@ -67,7 +67,6 @@ class Edition(Book):
|
|||
type: str = "Edition"
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
class Work(Book):
|
||||
"""work instance of a book object"""
|
||||
|
|
|
@ -18,7 +18,6 @@ class OrderedCollection(ActivityObject):
|
|||
type: str = "OrderedCollection"
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
class OrderedCollectionPrivate(OrderedCollection):
|
||||
"""an ordered collection with privacy settings"""
|
||||
|
|
|
@ -22,7 +22,6 @@ class Verb(ActivityObject):
|
|||
self.object.to_model(allow_external_connections=allow_external_connections)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
class Create(Verb):
|
||||
"""Create activity"""
|
||||
|
@ -33,7 +32,6 @@ class Create(Verb):
|
|||
type: str = "Create"
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
class Delete(Verb):
|
||||
"""Create activity"""
|
||||
|
@ -63,7 +61,6 @@ class Delete(Verb):
|
|||
# if we can't find it, we don't need to delete it because we don't have it
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
class Update(Verb):
|
||||
"""Update activity"""
|
||||
|
@ -227,7 +224,6 @@ class Like(Verb):
|
|||
self.to_model(allow_external_connections=allow_external_connections)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
class Announce(Verb):
|
||||
"""boosting a status"""
|
||||
|
|
|
@ -32,7 +32,7 @@ class ActivityStream(RedisStore):
|
|||
stream_id = self.stream_id(user_id)
|
||||
return f"{stream_id}-unread-by-type"
|
||||
|
||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||
def get_rank(self, obj):
|
||||
"""statuses are sorted by date published"""
|
||||
return obj.published_date.timestamp()
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ class BookwyrmConfig(AppConfig):
|
|||
name = "bookwyrm"
|
||||
verbose_name = "BookWyrm"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def ready(self):
|
||||
"""set up OTLP and preview image files, if desired"""
|
||||
if settings.OTEL_EXPORTER_OTLP_ENDPOINT or settings.OTEL_EXPORTER_CONSOLE:
|
||||
|
|
|
@ -36,7 +36,6 @@ def search(
|
|||
...
|
||||
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def search(
|
||||
query: str,
|
||||
*,
|
||||
|
|
|
@ -86,7 +86,7 @@ class AbstractMinimalConnector(ABC):
|
|||
),
|
||||
"User-Agent": USER_AGENT,
|
||||
}
|
||||
params = {"min_confidence": min_confidence}
|
||||
params = {"min_confidence": str(min_confidence)}
|
||||
try:
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
if not response.ok:
|
||||
|
|
|
@ -222,9 +222,10 @@ class Connector(AbstractConnector):
|
|||
def get_description(self, links: JsonDict) -> str:
|
||||
"""grab an extracted excerpt from wikipedia"""
|
||||
link = links.get("enwiki")
|
||||
if not link:
|
||||
if not link or not link.get("title"):
|
||||
return ""
|
||||
url = f"{self.base_url}/api/data?action=wp-extract&lang=en&title={link}"
|
||||
title = link.get("title")
|
||||
url = f"{self.base_url}/api/data?action=wp-extract&lang=en&title={title}"
|
||||
try:
|
||||
data = get_data(url)
|
||||
except ConnectorException:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from bookwyrm import models, settings
|
||||
|
||||
|
||||
def site_settings(request): # pylint: disable=unused-argument
|
||||
def site_settings(request):
|
||||
"""include the custom info about the site"""
|
||||
request_protocol = "https://"
|
||||
if not request.is_secure():
|
||||
|
|
|
@ -15,9 +15,9 @@ class StyledForm(ModelForm):
|
|||
css_classes["number"] = "input"
|
||||
css_classes["checkbox"] = "checkbox"
|
||||
css_classes["textarea"] = "textarea"
|
||||
# pylint: disable=super-with-arguments
|
||||
super().__init__(*args, **kwargs)
|
||||
for visible in self.visible_fields():
|
||||
input_type = ""
|
||||
if hasattr(visible.field.widget, "input_type"):
|
||||
input_type = visible.field.widget.input_type
|
||||
if isinstance(visible.field.widget, Textarea):
|
||||
|
|
|
@ -18,6 +18,7 @@ class EditUserForm(CustomForm):
|
|||
"email",
|
||||
"summary",
|
||||
"show_goal",
|
||||
"show_ratings",
|
||||
"show_suggested_users",
|
||||
"manually_approves_followers",
|
||||
"default_post_privacy",
|
||||
|
|
|
@ -34,7 +34,6 @@ class LoginForm(CustomForm):
|
|||
|
||||
def add_invalid_password_error(self):
|
||||
"""We don't want to be too specific about this"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.non_field_errors = _("Username or password are incorrect")
|
||||
|
||||
|
||||
|
@ -65,6 +64,10 @@ class InviteRequestForm(CustomForm):
|
|||
if email and models.User.objects.filter(email=email).exists():
|
||||
self.add_error("email", _("A user with this email already exists."))
|
||||
|
||||
email_domain = email.split("@")[-1]
|
||||
if email and models.EmailBlocklist.objects.filter(domain=email_domain).exists():
|
||||
self.add_error("email", _("This email address cannot be registered."))
|
||||
|
||||
class Meta:
|
||||
model = models.InviteRequest
|
||||
fields = ["email", "answer"]
|
||||
|
|
|
@ -5,8 +5,6 @@ from django import forms
|
|||
class ArrayWidget(forms.widgets.TextInput):
|
||||
"""Inputs for postgres array fields"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=no-self-use
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""get all values for this name"""
|
||||
return [i for i in data.getlist(name) if i]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
""" import classes """
|
||||
|
||||
from .importer import Importer
|
||||
from .bookwyrm_import import BookwyrmImporter
|
||||
from .bookwyrm_import import BookwyrmImporter, BookwyrmBooksImporter
|
||||
from .calibre_import import CalibreImporter
|
||||
from .goodreads_import import GoodreadsImporter
|
||||
from .librarything_import import LibrarythingImporter
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.http import QueryDict
|
|||
|
||||
from bookwyrm.models import User
|
||||
from bookwyrm.models.bookwyrm_import_job import BookwyrmImportJob
|
||||
from . import Importer
|
||||
|
||||
|
||||
class BookwyrmImporter:
|
||||
|
@ -22,3 +23,17 @@ class BookwyrmImporter:
|
|||
user=user, archive_file=archive_file, required=required
|
||||
)
|
||||
return job
|
||||
|
||||
|
||||
class BookwyrmBooksImporter(Importer):
|
||||
"""
|
||||
Handle reading a csv from BookWyrm.
|
||||
Goodreads is the default importer, we basically just use the same structure
|
||||
But BookWyrm has additional attributes in the csv
|
||||
"""
|
||||
|
||||
service = "BookWyrm"
|
||||
row_mappings_guesses = Importer.row_mappings_guesses + [
|
||||
("shelf_name", ["shelf_name"]),
|
||||
("review_published", ["review_published"]),
|
||||
]
|
||||
|
|
|
@ -18,17 +18,26 @@ class Importer:
|
|||
row_mappings_guesses = [
|
||||
("id", ["id", "book id"]),
|
||||
("title", ["title"]),
|
||||
("authors", ["author", "authors", "primary author"]),
|
||||
("isbn_10", ["isbn10", "isbn", "isbn/uid"]),
|
||||
("isbn_13", ["isbn13", "isbn", "isbns", "isbn/uid"]),
|
||||
("authors", ["author_text", "author", "authors", "primary author"]),
|
||||
("isbn_10", ["isbn_10", "isbn10", "isbn", "isbn/uid"]),
|
||||
("isbn_13", ["isbn_13", "isbn13", "isbn", "isbns", "isbn/uid"]),
|
||||
("shelf", ["shelf", "exclusive shelf", "read status", "bookshelf"]),
|
||||
("review_name", ["review name"]),
|
||||
("review_body", ["my review", "review"]),
|
||||
("review_name", ["review_name", "review name"]),
|
||||
("review_body", ["review_content", "my review", "review"]),
|
||||
("rating", ["my rating", "rating", "star rating"]),
|
||||
("date_added", ["date added", "entry date", "added"]),
|
||||
("date_started", ["date started", "started"]),
|
||||
("date_finished", ["date finished", "last date read", "date read", "finished"]),
|
||||
(
|
||||
"date_added",
|
||||
["shelf_date", "date_added", "date added", "entry date", "added"],
|
||||
),
|
||||
("date_started", ["start_date", "date started", "started"]),
|
||||
(
|
||||
"date_finished",
|
||||
["finish_date", "date finished", "last date read", "date read", "finished"],
|
||||
),
|
||||
]
|
||||
|
||||
# TODO: stopped
|
||||
|
||||
date_fields = ["date_added", "date_started", "date_finished"]
|
||||
shelf_mapping_guesses = {
|
||||
"to-read": ["to-read", "want to read"],
|
||||
|
@ -36,9 +45,14 @@ class Importer:
|
|||
"reading": ["currently-reading", "reading", "currently reading"],
|
||||
}
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-arguments
|
||||
def create_job(
|
||||
self, user: User, csv_file: Iterable[str], include_reviews: bool, privacy: str
|
||||
self,
|
||||
user: User,
|
||||
csv_file: Iterable[str],
|
||||
include_reviews: bool,
|
||||
privacy: str,
|
||||
create_shelves: bool = True,
|
||||
) -> ImportJob:
|
||||
"""check over a csv and creates a database entry for the job"""
|
||||
csv_reader = csv.DictReader(csv_file, delimiter=self.delimiter)
|
||||
|
@ -55,6 +69,7 @@ class Importer:
|
|||
job = ImportJob.objects.create(
|
||||
user=user,
|
||||
include_reviews=include_reviews,
|
||||
create_shelves=create_shelves,
|
||||
privacy=privacy,
|
||||
mappings=mappings,
|
||||
source=self.service,
|
||||
|
@ -114,7 +129,7 @@ class Importer:
|
|||
shelf = [
|
||||
s for (s, gs) in self.shelf_mapping_guesses.items() if shelf_name in gs
|
||||
]
|
||||
return shelf[0] if shelf else None
|
||||
return shelf[0] if shelf else normalized_row.get("shelf") or None
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def normalize_row(
|
||||
|
@ -149,6 +164,7 @@ class Importer:
|
|||
job = ImportJob.objects.create(
|
||||
user=user,
|
||||
include_reviews=original_job.include_reviews,
|
||||
create_shelves=original_job.create_shelves,
|
||||
privacy=original_job.privacy,
|
||||
source=original_job.source,
|
||||
# TODO: allow users to adjust mappings
|
||||
|
|
|
@ -20,7 +20,7 @@ class LibrarythingImporter(Importer):
|
|||
|
||||
def normalize_row(
|
||||
self, entry: dict[str, str], mappings: dict[str, Optional[str]]
|
||||
) -> dict[str, Optional[str]]: # pylint: disable=no-self-use
|
||||
) -> dict[str, Optional[str]]:
|
||||
"""use the dataclass to create the formatted row of data"""
|
||||
normalized = {
|
||||
k: _remove_brackets(entry.get(v) if v else None)
|
||||
|
|
|
@ -18,7 +18,7 @@ class ListsStream(RedisStore):
|
|||
return f"{user}-lists"
|
||||
return f"{user.id}-lists"
|
||||
|
||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||
def get_rank(self, obj):
|
||||
"""lists are sorted by updated date"""
|
||||
return obj.updated_date.timestamp()
|
||||
|
||||
|
|
18
bookwyrm/migrations/0189_importjob_create_shelves.py
Normal file
18
bookwyrm/migrations/0189_importjob_create_shelves.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.23 on 2023-11-25 05:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0188_theme_loads"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="importjob",
|
||||
name="create_shelves",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
13
bookwyrm/migrations/0207_merge_20240629_0626.py
Normal file
13
bookwyrm/migrations/0207_merge_20240629_0626.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 4.2.11 on 2024-06-29 06:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0189_importjob_create_shelves"),
|
||||
("bookwyrm", "0206_merge_20240415_1537"),
|
||||
]
|
||||
|
||||
operations = []
|
51
bookwyrm/migrations/0207_sqlparse_update.py
Normal file
51
bookwyrm/migrations/0207_sqlparse_update.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-27 18:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0206_merge_20240415_1537"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="author",
|
||||
name="reset_book_search_vector_on_author_edit",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="book",
|
||||
name="update_search_vector_on_book_edit",
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="author",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="reset_book_search_vector_on_author_edit",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="WITH updated_books AS (SELECT book_id FROM bookwyrm_book_authors WHERE author_id = new.id) UPDATE bookwyrm_book SET search_vector = '' FROM updated_books WHERE id = updated_books.book_id;RETURN NEW;",
|
||||
hash="4eeb17d1c9c53f543615bcae1234bd0260adefcc",
|
||||
operation='UPDATE OF "name", "aliases"',
|
||||
pgid="pgtrigger_reset_book_search_vector_on_author_edit_a50c7",
|
||||
table="bookwyrm_author",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="book",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_search_vector_on_book_edit",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="WITH author_names AS (SELECT array_to_string(bookwyrm_author.name || bookwyrm_author.aliases, ' ') AS name_and_aliases FROM bookwyrm_author LEFT JOIN bookwyrm_book_authors ON bookwyrm_author.id = bookwyrm_book_authors.author_id WHERE bookwyrm_book_authors.book_id = new.id) SELECT setweight(coalesce(nullif(to_tsvector('english', new.title), ''), to_tsvector('simple', new.title)), 'A') || setweight(to_tsvector('english', coalesce(new.subtitle, '')), 'B') || (SELECT setweight(to_tsvector('simple', coalesce(array_to_string(array_agg(name_and_aliases), ' '), '')), 'C') FROM author_names) || setweight(to_tsvector('english', coalesce(new.series, '')), 'D') INTO new.search_vector;RETURN NEW;",
|
||||
hash="676d929ce95beff671544b6add09cf9360b6f299",
|
||||
operation='INSERT OR UPDATE OF "title", "subtitle", "series", "search_vector"',
|
||||
pgid="pgtrigger_update_search_vector_on_book_edit_bec58",
|
||||
table="bookwyrm_book",
|
||||
when="BEFORE",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-28 11:07
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0207_merge_20240629_0626"),
|
||||
("bookwyrm", "0207_sqlparse_update"),
|
||||
]
|
||||
|
||||
operations = []
|
18
bookwyrm/migrations/0209_user_show_ratings.py
Normal file
18
bookwyrm/migrations/0209_user_show_ratings.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.15 on 2024-08-24 01:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0208_merge_0207_merge_20240629_0626_0207_sqlparse_update"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="show_ratings",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -31,7 +31,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def set_activity_from_property_field(activity, obj, field):
|
||||
"""assign a model property value to the activity json"""
|
||||
activity[field[1]] = getattr(obj, field[0])
|
||||
|
|
|
@ -133,7 +133,6 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
|||
related_models = [
|
||||
(r.remote_field.name, r.related_model) for r in self._meta.related_objects
|
||||
]
|
||||
# pylint: disable=protected-access
|
||||
for related_field, related_model in related_models:
|
||||
# Skip the ManyToMany fields that aren’t auto-created. These
|
||||
# should have a corresponding OneToMany field in the model for
|
||||
|
|
|
@ -7,7 +7,6 @@ from boto3.session import Session as BotoSession
|
|||
from s3_tar import S3Tar
|
||||
|
||||
from django.db.models import BooleanField, FileField, JSONField
|
||||
from django.db.models import Q
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import storages
|
||||
|
@ -28,7 +27,7 @@ logger = logging.getLogger(__name__)
|
|||
class BookwyrmAwsSession(BotoSession):
|
||||
"""a boto session that always uses settings.AWS_S3_ENDPOINT_URL"""
|
||||
|
||||
def client(self, *args, **kwargs): # pylint: disable=arguments-differ
|
||||
def client(self, *args, **kwargs):
|
||||
kwargs["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
|
||||
return super().client("s3", *args, **kwargs)
|
||||
|
||||
|
@ -315,19 +314,28 @@ def export_book(user: User, edition: Edition):
|
|||
|
||||
|
||||
def get_books_for_user(user):
|
||||
"""Get all the books and editions related to a user"""
|
||||
"""
|
||||
Get all the books and editions related to a user.
|
||||
|
||||
editions = (
|
||||
Edition.objects.select_related("parent_work")
|
||||
.filter(
|
||||
Q(shelves__user=user)
|
||||
| Q(readthrough__user=user)
|
||||
| Q(review__user=user)
|
||||
| Q(list__user=user)
|
||||
| Q(comment__user=user)
|
||||
| Q(quotation__user=user)
|
||||
)
|
||||
.distinct()
|
||||
We use union() instead of Q objects because it creates
|
||||
multiple simple queries in stead of a much more complex DB query
|
||||
that can time out.
|
||||
|
||||
"""
|
||||
|
||||
shelf_eds = Edition.objects.select_related("parent_work").filter(shelves__user=user)
|
||||
rt_eds = Edition.objects.select_related("parent_work").filter(
|
||||
readthrough__user=user
|
||||
)
|
||||
review_eds = Edition.objects.select_related("parent_work").filter(review__user=user)
|
||||
list_eds = Edition.objects.select_related("parent_work").filter(list__user=user)
|
||||
comment_eds = Edition.objects.select_related("parent_work").filter(
|
||||
comment__user=user
|
||||
)
|
||||
quote_eds = Edition.objects.select_related("parent_work").filter(
|
||||
quotation__user=user
|
||||
)
|
||||
|
||||
editions = shelf_eds.union(rt_eds, review_eds, list_eds, comment_eds, quote_eds)
|
||||
|
||||
return editions
|
||||
|
|
|
@ -193,8 +193,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
|
|||
|
||||
def __init__(self, activitypub_field="preferredUsername", **kwargs):
|
||||
self.activitypub_field = activitypub_field
|
||||
# I don't totally know why pylint is mad at this, but it makes it work
|
||||
super(ActivitypubFieldMixin, self).__init__( # pylint: disable=bad-super-call
|
||||
super(ActivitypubFieldMixin, self).__init__(
|
||||
_("username"),
|
||||
max_length=150,
|
||||
unique=True,
|
||||
|
@ -234,7 +233,6 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, max_length=255, choices=PrivacyLevels, default="public")
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def set_field_from_activity(
|
||||
self, instance, data, overwrite=True, allow_external_connections=True
|
||||
):
|
||||
|
@ -276,7 +274,6 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
|||
if hasattr(instance, "mention_users"):
|
||||
mentions = [u.remote_id for u in instance.mention_users.all()]
|
||||
# this is a link to the followers list
|
||||
# pylint: disable=protected-access
|
||||
followers = instance.user.followers_url
|
||||
if instance.privacy == "public":
|
||||
activity["to"] = [self.public]
|
||||
|
@ -444,7 +441,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
|||
self.alt_field = alt_field
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# pylint: disable=arguments-differ,arguments-renamed,too-many-arguments
|
||||
# pylint: disable=arguments-renamed,too-many-arguments
|
||||
def set_field_from_activity(
|
||||
self, instance, data, save=True, overwrite=True, allow_external_connections=True
|
||||
):
|
||||
|
|
|
@ -4,6 +4,7 @@ import math
|
|||
import re
|
||||
import dateutil.parser
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -59,6 +60,7 @@ class ImportJob(models.Model):
|
|||
created_date = models.DateTimeField(default=timezone.now)
|
||||
updated_date = models.DateTimeField(default=timezone.now)
|
||||
include_reviews: bool = models.BooleanField(default=True)
|
||||
create_shelves: bool = models.BooleanField(default=True)
|
||||
mappings = models.JSONField()
|
||||
source = models.CharField(max_length=100)
|
||||
privacy = models.CharField(max_length=255, default="public", choices=PrivacyLevels)
|
||||
|
@ -245,11 +247,26 @@ class ImportItem(models.Model):
|
|||
"""the goodreads shelf field"""
|
||||
return self.normalized_data.get("shelf")
|
||||
|
||||
@property
|
||||
def shelf_name(self):
|
||||
"""the goodreads shelf field"""
|
||||
return self.normalized_data.get("shelf_name")
|
||||
|
||||
@property
|
||||
def review(self):
|
||||
"""a user-written review, to be imported with the book data"""
|
||||
return self.normalized_data.get("review_body")
|
||||
|
||||
@property
|
||||
def review_name(self):
|
||||
"""a user-written review name, to be imported with the book data"""
|
||||
return self.normalized_data.get("review_name")
|
||||
|
||||
@property
|
||||
def review_published(self):
|
||||
"""date the review was published - included in BookWyrm export csv"""
|
||||
return self.normalized_data.get("review_published", None)
|
||||
|
||||
@property
|
||||
def rating(self):
|
||||
"""x/5 star rating for a book"""
|
||||
|
@ -352,7 +369,7 @@ def import_item_task(item_id):
|
|||
|
||||
try:
|
||||
item.resolve()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
item.fail_reason = _("Error loading book")
|
||||
item.save()
|
||||
item.update_job()
|
||||
|
@ -368,7 +385,7 @@ def import_item_task(item_id):
|
|||
item.update_job()
|
||||
|
||||
|
||||
def handle_imported_book(item):
|
||||
def handle_imported_book(item): # pylint: disable=too-many-branches
|
||||
"""process a csv and then post about it"""
|
||||
job = item.job
|
||||
if job.complete:
|
||||
|
@ -385,13 +402,31 @@ def handle_imported_book(item):
|
|||
item.book = item.book.edition
|
||||
|
||||
existing_shelf = ShelfBook.objects.filter(book=item.book, user=user).exists()
|
||||
if job.create_shelves and item.shelf and not existing_shelf:
|
||||
# shelve the book if it hasn't been shelved already
|
||||
|
||||
# shelve the book if it hasn't been shelved already
|
||||
if item.shelf and not existing_shelf:
|
||||
desired_shelf = Shelf.objects.get(identifier=item.shelf, user=user)
|
||||
shelved_date = item.date_added or timezone.now()
|
||||
shelfname = getattr(item, "shelf_name", item.shelf)
|
||||
|
||||
try:
|
||||
shelf = Shelf.objects.get(name=shelfname, user=user)
|
||||
except ObjectDoesNotExist:
|
||||
try:
|
||||
shelf = Shelf.objects.get(identifier=item.shelf, user=user)
|
||||
except ObjectDoesNotExist:
|
||||
|
||||
shelf = Shelf.objects.create(
|
||||
user=user,
|
||||
identifier=item.shelf,
|
||||
name=shelfname,
|
||||
privacy=job.privacy,
|
||||
)
|
||||
|
||||
ShelfBook(
|
||||
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
|
||||
book=item.book,
|
||||
shelf=shelf,
|
||||
user=user,
|
||||
shelved_date=shelved_date,
|
||||
).save(priority=IMPORT_TRIGGERED)
|
||||
|
||||
for read in item.reads:
|
||||
|
@ -408,19 +443,25 @@ def handle_imported_book(item):
|
|||
read.save()
|
||||
|
||||
if job.include_reviews and (item.rating or item.review) and not item.linked_review:
|
||||
# we don't know the publication date of the review,
|
||||
# but "now" is a bad guess
|
||||
published_date_guess = item.date_read or item.date_added
|
||||
# we don't necessarily know the publication date of the review,
|
||||
# but "now" is a bad guess unless we have no choice
|
||||
|
||||
published_date_guess = (
|
||||
item.review_published or item.date_read or item.date_added or timezone.now()
|
||||
)
|
||||
if item.review:
|
||||
|
||||
# pylint: disable=consider-using-f-string
|
||||
review_title = "Review of {!r} on {!r}".format(
|
||||
item.book.title,
|
||||
job.source,
|
||||
)
|
||||
review_name = getattr(item, "review_name", review_title)
|
||||
|
||||
review = Review.objects.filter(
|
||||
user=user,
|
||||
book=item.book,
|
||||
name=review_title,
|
||||
name=review_name,
|
||||
rating=item.rating,
|
||||
published_date=published_date_guess,
|
||||
).first()
|
||||
|
@ -428,7 +469,7 @@ def handle_imported_book(item):
|
|||
review = Review(
|
||||
user=user,
|
||||
book=item.book,
|
||||
name=review_title,
|
||||
name=review_name,
|
||||
content=item.review,
|
||||
rating=item.rating,
|
||||
published_date=published_date_guess,
|
||||
|
|
|
@ -135,7 +135,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
status = "follow_request"
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
def save(self, *args, broadcast=True, **kwargs): # pylint: disable=arguments-differ
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
"""make sure the follow or block relationship doesn't already exist"""
|
||||
# if there's a request for a follow that already exists, accept it
|
||||
# without changing the local database state
|
||||
|
|
|
@ -98,7 +98,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
self.thread_id = self.id
|
||||
super().save(broadcast=False, update_fields=["thread_id"])
|
||||
|
||||
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
def delete(self, *args, **kwargs):
|
||||
""" "delete" a status"""
|
||||
if hasattr(self, "boosted_status"):
|
||||
# okay but if it's a boost really delete it
|
||||
|
@ -213,7 +213,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
**kwargs,
|
||||
).serialize()
|
||||
|
||||
def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ
|
||||
def to_activity_dataclass(self, pure=False):
|
||||
"""return tombstone if the status is deleted"""
|
||||
if self.deleted:
|
||||
return activitypub.Tombstone(
|
||||
|
|
|
@ -141,7 +141,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
hide_follows = fields.BooleanField(default=False)
|
||||
|
||||
# migration fields
|
||||
|
||||
moved_to = fields.RemoteIdField(
|
||||
null=True, unique=False, activitypub_field="movedTo", deduplication_field=False
|
||||
)
|
||||
|
@ -158,6 +157,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
show_suggested_users = models.BooleanField(default=True)
|
||||
discoverable = fields.BooleanField(default=False)
|
||||
show_guided_tour = models.BooleanField(default=True)
|
||||
show_ratings = models.BooleanField(default=True)
|
||||
|
||||
# feed options
|
||||
feed_status_types = DjangoArrayField(
|
||||
|
@ -409,7 +409,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""We don't actually delete the database entry"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.is_active = False
|
||||
self.allow_reactivation = False
|
||||
self.is_deleted = True
|
||||
|
@ -452,7 +451,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
|
||||
def deactivate(self):
|
||||
"""Disable the user but allow them to reactivate"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.is_active = False
|
||||
self.deactivation_reason = "self_deactivation"
|
||||
self.allow_reactivation = True
|
||||
|
@ -460,7 +458,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
|
||||
def reactivate(self):
|
||||
"""Now you want to come back, huh?"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
if not self.allow_reactivation:
|
||||
return
|
||||
self.is_active = True
|
||||
|
|
|
@ -420,7 +420,6 @@ def save_and_cleanup(image, instance=None):
|
|||
return True
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@app.task(queue=IMAGES)
|
||||
def generate_site_preview_image_task():
|
||||
"""generate preview_image for the website"""
|
||||
|
@ -445,7 +444,6 @@ def generate_site_preview_image_task():
|
|||
save_and_cleanup(image, instance=site)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@app.task(queue=IMAGES)
|
||||
def generate_edition_preview_image_task(book_id):
|
||||
"""generate preview_image for a book"""
|
||||
|
|
|
@ -404,6 +404,13 @@ if USE_S3:
|
|||
"default_acl": "public-read",
|
||||
},
|
||||
},
|
||||
"sass_processor": {
|
||||
"BACKEND": "storages.backends.s3.S3Storage",
|
||||
"OPTIONS": {
|
||||
"location": "static",
|
||||
"default_acl": "public-read",
|
||||
},
|
||||
},
|
||||
"exports": {
|
||||
"BACKEND": "storages.backends.s3.S3Storage",
|
||||
"OPTIONS": {
|
||||
|
|
|
@ -6,7 +6,7 @@ from base64 import b64encode, b64decode
|
|||
|
||||
from Crypto import Random
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15 # pylint: disable=no-name-in-module
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Hash import SHA256
|
||||
|
||||
MAX_SIGNATURE_AGE = 300
|
||||
|
@ -84,7 +84,6 @@ class Signature:
|
|||
self.headers = headers
|
||||
self.signature = signature
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@classmethod
|
||||
def parse(cls, request):
|
||||
"""extract and parse a signature from an http request"""
|
||||
|
|
|
@ -14,6 +14,10 @@ let BookWyrm = new (class {
|
|||
.querySelectorAll("[data-controls]")
|
||||
.forEach((button) => button.addEventListener("click", this.toggleAction.bind(this)));
|
||||
|
||||
document
|
||||
.querySelectorAll("[data-disappear]")
|
||||
.forEach((button) => button.addEventListener("click", this.hideSelf.bind(this)));
|
||||
|
||||
document
|
||||
.querySelectorAll(".interaction")
|
||||
.forEach((button) => button.addEventListener("submit", this.interact.bind(this)));
|
||||
|
@ -181,6 +185,18 @@ let BookWyrm = new (class {
|
|||
this.addRemoveClass(visible, "is-hidden", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the element you just clicked
|
||||
*
|
||||
* @param {Event} event
|
||||
* @return {undefined}
|
||||
*/
|
||||
hideSelf(event) {
|
||||
let trigger = event.currentTarget;
|
||||
|
||||
this.addRemoveClass(trigger, "is-hidden", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute actions on targets based on triggers.
|
||||
*
|
||||
|
|
|
@ -34,7 +34,6 @@ class SuggestedUsers(RedisStore):
|
|||
|
||||
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
|
||||
"""calculate mutuals count and shared books count from rank"""
|
||||
# pylint: disable=c-extension-no-member
|
||||
return {
|
||||
"mutuals": math.floor(rank),
|
||||
# "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1,
|
||||
|
@ -128,7 +127,6 @@ def get_annotated_users(viewer, *args, **kwargs):
|
|||
),
|
||||
distinct=True,
|
||||
),
|
||||
# pylint: disable=line-too-long
|
||||
# shared_books=Count(
|
||||
# "shelfbook",
|
||||
# filter=Q(
|
||||
|
@ -202,7 +200,7 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs):
|
|||
|
||||
|
||||
@receiver(signals.post_save, sender=models.User)
|
||||
# pylint: disable=unused-argument, too-many-arguments
|
||||
# pylint: disable=unused-argument
|
||||
def update_user(sender, instance, created, update_fields=None, **kwargs):
|
||||
"""an updated user, neat"""
|
||||
# a new user is found, create suggestions for them
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<meta itemprop="name" content="{{ author.name }}">
|
||||
|
||||
{% firstof author.aliases author.born author.died as details %}
|
||||
{% firstof author.wikipedia_link author.website author.openlibrary_key author.inventaire_id author.isni author.isfdb as links %}
|
||||
{% firstof author.wikipedia_link author.website author.openlibrary_key author.inventaire_id author.isni author.isfdb author.wikidata as links %}
|
||||
{% if details or links %}
|
||||
<div class="column is-3">
|
||||
{% if details %}
|
||||
|
@ -73,6 +73,14 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.wikidata %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="https://www.wikidata.org/wiki/{{ author.wikidata }}" rel="nofollow noopener noreferrer" target="_blank">
|
||||
{% trans "View on Wikidata" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.website %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="{{ author.website }}" rel="nofollow noopener noreferrer" target="_blank">
|
||||
|
|
|
@ -21,9 +21,10 @@ Is that where you'd like to go?
|
|||
<div class="is-flex-grow-1">
|
||||
<a href="{% url 'report-link' link.id %}">{% trans "Report spam" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<a href="{{ link.url }}" target="_blank" rel="nofollow noopener noreferrer" noreferrer" class="button is-primary">{% trans "Continue" %}</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,10 +2,31 @@
|
|||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% trans "Edit status" %}{% endblock %}
|
||||
{% block title %}
|
||||
{% if draft.status_type == "Review" %}
|
||||
{% trans "Edit review" %}
|
||||
{% elif draft.status_type == "Quotation" %}
|
||||
{% trans "Edit quote" %}
|
||||
{% elif draft.status_type == "Comment" %}
|
||||
{% trans "Edit comment" %}
|
||||
{% else %}
|
||||
{% trans "Edit status" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="block content">
|
||||
<h1>{% trans "Edit status" %}</h1>
|
||||
<h1>
|
||||
{% if draft.status_type == "Review" %}
|
||||
{% trans "Edit review" %}
|
||||
{% elif draft.status_type == "Quotation" %}
|
||||
{% trans "Edit quote" %}
|
||||
{% elif draft.status_type == "Comment" %}
|
||||
{% trans "Edit comment" %}
|
||||
{% else %}
|
||||
{% trans "Edit status" %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{% with 0|uuid as uuid %}
|
||||
|
|
|
@ -69,6 +69,9 @@
|
|||
<option value="Calibre" {% if current == 'Calibre' %}selected{% endif %}>
|
||||
{% trans "Calibre (CSV)" %}
|
||||
</option>
|
||||
<option value="BookWyrm" {% if current == 'BookWyrm' %}selected{% endif %}>
|
||||
{% trans "BookWyrm (CSV)" %}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -93,9 +96,14 @@
|
|||
<input type="checkbox" name="include_reviews" checked> {% trans "Include reviews" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<input type="checkbox" name="create_shelves" checked> {% trans "Create new shelves if they do not exist" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="privacy_import">
|
||||
{% trans "Privacy setting for imported reviews:" %}
|
||||
{% trans "Privacy setting for imported reviews and shelves:" %}
|
||||
</label>
|
||||
{% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %}
|
||||
</div>
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
{% blocktrans trimmed count counter=notification.related_link_domains.count with display_count=notification.related_link_domains.count|intcomma %}
|
||||
A new <a href="{{ path }}">link domain</a> needs review
|
||||
{% plural %}
|
||||
{{ display_count }} new <a href="{{ path }}">link domains</a> need moderation
|
||||
{{ display_count }} new <a href="{{ path }}">link domains</a> need review
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
{% load utilities %}
|
||||
|
||||
{% block heading %}
|
||||
{% block title %}
|
||||
{% blocktrans with username=user.localname sitename=site.name %}Follow {{ username }} on the fediverse{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}
|
||||
{% blocktrans with display_name=user.display_name %}You are now following {{ display_name }}!{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
|
|
|
@ -69,6 +69,12 @@
|
|||
{% trans "Show reading goal prompt in feed" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_ratings">
|
||||
{{ form.show_ratings }}
|
||||
{% trans "Show ratings" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_suggested_users">
|
||||
{{ form.show_suggested_users }}
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
{% spaceless %}
|
||||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
<span class="stars">
|
||||
{% with 0|uuid as uuid %}
|
||||
<span class="stars tag">
|
||||
{% if not request.user.show_ratings %}
|
||||
|
||||
<button type="button" data-controls="rating-{{ uuid }}" id="rating-button-{{ uuid }}" aria-pressed="false" data-disappear>
|
||||
<em>{% trans "Show rating" %} </em>
|
||||
</button>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if rating %}
|
||||
<span class="{% if not request.user.show_ratings %}is-hidden{% endif %}" id="rating-{{ uuid }}">
|
||||
<span class="is-sr-only">
|
||||
{% blocktranslate trimmed with rating=rating|floatformat:0 count counter=rating|floatformat:0|add:0 %}
|
||||
{{ rating }} star
|
||||
|
@ -19,8 +30,11 @@
|
|||
aria-hidden="true"
|
||||
></span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="no-rating">{% trans "No rating" %}</span>
|
||||
{% endif %}
|
||||
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
||||
|
|
|
@ -71,14 +71,8 @@ def get_landing_books():
|
|||
"""list of books for the landing page"""
|
||||
return list(
|
||||
set(
|
||||
models.Edition.objects.filter(
|
||||
review__published_date__isnull=False,
|
||||
review__deleted=False,
|
||||
review__user__local=True,
|
||||
review__privacy__in=["public", "unlisted"],
|
||||
)
|
||||
.exclude(cover__exact="")
|
||||
models.Edition.objects.exclude(cover__exact="")
|
||||
.distinct()
|
||||
.order_by("-review__published_date")[:6]
|
||||
.order_by("-updated_date")[:6]
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" template filters """
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime, naturalday
|
||||
from django.template.loader import select_template
|
||||
from django.utils import timezone
|
||||
|
@ -60,8 +61,8 @@ def get_published_date(date):
|
|||
delta = relativedelta(now, date)
|
||||
if delta.years:
|
||||
return naturalday(date)
|
||||
if delta.days:
|
||||
return naturalday(date, "M j")
|
||||
if delta.days or delta.months:
|
||||
return naturalday(date, settings.MONTH_DAY_FORMAT)
|
||||
return naturaltime(date)
|
||||
|
||||
|
||||
|
|
|
@ -46,6 +46,18 @@ class BaseActivity(TestCase):
|
|||
# don't try to load the user icon
|
||||
del self.userdata["icon"]
|
||||
|
||||
remote_datafile = pathlib.Path(__file__).parent.joinpath(
|
||||
"../data/ap_user_external.json"
|
||||
)
|
||||
self.remote_userdata = json.loads(remote_datafile.read_bytes())
|
||||
del self.remote_userdata["icon"]
|
||||
|
||||
alias_datafile = pathlib.Path(__file__).parent.joinpath(
|
||||
"../data/ap_user_aliased.json"
|
||||
)
|
||||
self.alias_userdata = json.loads(alias_datafile.read_bytes())
|
||||
del self.alias_userdata["icon"]
|
||||
|
||||
image_path = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
|
@ -118,6 +130,48 @@ class BaseActivity(TestCase):
|
|||
self.assertEqual(result.remote_id, "https://example.com/user/mouse")
|
||||
self.assertEqual(result.name, "MOUSE?? MOUSE!!")
|
||||
|
||||
@responses.activate
|
||||
def test_resolve_remote_alias(self, *_):
|
||||
"""look up or load user who has an unknown alias"""
|
||||
|
||||
self.assertEqual(models.User.objects.count(), 1)
|
||||
|
||||
# remote user with unknown user as an alias
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://example.com/user/moose",
|
||||
json=self.alias_userdata,
|
||||
status=200,
|
||||
)
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://example.com/user/ali",
|
||||
json=self.remote_userdata,
|
||||
status=200,
|
||||
)
|
||||
|
||||
with patch("bookwyrm.models.user.set_remote_server.delay"):
|
||||
result = resolve_remote_id(
|
||||
"https://example.com/user/moose", model=models.User
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
models.User.objects.filter(
|
||||
remote_id="https://example.com/user/moose"
|
||||
).exists()
|
||||
) # moose has been added to DB
|
||||
self.assertTrue(
|
||||
models.User.objects.filter(
|
||||
remote_id="https://example.com/user/ali"
|
||||
).exists()
|
||||
) # Ali has been added to DB
|
||||
self.assertIsInstance(result, models.User)
|
||||
self.assertEqual(result.name, "moose?? moose!!")
|
||||
alias = models.User.objects.last()
|
||||
self.assertEqual(alias.name, "Ali As")
|
||||
self.assertEqual(result.also_known_as.first(), alias) # Ali is alias of Moose
|
||||
|
||||
def test_to_model_invalid_model(self, *_):
|
||||
"""catch mismatch between activity type and model type"""
|
||||
instance = ActivityObject(id="a", type="b")
|
||||
|
|
|
@ -115,7 +115,6 @@ class AbstractConnector(TestCase):
|
|||
@responses.activate
|
||||
def test_get_or_create_author(self):
|
||||
"""load an author"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
|
@ -141,7 +140,6 @@ class AbstractConnector(TestCase):
|
|||
def test_update_author_from_remote(self):
|
||||
"""trigger the function that looks up the remote data"""
|
||||
author = models.Author.objects.create(name="Test", openlibrary_key="OL123A")
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
|
|
|
@ -273,7 +273,9 @@ class Inventaire(TestCase):
|
|||
json={"extract": "hi hi"},
|
||||
)
|
||||
|
||||
extract = self.connector.get_description({"enwiki": "test_path"})
|
||||
extract = self.connector.get_description(
|
||||
{"enwiki": {"title": "test_path", "badges": "hello"}}
|
||||
)
|
||||
self.assertEqual(extract, "hi hi")
|
||||
|
||||
def test_remote_id_from_model(self):
|
||||
|
|
40
bookwyrm/tests/data/ap_user_aliased.json
Normal file
40
bookwyrm/tests/data/ap_user_aliased.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"schema": "http://schema.org#",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value"
|
||||
}
|
||||
],
|
||||
"id": "https://example.com/user/moose",
|
||||
"type": "Person",
|
||||
"preferredUsername": "moose",
|
||||
"name": "moose?? moose!!",
|
||||
"inbox": "https://example.com/user/moose/inbox",
|
||||
"outbox": "https://example.com/user/moose/outbox",
|
||||
"followers": "https://example.com/user/moose/followers",
|
||||
"following": "https://example.com/user/moose/following",
|
||||
"summary": "",
|
||||
"publicKey": {
|
||||
"id": "https://example.com/user/moose/#main-key",
|
||||
"owner": "https://example.com/user/moose",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----"
|
||||
},
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://example.com/inbox"
|
||||
},
|
||||
"bookwyrmUser": true,
|
||||
"manuallyApprovesFollowers": false,
|
||||
"discoverable": false,
|
||||
"alsoKnownAs": ["https://example.com/user/ali"],
|
||||
"devices": "",
|
||||
"tag": [],
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://example.com/images/avatars/AL-3-crop-50.png"
|
||||
}
|
||||
}
|
40
bookwyrm/tests/data/ap_user_external.json
Normal file
40
bookwyrm/tests/data/ap_user_external.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"schema": "http://schema.org#",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value"
|
||||
}
|
||||
],
|
||||
"id": "https://example.com/user/ali",
|
||||
"type": "Person",
|
||||
"preferredUsername": "alias",
|
||||
"name": "Ali As",
|
||||
"inbox": "https://example.com/user/ali/inbox",
|
||||
"outbox": "https://example.com/user/ali/outbox",
|
||||
"followers": "https://example.com/user/ali/followers",
|
||||
"following": "https://example.com/user/ali/following",
|
||||
"summary": "",
|
||||
"publicKey": {
|
||||
"id": "https://example.com/user/ali/#main-key",
|
||||
"owner": "https://example.com/user/ali",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----"
|
||||
},
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://example.com/inbox"
|
||||
},
|
||||
"bookwyrmUser": true,
|
||||
"manuallyApprovesFollowers": false,
|
||||
"alsoKnownAs": [],
|
||||
"discoverable": false,
|
||||
"devices": "",
|
||||
"tag": [],
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://example.com/images/avatars/ALIAS-2-crop-50.png"
|
||||
}
|
||||
}
|
4
bookwyrm/tests/data/bookwyrm.csv
Normal file
4
bookwyrm/tests/data/bookwyrm.csv
Normal file
|
@ -0,0 +1,4 @@
|
|||
title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,start_date,finish_date,stopped_date,rating,review_name,review_cw,review_content,review_published,shelf,shelf_name,shelf_date
|
||||
我穿我自己,琅俨,https://example.com/book/2010,,,,,,,,,,,,,,,,,,,,,,to-read,To Read,2024-08-10
|
||||
Ottolenghi Simple,Yotam Ottolenghi,https://example.com/book/2,OL43065148M,,,,,,,,,,0449017036,9780449017036,,2022-08-10,2022-10-10,,4,Too much tahini,,...in his hummus,2022-11-10,cooking-9,Cooking,2024-08-10
|
||||
The Blue Bedspread,Raj Kamal Jha,https://example.com/book/270,OL7425890M,,,,,,,,,,0375503129,9780375503122,41754476,2001-06-01,2001-07-10,,5,,,,,read,Read,2024-08-10
|
|
182
bookwyrm/tests/importers/test_bookwyrm_import.py
Normal file
182
bookwyrm/tests/importers/test_bookwyrm_import.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
""" testing bookwyrm csv import """
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.importers import BookwyrmBooksImporter
|
||||
from bookwyrm.models.import_job import handle_imported_book
|
||||
|
||||
|
||||
def make_date(*args):
|
||||
"""helper function to easily generate a date obj"""
|
||||
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||
class BookwyrmBooksImport(TestCase):
|
||||
"""importing from BookWyrm csv"""
|
||||
|
||||
def setUp(self):
|
||||
"""use a test csv"""
|
||||
self.importer = BookwyrmBooksImporter()
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/bookwyrm.csv")
|
||||
# pylint: disable-next=consider-using-with
|
||||
self.csv = open(datafile, "r", encoding=self.importer.encoding)
|
||||
|
||||
def tearDown(self):
|
||||
"""close test csv"""
|
||||
self.csv.close()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""populate database"""
|
||||
with (
|
||||
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
|
||||
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||
):
|
||||
cls.local_user = models.User.objects.create_user(
|
||||
"mouse", "mouse@mouse.mouse", "password", local=True
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
work = models.Work.objects.create(title="Test Work")
|
||||
cls.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).all()
|
||||
self.assertEqual(len(import_items), 3)
|
||||
self.assertEqual(import_items[0].index, 0)
|
||||
self.assertEqual(import_items[0].normalized_data["isbn_13"], "")
|
||||
self.assertEqual(import_items[0].normalized_data["isbn_10"], "")
|
||||
self.assertEqual(import_items[0].shelf_name, "To Read")
|
||||
|
||||
self.assertEqual(import_items[1].index, 1)
|
||||
self.assertEqual(import_items[1].normalized_data["isbn_13"], "9780449017036")
|
||||
self.assertEqual(import_items[1].normalized_data["isbn_10"], "0449017036")
|
||||
self.assertEqual(import_items[1].shelf_name, "Cooking")
|
||||
|
||||
self.assertEqual(import_items[2].index, 2)
|
||||
self.assertEqual(import_items[2].normalized_data["isbn_13"], "9780375503122")
|
||||
self.assertEqual(import_items[2].normalized_data["isbn_10"], "0375503129")
|
||||
self.assertEqual(import_items[2].shelf_name, "Read")
|
||||
|
||||
def test_create_retry_job(self, *_):
|
||||
"""trying again with items that didn't import"""
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, False, "unlisted"
|
||||
)
|
||||
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
|
||||
|
||||
retry = self.importer.create_retry_job(
|
||||
self.local_user, import_job, import_items
|
||||
)
|
||||
self.assertNotEqual(import_job, retry)
|
||||
self.assertEqual(retry.user, self.local_user)
|
||||
self.assertEqual(retry.include_reviews, False)
|
||||
self.assertEqual(retry.privacy, "unlisted")
|
||||
|
||||
retry_items = models.ImportItem.objects.filter(job=retry).all()
|
||||
self.assertEqual(len(retry_items), 2)
|
||||
self.assertEqual(retry_items[0].index, 0)
|
||||
self.assertEqual(retry_items[0].data["title"], "我穿我自己")
|
||||
self.assertEqual(retry_items[1].index, 1)
|
||||
self.assertEqual(retry_items[1].data["author_text"], "Yotam Ottolenghi")
|
||||
|
||||
def test_handle_imported_book(self, *_):
|
||||
"""import added a book, this adds related connections"""
|
||||
shelf = self.local_user.shelf_set.filter(
|
||||
identifier=models.Shelf.READ_FINISHED
|
||||
).first()
|
||||
self.assertIsNone(shelf.books.first())
|
||||
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, False, "public"
|
||||
)
|
||||
import_item = import_job.items.last()
|
||||
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)
|
||||
self.assertEqual(
|
||||
shelf.shelfbook_set.first().shelved_date, make_date(2024, 8, 10)
|
||||
)
|
||||
|
||||
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
self.assertEqual(readthrough.start_date, make_date(2001, 6, 1))
|
||||
self.assertEqual(readthrough.finish_date, make_date(2001, 7, 10))
|
||||
|
||||
def test_create_new_shelf(self, *_):
|
||||
"""import added a book, was a new shelf created?"""
|
||||
shelf = self.local_user.shelf_set.filter(identifier="cooking").first()
|
||||
self.assertIsNone(shelf)
|
||||
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, False, "public"
|
||||
)
|
||||
import_item = models.ImportItem.objects.filter(job=import_job).all()[1]
|
||||
import_item.book = self.book
|
||||
import_item.save()
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
handle_imported_book(import_item)
|
||||
|
||||
shelf_after = self.local_user.shelf_set.filter(identifier="cooking-9").first()
|
||||
self.assertEqual(shelf_after.books.first(), self.book)
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
def test_handle_imported_book_review(self, *_):
|
||||
"""review import"""
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, True, "unlisted"
|
||||
)
|
||||
import_item = import_job.items.get(index=1)
|
||||
import_item.book = self.book
|
||||
import_item.save()
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
handle_imported_book(import_item)
|
||||
|
||||
review = models.Review.objects.get(book=self.book, user=self.local_user)
|
||||
self.assertEqual(review.name, "Too much tahini")
|
||||
self.assertEqual(review.content, "...in his hummus")
|
||||
self.assertEqual(review.rating, 4)
|
||||
self.assertEqual(review.published_date, make_date(2022, 11, 10))
|
||||
self.assertEqual(review.privacy, "unlisted")
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
def test_handle_imported_book_rating(self, *_):
|
||||
"""rating import"""
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, True, "followers"
|
||||
)
|
||||
import_item = import_job.items.filter(index=2).first()
|
||||
import_item.book = self.book
|
||||
import_item.save()
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
handle_imported_book(import_item)
|
||||
|
||||
review = models.ReviewRating.objects.get(book=self.book, user=self.local_user)
|
||||
self.assertIsInstance(review, models.ReviewRating)
|
||||
self.assertEqual(review.rating, 5)
|
||||
self.assertEqual(review.published_date, make_date(2001, 7, 10))
|
||||
self.assertEqual(review.privacy, "followers")
|
|
@ -63,7 +63,9 @@ class GenericImporter(TestCase):
|
|||
self.assertEqual(import_job.include_reviews, False)
|
||||
self.assertEqual(import_job.privacy, "public")
|
||||
|
||||
import_items = models.ImportItem.objects.filter(job=import_job).all()
|
||||
import_items = (
|
||||
models.ImportItem.objects.filter(job=import_job).all().order_by("id")
|
||||
)
|
||||
self.assertEqual(len(import_items), 4)
|
||||
self.assertEqual(import_items[0].index, 0)
|
||||
self.assertEqual(import_items[0].normalized_data["id"], "38")
|
||||
|
|
|
@ -20,7 +20,7 @@ from bookwyrm.models.activitypub_mixin import (
|
|||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,too-many-public-methods
|
||||
# pylint: disable=too-many-public-methods
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||
class ActivitypubMixins(TestCase):
|
||||
|
|
|
@ -42,7 +42,7 @@ class BaseModel(TestCase):
|
|||
|
||||
def test_remote_id(self):
|
||||
"""these should be generated"""
|
||||
self.test_model.id = 1 # pylint: disable=invalid-name
|
||||
self.test_model.id = 1
|
||||
expected = self.test_model.get_remote_id()
|
||||
self.assertEqual(expected, f"{BASE_URL}/bookwyrmtestmodel/1")
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from bookwyrm.utils.tar import BookwyrmTarFile
|
|||
from bookwyrm.models import bookwyrm_import_job
|
||||
|
||||
|
||||
class BookwyrmImport(TestCase): # pylint: disable=too-many-public-methods
|
||||
class BookwyrmImport(TestCase):
|
||||
"""testing user import functions"""
|
||||
|
||||
def setUp(self):
|
||||
|
|
107
bookwyrm/tests/models/test_job.py
Normal file
107
bookwyrm/tests/models/test_job.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
""" testing models """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.job import ChildJob, ParentJob
|
||||
|
||||
|
||||
class TestParentJob(TestCase):
|
||||
"""job manager"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""we're trying to transport user data"""
|
||||
with (
|
||||
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
|
||||
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||
):
|
||||
cls.local_user = models.User.objects.create_user(
|
||||
"mouse", "mouse@mouse.mouse", "password", local=True
|
||||
)
|
||||
|
||||
def test_complete_job(self):
|
||||
"""mark a job as complete"""
|
||||
job = ParentJob.objects.create(user=self.local_user)
|
||||
self.assertFalse(job.complete)
|
||||
self.assertEqual(job.status, "pending")
|
||||
|
||||
job.complete_job()
|
||||
|
||||
job.refresh_from_db()
|
||||
self.assertTrue(job.complete)
|
||||
self.assertEqual(job.status, "complete")
|
||||
|
||||
def test_complete_job_with_children(self):
|
||||
"""mark a job with children as complete"""
|
||||
job = ParentJob.objects.create(user=self.local_user)
|
||||
child = ChildJob.objects.create(parent_job=job)
|
||||
self.assertFalse(child.complete)
|
||||
self.assertEqual(child.status, "pending")
|
||||
|
||||
job.complete_job()
|
||||
|
||||
child.refresh_from_db()
|
||||
self.assertEqual(child.status, "stopped")
|
||||
|
||||
def test_pending_child_jobs(self):
|
||||
"""queryset of child jobs for a parent"""
|
||||
job = ParentJob.objects.create(user=self.local_user)
|
||||
child = ChildJob.objects.create(parent_job=job)
|
||||
ChildJob.objects.create(parent_job=job, complete=True)
|
||||
|
||||
self.assertEqual(job.pending_child_jobs.count(), 1)
|
||||
self.assertEqual(job.pending_child_jobs.first(), child)
|
||||
|
||||
|
||||
class TestChildJob(TestCase):
|
||||
"""job manager"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""we're trying to transport user data"""
|
||||
with (
|
||||
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
|
||||
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||
):
|
||||
cls.local_user = models.User.objects.create_user(
|
||||
"mouse", "mouse@mouse.mouse", "password", local=True
|
||||
)
|
||||
|
||||
def test_complete_job(self):
|
||||
"""a child job completed, so its parent is complete"""
|
||||
job = ParentJob.objects.create(user=self.local_user)
|
||||
child = ChildJob.objects.create(parent_job=job)
|
||||
self.assertFalse(job.complete)
|
||||
|
||||
child.complete_job()
|
||||
|
||||
job.refresh_from_db()
|
||||
self.assertTrue(job.complete)
|
||||
self.assertEqual(job.status, "complete")
|
||||
|
||||
def test_complete_job_with_siblings(self):
|
||||
"""a child job completed, but its parent is not complete"""
|
||||
job = ParentJob.objects.create(user=self.local_user)
|
||||
child = ChildJob.objects.create(parent_job=job)
|
||||
ChildJob.objects.create(parent_job=job)
|
||||
self.assertFalse(job.complete)
|
||||
|
||||
child.complete_job()
|
||||
|
||||
job.refresh_from_db()
|
||||
self.assertFalse(job.complete)
|
||||
|
||||
def test_set_status(self):
|
||||
"""a parent job is activated when a child task is activated"""
|
||||
job = ParentJob.objects.create(user=self.local_user)
|
||||
child = ChildJob.objects.create(parent_job=job)
|
||||
self.assertEqual(job.status, "pending")
|
||||
|
||||
child.set_status("active")
|
||||
job.refresh_from_db()
|
||||
|
||||
self.assertEqual(job.status, "active")
|
|
@ -6,7 +6,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, settings
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
@patch("bookwyrm.lists_stream.populate_lists_task.delay")
|
||||
|
|
|
@ -109,4 +109,17 @@ class StatusDisplayTags(TestCase):
|
|||
2022, 1, 8, 0, 0, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
result = status_display.get_published_date(date)
|
||||
self.assertEqual(result, "Jan 1")
|
||||
self.assertEqual(result, "January 1")
|
||||
|
||||
with patch("django.utils.timezone.now") as timezone_mock:
|
||||
timezone_mock.return_value = datetime.datetime(
|
||||
# bookwyrm-social#3365: bug with exact month deltas
|
||||
2022,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
tzinfo=datetime.timezone.utc,
|
||||
)
|
||||
result = status_display.get_published_date(date)
|
||||
self.assertEqual(result, "January 1")
|
||||
|
|
|
@ -89,3 +89,17 @@ class UtilitiesTags(TestCase):
|
|||
|
||||
result = utilities.get_isni_bio(data, self.author)
|
||||
self.assertEqual(result, "Author of <em>One\\Dtwo</em>")
|
||||
|
||||
def test_id_to_username(self, *_):
|
||||
"""given an arbitrary remote id, return the username"""
|
||||
self.assertEqual(
|
||||
utilities.id_to_username("http://example.com/rat"), "rat@example.com"
|
||||
)
|
||||
self.assertEqual(utilities.id_to_username(None), "a new user account")
|
||||
|
||||
def test_get_file_size(self, *_):
|
||||
"""display the size of a file in human readable terms"""
|
||||
self.assertEqual(utilities.get_file_size(5), "5.0 bytes")
|
||||
self.assertEqual(utilities.get_file_size(5120), "5.00 KB")
|
||||
self.assertEqual(utilities.get_file_size(5242880), "5.00 MB")
|
||||
self.assertEqual(utilities.get_file_size(5368709000), "5.00 GB")
|
||||
|
|
|
@ -10,7 +10,7 @@ class MergeBookDataModel(TestCase):
|
|||
"""test merging of subclasses of BookDataModel"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls): # pylint: disable=invalid-name
|
||||
def setUpTestData(cls):
|
||||
"""shared data"""
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ def validate_html(html):
|
|||
validator.feed(str(html.content))
|
||||
|
||||
|
||||
class HtmlValidator(HTMLParser): # pylint: disable=abstract-method
|
||||
class HtmlValidator(HTMLParser):
|
||||
"""Checks for custom html validation requirements"""
|
||||
|
||||
def __init__(self):
|
||||
|
|
|
@ -14,7 +14,6 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class ImportUserViews(TestCase):
|
||||
"""user import views"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
|
|
|
@ -11,7 +11,6 @@ from django.test.client import RequestFactory
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class Inbox(TestCase):
|
||||
"""readthrough tests"""
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import responses
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxAdd(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import responses
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxActivities(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxBlock(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ from bookwyrm import models, views
|
|||
from bookwyrm.activitypub import ActivitySerializerError
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class TransactionInboxCreate(TransactionTestCase):
|
||||
"""readthrough tests"""
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxActivities(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxRelationships(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxActivities(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxRemove(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.test import TestCase
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class InboxUpdate(TestCase):
|
||||
"""inbox tests"""
|
||||
|
||||
|
@ -161,7 +160,6 @@ class InboxUpdate(TestCase):
|
|||
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json")
|
||||
bookdata = json.loads(datafile.read_bytes())
|
||||
del bookdata["authors"]
|
||||
# pylint: disable=line-too-long
|
||||
link_data = {
|
||||
"href": "https://openlibrary.org/books/OL11645413M/Queen_Victoria/daisy",
|
||||
"mediaType": "Daisy",
|
||||
|
|
|
@ -11,7 +11,6 @@ from bookwyrm import forms, models, views
|
|||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
class LoginViews(TestCase):
|
||||
|
|
|
@ -11,7 +11,6 @@ from bookwyrm import models, views
|
|||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
class ListViews(TestCase):
|
||||
"""list view"""
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ from bookwyrm import models, views
|
|||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
class ListViews(TestCase):
|
||||
"""list view"""
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ from bookwyrm.activitypub import ActivitypubResponse
|
|||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=too-many-public-methods
|
||||
class ListViews(TestCase):
|
||||
"""list view"""
|
||||
|
|
|
@ -7,8 +7,6 @@ from django.test.client import RequestFactory
|
|||
from bookwyrm import models, views
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=too-many-public-methods
|
||||
class ListItemViews(TestCase):
|
||||
"""list view"""
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.test.client import RequestFactory
|
|||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
class ListViews(TestCase):
|
||||
"""lists of lists"""
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class ExportViews(TestCase):
|
|||
"""viewing and creating statuses"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls): # pylint: disable=invalid-name
|
||||
def setUpTestData(cls):
|
||||
"""we need basic test data and mocks"""
|
||||
with (
|
||||
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||
|
@ -41,7 +41,6 @@ class ExportViews(TestCase):
|
|||
bnf_id="beep",
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""individual test setup"""
|
||||
self.factory = RequestFactory()
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.test.client import RequestFactory
|
|||
|
||||
from bookwyrm import forms, models, views
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
class TwoFactorViews(TestCase):
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.test.client import RequestFactory
|
|||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
class DirectoryViews(TestCase):
|
||||
"""tag views"""
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ from bookwyrm import models, views
|
|||
from bookwyrm.settings import USER_AGENT
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||
class OutboxView(TestCase):
|
||||
"""sends out activities"""
|
||||
|
|
|
@ -103,6 +103,20 @@ class Views(TestCase):
|
|||
connector_results = response.context_data["remote_results"]
|
||||
self.assertEqual(connector_results[0]["results"][0].title, "Mock Book")
|
||||
|
||||
def test_search_books_extra_whitespace(self):
|
||||
"""just the search page"""
|
||||
view = views.Search.as_view()
|
||||
request = self.factory.get("", {"q": " Test Book ", "remote": False})
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.search.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
response = view(request)
|
||||
self.assertIsInstance(response, TemplateResponse)
|
||||
validate_html(response.render())
|
||||
|
||||
local_results = response.context_data["results"]
|
||||
self.assertEqual(local_results[0].title, "Test Book")
|
||||
|
||||
def test_search_book_anonymous(self):
|
||||
"""Don't search remote for logged out user"""
|
||||
view = views.Search.as_view()
|
||||
|
@ -150,6 +164,17 @@ class Views(TestCase):
|
|||
validate_html(response.render())
|
||||
self.assertEqual(response.context_data["results"][0], self.local_user)
|
||||
|
||||
def test_search_users_extra_whitespace(self):
|
||||
"""searches remote connectors"""
|
||||
view = views.Search.as_view()
|
||||
request = self.factory.get("", {"q": " mouse ", "type": "user"})
|
||||
request.user = self.local_user
|
||||
response = view(request)
|
||||
|
||||
self.assertIsInstance(response, TemplateResponse)
|
||||
validate_html(response.render())
|
||||
self.assertEqual(response.context_data["results"][0], self.local_user)
|
||||
|
||||
def test_search_users_logged_out(self):
|
||||
"""searches remote connectors"""
|
||||
view = views.Search.as_view()
|
||||
|
@ -181,3 +206,21 @@ class Views(TestCase):
|
|||
self.assertIsInstance(response, TemplateResponse)
|
||||
validate_html(response.render())
|
||||
self.assertEqual(response.context_data["results"][0], booklist)
|
||||
|
||||
def test_search_lists_extra_whitespace(self):
|
||||
"""searches remote connectors"""
|
||||
with (
|
||||
patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"),
|
||||
patch("bookwyrm.lists_stream.remove_list_task.delay"),
|
||||
):
|
||||
booklist = models.List.objects.create(
|
||||
user=self.local_user, name="test list"
|
||||
)
|
||||
view = views.Search.as_view()
|
||||
request = self.factory.get("", {"q": " test ", "type": "list"})
|
||||
request.user = self.local_user
|
||||
response = view(request)
|
||||
|
||||
self.assertIsInstance(response, TemplateResponse)
|
||||
validate_html(response.render())
|
||||
self.assertEqual(response.context_data["results"][0], booklist)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
""" test for app action functionality """
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
import dateutil
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.views.status import find_mentions, find_or_create_hashtags
|
||||
|
@ -167,6 +169,37 @@ class StatusViews(TestCase):
|
|||
self.assertEqual(status.rating, 4.0)
|
||||
self.assertIsNone(status.edited_date)
|
||||
|
||||
def test_create_status_progress(self, *_):
|
||||
"""create a status that updates a readthrough"""
|
||||
start_date = timezone.make_aware(dateutil.parser.parse("2024-07-27"))
|
||||
readthrough = models.ReadThrough.objects.create(
|
||||
book=self.book, user=self.local_user, start_date=start_date
|
||||
)
|
||||
|
||||
self.assertEqual(start_date, readthrough.start_date)
|
||||
self.assertIsNone(readthrough.progress)
|
||||
|
||||
view = views.CreateStatus.as_view()
|
||||
form = forms.CommentForm(
|
||||
{
|
||||
"progress": 1,
|
||||
"progress_mode": "PG",
|
||||
"content": "I started the book",
|
||||
"id": readthrough.id,
|
||||
"book": self.book.id,
|
||||
"user": self.local_user.id,
|
||||
"privacy": "public",
|
||||
}
|
||||
)
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
view(request, "comment")
|
||||
readthrough.refresh_from_db()
|
||||
|
||||
self.assertEqual(1, readthrough.progress)
|
||||
self.assertEqual(start_date, readthrough.start_date) # not overwritten
|
||||
|
||||
def test_create_status_wrong_user(self, *_):
|
||||
"""You can't compose statuses for someone else"""
|
||||
view = views.CreateStatus.as_view()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http.response import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -12,6 +14,11 @@ from bookwyrm.activitypub import ActivitypubResponse
|
|||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
def make_date(*args):
|
||||
"""helper function to easily generate a date obj"""
|
||||
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||
|
||||
|
||||
class UserViews(TestCase):
|
||||
"""view user and edit profile"""
|
||||
|
||||
|
@ -36,6 +43,10 @@ class UserViews(TestCase):
|
|||
cls.book = models.Edition.objects.create(
|
||||
title="test", parent_work=models.Work.objects.create(title="test work")
|
||||
)
|
||||
cls.book_recently_shelved = models.Edition.objects.create(
|
||||
title="recently shelved",
|
||||
parent_work=models.Work.objects.create(title="recent shelved"),
|
||||
)
|
||||
with (
|
||||
patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"),
|
||||
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||
|
@ -45,6 +56,14 @@ class UserViews(TestCase):
|
|||
book=cls.book,
|
||||
user=cls.local_user,
|
||||
shelf=cls.local_user.shelf_set.first(),
|
||||
shelved_date=make_date(2020, 10, 21),
|
||||
)
|
||||
|
||||
models.ShelfBook.objects.create(
|
||||
book=cls.book_recently_shelved,
|
||||
user=cls.local_user,
|
||||
shelf=cls.local_user.shelf_set.first(),
|
||||
shelved_date=make_date(2024, 7, 1),
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
@ -119,6 +138,23 @@ class UserViews(TestCase):
|
|||
with self.assertRaises(Http404):
|
||||
view(request, "rat")
|
||||
|
||||
def test_user_page_activity_sorted(self):
|
||||
"""the most recently shelved book should be displayed first"""
|
||||
view = views.User.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.user.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, "mouse")
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
first_shelf = result.context_data["shelves"][0]
|
||||
first_book = first_shelf["books"][0]
|
||||
|
||||
self.assertEqual(first_book, self.book_recently_shelved)
|
||||
|
||||
def test_followers_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Relationships.as_view()
|
||||
|
|
|
@ -52,7 +52,6 @@ class PartialDate(datetime):
|
|||
Use subclasses to specify precision. If `dt` is naive, `ValueError`
|
||||
is raised.
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
if timezone.is_naive(dt):
|
||||
raise ValueError("naive datetime not accepted")
|
||||
return cls.combine(dt.date(), dt.time(), tzinfo=dt.tzinfo)
|
||||
|
|
|
@ -88,6 +88,7 @@ class CeleryStatus(View):
|
|||
def post(self, request):
|
||||
"""Submit form to clear queues"""
|
||||
form = ClearCeleryForm(request.POST)
|
||||
results = []
|
||||
if form.is_valid():
|
||||
if len(celery.control.ping()) != 0:
|
||||
return HttpResponse(
|
||||
|
|
|
@ -43,7 +43,6 @@ class Dashboard(View):
|
|||
) or not re.match(regex.DOMAIN, settings.EMAIL_SENDER_DOMAIN)
|
||||
|
||||
data["email_config_error"] = email_config_error
|
||||
# pylint: disable=line-too-long
|
||||
data[
|
||||
"email_sender"
|
||||
] = f"{settings.EMAIL_SENDER_NAME}@{settings.EMAIL_SENDER_DOMAIN}"
|
||||
|
@ -84,7 +83,7 @@ class Dashboard(View):
|
|||
schedule, _ = IntervalSchedule.objects.get_or_create(
|
||||
**schedule_form.cleaned_data
|
||||
)
|
||||
PeriodicTask.objects.get_or_create(
|
||||
PeriodicTask.objects.update_or_create(
|
||||
interval=schedule,
|
||||
name="check-for-updates",
|
||||
task="bookwyrm.models.site.check_for_updates_task",
|
||||
|
|
|
@ -152,7 +152,7 @@ class FederatedServer(View):
|
|||
}
|
||||
return TemplateResponse(request, "settings/federation/instance.html", data)
|
||||
|
||||
def post(self, request, server): # pylint: disable=unused-argument
|
||||
def post(self, request, server):
|
||||
"""update note"""
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
server.notes = request.POST.get("notes")
|
||||
|
|
|
@ -63,7 +63,6 @@ class ImportList(View):
|
|||
}
|
||||
return TemplateResponse(request, "settings/imports/imports.html", data)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def post(self, request, import_id):
|
||||
"""Mark an import as complete"""
|
||||
import_job = get_object_or_404(models.ImportJob, id=import_id)
|
||||
|
@ -95,7 +94,6 @@ def enable_imports(request):
|
|||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def set_import_size_limit(request):
|
||||
"""Limit the amount of books users can import at once"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
|
@ -120,7 +118,6 @@ def set_user_import_completed(request, import_id):
|
|||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def set_user_import_limit(request):
|
||||
"""Limit how ofter users can import and export their account"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
|
|
|
@ -204,7 +204,6 @@ def resolve_book(request):
|
|||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_book_from_remote(request, book_id, connector_identifier):
|
||||
"""load the remote data for this book"""
|
||||
connector = connector_manager.load_connector(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue