Compare commits

...

695 commits

Author SHA1 Message Date
Mouse Reeve 3f8cf2e134
Merge branch 'main' into flowerproxy 2024-04-24 15:34:46 -07:00
Mouse Reeve ad830dd885
Merge pull request #3350 from Minnozz/custom-port
Correctly handle serving BookWyrm on custom port
2024-04-24 15:27:01 -07:00
Mouse Reeve 366c647585
Merge pull request #3359 from bookwyrm-social/dependabot/pip/aiohttp-3.9.4
Bump aiohttp from 3.9.2 to 3.9.4
2024-04-24 15:13:30 -07:00
Bart Schuurmans 4f58b11330 Include the correct protocol and port in remote IDs 2024-04-24 15:35:19 +02:00
Bart Schuurmans 609bc15406 Support http:// protocol in BookWyrm connector 2024-04-24 15:30:47 +02:00
Bart Schuurmans c42db40a63 Construct absolute URLs with the correct protocol and port 2024-04-24 15:30:47 +02:00
Bart Schuurmans 3aefbb548e Allow serving BookWyrm on a non-standard port 2024-04-24 15:30:47 +02:00
Bart Schuurmans baea105c18 pytest.ini env values should be unquoted
Otherwise the quotes end up in the strings.
2024-04-24 15:30:47 +02:00
Bart Schuurmans c73d1fff6a Remove unnecessary exceptions from validate_url_domain 2024-04-24 15:30:47 +02:00
Bart Schuurmans 3d183a393f
Merge pull request #3360 from hughrun/move-fix
refactor Move for more redundancy
2024-04-24 15:30:19 +02:00
Bart Schuurmans f24fdf73b5 Update to match newer code style 2024-04-24 15:08:48 +02:00
Bart Schuurmans 839ab2fafd
Merge branch 'main' into move-fix 2024-04-24 14:56:32 +02:00
Bart Schuurmans 637f19b208
Merge pull request #3336 from Minnozz/s3-url-protocol
Support AWS_S3_URL_PROTOCOL
2024-04-24 14:53:55 +02:00
Bart Schuurmans 031223104f Clarify AWS_S3_URL_PROTOCOL in .env.example 2024-04-24 14:46:57 +02:00
Hugh Rundle 6684d60526
refactor Move for more redundancy
As outlined in #3354, a use `Move` fails if the user is moving from a BookWyrm server to another BookWrym server.
This is because:

1. the original code did not announce changes to alsoKnownAs;
2. the original code always checked the locally saved profile rather than refetching the remote data;

This commit fixes both these problems by forcing `MoveUser` to always perform a "refresh" of the local data from the remote, and by saving the user with broadcast=True when updating alsoKnownAs ids.
2024-04-22 13:35:08 +10:00
dependabot[bot] cca58023ed
Bump aiohttp from 3.9.2 to 3.9.4
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.2 to 3.9.4.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.2...v3.9.4)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-18 15:51:34 +00:00
Bart Schuurmans bf5c08dbf3 Add docker-compose.override.yml to .gitignore 2024-04-15 13:17:00 +02:00
Bart Schuurmans be872ed672 Support AWS_S3_URL_PROTOCOL
- Allow setting in .env
- Default to PROTOCOL (same as before)
- Propagate to django-storages so it generates the correct URLs in sass_src
2024-04-15 13:16:51 +02:00
Bart Schuurmans 70f803a1f6
Merge pull request #3353 from dato/fix_quotation_str_pagenum
Fix creation of quotations with no end position
2024-04-15 13:11:55 +02:00
Adeodato Simó 4304cd4a79
use re.escape 2024-04-13 21:26:41 -03:00
Adeodato Simó 8733369605
test_quotation_page_serialization: add test with no position 2024-04-13 21:26:41 -03:00
Adeodato Simó df78cc64a6
Quotation._format_position: do not treat page numbers as integers
Fixes: #3352
2024-04-13 21:26:41 -03:00
Adeodato Simó f844abcad9
test_quotation_page_serialization: use strings for page numbers
This follows from #3273, "Allow page numbers to be text, instead of
integers".
2024-04-13 21:26:39 -03:00
Bart Schuurmans 21a39f8170
Merge pull request #3228 from hughrun/user-export
Fix user exports to deal with s3 storage
2024-04-13 22:53:58 +02:00
Hugh Rundle c3c46144fe
add merge migration 2024-04-13 12:39:40 +10:00
Hugh Rundle d48d312c0a
Merge branch 'main' into user-export 2024-04-13 12:26:13 +10:00
Hugh Rundle 501fb45528
export avatars to own directory
Saving avatars to /images is problematic because it changes the original filepath from avatars/filename to images/avatars/filename.
In this PR prior to this commit, imports failed as they are looking for a file path beginning with "avatar"
2024-04-13 12:03:35 +10:00
Bart Schuurmans 7d581759da
Merge pull request #3342 from hbrunn/main-pilkit
[FIX] make sure to get Pillow>=10 compatible pilkit
2024-04-11 14:52:22 +02:00
Bart Schuurmans d5a536ae36 Change pilkit constraint to the version that does work 2024-04-11 14:45:13 +02:00
Bart Schuurmans 26f92db5d8 Merge branch 'main' into main-pilkit 2024-04-11 14:43:10 +02:00
Bart Schuurmans 5686c5ae5d
Merge pull request #3356 from Minnozz/quick-fix-frontend-ci
Install same version of eslint in CI as in dev-tools
2024-04-10 22:10:07 +02:00
Bart Schuurmans 9d9e64399c Install same version of eslint in CI as in dev-tools 2024-04-10 21:26:34 +02:00
Mouse Reeve b6aba44e42
Merge pull request #3355 from bookwyrm-social/merge-migration
Adds merge migration
2024-04-09 06:04:15 -05:00
Mouse Reeve 3ffbb242a4 Black 2024-04-09 05:59:01 -05:00
Mouse Reeve af0bd90c15 Adds merge migration 2024-04-09 05:57:27 -05:00
Mouse Reeve 73630331d1
Merge pull request #3299 from Minnozz/absorb
Track which Author/Work/Edition a duplicate has been merged into
2024-04-09 05:55:44 -05:00
Mouse Reeve ca6dbcb483
Merge pull request #3348 from Minnozz/more-indexes
Define more indexes for slow queries
2024-04-04 15:18:07 -07:00
Bart Schuurmans e1c54b2933 Remove optimizations with adverse effects
`if not audience` actually causes the entire query to be evaluated, before .values_list() is called.
2024-04-04 13:47:56 +02:00
Bart Schuurmans 439cb3ccaa Remove unnecessary conversions between list and set 2024-04-04 13:15:31 +02:00
Bart Schuurmans 321397a349 Specify which column DISTINCT should apply to 2024-04-03 21:28:22 +02:00
Bart Schuurmans 464a0298c6 Add index for finding active (and local) users 2024-04-03 21:27:52 +02:00
Bart Schuurmans 0501ce39cd Add index for looking up User by username 2024-04-03 21:15:24 +02:00
Bart Schuurmans 4d5a30d953 Add index for looking up KeyPair by remote id 2024-04-03 21:11:27 +02:00
Bart Schuurmans 5cfe7eca6f Add index for finding all statuses in a thread 2024-04-03 21:11:09 +02:00
Bart Schuurmans 5082806b82
Merge pull request #3338 from Minnozz/fix-nginx-location
Make nginx config safer
2024-04-03 19:22:16 +02:00
Mouse Reeve d1d91f0c2b
Merge pull request #3347 from bookwyrm-social/dependabot/pip/pillow-10.3.0
Bump pillow from 10.2.0 to 10.3.0
2024-04-03 10:01:59 -07:00
dependabot[bot] ea0ade955b
Bump pillow from 10.2.0 to 10.3.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 16:45:11 +00:00
Mouse Reeve f085d3d0fe
Merge pull request #3346 from Minnozz/status-remote-id-index
Add index on Status.remote_id
2024-04-02 13:02:35 -07:00
Bart Schuurmans 4bbdd0b2d0 Add index on Status.remote_id
This field is often used in WHERE-clauses in queries that are very slow on bookwyrm.social.
2024-04-02 21:54:30 +02:00
Mouse Reeve f28800af7f
Merge pull request #3339 from Minnozz/fix-file-leaks
Fix resource leaks
2024-03-31 12:43:19 -07:00
Bart Schuurmans 72ed878eeb
Merge pull request #3343 from Minnozz/update-codeql
Update CodeQL workflows to v3
2024-03-30 22:01:49 +01:00
Bart Schuurmans f666951934 Update CodeQL workflows to v3
https://github.blog/changelog/2024-01-12-code-scanning-deprecation-of-codeql-action-v2/
2024-03-30 21:56:44 +01:00
Holger Brunn fcd0087589 [FIX] make sure to get Pillow>=10 compatible pilkit 2024-03-30 01:58:41 +01:00
Bart Schuurmans ffee29d8e2 Fix resource leaks
Rewrite places where files (or other resources) are opened but not closed to "with" blocks, which
automatically call close() at the end of the scope.

Also simplify some tests where images need to be saved to a model field: an opened file can be
passed directly to FileField.save().
2024-03-29 20:14:10 +01:00
Bart Schuurmans 75bc4f8cb0 Make nginx config safer
Instead of allowing all image files anywhere, and disallowing non-image file under /images/, only
allow image files under /images/ and don't match non-image files elsewhere. They get proxied to web
instead and result in a 404 there.

For example, the old config allowed /exports/foo.jpg to be served, while the new config does not.
2024-03-29 15:04:38 +01:00
Bart Schuurmans e7ae0fdf93
Merge pull request #3337 from prolibre/apport-perso
flower 2.0.1 fixes a few link bugs (particularly for favicon)
2024-03-29 14:45:59 +01:00
Bart Schuurmans 5d597f1ca9 Use new "with ()" style 2024-03-29 14:25:08 +01:00
Bart Schuurmans 0ac9d12d1c Merge branch 'main' into user-export 2024-03-29 14:23:10 +01:00
Bart Schuurmans e74de94640
Merge pull request #3334 from ccamara/patch-1
Remove twitter from README.md
2024-03-29 14:21:49 +01:00
Bart Schuurmans 1464d09a43
Merge pull request #3320 from dato/better-fmt-patch-calls
bulk-fmt: bracket-wrap calls to patch() for better readability
2024-03-29 14:19:16 +01:00
Anthony 2272e7a326 flower 2.0.1 fixes a few link bugs (particularly for favicon) 2024-03-29 12:07:52 +01:00
Bart Schuurmans 2bbe3d4c32 Test user export archive contents 2024-03-28 13:50:55 +01:00
Bart Schuurmans bb5d8152f1 Fix mypy error 2024-03-28 13:21:30 +01:00
Bart Schuurmans dabf7c6e10 User export testing fixes 2024-03-28 13:09:21 +01:00
Bart Schuurmans cdbc1d172c Fix double exports subdir in S3 user export 2024-03-27 23:28:24 +01:00
Adeodato Simó 3133a47b7c
Merge from main into 'better-fmt-patch-calls'
Conflicts:
	bookwyrm/tests/test_book_search.py
2024-03-27 17:13:08 -03:00
Bart Schuurmans c6ca547d58 Fix migration formatting 2024-03-27 20:41:59 +01:00
Bart Schuurmans 797d5cb508 Update BookwyrmExportJob tests 2024-03-27 20:39:57 +01:00
Adeodato Simó 699d637bae
Fix detection of unlisted posts (#3258)
Merged from dato/fix_unlisted_set_from_activity.
2024-03-27 16:29:09 -03:00
Bart Schuurmans 9afd0ebb54 Update migrations 2024-03-27 20:15:06 +01:00
Bart Schuurmans 9685ae5a0a Consolidate BookwyrmExportJob into two tasks
Creating the export JSON and export TAR are now the only two tasks.
2024-03-27 20:13:49 +01:00
Carlos Cámara 98600440d8
Remove twitter from README.md
The Twitter/X account doesn't seem to exist, so removing the badge
2024-03-26 17:14:09 +00:00
Bart Schuurmans ed2e9e5ea8 Merge migration 2024-03-26 13:41:39 +01:00
Bart Schuurmans ef57c0bc8b Check last user export too in post handler 2024-03-26 13:41:39 +01:00
Bart Schuurmans 145c67dd21 Merge BookwyrmExportJob export_data field back into one with dynamic storage backend 2024-03-26 13:41:39 +01:00
Bart Schuurmans 6a67943408
Merge branch 'main' into user-export 2024-03-26 13:15:40 +01:00
Mouse Reeve 9dfa218ba5
Merge pull request #3333 from bookwyrm-social/locales
Updates locales and version number
2024-03-25 16:36:51 -07:00
Mouse Reeve bf52eeaa9e Bump version to 0.7.3. 2024-03-25 16:15:02 -07:00
Mouse Reeve 011e4a27a6 Updates locales and adds missing trimmed on blocktrans 2024-03-25 16:13:00 -07:00
Mouse Reeve 7192449b21
Merge pull request #3325 from Minnozz/author-search-vector
Rework author search
2024-03-25 14:41:25 -07:00
Bart Schuurmans d9bf848cfa Fix pylint warnings 2024-03-25 18:25:43 +01:00
Bart Schuurmans bd95bcd50b Add test for special character in cover filename 2024-03-25 18:14:45 +01:00
Bart Schuurmans f721289b1d Simplify logic for rendering user exports 2024-03-25 18:14:45 +01:00
Bart Schuurmans a51402241b Refactor creation of user export archive 2024-03-25 18:14:45 +01:00
Bart Schuurmans e0decbfd1d Fix urlescaped relative path to cover image in export
Fixes #3292
2024-03-25 17:59:39 +01:00
Bart Schuurmans aee8dc16af Fix pylint warning 2024-03-24 13:27:01 +01:00
Bart Schuurmans 5bd66cb3f7 Only generate signed S3 link to user export when user clicks download 2024-03-24 13:08:33 +01:00
Bart Schuurmans ab7b0893e0 User exports: handle files that no longer exist on file storage 2024-03-24 12:47:26 +01:00
Bart Schuurmans 471233c1dc Use different export job fields for the different storage backends
This way, the database definition is not depdendent on the runtime configuration.
2024-03-24 12:46:42 +01:00
Bart Schuurmans 073f62d5bb Add exports_volume to docker-compose.yml
Exports should be written to a Docker volume instead of to the bind mount (= source directory). This
way they are shared between different containers even when they run on different machines.
2024-03-24 12:08:29 +01:00
Bart Schuurmans a770689245 Merge branch 'main' into user-export 2024-03-24 12:07:14 +01:00
Bart Schuurmans 69f464418d Remove problematic migration
This migration is dependent on the runtime configuration (.env); a structural fix will follow.
2024-03-24 12:06:44 +01:00
Bart Schuurmans f11c80162a
Merge pull request #3331 from Minnozz/revert-docker-mount-ro
Revert "docker-compose.yml: make all bind mounts read only"
2024-03-24 11:30:56 +01:00
Bart Schuurmans 7c2fa746ae Revert "docker-compose.yml: make all bind mounts read only"
This reverts commit 864304f128.
2024-03-24 11:23:23 +01:00
Hugh Rundle 03587dfdc7
migrations 2024-03-24 20:56:20 +11:00
Hugh Rundle dd27684d4b
set signed s3 url expiry with env value
Adds S3_SIGNED_URL_EXPIRY val to .env and settings (defaults to 15 mins)
Note that this is reset every time the user loads the exports page
and is independent of the _creation_ of export files.
2024-03-24 20:53:49 +11:00
Bart Schuurmans caebebeb37
Merge pull request #3261 from bSolt/book-series-3256
Add book series by title in feed posts
2024-03-23 20:01:03 +01:00
Bart Schuurmans 592914dc91 Render series number with comma and outside of link on book page 2024-03-23 19:51:20 +01:00
Bart Schuurmans 2915133223
Merge branch 'main' into book-series-3256 2024-03-23 19:37:07 +01:00
Bart Schuurmans 2d2ccd51df Factor out book series info into separate template 2024-03-23 19:35:24 +01:00
Bart Schuurmans 4a690e675a BookDataModel: add dry_run argument to merge_into 2024-03-23 19:28:57 +01:00
Bart Schuurmans fb82c7a579 Add test for merging authors 2024-03-23 19:28:57 +01:00
Bart Schuurmans 6f191acb27 BookDataModel: fix absorbing data from array and partial date fields 2024-03-23 19:28:57 +01:00
Bart Schuurmans 7fb079cb43 PartialDate: fix __eq__ method 2024-03-23 19:28:57 +01:00
Bart Schuurmans 7066e2815b BookDataModel.merge_into: return and log absorbed fields 2024-03-23 19:28:57 +01:00
Bart Schuurmans e04cd79ff8 Redirect to new URL when a merged object is requested 2024-03-23 19:28:57 +01:00
Bart Schuurmans 5e123972e8 BookDataModel: implement merge_into method 2024-03-23 19:28:57 +01:00
Bart Schuurmans b3753ab6da Add MergedBookDataModel 2024-03-23 19:28:57 +01:00
Bart Schuurmans b8995bd4b1 Add tests for author search 2024-03-23 19:26:51 +01:00
Bart Schuurmans 769d9726e5 Add book search test cases for author aliases 2024-03-23 19:26:51 +01:00
Bart Schuurmans 36222afa79 Switch author search from TrigramSimilarity to SearchQuery 2024-03-23 19:26:51 +01:00
Bart Schuurmans 0795b4d171 Include Author aliases in Book search vector 2024-03-23 19:26:51 +01:00
Bart Schuurmans 2de35f3fc7 Calculate Author search vector with name and aliases 2024-03-23 19:26:51 +01:00
Mouse Reeve bac52eef3e
Merge pull request #3275 from ccamara/wikidata
Add wikidata field for authors
2024-03-23 08:12:09 -07:00
Mouse Reeve 8bbac458a6
Merge pull request #3217 from dato/switch_edition_invalidate_active_shelves
Invalidate `active_shelf` when switching editions
2024-03-23 07:59:40 -07:00
Mouse Reeve 5b71e94888
Merge branch 'main' into user-export 2024-03-23 07:55:46 -07:00
Mouse Reeve a914a44fba
Removes unnecessary redeclaration of wikidata model field in Author 2024-03-23 07:54:54 -07:00
Mouse Reeve 8e088a6d53
Merge branch 'main' into switch_edition_invalidate_active_shelves 2024-03-23 07:53:24 -07:00
Mouse Reeve b508b4cd33
Merge pull request #3323 from Minnozz/docker-bind-ro
Docker: make bind mounts of source code read only
2024-03-23 07:51:00 -07:00
Mouse Reeve 886d6ec9f7
Merge branch 'main' into docker-bind-ro 2024-03-23 07:48:27 -07:00
Mouse Reeve 21f75da75e
Merge pull request #3328 from Minnozz/escape-query-in-link
Escape search query in generated URLs
2024-03-23 07:46:04 -07:00
Mouse Reeve 20db968315
Merge pull request #3322 from Minnozz/fix-font-download
Fix font download
2024-03-23 07:36:43 -07:00
Bart Schuurmans c3d25c59c5 Escape search query in generated URLs
Otherwise, a query containing '&' or other special characters results in a broken URL.
2024-03-21 16:48:34 +01:00
Bart Schuurmans 3cde6dbe5a
Merge pull request #3326 from Minnozz/black-required-version
black: specify major version 22 only
2024-03-21 16:30:56 +01:00
Bart Schuurmans 682bb3b62f dev-tools: relax black version constraint 2024-03-21 16:25:29 +01:00
Bart Schuurmans b5b9eddaf0 CI: relax black version constraints 2024-03-20 12:46:37 +01:00
Bart Schuurmans ab430e0208 requirements.txt: add black
This way, IDEs can be set up to use the black version from the environment instead of a globally
available/bundled black version.
2024-03-20 12:43:17 +01:00
Bart Schuurmans e13e4237f4 black: specify required-version
This ensures consistent formatting among different contributors / development setups.

https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#required-version
2024-03-20 12:26:21 +01:00
Bart Schuurmans 762786839c
Merge pull request #3134 from dato/trigger_migrations
Support trigger migrations
2024-03-20 12:11:34 +01:00
Bart Schuurmans 4ca52c0b38
Merge branch 'main' into trigger_migrations 2024-03-20 11:47:54 +01:00
Bart Schuurmans 6a87713f9f Recalculate all book search vectors after fixing the author trigger 2024-03-20 11:45:12 +01:00
Mouse Reeve d08147c6d9
Merge pull request #3244 from bookwyrm-social/dependabot/pip/pillow-10.2.0
Bump pillow from 10.0.1 to 10.2.0
2024-03-19 15:10:30 -07:00
Bart Schuurmans f423834bd0 Catch the correct exception type from Pillow 2024-03-19 12:42:52 +01:00
Mouse Reeve d304ceb437
Merge pull request #3324 from bookwyrm-social/dependabot/pip/django-3.2.25
Bump django from 3.2.24 to 3.2.25
2024-03-18 15:05:30 -07:00
dependabot[bot] 47afe34d97
Bump django from 3.2.24 to 3.2.25
Bumps [django](https://github.com/django/django) from 3.2.24 to 3.2.25.
- [Commits](https://github.com/django/django/compare/3.2.24...3.2.25)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 21:48:21 +00:00
Bart Schuurmans 4d23edddca Make sure /images/ and /static/ exist now that the bind mount is read only
Otherwise the static_volume and media_volume can't be mounted there.
2024-03-18 21:35:12 +01:00
Bart Schuurmans 68cb94daf2 docker-compose.yml: don't automatically start dev-tools by assigning profile 2024-03-18 21:34:51 +01:00
Bart Schuurmans 864304f128 docker-compose.yml: make all bind mounts read only
Except dev-tools, since it needs to be able to change the source.
2024-03-18 21:34:09 +01:00
Bart Schuurmans 7690247ab4 Font download: log the exact error 2024-03-18 20:34:47 +01:00
Bart Schuurmans 3367b20965 Font download: destination dir is allowed to exist
Without this argument, an existing directory (but not the file) causes an error.
2024-03-18 20:23:31 +01:00
Bart Schuurmans 748418590f docker-compose.yml: mount static_volume for flower
Because flower also uses BookwyrmConfig, it wants to download fonts, and will download them to an
incorrect location if the static_volume is not mounted.
2024-03-18 20:22:19 +01:00
Bart Schuurmans ccf2b16d73 requirements.txt: make typing-Pillow match Pillow 2024-03-18 19:52:40 +01:00
dependabot[bot] 3be227fc86 Bump pillow from 10.0.1 to 10.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.0.1 to 10.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.0.1...10.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 19:51:24 +01:00
Adeodato Simó a6dc5bd13f
Make get_file_size robust against typing errors 2024-03-18 15:03:07 -03:00
Adeodato Simó 518da3b9cf Merge from main into 'user-export'
Conflicts:
	bookwyrm/models/bookwyrm_export_job.py
	requirements.txt
2024-03-18 14:47:34 -03:00
Adeodato Simó 2cf7ed477d Consolidate test_posgres.py into test_book_search.py
These are tests I missed when first writing trigger tests in
test_book_search.py.
2024-03-17 22:38:44 -03:00
Adeodato Simó cceccd1ecf
Merge from main into 'trigger_migrations'
Conflicts:
	requirements.txt
2024-03-17 21:54:15 -03:00
Adeodato Simó beb49af514
Upgade django-pgtrigger to 4.11 2024-03-17 21:46:34 -03:00
Adeodato Simó 90bd893568 Fix remaining instances of bad-classmethod-argument 2024-03-17 21:28:55 -03:00
Adeodato Simó e2c9ea3cd2 Fix instances of bad-classmethod-argument in recently edited files 2024-03-17 21:28:55 -03:00
Adeodato Simó 4b9fe0af0c Remove nesting in several with..patch calls 2024-03-17 20:57:39 -03:00
Adeodato Simó 1b9e0546e6 Bracket-wrap calls to patch() for better readability 2024-03-17 20:34:12 -03:00
Bart Schuurmans 8cf52e0a77
Merge pull request #3318 from Minnozz/ci-annotations
CI: update pytest setup and show annotations on PRs
2024-03-17 11:24:01 +01:00
Bart Schuurmans 0282e20b89
Merge branch 'main' into book-series-3256 2024-03-16 11:23:40 +01:00
Bart Schuurmans 4e20e43037 CI: merge all Python actions into one file 2024-03-13 23:36:26 +01:00
Bart Schuurmans 383e6533e1 CI: use pytest-github-actions-annotate-failures 2024-03-13 23:35:05 +01:00
Bart Schuurmans 74fdd9a85a CI: simplify pytest setup 2024-03-13 23:35:05 +01:00
Bart Schuurmans 6af0a08838 CI: use actions/setup-python@v5 and cache pip 2024-03-13 23:35:03 +01:00
Bart Schuurmans 12b469a0d6 CI: use actions/checkout@v4 2024-03-13 23:33:40 +01:00
Mouse Reeve 288743b686
Merge pull request #3315 from Minnozz/fix-pytest-env
pytest.ini: define ALLOWED_HOSTS
2024-03-13 15:29:15 -07:00
Mouse Reeve a3465e6154
Merge pull request #3303 from MaggieFero/main
Upgrade Python Version and Several Other Packages for Security
2024-03-13 15:28:54 -07:00
Bart Schuurmans 3ba528ecdd pytest.ini: define ALLOWED_HOSTS
This fixes running `./bw-dev pytest` locally when having a different value defined for
`ALLOWED_HOSTS` in `.env`.
2024-03-11 20:12:46 +01:00
Adeodato Simó 304c47863b
FileLinkForm: fix duplicate check (#3311)
Merged from: Minnozz/filelink-duplicate-check.
2024-03-11 15:10:28 -03:00
Mouse Reeve b68a4cc392
Merge branch 'main' into filelink-duplicate-check 2024-03-09 07:37:26 -08:00
Mouse Reeve 6dfb5000cc
Merge pull request #3305 from dato/export_catch_missing_key_icon
json_export: also detect absent "icon" key
2024-03-09 07:37:14 -08:00
Bart Schuurmans 8d018b872f FileLinkForm: fix duplicate check 2024-03-09 15:49:42 +01:00
Adeodato Simó 9e7b040b73
Fix shelving date changing when changing editions (#3193)
Merged from  from jakejack13/switch-edition
Fixes: #3139.
2024-03-03 18:48:04 -03:00
Adeodato Simó 09c3d9c0dc
json_export: also detect absent "icon" key 2024-03-03 18:42:27 -03:00
Mouse Reeve dd9d68c97d
Merge pull request #3096 from bookwyrm-social/image-ap-serialization
Changes to how images are serialized
2024-03-02 18:58:08 -08:00
Margaret Fero d138395c75 Add linter exclusion for TBookWyrmModel 2024-03-02 17:43:49 -08:00
Margaret Fero 91fe4ad535 Fix spacing for linter 2024-03-02 17:31:16 -08:00
Margaret Fero 9fa09d5ebe Add extra space required by linter 2024-03-02 17:30:37 -08:00
Margaret Fero eadb0e640f Fix typo in operator 2024-03-02 17:29:42 -08:00
Margaret Fero be140d5e5a Pin setuptools at 65.5.1 2024-03-02 17:20:48 -08:00
Margaret Fero 22c4155c7c Upgrade pytest to 6.2.5 2024-03-02 16:09:34 -08:00
Margaret Fero 498dc35d99 Upgrade Pylint to 2.15.0 2024-03-02 16:09:06 -08:00
Margaret Fero 0f5a3e9163 Pin Tornado at 6.3.3 2024-03-02 16:08:41 -08:00
Margaret Fero da2636fa29 Add grpcio pin @ 1.57.0 2024-03-02 16:07:50 -08:00
Margaret Fero c1520da56d Upgrade flower to 2.0.0 2024-03-02 16:05:11 -08:00
Margaret Fero fee3fdd5a8 Upgrade django-compressor to 4.4 2024-03-02 16:04:37 -08:00
Margaret Fero c944824ac7 Upgrade django-celery-beat to 2.5.0 2024-03-02 16:04:06 -08:00
Margaret Fero 4312e9bba0 Upgrade Celery to 5.3.1 2024-03-02 16:03:19 -08:00
Margaret Fero 39da471f79 Disable Pylint Failure for imghdr deprecation for now 2024-03-02 15:59:17 -08:00
Margaret Fero 570017d3b0 Upgrade Python Version from 3.9 to 3.11 2024-03-02 15:57:06 -08:00
Margaret Fero 3652ac8100
Alphabetize requirements.txt
Alphabetize requirements.txt for developer convenience; this helps to find duplicates and unnecessarily-pinned subdependencies, as well as making the file easier to read and use.
2024-03-02 15:41:06 -08:00
Margaret Fero f8fd76cff0
Remove duplicate types-requests==2.31.0.2
The types-requests==2.31.0.2 dependency was double-listed right next to each other; this commit removes one.
2024-03-02 13:57:09 -08:00
Margaret Fero 206ed9f7fb
Merge pull request #2 from bookwyrm-social/main
No Actual Changes
2024-03-02 13:55:24 -08:00
Mouse Reeve 218171e9bc
Merge pull request #3300 from MaggieFero/MaggieFero-add-timeouts-to-requests.get
Add timeouts to requests.get
2024-03-01 22:49:44 -08:00
Margaret Fero 50b811d9aa
Typo fix
Add a comma
2024-03-01 20:11:14 -08:00
Margaret Fero 1ae9870862
Add timeout to base_activity.py
An instance of requests.get was missing a timeout; this commit adds a timeout of 15 as used in other places in this codebase which already have timeouts.
2024-03-01 20:02:40 -08:00
Margaret Fero db97d76a24
Add timeout to isbn.py
An instance of requests.get in isbn.py lacks a timeout, and this commit adds one with a default of 15 as used other places in the code, where requests.get does already have a timeout.
2024-03-01 19:58:11 -08:00
Mouse Reeve 354388cc8f
Merge pull request #3238 from hughrun/export-fixes
fix multiple issues from user exports config changes
2024-02-29 16:16:25 -08:00
Mouse Reeve 2c59908ddd
Merge branch 'main' into export-fixes 2024-02-29 16:10:20 -08:00
Mouse Reeve 6a70eadba8
Merge pull request #3284 from NetspherePub/072NginxSecurityFixed
Adds production.conf security configuration missing in version 0.7.2
2024-02-29 15:55:56 -08:00
Mouse Reeve ec52460f02
Merge pull request #3274 from Minnozz/author-search
Add search for author
2024-02-29 15:55:12 -08:00
Adeodato Simó 1fabe51261
Move ratings and reviews when switching editions (#3117)
Merged from mattlehrer/move-ratings-and-reviews-when-switching-editions.
Fixes: #2926.
2024-02-21 18:48:32 -03:00
Adeodato Simó e6b6bd648d
Merge branch 'main' into move-ratings-and-reviews-when-switching-editions 2024-02-21 18:42:18 -03:00
Mouse Reeve 9d7965780d
Merge pull request #3285 from polarbirke/fix-label-input-association-for-shelves-filter
Fix label and input association for shelves filter
2024-02-20 16:56:57 -08:00
Mouse Reeve 333fb03c2c
Merge pull request #3290 from bookwyrm-social/korean-locale
Korean locale
2024-02-20 16:56:26 -08:00
Mouse Reeve 8f537ef56a Adds missing migration for Korean locale 2024-02-20 16:45:16 -08:00
Mouse Reeve 6163e1a6be
Merge pull request #3283 from NetspherePub/ko_KR
Add Korean (ko-kr) to LANGUAGES and locale.
2024-02-20 16:44:31 -08:00
Ross Chapman dd1999eb8e
Adds view tests for shelf filters (#3162)
* Adds test file

* Adds success assertion

* Updates tests

* Updates shelf books creation

* Updates assertion to use isbn for Edition model

* Updates query

* trigger workflow test

* Updates validate_html

* Updates comment and test

* Fixes none test

* Adds management command to clear all deleted user data

* Adds success message

---------

Co-authored-by: Mouse Reeve <mousereeve@riseup.net>
Co-authored-by: Mouse Reeve <mouse.reeve@gmail.com>
2024-02-20 16:25:01 -08:00
Søren Birkemeyer 4c0d5ede86 Fix label and input association for shelves filter
This PR correctly associates label and text input of the shelves
filter via for- and id-attributes. With the association in place,
the aria-label can be removed (the label will be announced by
assistive software when the input is focused). This also fixes the
issue that the aria-label was not translated, whereas the label is.
2024-02-10 16:24:52 +00:00
FoW 1c587c5e53 Adds production.conf security configuration missing in version 0.7.2 2024-02-10 17:54:25 +09:00
FoW ddd13a3e2e Add Korean (ko-kr) to LANGUAGES and locale 2024-02-10 16:17:25 +09:00
Mouse Reeve 7469f1f4ca
Merge pull request #3281 from bookwyrm-social/dependabot/pip/django-3.2.24
Bump django from 3.2.23 to 3.2.24
2024-02-07 14:54:50 -08:00
dependabot[bot] 363cb79951
Bump django from 3.2.23 to 3.2.24
Bumps [django](https://github.com/django/django) from 3.2.23 to 3.2.24.
- [Commits](https://github.com/django/django/compare/3.2.23...3.2.24)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-07 22:50:00 +00:00
Hugh Rundle 46a158d701
Merge branch 'main' into export-fixes 2024-02-06 18:31:19 +11:00
Hugh Rundle 8773caa26b
Merge pull request #4 from dato/data_upload_max_size_mb
Support DATA_UPLOAD_MAX_MEMORY_MiB, only, in .env
2024-02-06 18:25:44 +11:00
Carlos Camara 89d8537e1b Add wikidata field to author's template 2024-02-05 22:08:34 +00:00
Carlos Cámara 71f527eb1b
Merge branch 'main' into wikidata 2024-02-04 20:34:51 +01:00
Adeodato Simó 4a9d69e169
Support DATA_UPLOAD_MAX_MEMORY_MiB, only, in .env
Since arithmetic is not allowed in .env files, a change in unit for
the variable seems most usable.
2024-02-04 15:34:04 -03:00
Mouse Reeve d97747078e
Merge pull request #3276 from bookwyrm-social/fixes-version-number
Fixes version number mistakenly reverted
2024-02-03 19:49:48 -08:00
Mouse Reeve db629255db Fixes version number mistakenly reverted 2024-02-03 18:27:59 -08:00
Carlos Cámara 6ac38564e2 Add wikidata field for authors 2024-02-03 22:55:33 +00:00
Bart Schuurmans 6c9ca0bf19 Add search for author 2024-02-03 21:55:46 +01:00
Mouse Reeve 6b1ffbc634
Merge pull request #3185 from bookwyrm-social/check-version-number
Check version number asynchronously
2024-02-03 08:25:52 -08:00
Mouse Reeve 748c934986 Merge migrations upon merge migrations 2024-02-03 08:20:12 -08:00
Mouse Reeve f7580c59a5 Merge branch 'main' into check-version-number 2024-02-03 08:19:46 -08:00
Mouse Reeve 4e2b8af147 Adds merge migration 2024-02-03 08:02:51 -08:00
Mouse Reeve 48f8ee57a6 Merge branch 'main' into check-version-number 2024-02-03 08:02:15 -08:00
Mouse Reeve faf45cf956
Merge pull request #3273 from bookwyrm-social/WesleyAC-freeform-page-number
Allow page numbers to be text, instead of integers
2024-02-03 08:00:34 -08:00
Mouse Reeve a1ac9494b2 Allow admins to un-schedule tasks 2024-02-03 08:00:07 -08:00
Mouse Reeve 6d5752fb4e Adds merge migration for page numbering fix 2024-02-03 07:40:23 -08:00
Mouse Reeve 37aa7ad2f6 Merge branch 'freeform-page-number' of github.com:WesleyAC/bookwyrm into WesleyAC-freeform-page-number 2024-02-03 07:38:02 -08:00
Mouse Reeve e0667c6a03
Merge pull request #3237 from Minnozz/status-title-description
Improve OpenGraph tags for status and book pages
2024-02-03 07:37:00 -08:00
Mouse Reeve 103da863c4
Merge pull request #3239 from Minnozz/user-agent
Replace python-requests with BookWyrm in user agent
2024-02-03 07:27:58 -08:00
Mouse Reeve fa66284000
Merge pull request #3253 from skobkin/patch-autocomplete-fictionbook-format
Adding FictionBook format ("FB2", "FB3") to autocomplete options in "get a copy" block.
2024-02-03 07:26:58 -08:00
Mouse Reeve 0f0420ce04
Merge pull request #3257 from dato/prefer_shared_inbox
Use shared inboxes for mentions too
2024-02-03 07:25:51 -08:00
Mouse Reeve 438d88d8d4
Merge pull request #3260 from bSolt/fix-widths-2023
Fix awkward layout for tablets on /confirm-email, /login, /invite, and /preferences/reactivate
2024-02-03 07:18:35 -08:00
Mouse Reeve 5f2f321ed5
Merge branch 'main' into export-fixes 2024-02-03 07:04:05 -08:00
Mouse Reeve 45cc3dc979
Merge pull request #3249 from dato/cookie_age_setting
Set SESSION_COOKIE_AGE from environment
2024-02-03 07:03:12 -08:00
Mouse Reeve 9c5f6c527b
Fixes translation tags 2024-02-03 06:51:23 -08:00
Mouse Reeve efa29b269c
Merge pull request #3269 from bookwyrm-social/dependabot/pip/aiohttp-3.9.2
Bump aiohttp from 3.9.0 to 3.9.2
2024-01-30 18:03:00 -08:00
Jacob Kerr 2ba7dff845 Fixed shelving date changing when changing editions 2024-01-30 16:53:59 -05:00
Hugh Rundle 21a8570035
Merge pull request #3207 from rsk2/issue-3187
Hide "year in the books" for newly registered users
2024-01-31 07:03:43 +11:00
Hugh Rundle ef6fd608fa
Merge branch 'main' into issue-3187 2024-01-30 18:47:07 +11:00
dependabot[bot] b05621005e
Bump aiohttp from 3.9.0 to 3.9.2
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.0 to 3.9.2.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.0...v3.9.2)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 00:10:13 +00:00
Hugh Rundle 3675a4cf3f
disable user exports if using azure 2024-01-29 14:28:30 +11:00
Hugh Rundle 5f7be848fc
subclass boto3 session instead of adding new env value
Thanks Dato!
2024-01-29 14:10:36 +11:00
Hugh Rundle f96ddaa3e1
Merge pull request #3 from dato/export_job_inject_aws_endpoint_setting
Subclass boto3.Session to use AWS_S3_ENDPOINT_URL
2024-01-29 13:49:45 +11:00
Hugh Rundle adff3c4251
allow user exports with s3
also undoes a line space change in settings.py to make the PR cleaner
2024-01-29 13:45:35 +11:00
Hugh Rundle 765fc1e43d
fix tests 2024-01-29 12:28:37 +11:00
Adeodato Simó c106b2a988
Subclass boto3.Session to use AWS_S3_ENDPOINT_URL
As of 0.1.13, the s3-tar library uses an environment variable
(`S3_ENDPOINT_URL`) to determine the AWS endpoint. See:
https://github.com/xtream1101/s3-tar/blob/0.1.13/s3_tar/utils.py#L25-L29.

To save BookWyrm admins from having to set it (e.g., through `.env`)
when they are already setting `AWS_S3_ENDPOINT_URL`, we create a Session
class that unconditionally uses that URL, and feed it to S3Tar.
2024-01-28 22:21:44 -03:00
Hugh Rundle 2c231acebe
linting and tests 2024-01-28 20:35:47 +11:00
Hugh Rundle a3e05254b5
fix avatar import path 2024-01-28 15:56:44 +11:00
Hugh Rundle 582e97e4a5
Merge branch 'image-serialize' into user-export
pulls Mouse's fix for imagefile serialization
2024-01-28 15:12:15 +11:00
Hugh Rundle 0d619f7eb4
Merge branch 'main' into user-export 2024-01-28 15:11:02 +11:00
Hugh Rundle 2bb9a85591
various fixes
- use signed url for s3 downloads
- re-arrange tar.gz file to match original
- delete all working files after tarring
- import from s3 export

TODO

- check local export and import
- fix error when avatar missing
- deal with multiple s3 storage options (e.g. Azure)
2024-01-28 15:07:55 +11:00
Braden Solt 6add81cf15 move outside of authors "if" 2024-01-27 11:02:42 -07:00
Braden Solt 629acbaa19 add series number on posts in the feed 2024-01-27 10:58:57 -07:00
Braden Solt 940274b1c2 classes that fix widths 2024-01-26 15:47:55 -07:00
Adeodato Simó accb3273f1
When determining privacy, check for unlisted early
If `followers_url` is found in `to`, the post may still be _unlisted_
if `"https://www.w3.org/ns/activitystreams#Public"` appears in `cc`.
Hence this should be checked earlier.
2024-01-26 06:45:54 -03:00
Adeodato Simó 8ac873419f
refactor: eagerly use a set in recipients, get_recipients 2024-01-26 06:29:59 -03:00
Adeodato Simó 31babdfa51
Always prefer shared inboxes when computing receipent lists
This avoids duplicate submissions to remote instances when mentioning
followers (i.e., `POST /user/foo/inbox` followed by `POST /inbox`, which
results in two separate `add_status` tasks, and might generate duplicates
in the target instance).
2024-01-26 06:18:02 -03:00
Adeodato Simó 80ad36e75b
Include SESSION_COOKIE_AGE in .env.example
Suggested-by: Alexey Skobkin <skobkin-ru@ya.ru>
2024-01-25 20:28:15 +01:00
Adeodato Simó 500e4eb4f5
Merge from main to avoid conflicts 2024-01-25 20:27:54 +01:00
Adeodato Simó 82f9aa9da4
Set SESSION_COOKIE_AGE from environment, default to one month
While we do wish for a longer maximum age (up to one year, see #3082),
we only want to do that after termination of active sessions is
implemented (see #2278).

In the meantime, by reading and setting the variable from settings,
we allow site admins to alter the default.
2024-01-25 20:27:24 +01:00
Alexey Skobkin 2d4b11aaee
Adding FictionBook format ("FB2", "FB3") to autocomplete options in "Get a copy" block. 2024-01-25 01:50:10 +03:00
Mouse Reeve 193aeff4d2
Merge pull request #3245 from WesleyAC/redis-aof-auto-compact
Add redis automatic rewrite configuration.
2024-01-24 08:26:45 -08:00
Rohan Sureshkumar c4596544a3 Issue-3187: fix failing tests 2024-01-24 19:18:46 +05:30
Wesley Aptekar-Cassels 30ba8d37dc Add redis automatic rewrite configuration.
This should hopefully prevent the AOF file from growing too large.
2024-01-23 18:19:31 -05:00
Bart Schuurmans eb6bea013f Fix pylint warning 2024-01-21 11:04:08 +01:00
Bart Schuurmans 646b27b7a7 OpenGraph: fall back on book cover when preview images are disabled 2024-01-20 17:34:52 +01:00
Bart Schuurmans ea9d3f8ba1 Use Status.page_image for OpenGraph tags 2024-01-20 17:34:52 +01:00
Bart Schuurmans 290ee997b3 Refactor OpenGraph tags logic 2024-01-20 17:34:52 +01:00
Bart Schuurmans ad56024ffe Add Status.page_image property 2024-01-20 17:34:52 +01:00
Bart Schuurmans f7b4d9ea50 Give individual status page a title and OpenGraph description 2024-01-20 17:34:52 +01:00
Bart Schuurmans 6cb3b97144 Replace python-requests with BookWyrm in user agent
Fixes #3108
2024-01-20 16:15:17 +01:00
Hugh Rundle a563275308
fix comment in env example 2024-01-20 13:27:30 +11:00
Hugh Rundle ddc35a7a52
fix multiple issues from user exports config changes
- improve nginx config
- fix DATA_UPLOAD_MAX_MEMORY_SIZE default not being an int
- translate fallback value in id_to_username template tag
- make location of setting to turn on user exports easier to locate for admins

fixes #3227
fixes #3231
fixes #3232
fixes #3236
2024-01-20 13:19:13 +11:00
Hugh Rundle 26c37de2d4
linting 2024-01-20 07:16:42 +11:00
Mouse Reeve fd0b1d90b0
Merge pull request #3229 from verymilan/nginx-ttf
nginx: fix missing ttf static files
2024-01-18 14:43:05 -08:00
Milan dd5c314bd5 nginx: also serve svg static files 2024-01-18 22:29:43 +01:00
Milan a59dcfc890 nginx: fix missing ttf static files 2024-01-18 17:03:02 +01:00
Rohan Sureshkumar 8e2649ba3b Issue-3187: change variable name and code formatting 2024-01-18 21:23:25 +05:30
Rohan d73141792d
Merge branch 'main' into issue-3187 2024-01-18 21:19:20 +05:30
Hugh Rundle 469172947b
cleanup and linting 2024-01-18 18:43:45 +11:00
Hugh Rundle 833f26fd0e
Merge branch 'main' into user-export 2024-01-18 18:24:56 +11:00
Mouse Reeve fb5fae4251
Merge pull request #3219 from bSolt/issue-3178
Fix awkward clipping on about page
2024-01-17 15:31:52 -08:00
Mouse Reeve c22f189c86
Merge pull request #3216 from dato/dev-tools_require_bookworm
Ensure dev-tools uses bookworm
2024-01-17 15:31:43 -08:00
Mouse Reeve 61a6ee29d8
Merge pull request #3224 from hughrun/move-fix
Pass correct user id in Move notification
2024-01-17 14:25:28 -08:00
Mouse Reeve a585321ef9
Merge pull request #3226 from hughrun/disable-exports
Disable user exports
2024-01-17 14:23:29 -08:00
Hugh Rundle 45d6f1f890
Merge pull request #3215 from ccamara/export_bookshelf
Export bookshelves and review date
2024-01-17 21:20:32 +11:00
Hugh Rundle b990d9ccd8
Pass correct user id in Move notification
We were passing the *requesting* user's moved_to value to the Move notification template, instead of the id of the user that they are being notified about.
Additionally, the id_to_username template tag had no fallback for if the user_id is None.

This resolves both problems and removes an unnecessary space in a template for when the logged in user made the move.

Fixes #3196
2024-01-17 21:06:04 +11:00
Hugh Rundle ea7f3c297e
allow js and css 2024-01-17 20:12:06 +11:00
Hugh Rundle d640e4ac96
disable user exports by default
- new setting to enable user exports defaults to False
- add setting to enable and disable user exports
- do not allow user exports when using s3 storage
- do not serve non-image files from /images/ (requires update to nginx settings)
- increase default file upload limit to 100MB to enable user exports to be imported (can be changed in .env)
2024-01-16 21:32:13 +11:00
Carlos Camara ddbda3ab9c Fix test_export 2024-01-16 08:12:59 +00:00
bSolt 76a3874662 add bulma classes to fix awkward spacing 2024-01-15 23:25:52 -07:00
Rohan 8144507893
Merge branch 'main' into issue-3187 2024-01-15 17:25:36 +05:30
Rohan Sureshkumar 70adf878e8 Merge branch 'issue-3187' of https://github.com/rsk2/bookwyrm into issue-3187 2024-01-15 17:23:17 +05:30
Rohan Sureshkumar 5ef104b802 Issue-3187: addressing review comments 2024-01-15 17:22:33 +05:30
Hugh Rundle d4d2734dab
ignore exports dir 2024-01-14 14:14:20 +11:00
Hugh Rundle 62cc6c298f
oops
- remove test export files
- check in emblackened files
2024-01-14 12:19:59 +11:00
Hugh Rundle cbd08127ef
initial work on fixing user exports with s3
- custom storages
- tar.gz within bucket using s3_tar
- slightly changes export directory structure
- major problems still outstanding re delivering s3 files to end users
2024-01-14 12:14:44 +11:00
Adeodato Simó eb13eb9882
Invalidate active_shelf when switching editions 2024-01-13 19:00:57 +01:00
Adeodato Simó 9a487b0442
Ensure dev-tools uses bookworm
In 1937177e1 ("dev-tools: use apt source for Node instead of setup script"),
I introduced the use of `Signed-By` with a public key block, which is only
supported in bookworm (bullseye only supports fingerprints, TTBOMK).

Python's Docker images already use bookworm by default, but we explicitly
require it now to avoid build errors if someone has a very old image laying
around (see, e.g., #3190).

(This can be dropped after Debian 13 ‘trixie’ is released.)
2024-01-13 17:55:21 +01:00
Carlos Camara 854eb36618 Export bookshelves and review date 2024-01-13 16:47:51 +00:00
Hugh Rundle b04ebe397b
Merge pull request #3189 from ccamara/2965_export_readthrough
Export ReadThrough in the csv export
2024-01-12 16:35:41 +11:00
Hugh Rundle 5d13bf8e49
Merge branch 'main' into 2965_export_readthrough 2024-01-12 16:12:01 +11:00
Rohan 6dc95a82d6
Merge branch 'bookwyrm-social:main' into issue-3187 2024-01-09 17:06:22 +05:30
Rohan Sureshkumar 1a682753c0 Issue-3187: changes 2024-01-09 15:31:05 +05:30
Mouse Reeve a4599d0374
Merge pull request #3205 from bookwyrm-social/revert-3079-deleted_user_follow_request
Revert "Don't show notification for user follow request if the user is inactive"
2024-01-08 10:28:27 -08:00
Mouse Reeve 83ff880603
Revert "Don't show notification for user follow request if the user is inactive" 2024-01-07 08:31:48 -08:00
Carlos Camara ce18d343e8 Fix pylint error and code format 2024-01-06 09:55:39 +01:00
Carlos Camara 93cab480d6 Code format 2024-01-06 09:50:14 +01:00
Mouse Reeve 1966f1d9a3
Merge pull request #3199 from bookwyrm-social/dependabot/pip/pycryptodome-3.19.1
Bump pycryptodome from 3.16.0 to 3.19.1
2024-01-05 16:54:55 -08:00
dependabot[bot] f267fc3235
Bump pycryptodome from 3.16.0 to 3.19.1
Bumps [pycryptodome](https://github.com/Legrandin/pycryptodome) from 3.16.0 to 3.19.1.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.16.0...v3.19.1)

---
updated-dependencies:
- dependency-name: pycryptodome
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-05 17:42:04 +00:00
Wesley Aptekar-Cassels 6cd2c91135 Allow page numbers to be text, instead of integers.
Fixes: #2640
2024-01-04 19:09:39 -05:00
Carlos Camara c2622a510c Change else statement to None vs "" 2024-01-04 11:40:40 +01:00
Carlos Camara ebcc81dd73 Revert changes to default book
These changes were introduced by mistake in my previous commit.
2024-01-04 11:33:26 +01:00
Carlos Camara 30c9ec9611 Prevent lint error
See @hughrun 's explanation https://github.com/bookwyrm-social/bookwyrm/pull/3189#issuecomment-1876145423
2024-01-04 11:28:17 +01:00
Carlos Camara 51cb70d344 Change readhtrough order 2024-01-04 11:27:17 +01:00
Carlos Camara 9acb5f66fe Convert DateTime to date 2024-01-04 11:26:44 +01:00
Carlos Camara ae5950f187 Add readthrough fields to text_export.py 2024-01-04 11:10:38 +01:00
Carlos Camara 766a2163dd Code formatting 2024-01-03 20:41:31 +01:00
Carlos Camara db8c686dd3 Include book Readtrhough in the csv export 2024-01-03 15:43:15 +01:00
Mouse Reeve 597378bb78
Merge pull request #3183 from bookwyrm-social/erase_user_command
Adds management command to clear all deleted user data
2024-01-02 20:13:03 -08:00
Mouse Reeve 9c3e6384f8
Merge pull request #3118 from rosschapman/let-a-user-search-within-their-books
Let a user search books within their shelves
2024-01-02 18:37:33 -08:00
Mouse Reeve 01db77a745 Adds success message 2024-01-02 18:29:55 -08:00
Mouse Reeve d287581620 Fixes html validation error 2024-01-02 13:31:18 -08:00
Mouse Reeve 193a1c7d54 updates wording and fixes get or create logic 2024-01-02 13:28:25 -08:00
Mouse Reeve 8be9e91d21 Re-use schedules rather than creating new ones 2024-01-02 13:18:26 -08:00
Mouse Reeve f36af42f41 Adds view to see scheduled tasks 2024-01-02 13:05:44 -08:00
Mouse Reeve 5509941aa4 Adds schedule-able task to check for version updates 2024-01-02 13:05:26 -08:00
Mouse Reeve d6f7f76c4d Removes outdated/unused version and updating code
I had the bright idea of creating this update script but it doesn't work
and hasn't been maintained, so it's just sitting there causing confusing
and requiring weird things to exist in other places.

Now, the unused `version` field can be removed and I can scrap the
management command for getting versions.
2024-01-02 11:37:01 -08:00
Mouse Reeve 381490e31d Adds management command to clear all deleted user data 2024-01-02 10:50:46 -08:00
Mouse Reeve addfee0607
Merge pull request #3182 from bookwyrm-social/broken-migration
Removes part of migration causing upgrade issues
2024-01-02 10:50:35 -08:00
Mouse Reeve 2a85378456 Removes part of migration causing upgrade issues 2024-01-02 09:57:41 -08:00
Mouse Reeve d9a640c809 Fixes version number 2024-01-02 08:36:42 -08:00
Mouse Reeve 0756c5ac5c
Merge pull request #3180 from bookwyrm-social/version-0-7-0
Version 0.7.0 miscellenea
2024-01-01 19:58:15 -08:00
Mouse Reeve 913a19c8f0 Formats migration file 2024-01-01 19:33:49 -08:00
Mouse Reeve e2249f2515 Updates locales 2024-01-01 19:30:03 -08:00
Mouse Reeve f72ada4780 Updates javascript cache buster just in case 2024-01-01 19:29:43 -08:00
Mouse Reeve 86d79f537a Adds merge migration 2024-01-01 19:29:24 -08:00
Mouse Reeve fb16806afe
Merge pull request #3177 from dato/naturalday_partial_fixes
Adjustments to naturalday_partial
2024-01-01 19:16:16 -08:00
Mouse Reeve ffeca9f908
Merge pull request #3150 from dato/get_representative_atomic
Make get_representative() atomic
2024-01-01 19:14:19 -08:00
Mouse Reeve 45d33c37ea
Merge pull request #3175 from dato/prefer_nodesource
dev-tools: ensure we install Node from upstream
2024-01-01 19:12:02 -08:00
Mouse Reeve ca79cb1ca7
Merge pull request #3054 from bookwyrm-social/user-migration
User migration via export file
2024-01-01 19:04:43 -08:00
Mouse Reeve 5647477ba7
Merge pull request #3154 from bookwyrm-social/ukrainian
Adds Ukranian locale and updates locales
2024-01-01 18:54:43 -08:00
Adeodato Simó 4711b3bc19
naturalday_partial: simplify/refactor 2024-01-01 18:36:31 +01:00
Adeodato Simó 0d908b594c
naturalday_partial: do not naturalize dates with missing parts 2024-01-01 18:36:31 +01:00
Adeodato Simó 0e3936cb61
naturalday_partial: do naturalize date and datetime objects 2024-01-01 18:36:30 +01:00
Dato Simó 09b2dea995 dev-tools: ensure we install Node from upstream
Fixes: #3173 ("`bw-dev build` fails")
2024-01-01 09:01:21 -03:00
Mouse Reeve 3754718916 Updates locales again 2023-12-30 15:54:06 -08:00
Mouse Reeve 9b3f4933ac Fixes language code for Ukrainian 2023-12-17 06:57:05 -08:00
Mouse Reeve 47cdc14bc0
Update bookwyrm/migrations/0189_alter_user_preferred_language.py
Co-authored-by: Demid <grrrr@protonmail.com>
2023-12-17 06:54:39 -08:00
Mouse Reeve 430e4eb90d
Update bookwyrm/settings.py
Co-authored-by: Demid <grrrr@protonmail.com>
2023-12-17 06:52:49 -08:00
Ross Chapman b728bb4323 Uses block trans 2023-12-16 12:05:35 -08:00
Ross Chapman a4172214d1 Updates size of filters panel label 2023-12-15 13:17:23 -08:00
Ross Chapman fb36958444 Removes unused variable 2023-12-14 13:47:51 -08:00
Ross Chapman 44d21d1ba4 Updates view logic 2023-12-14 13:04:45 -08:00
Ross Chapman bd3acdbf31 Puts string in template 2023-12-14 12:33:27 -08:00
Ross Chapman 4a4046a704 Shows message if empty and renames "search" to "filter" 2023-12-14 11:30:01 -08:00
Ross Chapman 7cca199a11 Merge branch 'main' into let-a-user-search-within-their-books 2023-12-14 10:25:05 -08:00
Hugh Rundle 1649457372
Merge pull request #3156 from hughrun/user-migration
fix upsert_statuses
2023-12-13 20:59:47 +11:00
Hugh Rundle 7fcadb1d4d
fix upsert_statuses
- remote_id is now updated on import of statuses
- statuses cannot be imported unless source has target listed in alsoKnownAs or movedTo
- add alert boxes to import and export screens advising of the above
- update tests accordingly
2023-12-13 20:55:38 +11:00
Mouse Reeve 5c0e159d43 Adds Ukranian locale and updates locales 2023-12-12 15:42:40 -08:00
Mouse Reeve 000e5e6145
Merge pull request #3152 from bookwyrm-social/fixes-typo-in-move-notice
Fixes incorrect translation and display of moved user page
2023-12-12 14:59:00 -08:00
Mouse Reeve 8bb5a664c5 Fixes incorrect translation and display of moved user page 2023-12-11 20:12:14 -08:00
Mouse Reeve e032e5491d
Merge pull request #3144 from villasv/patch-1
Update page formatter on ordered collection
2023-12-11 19:54:24 -08:00
Mouse Reeve 4bfa1ca5b8
Merge pull request #3124 from hughrun/softblock
Allow removing followers and fix follow rejections
2023-12-11 19:49:45 -08:00
Adeodato Simó 13374917f3
Make get_representative() atomic 2023-12-11 20:48:32 -03:00
Mouse Reeve 799f842115
Merge pull request #3146 from dato/setup_test_data
Faster tests with setUpTestData
2023-12-11 15:45:40 -08:00
Adeodato Simó aa67f598dd
Explicitly set doctype to html5 when invoking tidy_document()
Many tests break without this on newer versions of html-tidy.
2023-12-11 19:40:48 -03:00
Adeodato Simó 9d502f5ee2
Use setUpTestData() to speed up tests
Pylint's `bad-classmethod-argument` is disabled for each definition
to avoid rewriting the method bodies just to rename `self` → `cls`.
This can be done gradually, as the setUpTestData methods are modified
along the way.
2023-12-11 19:40:30 -03:00
Mouse Reeve 198c0037c6
Merge pull request #3121 from hughrun/413
Display custom page on 413 errors
2023-12-09 08:31:25 -08:00
Mouse Reeve e5d292919c
Merge pull request #3143 from dato/test_ordered_collection_use_bulk_create
Use bulk_create to test ordered collections
2023-12-09 08:30:23 -08:00
Mouse Reeve 029b438355 Clarify import/export of book vs user
I think this wording is a little clearer
2023-12-09 08:18:31 -08:00
Mouse Reeve dd72013225 Small fixes for notifications
Adds a link in the text of the notification, and fixes references to
notification type in the model
2023-12-09 08:09:22 -08:00
Ross Chapman aac8aa1adf Fixes formatting 2023-12-06 11:36:15 -08:00
Ross Chapman 0f6e567b21 Clean up 2023-12-05 19:49:38 -08:00
Ross Chapman c65e165aeb Hides filter if shelf empty 2023-12-05 19:37:29 -08:00
Ross Chapman 979162da10 Uses filters, fixes for any shelf 2023-12-05 19:33:59 -08:00
Ross Chapman b27ed847d5 Fixes result set passed to template 2023-12-05 16:36:58 -08:00
Ross Chapman d93da4e86d Checkpoint 2023-12-05 15:46:08 -08:00
Victor Villas 8fd05004ea
Update page formatter on ordered collection 2023-12-03 20:03:33 -08:00
Adeodato Simó 5384e4c470
Use bulk_create to test ordered collections 2023-11-30 15:58:48 -03:00
Matt Lehrer 7f55495287 Merge branch 'move-ratings-and-reviews-when-switching-editions' of github.com:mattlehrer/bookwyrm into move-ratings-and-reviews-when-switching-editions 2023-11-30 11:15:33 +01:00
Matt Lehrer 31a78a5c9e linted 2023-11-30 11:13:11 +01:00
Mouse Reeve 193a36390b
Merge pull request #3083 from hughrun/file-resubmit
use bw-file-resubmit to retain images during validation checks
2023-11-29 15:36:33 -08:00
Mouse Reeve cf1afefc84
Merge pull request #3133 from dato/search_results_fix_work_order
Do not create a set for already-distinct query result
2023-11-29 15:27:57 -08:00
Mouse Reeve b8bf3d5bd9
Merge pull request #3138 from bookwyrm-social/dependabot/pip/aiohttp-3.9.0
Bump aiohttp from 3.8.6 to 3.9.0
2023-11-27 17:33:18 -08:00
dependabot[bot] 58f149d889
Bump aiohttp from 3.8.6 to 3.9.0
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.6 to 3.9.0.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.6...v3.9.0)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-28 01:02:35 +00:00
Ross Chapman 90cc28986e Merge branch 'main' into let-a-user-search-within-their-books 2023-11-27 11:40:57 -08:00
Adeodato Simó d6eb390cee
Add test that forces book_authors_search_vector_trigger to execute 2023-11-26 15:59:17 -03:00
Adeodato Simó b5805accac
Minor improvements to bookwyrm_book trigger code
- do not COALESCE columns that cannot be NULL
- do not bring bookwyrm_book to author names JOIN
- add comments documenting the four steps
2023-11-25 21:49:15 -03:00
Adeodato Simó bbfbd1e97a
Add tests for trigger code (i.e. how search_vector is computed) 2023-11-25 20:54:49 -03:00
Adeodato Simó 9bcb5b80ea
Further simplify bookwyrm_author trigger 2023-11-25 18:13:40 -03:00
Adeodato Simó 8df408e07e
Define search_vector_trigger via Book.Meta.triggers 2023-11-25 17:02:54 -03:00
Adeodato Simó bcb3a343d4
Fix JOIN in author_search_vector_trigger, add missing WHERE clause 2023-11-25 16:23:21 -03:00
Adeodato Simó 416a6caf2d
Define author_search_vector_trigger via Author.Meta.triggers
Previously, triggers lived only in a particular migration file. With
this change, code for the triggers resides in the model, and their
lifecycle is managed through normal Django migrations.
2023-11-25 16:17:51 -03:00
Adeodato Simó 44ef928c3c
Alter object row IDs to force test failure in original code 2023-11-25 16:11:01 -03:00
Adeodato Simó e4d688665c
Remove index for author.search_vector, which is never used 2023-11-24 22:43:12 -03:00
Adeodato Simó 0299f2e235
Add functional tests for search_vector triggers
As metadata changes, search continues to work.
2023-11-24 22:28:41 -03:00
Adeodato Simó c997d2d44a
Add test to assert distinct() clause
Also, tweak other `search_title_author()` tests to verify ordering by
edition rank.
2023-11-24 02:28:27 -03:00
Adeodato Simó e322d3cae1
Do not create a set for already-distinct query result 2023-11-23 23:01:56 -03:00
Hugh Rundle 48904fc60b
Merge pull request #3132 from hughrun/user-migration
notification type migration after merge
2023-11-24 06:51:51 +11:00
Hugh Rundle 99a9a64708
notification type migration after merge 2023-11-24 06:50:32 +11:00
Hugh Rundle 065e15e4db
Merge pull request #3131 from hughrun/user-migration
merge migrations and lint
2023-11-22 21:31:12 +11:00
Hugh Rundle 72c1c6ee3d
merge migrations and lint 2023-11-22 21:29:54 +11:00
Hugh Rundle 0276c15948
Merge branch 'main' into user-migration 2023-11-22 21:00:04 +11:00
Hugh Rundle c6dea2523c
Merge branch 'main' into softblock 2023-11-22 20:06:02 +11:00
Hugh Rundle 6ba7418121
improve tests and minor cleanup 2023-11-22 20:04:17 +11:00
Hugh Rundle 8ed4a997f8
add comment back to bookwyrm.js 2023-11-21 20:20:11 +11:00
Hugh Rundle 2c9ebba5d7
fix reject PR
- rationalise activitypub.Reject and fix model being undefined
- fix not being able to follow users from followers page: 'delete' option now in user_options dropdown
- revert bookwyrm.js
- fix delete_follow_request deleting instead of rejecting
- add user id to 'remove-follow' path
2023-11-21 20:13:56 +11:00
Mouse Reeve 7c2de92df3
Merge pull request #3128 from bookwyrm-social/test-themes
Give admins option to test if a theme loads correctly
2023-11-20 12:26:09 -08:00
Mouse Reeve b6325da9ab
Update bookwyrm/tests/views/admin/test_themes.py
Co-authored-by: Adeodato Simó <73768+dato@users.noreply.github.com>
2023-11-20 10:37:12 -08:00
Mouse Reeve 179dbd75aa Adds tests 2023-11-20 10:23:59 -08:00
Mouse Reeve b022b5a1b7
Merge pull request #3120 from hughrun/permission-required
403 handler
2023-11-20 10:06:24 -08:00
Mouse Reeve c2742b4d80 Updates migrations 2023-11-20 10:02:49 -08:00
Mouse Reeve cfe42305be Merge branch 'main' into test-themes 2023-11-20 10:02:23 -08:00
Mouse Reeve d828ba0bc6 Give admins option to test if a theme loads correctly
If a theme is uploaded incorrectly or has errors in it, users can still
select the theme but it will cause a 500 error on every page, making the
app unusable and also making it impossible for them to switch to a
functional theme.

A better fix would be to fail gracefully, but in lieu of that, this will
at least let admins confirm if a theme is broken safely.
2023-11-20 09:56:51 -08:00
Matt Lehrer 6933f70af3
Merge branch 'bookwyrm-social:main' into move-ratings-and-reviews-when-switching-editions 2023-11-20 09:31:45 +01:00
Mouse Reeve d94b27b723
Merge branch 'main' into user-migration 2023-11-19 19:18:22 -08:00
Mouse Reeve 3d9f339bd5
Merge pull request #3059 from dato/stable_dates_v2
Partial, stable dates with automatic precision field
2023-11-19 19:17:49 -08:00
Mouse Reeve 1d5cc83347
Merge branch 'main' into permission-required 2023-11-19 19:12:32 -08:00
Mouse Reeve d8018cb937
Merge pull request #3125 from hughrun/instance-actor
hide instance actor from users
2023-11-19 19:11:17 -08:00
Mouse Reeve 4da96d937e
Merge pull request #3126 from hughrun/savedlist-pagination
fix saved list pagination
2023-11-19 18:57:57 -08:00
Hugh Rundle 446854ccf0
fix saved list pagination
The SavedLists view was passing through an incorrect "path" value. Now it's not.
2023-11-20 12:45:39 +11:00
Hugh Rundle f011f2bce9
hide instance actor from users
The Instance Actor is required for signing http GET requests but is not a "user" and should not be otherwise interacted with.

- hides instance actor profile page, returning a 404
- excludes instance actor from search results and suggestions including in Getting Started
- replaces link to user profile in user admin page with a brief message box
- replaces panel in user admin page that allows for user to be suspended or removed with a message explaining why that is a very bad idea

fixes #3119
2023-11-20 12:17:52 +11:00
Adeodato Simó ff1f239a57
Use typing_extensions.Self instead of TypeVar 2023-11-19 15:10:14 -03:00
Adeodato Simó 6aaff28c13
Accept argument in naturalday_partial, downcast format if necessary 2023-11-19 15:10:14 -03:00
Adeodato Simó aaea1b1b9e
Add tests for naturalday_partial tag 2023-11-19 15:10:13 -03:00
Adeodato Simó 8dbfba17d6
Merge from 'main' into stable_dates 2023-11-19 15:09:52 -03:00
Hugh Rundle 2ba0e3d7ff
Allow removing followers and fix follow rejections
* adds the ability to remove a user from your followers list
* fixes verbs.Reject to process reject activities for previously accepted follows in both directions

fixes #2635
2023-11-19 20:03:48 +11:00
Hugh Rundle a7fcd898c2
middleware for displaying 413 page
When a RequestDataTooBig exception is thrown, users are largely in the dark about what happened and how it can be fixed.
This commit resolves this by inserting middleware to redirect the request to a custom 413 error page.

This exception is thrown when DATA_UPLOAD_MAX_MEMORY_SIZE is exceeded. The default value is 2.5MB.

Fixes #2340
Fixes #2633
2023-11-18 22:10:36 +11:00
Hugh Rundle 97757fa1ee
fix blocktrans 2023-11-18 15:58:01 +11:00
Hugh Rundle a56ba0ce1c
always return 403 to POST requests
- POST requests need to receive a 403 error code
- minor wording updates
2023-11-18 13:41:52 +11:00
Hugh Rundle 8ddafafa84
make naming consistent 2023-11-18 12:40:36 +11:00
Hugh Rundle d620bd7350
add handler for 403s
fixes #3104
2023-11-18 12:36:03 +11:00
Ross Chapman 68f54cf5a4 Initial commit to create PR 2023-11-16 17:20:23 -08:00
Matt Lehrer f4da9fbf34 remove unnecessary loop.
ReviewRatings are a subclass and are included in the models.Review block
2023-11-16 20:37:46 +01:00
Matt Lehrer bf81192d73
Merge branch 'main' into move-ratings-and-reviews-when-switching-editions 2023-11-16 10:49:05 +01:00
Matt Lehrer bd920a4630 move reviews to new edition 2023-11-16 10:38:45 +01:00
Matt Lehrer 7684101f15 move ratings to new edition 2023-11-16 10:38:41 +01:00
Mouse Reeve 06568aab88
Merge pull request #3105 from dato/notify_invitation_request
Create notifications for incoming invite requests
2023-11-15 17:27:34 -08:00
Mouse Reeve 5bf27d4fb2
Merge pull request #3115 from bookwyrm-social/dependabot/pip/aiohttp-3.8.6
Bump aiohttp from 3.8.5 to 3.8.6
2023-11-15 17:12:59 -08:00
Mouse Reeve 1a7a843dea Re-creates migrations and removes failing test
I think the test was failing because it was extremely brittle, not
because of anything wrong with the code itself.
2023-11-15 17:08:15 -08:00
Mouse Reeve 62f985edb8 Merge branch 'main' into user-migration 2023-11-15 16:46:18 -08:00
Hugh Rundle 54ec5e2ae0
Fix migrations properly (#3116)
* Revert "fix migrations and linting"
This reverts commit 53e410627f.
* really fix migrations
2023-11-15 19:54:16 +11:00
dependabot[bot] 63530294d4
Bump aiohttp from 3.8.5 to 3.8.6
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.5 to 3.8.6.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.5...v3.8.6)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-14 23:35:55 +00:00
Adeodato Simó 01d4381898
Create notifications for incoming invite requests
Closes: #2066
2023-11-14 07:09:04 -03:00
Mouse Reeve ab9cea1742
Merge pull request #3112 from dato/makemigrations_check
Check no missing migrations in django-tests workflow
2023-11-13 15:21:44 -08:00
Adeodato Simó b81170c149
Add missing migration from #3099 2023-11-13 19:56:00 -03:00
Adeodato Simó a884825b3c
Check no missing migrations in django-tests workflow 2023-11-13 19:56:00 -03:00
Hugh Rundle bbc78f03ae
fix DB migrations (#3111)
fix migrations and linting
2023-11-14 07:21:27 +11:00
Hugh Rundle d5762f1d52
Merge branch 'main' into user-migration 2023-11-13 21:17:07 +11:00
Hugh Rundle 891b72c79c
update user export file to use ActivityPub objects where possible. (#3109)
* add more context to user export page
* fix BookData fields wrong for files
* use to_activity and to_model where possible
* fixes for import and export
- use AP JSON where possible
- minor template wording updates
* import fixes and updates tests
* minor cleanup
* remove todo for mastodon
2023-11-13 21:14:03 +11:00
Mouse Reeve ddf94f8714
Merge pull request #3097 from Tak/fix-initdb
Fix `bw-dev initdb`
2023-11-12 09:37:42 -08:00
Mouse Reeve 43324cf43a
Merge pull request #3099 from dato/notification_type_top_level
Create NotificationType as class, not through API
2023-11-12 09:34:28 -08:00
Hugh Rundle 1bedcdaebd
Merge branch 'main' into file-resubmit 2023-11-11 13:14:52 +11:00
Hugh Rundle f3fc5f6179
add file_resubmit to DUMMY caches settings 2023-11-11 12:45:04 +11:00
Adeodato Simó 99a9dbe5f4
Create NotificationType as class, not through API
This way, we need not list every value again to create the enum.

N.B.: enum values are now accessed as `models.NotificationType.FOO`,
instead of `models.Notification.FOO`.
2023-11-09 22:43:36 -03:00
Adeodato Simó be9d92b1c2
Remove last references to "seal" in partial_date.py and migration 2023-11-09 14:00:45 -03:00
Adeodato Simó edfa6b18a1
Rename utils.sealed_date module (and tests) to utils.partial_date 2023-11-09 14:00:44 -03:00
Adeodato Simó fa80aa54a9
SealedDate renames, pt. 2
• SealedDate -> PartialDate
• MonthSeal  -> MonthParts
• YearSeal   -> YearParts
2023-11-09 14:00:44 -03:00
Adeodato Simó 0e4c5ed439
SealedDate renames, pt. 1
• SealedDateField      -> PartialDateModel
• SealedDateFormField  -> PartialDateFormField
• SealedDateDescriptor -> PartialDateDescriptor
2023-11-09 14:00:44 -03:00
Adeodato Simó c120fa8c87
Rename: templatetags/{sealed_dates => date_ext}.py 2023-11-09 14:00:44 -03:00
Adeodato Simó 2bb7652dfe
Update partial date migration to latest main 2023-11-09 14:00:22 -03:00
Adeodato Simó e928027e16
Merge from main for up-to-date migrations 2023-11-09 14:00:14 -03:00
Adeodato Simó dccac11527
PartialDateField: allow incoming dates without timezone 2023-11-09 13:04:09 -03:00
Levi Bard ebcacfc6c5 Fix bw-dev initdb 2023-11-09 12:57:45 +01:00
Mouse Reeve 44b14f4933 Fixes workflow errors 2023-11-08 16:00:10 -08:00
Mouse Reeve 774b1095a3
Merge pull request #3094 from hughrun/activitypub-files
fix missing types in `BookData` file fields
2023-11-08 15:27:40 -08:00
Mouse Reeve 0bb4b0d71d Changes to how images are serialized
I'm just going to see if any tests fail?
2023-11-08 15:24:47 -08:00
Hugh Rundle 2248206a66
fix missing types in BookData file fields
activitypub.BookData includes fields for 'files' and 'fileLinks'.
This is a problem because BookData is inherited by Book and Author, neither of which have 'files' as a field in the main model.
Additionally, Author doesn't have a value for 'file_links'.
When serializing to JSON, BookData therefore throws 'TypeError: Object of type _MISSING_TYPE is not JSON serializable'

This fixes the problem by removing links and moving fileLinks to activitypub.Book.
2023-11-08 18:30:49 +11:00
Hugh Rundle 0a5e1048ce
Add more info to user export page (#3093)
- match page title to menu
- change description on IMPORT page from 'readthroughs' to 'reading history'
- provide more information on export page about what is and is not included.
2023-11-07 12:09:06 +11:00
Mouse Reeve 9ddd631549
Merge pull request #3089 from bookwyrm-social/notification-and-download-links
UI changes for notification and download link in import/export flow
2023-11-06 16:31:09 -08:00
Mouse Reeve 1b958a9b31
Merge pull request #3091 from hughrun/notification-and-download-links
show filesize on user downloads page
2023-11-06 16:27:14 -08:00
Hugh Rundle 282f7dd8d6
show filesize on user downloads page
- add column to user download page to display filesize
- adds a filter to display file sizes
- don't download the user downloads page from notifications ;)
2023-11-07 11:04:11 +11:00
Mouse Reeve e152b625fa
Merge pull request #3090 from bookwyrm-social/user-migration-instructions
User migration instructions
2023-11-06 14:34:52 -08:00
Mouse Reeve ee88c3b914
Merge pull request #3081 from bookwyrm-social/handle-isbn-error
Fix error produced when an unexpected ISBN format is used
2023-11-06 11:06:46 -08:00
Mouse Reeve 8663e204c7
Merge pull request #3079 from bookwyrm-social/deleted_user_follow_request
Don't show notification for user follow request if the user is inactive
2023-11-06 11:05:50 -08:00
Mouse Reeve e7a1572450
Merge pull request #3086 from bookwyrm-social/user-deletion
Erase user data and statuses on account deletion
2023-11-06 09:49:06 -08:00
Mouse Reeve 3f038b4d67 Moves if to the right place 2023-11-06 09:42:58 -08:00
Mouse Reeve 06d822d9e0 Alternative format for user import guide 2023-11-06 09:35:04 -08:00
Mouse Reeve 85d1760b97 Changes recent exports table
I thought both dates seemed less necessary (happy to be told otherwise)
and the download link should be more explicit
2023-11-06 08:41:36 -08:00
Mouse Reeve 716e64de68 Changes notification links for user import/export
I found it unexpected that the export notification linked me directly to
the file, and wanted the import link to lead me to the import page
2023-11-06 08:27:30 -08:00
Hugh Rundle 15b7b7eaa7
Merge pull request #3088 from hughrun/user-migration
User migration fixes
2023-11-06 16:06:39 +11:00
Hugh Rundle d34b70cb7b
remove pointless viewer_aware 2023-11-06 16:01:34 +11:00
Mouse Reeve ee6e3ed7eb Adds a database field for is_deleted on user 2023-11-05 20:28:23 -08:00
Hugh Rundle 2d185dfb8a
remove unnecessary test data files 2023-11-06 14:51:52 +11:00
Mouse Reeve 27d99a0094 Removes failsafe that was overzealous 2023-11-05 19:47:32 -08:00
Hugh Rundle 93a32f4e15
update import/export user templates
- always explain what export file can be used for
- provide more information about overwrite vs upsert when importing
2023-11-06 14:40:19 +11:00
Hugh Rundle 8d3c2d9bd2
Merge pull request #3085 from bookwyrm-social/migration-explicit-imports
Uses explicit imports to avoid circular import in migrations code
2023-11-06 12:11:59 +11:00
Hugh Rundle 7a6b60772c
Merge pull request #3087 from hughrun/migration-explicit-imports
update references to bookwyrm models in export job
2023-11-06 12:10:47 +11:00
Hugh Rundle d2f06e804f
update references to bookwyrm models in export job 2023-11-06 12:07:40 +11:00
Hugh Rundle a93519ec3e
Merge pull request #3027 from dato/find_links_wrapped_punct
Fix parsing of punctuation in format_links()

fixes #2993  
fixes #3049
2023-11-06 09:42:57 +11:00
Hugh Rundle 1190ea7e69
Merge pull request #3078 from bookwyrm-social/tour-fixes
Update tour to reflect changes in #2201
2023-11-06 09:20:44 +11:00
Mouse Reeve c17a2ec55b Creates snippet for user tag in admin view
The existing display wasn't showing the correct colors and was repeating
code unnecessarily
2023-11-05 10:18:04 -08:00
Mouse Reeve d3668e413d Removes updates fields that was causing problems 2023-11-05 09:59:49 -08:00
Mouse Reeve f353b49d36 Another linting issues 2023-11-05 09:53:57 -08:00
Mouse Reeve 47953c84d7 Fixes linting errors
Apparently I didn't have a linter working!
2023-11-05 09:49:38 -08:00
Mouse Reeve 4de9907456 Adds migration tests 2023-11-05 09:26:49 -08:00
Mouse Reeve 61caeed5a3 Adds migration and more tests 2023-11-05 08:51:42 -08:00
Mouse Reeve 5e42afd85a Pass args and kwargs through status deletion 2023-11-05 08:10:03 -08:00
Mouse Reeve d0c652f0f5
Merge pull request #3084 from bookwyrm-social/find_existing_tests
Adds a couple more tests for find_existing
2023-11-05 08:06:15 -08:00
Mouse Reeve 93a7dd9cf3 Erase user data and statuses on account deletion 2023-11-05 08:00:29 -08:00
Mouse Reeve 9e9e9a9f85 Uses explicit imports to avoid circular import in migrations code 2023-11-05 07:04:05 -08:00
Mouse Reeve ff2bb513ed Adds migration for notification types 2023-11-05 06:56:10 -08:00
Mouse Reeve 89b87db1c8 Adds merge migration 2023-11-05 06:54:29 -08:00
Mouse Reeve 67822d3cb0
Merge branch 'main' into user-migration 2023-11-05 06:52:48 -08:00
Mouse Reeve 10e0f2224a Adds a couple more tests for find_existing 2023-11-05 06:44:39 -08:00
Mouse Reeve 7104e775d8 Updates working of header tour 2023-11-05 06:36:43 -08:00
Hugh Rundle d682e55812
swap out django-file-resubmit
- we decided to fork it, so this now uses the inaugural RC release of bw-file-resubmit (will need to be adjusted once we're confident it's ok to push a full release)
- I was accidentally using the wrong widget lol
2023-11-05 16:34:24 +11:00
Adeodato Simó afad39bf80
Use $ instead of \Z for end of string
They're identical here, since re.M is not used, and the better-known
should be used, for readability.
2023-11-03 19:38:24 -03:00
Adeodato Simó 954a02126e
format_links: parse punctuation inside brackets
Also, consolidate all punctuation tests into a single table-driven one.
2023-11-03 19:38:24 -03:00
Adeodato Simó 7d13cbb10b
Add failing tests for reported bugs in format_links() 2023-11-03 19:38:23 -03:00
Adeodato Simó 294788aa1a
format_links: refactor; support multiple punctuation 2023-11-03 19:38:23 -03:00
Mouse Reeve 116a838eef Fixes typo that confuses isbn 10 and 13 2023-11-02 19:37:58 -07:00
Mouse Reeve f839038c8f Add test for normalizing isbns in book model
Turns out this was actually working as expected
2023-11-02 19:12:46 -07:00
Mouse Reeve 285c513211 Adds test for invalid isbns and handle isbns with dashes 2023-11-02 19:03:15 -07:00
Mouse Reeve 95ba38524b
Merge pull request #3080 from bookwyrm-social/dependabot/pip/django-3.2.23
Bump django from 3.2.20 to 3.2.23
2023-11-02 18:48:30 -07:00
dependabot[bot] 68f1a69b6a
Bump django from 3.2.20 to 3.2.23
Bumps [django](https://github.com/django/django) from 3.2.20 to 3.2.23.
- [Commits](https://github.com/django/django/compare/3.2.20...3.2.23)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 22:13:52 +00:00
Mouse Reeve 8c950237a4
Merge pull request #3077 from bookwyrm-social/locales
Fixes plural string, "URL", and updates locales
2023-11-02 15:06:34 -07:00
Mouse Reeve e1217f2054 Don't show notification for user follow request if the user is inactive 2023-11-02 15:04:03 -07:00
Mouse Reeve ae51dcec63
Merge pull request #3039 from dato/ap_image_url
Fix creation of covers for ActivityPub imports
2023-11-02 14:44:44 -07:00
Mouse Reeve 22554f85ad Updates tour to reflect changes in #2201 2023-11-02 14:37:14 -07:00
Mouse Reeve c1a7e4d9eb
Merge pull request #2201 from bookwyrm-social/header-links
Adds "Your Books" to the header and removed "Feed"
2023-11-02 14:34:47 -07:00
Mouse Reeve 416bbd4d9e Fixes plural string, "URL", and updates locales 2023-11-02 14:33:38 -07:00
Mouse Reeve 45fc10e3bf
Merge pull request #3067 from hughrun/2989
add defaults of None to optional AWS values
2023-11-02 14:24:58 -07:00
Mouse Reeve 0502f6ba42
Merge pull request #3076 from bookwyrm-social/move
Add Move activity for user migration (with small change)
2023-11-01 18:19:56 -07:00
Mouse Reeve 86fd62a09e
Merge pull request #3075 from bookwyrm-social/small-migration-fix
Adds reverse migration for populate sort title
2023-11-01 18:19:34 -07:00
Mouse Reeve 2137737d9b Small changes to get blocktrans to work as expected in move code 2023-11-01 17:19:57 -07:00
Mouse Reeve 621cfa7ed2
Merge pull request #2970 from hughrun/move
Add `Move` activity for user migration
2023-11-01 17:14:34 -07:00
Mouse Reeve 6f9c7f39fb Adds reverse migration for populate sort title
This doesn't impact much, it just allows you to reverse the migration,
which you would probably (hopefully) only want ot do in development.
2023-11-01 16:24:47 -07:00
Hugh Rundle c486b9c37e
pylint fixes 2023-10-30 21:47:19 +11:00
Hugh Rundle df43a8e2c5
Use django-file-resubmit plugin
- save cover images to cache when checking author and work for existing records
- fixes #2760
2023-10-30 19:43:39 +11:00
Mouse Reeve 941efb3f72
Merge pull request #3068 from hughrun/3066
make options consistent in celery
2023-10-29 17:30:40 -07:00
Hugh Rundle d2b2cc0521
make options consistent in celery
- changes 'broadcasts' to 'broadcast' in Celery page in admin section
- re-orders celery queues on admin page to be in English alphabetical order (other than priority levels) - this makes them consistent with the Flower interface
- fixes #3066
2023-10-29 11:55:06 +11:00
Hugh Rundle 853b5f28a4
add defaults of None to optional AWS values
fixes #2989
2023-10-29 11:29:49 +11:00
Hugh Rundle 935779b5e3
Merge pull request #3064 from hughrun/user-migrate
stop pylint constantly whining in user-migration branch
2023-10-28 06:53:25 +11:00
Hugh Rundle 25a2615d5f
stop pylint constantly whining 2023-10-28 06:51:26 +11:00
Hugh Rundle 50ac691126
add alt tag 2023-10-28 06:28:43 +11:00
Hugh Rundle 4d35fd45df
template and migration fixes 2023-10-27 22:22:58 +11:00
Hugh Rundle 6f3b1b565f
fixes to move layout and notifs
- make Move notifications less complicated
- moved users cannot do anything other than unmove or log out
- refactor translations for moved users
2023-10-27 22:00:04 +11:00
Adeodato Simó 1952bb6ddc
fix mypy issues
The three "ignore" directives are:

  - avoid unreadable boilerplate from inherited `Field` methods; and:
  - https://github.com/typeddjango/django-stubs/issues/285#issuecomment-600029858
2023-10-24 18:14:05 -03:00
Adeodato Simó 170d1fe205
fix pylint issues (minus no-else-return) 2023-10-24 17:41:07 -03:00
Adeodato Simó 737ac8e908
Implement PartialDateField using SealedDate and a custom descriptor 2023-10-24 17:30:15 -03:00
Adeodato Simó 9752819bdb
Add support for parsing partial isoformats back 2023-10-24 17:30:13 -03:00
Adeodato Simó 4b47646e28
Fix typing hints in sealed_date module
In particular, SealedDate's class methods always return an instance
of the class they're invoked through (i.e., `SealedDate.from_date_parts`
intentionally never returns `MonthSeal` or `YearSeal`).

To propertly annotate this, a type variable is needed (or the much
simpler `Self` in Python 3.11).
2023-10-24 17:29:04 -03:00
Adeodato Simó 5f619d7a39
Implement SealedDateFormField to preserves partial dates
Note that Django forms _already_ have suppport for partial date data; we
just need to extend it when converting to Python (using SealedDate instead
of returning an error).
2023-10-24 17:29:00 -03:00
Adeodato Simó 777c8b4549
naturalday_partial filter for working with SealedDate 2023-10-24 04:32:27 -03:00
Adeodato Simó 46d80d56a5
Rename SealedDate.__str__ to partial_isoformat
Django uses `str(date)` for backends other than PostgreSQL, so do not
break "YYYY-MM-DD" formatting, just in case.
2023-10-24 04:32:27 -03:00
Adeodato Simó a9c605ea97
Add SealedDate class for globally-stable, maybe-incomplete dates 2023-10-24 04:32:27 -03:00
Adeodato Simó 52a979da2d
Add failing test case for "January 1st" offset bug 2023-10-24 04:32:27 -03:00
Hugh Rundle 5592a8e08b
Merge pull request #3061 from hughrun/user-migrate
minor pylint and mypy fixes
2023-10-23 21:32:36 +11:00
Hugh Rundle f30555be0f
minor pylint and mypy fixes 2023-10-23 21:30:17 +11:00
Hugh Rundle f662e4e049
Merge pull request #3060 from hughrun/user-migrate
Fix texts & linting plus minor bugfixes
2023-10-23 20:54:41 +11:00
Hugh Rundle e29c93a1e9
complete jobs more sensibly
- fix tuple in tar export I accidentally broke by following pylint blindly
- just use job.set_status to complete jobs since it does everything we need
- fix/avoid Celery "not JSON deserializable" error by not saving whole job including user value
2023-10-23 20:44:52 +11:00
Hugh Rundle ddec2dbaa9
fix tar types notification docstring 2023-10-23 20:43:49 +11:00
Hugh Rundle b8fc5c9b7a
fix tests 2023-10-23 20:42:56 +11:00
Hugh Rundle 8477d0b89d
Merge branch 'main' into user-migration 2023-10-22 18:47:41 +11:00
Hugh Rundle afb5c01947
Merge pull request #3058 from hughrun/user-migrate
oops import Any
2023-10-22 17:57:57 +11:00
Hugh Rundle 2b6852e7a0
oops import Any 2023-10-22 17:56:46 +11:00
Hugh Rundle d05cf8e59b
Merge pull request #3057 from hughrun/user-migrate
once more into the linting breach!
2023-10-22 17:50:58 +11:00
Hugh Rundle b6b55b2e65
once more into the linting breach! 2023-10-22 17:49:26 +11:00
Hugh Rundle c5e536aeaa
Merge pull request #3056 from hughrun/user-migrate
fix tests and linting
2023-10-22 17:27:57 +11:00
Hugh Rundle 07ef12ce8e
fix tests and linting 2023-10-22 17:26:27 +11:00
Hugh Rundle 0c846ca31f
Merge pull request #3055 from hughrun/user-migrate
formatting and linting fixes
2023-10-22 16:56:20 +11:00
Hugh Rundle 0a2efeb5aa
Merge branch 'user-migration' into user-migrate 2023-10-22 16:55:00 +11:00
Hugh Rundle 6222088f15
Merge branch 'user-migrate' of github.com:hughrun/bookwyrm into user-migrate 2023-10-22 16:53:32 +11:00
Hugh Rundle fd1ebf5f71
formatting and pylint fixes 2023-10-22 16:52:29 +11:00
Hugh Rundle 11a726b40b
Merge pull request #3037 from hughrun/user-migrate
complete most outstanding user migrate tasks
2023-10-22 15:40:22 +11:00
Hugh Rundle c0a5e55f7f
Merge branch 'user-migration' into user-migrate 2023-10-22 15:38:06 +11:00
Hugh Rundle b34a491172
run black 2023-10-22 15:34:25 +11:00
Hugh Rundle a27c652501
admin view for user imports
- makes user_import_time_limit a site setting rather than a value in settings.py (note this applies to exports as well as imports)
- admins can change user_import_time_limit from UI
- admins can cancel stuck user imports
- disabling new imports also disables user imports
2023-10-22 15:07:49 +11:00
Hugh Rundle 836127f369
cooldown period for user exports
add USER_EXPORT_COOLDOWN_HOURS setting for controlling user exports and imports
2023-10-22 10:49:13 +11:00
Hugh Rundle 20114b0059
add notifs and error handling for user export/import 2023-10-22 09:03:28 +11:00
Jascha Ezra Urbach c9e6dcc2d9
Merge pull request #3053 from bookwyrm-social/develop
Release 0.7.0
2023-10-21 18:30:45 +02:00
Jascha Ezra Urbach 00bf2903bc
Bumped version number to 0.7.0 (#3052) 2023-10-21 18:29:45 +02:00
Adeodato Simó 698e74a496
Minor vocabulary fixes and structured data improvements (#3036)
* Remove duplicate Review object under `rating` property

This was preventing validation, since `rating` is not a valid property
(`review` is, which is created from book.html already).

* Drop `bestRating` property in ratings, since it defaults to 5

See <https://schema.org/bestRating> ("If bestRating is omitted,
5 is assumed").

* Create Rating object (and its enclosing Review) in book/rating.html

* Use `position` property for Book objects in a series

`volumeNumber`, previously used, is only valid for objects of type
PublicationVolume (which series members are not).

* Give URL of book series when setting of `isPartOf`

* series.html: Add empty BookSeries object

---------

Co-authored-by: Adeodato Simó <dato@users.noreply.github.com>
2023-10-21 17:53:24 +02:00
Jascha Ezra Urbach 695c67a714
Merge pull request #3051 from bookwyrm-social/jaschaurbach-patch-1
Update bump-version.sh
2023-10-21 17:28:55 +02:00
Jascha Ezra Urbach abb6bcd199
Update bump-version.sh
just a little fix
2023-10-21 17:28:44 +02:00
Jascha Ezra Urbach 4e16800b52
Merge pull request #3050 from bookwyrm-social/bump-version-script
Create bump-version.sh
2023-10-21 17:18:31 +02:00
Jascha Urbach 4a9d80268a
Create bump-version.sh
This scripts reads VERSION (should be semantic version),  automatically
suggest a "minor" version update, and ask for input to use either suggestion, or a new value.

creates a pull request with updated VERSION and creates a tag for the new version.
2023-10-21 17:16:55 +02:00
Hugh Rundle 781b01a007
add error handling and status for user exports
* fix Safari not downloading with the correct filename
* add FAILED status
* don't provide download link for stopped jobs
2023-10-21 19:43:44 +11:00
Jascha Ezra Urbach 1685ac1953
Move version out of settings (#3045)
This removes the content of VERSION from settings.py and moves it into a seperate file which makes it easier to update versionnumbers via script in the future.
2023-10-19 11:29:59 +02:00
Mouse Reeve 2237a7eb9e
Merge pull request #3008 from dato/author_in_citation
Cite author in quotations and alt text
2023-10-18 17:31:55 -07:00
Mouse Reeve caa31de685
Merge pull request #3046 from dato/stylelint-fix
Remaining CSS lints and npm exec cleanup
2023-10-18 17:05:52 -07:00
Adeodato Simó f88a0f8229
Run stylelint to fix remaining issues
Including deletion of two duplicate stanzas introduced in 33c13608a8 ("Refixing
light and dark themes").
2023-10-18 20:39:12 -03:00
Adeodato Simó b78d51410b
bw-dev: drop use of npm exec by setting PATH in Docker image 2023-10-18 20:37:13 -03:00
Adeodato Simó 6392a8e01d
Merge pull request #2032 from viviicat/bw-dev-npm-fix
Conflicts:
	bw-dev
	dev-tools/Dockerfile
	bookwyrm/static/css/bookwyrm/_all.scss
	bookwyrm/static/css/themes/bookwyrm-dark.scss
	bookwyrm/static/css/themes/bookwyrm-light.scss
2023-10-18 18:54:53 -03:00
Jascha Ezra Urbach 912269303e
Merge pull request #2960 from hbrunn/main-systemd-sandboxing
Add sandboxing to systemd examples
2023-10-18 21:00:20 +02:00
Jascha Ezra Urbach abebf82042
Merge pull request #3029 from skmanohar/show-hide-password
Show/Hide password icon
2023-10-18 20:34:12 +02:00
Sidharth 25e8b259f7 Show/Hide password icon 2023-10-18 21:33:37 +05:30
Mouse Reeve 3624763073
Merge pull request #3043 from dato/prettier_rerun_final
Final prettier re-run
2023-10-17 14:28:48 -07:00
Adeodato Simó d55e0b6ba3
Final JS update to match 'es5' trailing comma style
This is  follow-up to cd247a668 ("Update .prettierrc").
2023-10-17 17:11:59 -03:00
Jascha Ezra Urbach 06923c64c1
Merge pull request #3003 from dato/book_info_first_pub_date
Fallback to showing first published date
2023-10-17 20:16:06 +02:00
Jascha Ezra Urbach 3ade72b90d
Merge pull request #3038 from dato/parsed_date_defaults
Stable defaults for incomplete parsed dates
2023-10-17 20:08:04 +02:00
Jascha Ezra Urbach 67f6c0a5a7
Merge pull request #3042 from bookwyrm-social/fix-prettierrc
Update .prettierrc
2023-10-17 19:52:10 +02:00
Jascha Urbach cd247a6689
Update .prettierrc
Forgot to change the file pre-merch
2023-10-17 19:49:32 +02:00
Jascha Ezra Urbach b97dafc303
Merge pull request #3034 from bookwyrm-social/prettier-trailing-comma
introduce .prettierrc
2023-10-17 19:32:04 +02:00
Jascha Ezra Urbach 4d352faae3
Merge branch 'main' into prettier-trailing-comma 2023-10-17 19:31:24 +02:00
Jascha Ezra Urbach f02faa1b74
Merge pull request #3040 from dato/update-node-installation
dev-tools: use apt source for Node instead of setup script
2023-10-17 19:13:41 +02:00
Adeodato Simó 1937177e1a
dev-tools: use apt source for Node instead of setup script
Extra bits:

  - use a single RUN instruction
  - silence upgrade notices from pip and npm
2023-10-17 02:26:22 -03:00
Adeodato Simó 3251ef0bf5
Fix creation of covers for ActivityPub imports
`cover` comes as a JSON dict, but the code was looking for URL as
an attribute.

(This commit leaves the attribute access in place, just in case
`cover` is updated to serialize as Document proper.)
2023-10-16 17:43:04 -03:00
Adeodato Simó 8afcb9b6d3
Fix tests warning: ImportJob.updated_date received a naive datetime 2023-10-15 22:03:45 -03:00
Adeodato Simó c02306a66b
Default to Jan 1st too on incomplete dates received from ActivityPub 2023-10-15 19:59:51 -03:00
Adeodato Simó c066d11eb1
Bugfix: default missing date components to 1, not today's
Fixes: #2660.
2023-10-15 19:49:00 -03:00
Adeodato Simó 8f0f3e6ace
ImportItem: preserve parsed timezones in date_started, date_read
This is a follow-up to b564e514f ("Handle parsed dates that already
have a timezone on import"), which was applied to `date_added` only.

(Appart from consistency, this will allow to apply future parsing fixes
more easily.)
2023-10-15 19:48:02 -03:00
Hugh Rundle f07d730e03
Merge pull request #2980 from CSDUMMI/upstream2
Complete Migrations of Bookwyrm Accounts across instances

Merging this into `user-migration` branch to enable final work on this within the main Bookwyrm repository. We will pull in the final PR from there into `main` when ready.

Thanks to @CSDUMMI and the crew for this huge job.
2023-10-15 15:19:01 +11:00
Hugh Rundle a4bfcb34d5
fix tests and clean up
* cleans up some test logging
* cleans up some commented-out code
* adds export_job model tests
* reconsiders some tests in export user view tests
2023-10-15 15:09:19 +11:00
Hugh Rundle 6667178703
Merge pull request #2981 from rg-wood/installable-pwa
Add: installable as PWA
Resolves #2558
2023-10-15 09:06:24 +11:00
Hugh Rundle c946e7dd82
Merge branch 'main' into installable-pwa 2023-10-15 08:42:17 +11:00
Jascha Urbach 0f79aea36f
introduce .prettierrc
With this pullrequest I introduce .prettierrc with one rule:
'trailingComma': 'none'
2023-10-14 17:52:58 +02:00
Jascha Ezra Urbach 66f62566d6
Merge pull request #3033 from bookwyrm-social/fix-eslint-warning
Fix eslint warning
2023-10-14 17:22:30 +02:00
Jascha Urbach 97adf2f7fd
I changed my own comments to get rid of this warning which I could have better written in the first place. 2023-10-14 17:21:15 +02:00
Jascha Ezra Urbach 0452e8698d
Merge branch 'main' into installable-pwa 2023-10-14 13:57:59 +02:00
Jascha Ezra Urbach 16b7db4639
Merge pull request #3022 from dato/drop_duplicate_isfdb_link
Drop duplicate author link to ISFDB
2023-10-14 13:57:08 +02:00
Jascha Ezra Urbach d7ba0e3a8a
Merge pull request #3023 from dato/series_name_urlencode
URL-encode book series name when linking to it
2023-10-14 13:56:38 +02:00
Adeodato Simó 17d741039c
Remove duplicate test
(Test case already part of test_format_links_simple_url.)
2023-10-09 21:37:39 -03:00
Adeodato Simó 0043329cc1
Simplify literals in _wrapped 2023-10-09 21:09:52 -03:00
Adeodato Simó c3c22022f6
Check for punctuation before checking for wrapping parenthesis
This allows to parse `(URL).` correctly, which was not detected
as URL before.
2023-10-09 21:09:51 -03:00
Adeodato Simó 1778c56be0
URL-encode book series name when linking to it
Closes: #3021.
2023-10-09 04:24:05 -03:00
Adeodato Simó 55eb81dbf9
Drop duplicate author link to ISFDB
Closes: #3018.
2023-10-09 01:16:32 -03:00
R.G. Wood a7e427efc2
Merge branch 'main' into installable-pwa 2023-10-07 13:34:35 +01:00
R.G. Wood 1798abfc3e
Display site name and description for PWA manifest 2023-10-07 13:32:49 +01:00
R.G. Wood 34d5c557d8
Fix syntax styling 2023-10-07 13:32:18 +01:00
Mouse Reeve 3d123bc2f2
Merge pull request #3017 from bookwyrm-social/dependabot/pip/pillow-10.0.1
Bump pillow from 9.4.0 to 10.0.1
2023-10-04 19:00:21 -07:00
dependabot[bot] 7cae5879c8
Bump pillow from 9.4.0 to 10.0.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.4.0 to 10.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.4.0...10.0.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-04 01:09:41 +00:00
Mouse Reeve bcfd4d2efa
Merge pull request #3016 from bookwyrm-social/release-changes
Updates locales and version number for release
2023-10-02 10:22:05 -07:00
Mouse Reeve e4ba09178f
Merge pull request #2949 from bookwyrm-social/user-search
Allow searching for local users when logged out
2023-10-02 10:21:08 -07:00
Mouse Reeve 703a56940c
Merge pull request #2953 from phildini/add-spanish-articles
Add spanish articles
2023-10-02 10:20:51 -07:00
Mouse Reeve 3deddf6355
Merge pull request #2912 from categulario/fix-form-labels
fix ids of labels in invite request form in admin
2023-10-02 10:04:34 -07:00
Mouse Reeve b3bfcf8665 Updates test for new logic 2023-10-02 10:02:42 -07:00
Mouse Reeve 458b258ad5
Merge pull request #3007 from hughrun/signed-get
create instance user on instance creation
2023-10-02 09:52:59 -07:00
Mouse Reeve fcfe34f2f6 Updates locales and version number for release 2023-10-02 09:42:22 -07:00
Hugh Rundle e34fe9a059
Merge pull request #2917 from jderuiter/mypy-utils
Type annotations for utils
2023-10-01 10:29:52 +11:00
Joeri de Ruiter d4088ac854
Merge branch 'main' into mypy-utils 2023-09-28 09:43:40 +02:00
Mouse Reeve 33e179e44b
Merge pull request #3010 from bookwyrm-social/locales
Updates locales
2023-09-27 16:52:36 -07:00
Mouse Reeve 2a08170fb5 Updates locales 2023-09-26 18:14:24 -07:00
Hugh Rundle 088b9ab555
Merge branch 'main' into move 2023-09-26 08:10:18 +10:00
Hugh Rundle 3e38fecd55
id_to_username cleanup 2023-09-26 07:45:45 +10:00
Adeodato Simó 7cfdf235bc
Include author in cover image alt text 2023-09-25 16:03:16 -03:00
Adeodato Simó 80a1180090
Include author name when serializing Quotation 2023-09-25 16:03:16 -03:00
Hugh Rundle b05f2e99e8
undo moves
also cleans up some templates
2023-09-25 22:05:43 +10:00
Hugh Rundle 4fd5e2094a
hopefully fix template 2023-09-25 16:35:02 +10:00
Hugh Rundle 9547edf845
oops close div 2023-09-25 16:21:19 +10:00
Hugh Rundle d67903fd4b
fix tests and templates 2023-09-25 15:49:25 +10:00
Hugh Rundle 01a56540d0
cleanup 2023-09-25 15:29:01 +10:00
Hugh Rundle c95f160216
fix MoveUser errors and clean up
- minor template fixes
- notification logic fixes
- don't dedupe on moved_to or also_known_as
- add migration
2023-09-25 15:14:21 +10:00
Hugh Rundle fe4bc28f37
fix tests 2023-09-24 15:58:52 +10:00
Hugh Rundle b69031c01a
formatting 2023-09-24 12:31:01 +10:00
Mouse Reeve bab28a8fc9
Merge pull request #3000 from dato/position_serialization
Minor improvements to Quotation pure content
2023-09-23 17:47:27 -07:00
Adeodato Simó ec2c5cb546
Only use first publish date if publisher is unknown 2023-09-23 18:23:15 -03:00
Adeodato Simó 2c968e94cb
Fallback to first published date if published date not present 2023-09-23 18:22:02 -03:00
Adeodato Simó fadf30b942
Also use italics for book title in editions.html template 2023-09-23 17:49:38 -03:00
Adeodato Simó cc05cabcb5
Note content: use italics for book titles + em-dash for Quotation 2023-09-23 17:49:38 -03:00
Mouse Reeve ef582f1bc2
Merge pull request #2986 from NetspherePub/2985
Correct EPUB spelling
2023-09-22 17:04:18 -07:00
Mouse Reeve b75b5cb165
Merge pull request #2987 from JJimenez71/main
Pinned versions of docker containers
2023-09-22 17:03:39 -07:00
Mouse Reeve 0a029e6e01
Merge pull request #2992 from jderuiter/new-edition-issues
parent_work was not always included when needed
2023-09-22 17:00:40 -07:00
Mouse Reeve 85b647b7ab
Merge pull request #3002 from dato/django_serve_static_debug
Serve static files in debug mode
2023-09-22 16:17:58 -07:00
Adeodato Simó 1e495684af
Serve static files in debug mode 2023-09-18 19:01:22 -03:00
Hugh Rundle 5b051631ec
Move MVP
* update User model to allow for moved_to and also_known_as values
* allow users to add aliases (also_known_as) in UI
* allow users to move account to another one (moved_to)
* redirect webfinger to the new account after a move
* present notification to followers inviting to follow at new account

Note: unlike Mastodon we're not running any unfollow/autofollow action here: users can decide for themselves
This makes undoing moves easier.

TODO

There is still a bug with incoming Moves, at least from Mastodon.
This seems to be something to do with Update activities (rather than Move, strictly).
2023-09-18 21:21:04 +10:00
Adeodato Simó ce3885d4f6
Use endposition when serializing Quotation 2023-09-17 15:18:48 -03:00
Adeodato Simó 1322a0c693
Substitute “p.” for “page” in page progress serialization 2023-09-17 15:18:21 -03:00
Adeodato Simó 25fd7276ea
pure_content() refactor: shorter conditionals 2023-09-17 15:01:04 -03:00
Joeri de Ruiter af5f71f5ac Revert return type for get_or_set 2023-09-13 10:21:30 +02:00
Joeri de Ruiter 05f8bd0d3c parent_work was not always included in work when needed 2023-09-13 09:46:31 +02:00
Joeri de Ruiter a5cf912ae8 Fix some annotations 2023-09-13 09:22:53 +02:00
Joeri de Ruiter a5ede835b2 Dump dependencies related to typing 2023-09-13 09:09:43 +02:00
FoW d8ba1f4309 Correct EPUB spelling 2023-09-08 22:52:11 +09:00
Joeri de Ruiter 6e9f64262c
Merge branch 'main' into mypy-utils 2023-09-08 08:53:38 +02:00
JJimenez71 2260e14868 Pinned versions of docker containers 2023-09-07 19:30:29 -06:00
CSDUMMI 688978369f Implement self-contained archives to import and export entire users between instances (#38)
Co-authored-by: Daniel Burgess <developerdannymate@gmail.com>
Co-authored-by: Hugh Rundle <hugh@hughrundle.net>
Co-authored-by: dannymate <dannymate@noreply.codeberg.org>
Co-authored-by: hughrun <hughrun@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/GuildAlpha/bookwyrm/pulls/38
Co-authored-by: CSDUMMI <csdummi.misquality@simplelogin.co>
Co-committed-by: CSDUMMI <csdummi.misquality@simplelogin.co>
2023-09-07 22:37:28 +02:00
R.G. Wood b9851d665e
Add: installable as PWA 2023-09-07 13:41:31 +01:00
Hugh Rundle e7ba6a3141
initial work to add 'Move' activity 2023-08-29 21:07:41 +10:00
Holger Brunn 0a9ef9e047 Add sandboxing to systemd examples 2023-08-28 07:29:42 +02:00
Abraham Toriz 4c526dfcaa
Don't rely on ids to target inputs from labels 2023-08-25 13:33:55 -06:00
Abraham Toriz dfa935bd72
fix pointed ids of labels in invite request form 2023-08-25 13:29:26 -06:00
Joeri de Ruiter 567c103e59
Merge branch 'main' into mypy-utils 2023-08-22 11:40:48 +02:00
Joeri de Ruiter 2e88e73509 Remove returned None to make pylint happy 2023-08-21 14:00:09 +02:00
Joeri de Ruiter 0f2c0c034d Removed TODOs. When data is invalid return None. 2023-08-21 13:28:08 +02:00
Joeri de Ruiter 767cd14639 Stricter checks for bookwyrm.utils 2023-08-21 13:10:12 +02:00
Joeri de Ruiter 8f8587f79d Set **kwargs type to Any for ActivityObject.__init__ 2023-08-21 13:09:42 +02:00
Joeri de Ruiter ff8e4597e5 Type annotations for utils 2023-08-21 12:56:32 +02:00
Margaret Fero d7adada29c
Add Spanish Articles
Added articles for spanish language to list of articles in settings
2023-08-15 22:53:43 -07:00
Philip James 2826e184d2
Merge pull request #7 from bookwyrm-social/main
Update to main
2023-08-15 21:58:38 -07:00
Mouse Reeve a05942fe15 Allow searching for local users when logged out 2023-08-06 18:23:57 -07:00
Joeri de Ruiter 0354e53eea Type annotations for utils 2023-07-23 20:50:44 +02:00
Philip James 9ff28d97b1
Merge pull request #6 from bookwyrm-social/main
Update to upstream
2023-05-24 23:42:35 -07:00
Philip James 6b6ed23e25
Merge pull request #5 from bookwyrm-social/main
Update from upstream
2023-03-16 17:18:01 -07:00
Philip James c878e11913
Merge pull request #4 from bookwyrm-social/main
Merge Updates from Upstream
2023-03-06 20:39:50 -08:00
Mouse Reeve e1f6110dc8
Merge branch 'main' into bw-dev-npm-fix 2022-11-14 09:11:16 -08:00
Mouse Reeve 336c62bfc2
Merge branch 'main' into header-links 2022-07-28 11:45:59 -07:00
Mouse Reeve 583d5b3bdb Remove redundant "Your Books" link from menu 2022-07-08 10:57:53 -07:00
Mouse Reeve 891a5d4dd8 Adds "Your Books" to the header and removed "Feed"
The instance icon is already a link to the feed, and I think it's weird
not to have a quick link to your books, since books are important.
2022-07-08 10:51:22 -07:00
Mouse Reeve 51f445bc72
Merge branch 'main' into bw-dev-npm-fix 2022-06-11 20:39:18 -07:00
Vivianne Langdon 51bb4c6f5d make bw-dev consistent 2022-03-17 00:50:04 -07:00
Vivianne Langdon 225957ba8a remove non-css extensions 2022-03-17 00:50:04 -07:00
Vivianne Langdon 54b8d2c3f3 add import rule override for styles 2022-03-17 00:50:04 -07:00
Vivianne Langdon 6f27b5fd2e auto-fixes from running this 2022-03-17 00:50:04 -07:00
Vivianne Langdon dbd5a02617 specify npm prefix and stylelint configs and adjust ignores 2022-03-17 00:50:04 -07:00
466 changed files with 47762 additions and 12807 deletions

View file

@ -16,6 +16,11 @@ DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
# Specify when the site is served from a port that is not the default
# for the protocol (80 for HTTP or 443 for HTTPS).
# Probably only necessary in development.
# PORT=1333
MEDIA_ROOT=images/
# Database configuration
@ -71,14 +76,20 @@ ENABLE_THUMBNAIL_GENERATION=true
USE_S3=false
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# seconds for signed S3 urls to expire
# this is currently only used for user export files
S3_SIGNED_URL_EXPIRY=900
# Commented are example values if you use a non-AWS, S3-compatible service
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL.
# AWS_S3_URL_PROTOCOL must end in ":" and defaults to the same protocol as
# the BookWyrm instance ("http:" or "https:", based on USE_SSL).
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
# AWS_S3_URL_PROTOCOL=None # "http:"
# AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
@ -133,7 +144,14 @@ HTTP_X_FORWARDED_PROTO=false
TWO_FACTOR_LOGIN_VALIDITY_WINDOW=2
TWO_FACTOR_LOGIN_MAX_SECONDS=60
# Additional hosts to allow in the Content-Security-Policy, "self" (should be DOMAIN)
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
# Value should be a comma-separated list of host names.
# Additional hosts to allow in the Content-Security-Policy, "self" (should be
# DOMAIN with optionally ":" + PORT) and AWS_S3_CUSTOM_DOMAIN (if used) are
# added by default. Value should be a comma-separated list of host names.
CSP_ADDITIONAL_HOSTS=
# Time before being logged out (in seconds)
# SESSION_COOKIE_AGE=2592000 # current default: 30 days
# Maximum allowed memory for file uploads (increase if users are having trouble
# uploading BookWyrm export files).
# DATA_UPLOAD_MAX_MEMORY_MiB=100

View file

@ -1,17 +0,0 @@
name: Python Formatting (run ./bw-dev black to fix)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: psf/black@22.12.0
with:
version: 22.12.0

View file

@ -36,11 +36,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -51,7 +51,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -65,4 +65,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View file

@ -10,7 +10,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install curlylint
run: pip install curlylint

View file

@ -1,61 +0,0 @@
name: Run Python Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-20.04
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
SECRET_KEY: beepbeep
DEBUG: false
USE_HTTPS: true
DOMAIN: your.domain.here
BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/
POSTGRES_PASSWORD: hunter2
POSTGRES_USER: postgres
POSTGRES_DB: github_actions
POSTGRES_HOST: 127.0.0.1
CELERY_BROKER: ""
REDIS_BROKER_PORT: 6379
REDIS_BROKER_PASSWORD: beep
USE_DUMMY_CACHE: true
FLOWER_PORT: 8888
EMAIL_HOST: "smtp.mailgun.org"
EMAIL_PORT: 587
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: false
ENABLE_THUMBNAIL_GENERATION: true
HTTP_X_FORWARDED_PROTO: false
run: |
pytest -n 3

View file

@ -19,10 +19,11 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install modules
run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint
# run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint
run: npm install eslint@^8.9.0
# See .stylelintignore for files that are not linted.
# - name: Run stylelint

View file

@ -1,50 +0,0 @@
name: Mypy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Analysing the code with mypy
env:
SECRET_KEY: beepbeep
DEBUG: false
USE_HTTPS: true
DOMAIN: your.domain.here
BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/
POSTGRES_PASSWORD: hunter2
POSTGRES_USER: postgres
POSTGRES_DB: github_actions
POSTGRES_HOST: 127.0.0.1
CELERY_BROKER: ""
REDIS_BROKER_PORT: 6379
REDIS_BROKER_PASSWORD: beep
USE_DUMMY_CACHE: true
FLOWER_PORT: 8888
EMAIL_HOST: "smtp.mailgun.org"
EMAIL_PORT: 587
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: false
ENABLE_THUMBNAIL_GENERATION: true
HTTP_X_FORWARDED_PROTO: false
run: |
mypy bookwyrm celerywyrm

View file

@ -14,7 +14,7 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install modules
run: npm install prettier@2.5.1

View file

@ -1,27 +0,0 @@
name: Pylint
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Analysing the code with pylint
run: |
pylint bookwyrm/

99
.github/workflows/python.yml vendored Normal file
View file

@ -0,0 +1,99 @@
name: Python
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# overrides for .env.example
env:
POSTGRES_HOST: 127.0.0.1
PGPORT: 5432
POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2
POSTGRES_DB: github_actions
SECRET_KEY: beepbeep
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
jobs:
pytest:
name: Tests (pytest)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env: # does not inherit from jobs.build.env
POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: pip
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-github-actions-annotate-failures
- name: Set up .env
run: cp .env.example .env
- name: Check migrations up-to-date
run: python ./manage.py makemigrations --check
- name: Run Tests
run: pytest -n 3
pylint:
name: Linting (pylint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: pip
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Analyse code with pylint
run: pylint bookwyrm/
mypy:
name: Typing (mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: pip
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Set up .env
run: cp .env.example .env
- name: Analyse code with mypy
run: mypy bookwyrm celerywyrm
black:
name: Formatting (black; run ./bw-dev black to fix)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: psf/black@stable
with:
version: "22.*"

5
.gitignore vendored
View file

@ -16,6 +16,8 @@
# BookWyrm
.env
/images/
/exports/
/static/
bookwyrm/static/css/bookwyrm.css
bookwyrm/static/css/themes/
!bookwyrm/static/css/themes/bookwyrm-*.scss
@ -36,3 +38,6 @@ nginx/default.conf
#macOS
**/.DS_Store
# Docker
docker-compose.override.yml

1
.prettierrc Normal file
View file

@ -0,0 +1 @@
'trailingComma': 'es5'

View file

@ -1,4 +1,4 @@
FROM python:3.9
FROM python:3.11
ENV PYTHONUNBUFFERED 1

View file

@ -13,14 +13,15 @@ User relationship interactions follow the standard ActivityPub spec.
- `Block`: prevent users from seeing one another's statuses, and prevents the blocked user from viewing the actor's profile
- `Update`: updates a user's profile and settings
- `Delete`: deactivates a user
- `Undo`: reverses a `Follow` or `Block`
- `Undo`: reverses a `Block` or `Follow`
### Activities
- `Create/Status`: saves a new status in the database.
- `Delete/Status`: Removes a status
- `Like/Status`: Creates a favorite on the status
- `Announce/Status`: Boosts the status into the actor's timeline
- `Undo/*`,: Reverses a `Like` or `Announce`
- `Undo/*`,: Reverses an `Announce`, `Like`, or `Move`
- `Move/User`: Moves a user from one ActivityPub id to another.
### Collections
User's books and lists are represented by [`OrderedCollection`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection)

View file

@ -10,7 +10,6 @@ BookWyrm is a social network for tracking your reading, talking about books, wri
## Links
[![Mastodon Follow](https://img.shields.io/mastodon/follow/000146121?domain=https%3A%2F%2Ftech.lgbt&style=social)](https://tech.lgbt/@bookwyrm)
[![Twitter Follow](https://img.shields.io/twitter/follow/BookWyrmSocial?style=social)](https://twitter.com/BookWyrmSocial)
- [Project homepage](https://joinbookwyrm.com/)
- [Support](https://patreon.com/bookwyrm)

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.7.3

View file

@ -4,7 +4,11 @@ import sys
from .base_activity import ActivityEncoder, Signature, naive_parse
from .base_activity import Link, Mention, Hashtag
from .base_activity import ActivitySerializerError, resolve_remote_id
from .base_activity import (
ActivitySerializerError,
resolve_remote_id,
get_representative,
)
from .image import Document, Image
from .note import Note, GeneratedNote, Article, Comment, Quotation
from .note import Review, Rating
@ -19,6 +23,7 @@ from .verbs import Create, Delete, Undo, Update
from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, Remove
from .verbs import Announce, Like
from .verbs import Move
# this creates a list of all the Activity types that we can serialize,
# so when an Activity comes in from outside, we can check if it's known

View file

@ -1,4 +1,5 @@
""" basics for an activitypub serializer """
from __future__ import annotations
from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder
import logging
@ -19,6 +20,7 @@ from bookwyrm.tasks import app, MISC
logger = logging.getLogger(__name__)
# pylint: disable=invalid-name
TBookWyrmModel = TypeVar("TBookWyrmModel", bound=base_model.BookWyrmModel)
@ -72,8 +74,10 @@ class ActivityObject:
def __init__(
self,
activity_objects: Optional[list[str, base_model.BookWyrmModel]] = None,
**kwargs: dict[str, Any],
activity_objects: Optional[
dict[str, Union[str, list[str], ActivityObject, base_model.BookWyrmModel]]
] = None,
**kwargs: Any,
):
"""this lets you pass in an object with fields that aren't in the
dataclass, which it ignores. Any field in the dataclass is required or
@ -233,7 +237,7 @@ class ActivityObject:
omit = kwargs.get("omit", ())
data = self.__dict__.copy()
# recursively serialize
for (k, v) in data.items():
for k, v in data.items():
try:
if issubclass(type(v), ActivityObject):
data[k] = v.serialize()
@ -393,19 +397,15 @@ def resolve_remote_id(
def get_representative():
"""Get or create an actor representing the instance
to sign requests to 'secure mastodon' servers"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
email = "bookwyrm@localhost"
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
user = models.User.objects.create_user(
username=username,
email=email,
to sign outgoing HTTP GET requests"""
return models.User.objects.get_or_create(
username=f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}",
defaults=dict(
email="bookwyrm@localhost",
local=True,
localname=INSTANCE_ACTOR_USERNAME,
)
return user
),
)[0]
def get_activitypub_data(url):
@ -424,6 +424,7 @@ def get_activitypub_data(url):
"Date": now,
"Signature": make_signature("get", sender, url, now),
},
timeout=15,
)
except requests.RequestException:
raise ConnectorException()

View file

@ -22,8 +22,6 @@ class BookData(ActivityObject):
aasin: Optional[str] = None
isfdb: Optional[str] = None
lastEditedBy: Optional[str] = None
links: list[str] = field(default_factory=list)
fileLinks: list[str] = field(default_factory=list)
# pylint: disable=invalid-name
@ -45,6 +43,8 @@ class Book(BookData):
firstPublishedDate: str = ""
publishedDate: str = ""
fileLinks: list[str] = field(default_factory=list)
cover: Optional[Document] = None
type: str = "Book"

View file

@ -1,5 +1,5 @@
""" actor serializer """
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Dict
from .base_activity import ActivityObject
@ -35,9 +35,11 @@ class Person(ActivityObject):
endpoints: Dict = None
name: str = None
summary: str = None
icon: Image = field(default_factory=lambda: {})
icon: Image = None
bookwyrmUser: bool = False
manuallyApprovesFollowers: str = False
discoverable: str = False
hideFollows: str = False
movedTo: str = None
alsoKnownAs: dict[str] = None
type: str = "Person"

View file

@ -171,9 +171,19 @@ class Reject(Verb):
type: str = "Reject"
def action(self, allow_external_connections=True):
"""reject a follow request"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()
"""reject a follow or follow request"""
for model_name in ["UserFollowRequest", "UserFollows", None]:
model = apps.get_model(f"bookwyrm.{model_name}") if model_name else None
if obj := self.object.to_model(
model=model,
save=False,
allow_create=False,
allow_external_connections=allow_external_connections,
):
# Reject the first model that can be built.
obj.reject()
break
@dataclass(init=False)
@ -231,3 +241,30 @@ class Announce(Verb):
def action(self, allow_external_connections=True):
"""boost"""
self.to_model(allow_external_connections=allow_external_connections)
@dataclass(init=False)
class Move(Verb):
"""a user moving an object"""
object: str
type: str = "Move"
origin: str = None
target: str = None
def action(self, allow_external_connections=True):
"""move"""
object_is_user = resolve_remote_id(remote_id=self.object, model="User")
if object_is_user:
model = apps.get_model("bookwyrm.MoveUser")
self.to_model(
model=model,
save=True,
allow_external_connections=allow_external_connections,
)
else:
# we might do something with this to move other objects at some point
pass

View file

@ -139,14 +139,14 @@ class ActivityStream(RedisStore):
| (
Q(following=status.user) & Q(following=status.reply_parent.user)
) # if the user is following both authors
).distinct()
)
# only visible to the poster's followers and tagged users
elif status.privacy == "followers":
audience = audience.filter(
Q(following=status.user) # if the user is following the author
)
return audience.distinct()
return audience.distinct("id")
@tracer.start_as_current_span("ActivityStream.get_audience")
def get_audience(self, status):
@ -156,7 +156,7 @@ class ActivityStream(RedisStore):
status_author = models.User.objects.filter(
is_active=True, local=True, id=status.user.id
).values_list("id", flat=True)
return list(set(list(audience) + list(status_author)))
return list(set(audience) | set(status_author))
def get_stores_for_users(self, user_ids):
"""convert a list of user ids into redis store ids"""
@ -183,15 +183,13 @@ class HomeStream(ActivityStream):
def get_audience(self, status):
trace.get_current_span().set_attribute("stream_id", self.key)
audience = super()._get_audience(status)
if not audience:
return []
# if the user is following the author
audience = audience.filter(following=status.user).values_list("id", flat=True)
# if the user is the post's author
status_author = models.User.objects.filter(
is_active=True, local=True, id=status.user.id
).values_list("id", flat=True)
return list(set(list(audience) + list(status_author)))
return list(set(audience) | set(status_author))
def get_statuses_for_user(self, user):
return models.Status.privacy_filter(
@ -239,9 +237,7 @@ class BooksStream(ActivityStream):
)
audience = super()._get_audience(status)
if not audience:
return models.User.objects.none()
return audience.filter(shelfbook__book__parent_work=work).distinct()
return audience.filter(shelfbook__book__parent_work=work)
def get_audience(self, status):
# only show public statuses on the books feed,

View file

@ -1,4 +1,5 @@
"""Do further startup configuration and initialization"""
import os
import urllib
import logging
@ -14,16 +15,16 @@ def download_file(url, destination):
"""Downloads a file to the given path"""
try:
# Ensure our destination directory exists
os.makedirs(os.path.dirname(destination))
os.makedirs(os.path.dirname(destination), exist_ok=True)
with urllib.request.urlopen(url) as stream:
with open(destination, "b+w") as outfile:
outfile.write(stream.read())
except (urllib.error.HTTPError, urllib.error.URLError):
logger.info("Failed to download file %s", url)
except OSError:
logger.info("Couldn't open font file %s for writing", destination)
except: # pylint: disable=bare-except
logger.info("Unknown error in file download")
except (urllib.error.HTTPError, urllib.error.URLError) as err:
logger.error("Failed to download file %s: %s", url, err)
except OSError as err:
logger.error("Couldn't open font file %s for writing: %s", destination, err)
except Exception as err: # pylint:disable=broad-except
logger.error("Unknown error in file download: %s", err)
class BookwyrmConfig(AppConfig):

View file

@ -43,6 +43,7 @@ def search(
min_confidence: float = 0,
filters: Optional[list[Any]] = None,
return_first: bool = False,
books: Optional[QuerySet[models.Edition]] = None,
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
"""search your local database"""
filters = filters or []
@ -54,13 +55,15 @@ def search(
# first, try searching unique identifiers
# unique identifiers never have spaces, title/author usually do
if not " " in query:
results = search_identifiers(query, *filters, return_first=return_first)
results = search_identifiers(
query, *filters, return_first=return_first, books=books
)
# if there were no identifier results...
if not results:
# then try searching title/author
results = search_title_author(
query, min_confidence, *filters, return_first=return_first
query, min_confidence, *filters, return_first=return_first, books=books
)
return results
@ -98,9 +101,17 @@ def format_search_result(search_result):
def search_identifiers(
query, *filters, return_first=False
query,
*filters,
return_first=False,
books=None,
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
"""tries remote_id, isbn; defined as dedupe fields on the model"""
"""search Editions by deduplication fields
Best for cases when we can assume someone is searching for an exact match on
commonly unique data identifiers like isbn or specific library ids.
"""
books = books or models.Edition.objects
if connectors.maybe_isbn(query):
# Oh did you think the 'S' in ISBN stood for 'standard'?
normalized_isbn = query.strip().upper().rjust(10, "0")
@ -111,7 +122,7 @@ def search_identifiers(
for f in models.Edition._meta.get_fields()
if hasattr(f, "deduplication_field") and f.deduplication_field
]
results = models.Edition.objects.filter(
results = books.filter(
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct()
@ -121,12 +132,17 @@ def search_identifiers(
def search_title_author(
query, min_confidence, *filters, return_first=False
query,
min_confidence,
*filters,
return_first=False,
books=None,
) -> QuerySet[models.Edition]:
"""searches for title and author"""
books = books or models.Edition.objects
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
results = (
models.Edition.objects.filter(*filters, search_vector=query)
books.filter(*filters, search_vector=query)
.annotate(rank=SearchRank(F("search_vector"), query))
.filter(rank__gt=min_confidence)
.order_by("-rank")
@ -137,7 +153,7 @@ def search_title_author(
# filter out multiple editions of the same work
list_results = []
for work_id in set(editions_of_work[:30]):
for work_id in editions_of_work[:30]:
result = (
results.filter(parent_work=work_id)
.order_by("-rank", "-edition_rank")

View file

@ -3,7 +3,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, TypedDict, Any, Callable, Union, Iterator
from urllib.parse import quote_plus
import imghdr
# pylint: disable-next=deprecated-module
import imghdr # Deprecated in 3.11 for removal in 3.13; no good alternative yet
import logging
import re
import asyncio

View file

@ -118,9 +118,11 @@ def get_connectors() -> Iterator[abstract_connector.AbstractConnector]:
def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnector:
"""get the connector related to the object's server"""
url = urlparse(remote_id)
identifier = url.netloc
identifier = url.hostname
if not identifier:
raise ValueError("Invalid remote id")
raise ValueError(f"Invalid remote id: {remote_id}")
base_url = f"{url.scheme}://{url.netloc}"
try:
connector_info = models.Connector.objects.get(identifier=identifier)
@ -128,10 +130,10 @@ def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnec
connector_info = models.Connector.objects.create(
identifier=identifier,
connector_file="bookwyrm_connector",
base_url=f"https://{identifier}",
books_url=f"https://{identifier}/book",
covers_url=f"https://{identifier}/images/covers",
search_url=f"https://{identifier}/search?q=",
base_url=base_url,
books_url=f"{base_url}/book",
covers_url=f"{base_url}/images/covers",
search_url=f"{base_url}/search?q=",
priority=2,
)
@ -188,8 +190,11 @@ def raise_not_valid_url(url: str) -> None:
if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url)
if not parsed.hostname:
raise ConnectorException("Hostname missing: ", url)
try:
ipaddress.ip_address(parsed.netloc)
ipaddress.ip_address(parsed.hostname)
raise ConnectorException("Provided url is an IP address: ", url)
except ValueError:
# it's not an IP address, which is good

View file

@ -4,7 +4,7 @@ from django.template.loader import get_template
from bookwyrm import models, settings
from bookwyrm.tasks import app, EMAIL
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import DOMAIN, BASE_URL
def email_data():
@ -14,6 +14,7 @@ def email_data():
"site_name": site.name,
"logo": site.logo_small_url,
"domain": DOMAIN,
"base_url": BASE_URL,
"user": None,
}

View file

@ -15,6 +15,7 @@ class AuthorForm(CustomForm):
"aliases",
"bio",
"wikipedia_link",
"wikidata",
"website",
"born",
"died",
@ -32,6 +33,7 @@ class AuthorForm(CustomForm):
"wikipedia_link": forms.TextInput(
attrs={"aria-describedby": "desc_wikipedia_link"}
),
"wikidata": forms.TextInput(attrs={"aria-describedby": "desc_wikidata"}),
"website": forms.TextInput(attrs={"aria-describedby": "desc_website"}),
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),

View file

@ -1,8 +1,9 @@
""" using django model forms """
from django import forms
from file_resubmit.widgets import ResubmitImageWidget
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
from .widgets import ArrayWidget, SelectDateWidget, Select
@ -70,9 +71,7 @@ class EditionForm(CustomForm):
"published_date": SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"cover": ResubmitImageWidget(attrs={"aria-describedby": "desc_cover"}),
"physical_format": Select(
attrs={"aria-describedby": "desc_physical_format"}
),

View file

@ -70,6 +70,22 @@ class DeleteUserForm(CustomForm):
fields = ["password"]
class MoveUserForm(CustomForm):
target = forms.CharField(widget=forms.TextInput)
class Meta:
model = models.User
fields = ["password"]
class AliasUserForm(CustomForm):
username = forms.CharField(widget=forms.TextInput)
class Meta:
model = models.User
fields = ["password"]
class ChangePasswordForm(CustomForm):
current_password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)

View file

@ -25,6 +25,10 @@ class ImportForm(forms.Form):
csv_file = forms.FileField()
class ImportUserForm(forms.Form):
archive_file = forms.FileField()
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf

View file

@ -1,4 +1,5 @@
""" using django model forms """
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
@ -25,7 +26,7 @@ class FileLinkForm(CustomForm):
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
domain = urlparse(url).hostname
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":
@ -37,10 +38,9 @@ class FileLinkForm(CustomForm):
),
)
if (
not self.instance
and models.FileLink.objects.filter(
url=url, book=book, filetype=filetype
).exists()
models.FileLink.objects.filter(url=url, book=book, filetype=filetype)
.exclude(pk=self.instance)
.exists()
):
# pylint: disable=line-too-long
self.add_error(

View file

@ -1,6 +1,7 @@
""" import classes """
from .importer import Importer
from .bookwyrm_import import BookwyrmImporter
from .calibre_import import CalibreImporter
from .goodreads_import import GoodreadsImporter
from .librarything_import import LibrarythingImporter

View file

@ -0,0 +1,24 @@
"""Import data from Bookwyrm export files"""
from django.http import QueryDict
from bookwyrm.models import User
from bookwyrm.models.bookwyrm_import_job import BookwyrmImportJob
class BookwyrmImporter:
"""Import a Bookwyrm User export file.
This is kind of a combination of an importer and a connector.
"""
# pylint: disable=no-self-use
def process_import(
self, user: User, archive_file: bytes, settings: QueryDict
) -> BookwyrmImportJob:
"""import user data from a Bookwyrm export file"""
required = [k for k in settings if settings.get(k) == "on"]
job = BookwyrmImportJob.objects.create(
user=user, archive_file=archive_file, required=required
)
return job

View file

@ -26,7 +26,7 @@ class IsbnHyphenator:
def update_range_message(self) -> None:
"""Download the range message xml file and save it locally"""
response = requests.get(self.__range_message_url)
response = requests.get(self.__range_message_url, timeout=15)
with open(self.__range_file_path, "w", encoding="utf-8") as file:
file.write(response.text)
self.__element_tree = None
@ -40,7 +40,12 @@ class IsbnHyphenator:
self.__element_tree = ElementTree.parse(self.__range_file_path)
gs1_prefix = isbn_13[:3]
reg_group = self.__find_reg_group(isbn_13, gs1_prefix)
try:
reg_group = self.__find_reg_group(isbn_13, gs1_prefix)
except ValueError:
# if the reg groups are invalid, just return the original isbn
return isbn_13
if reg_group is None:
return isbn_13 # failed to hyphenate

View file

@ -1,13 +1,14 @@
""" PROCEED WITH CAUTION: uses deduplication fields to permanently
merge book data objects """
from django.core.management.base import BaseCommand
from django.db.models import Count
from bookwyrm import models
from bookwyrm.management.merge import merge_objects
def dedupe_model(model):
def dedupe_model(model, dry_run=False):
"""combine duplicate editions and update related models"""
print(f"deduplicating {model.__name__}:")
fields = model._meta.get_fields()
dedupe_fields = [
f for f in fields if hasattr(f, "deduplication_field") and f.deduplication_field
@ -16,30 +17,42 @@ def dedupe_model(model):
dupes = (
model.objects.values(field.name)
.annotate(Count(field.name))
.filter(**{"%s__count__gt" % field.name: 1})
.filter(**{f"{field.name}__count__gt": 1})
.exclude(**{field.name: ""})
.exclude(**{f"{field.name}__isnull": True})
)
for dupe in dupes:
value = dupe[field.name]
if not value or value == "":
continue
print("----------")
print(dupe)
objs = model.objects.filter(**{field.name: value}).order_by("id")
canonical = objs.first()
print("keeping", canonical.remote_id)
action = "would merge" if dry_run else "merging"
print(
f"{action} into {model.__name__} {canonical.remote_id} based on {field.name} {value}:"
)
for obj in objs[1:]:
print(obj.remote_id)
merge_objects(canonical, obj)
print(f"- {obj.remote_id}")
absorbed_fields = obj.merge_into(canonical, dry_run=dry_run)
print(f" absorbed fields: {absorbed_fields}")
class Command(BaseCommand):
"""deduplicate allllll the book data models"""
help = "merges duplicate book data"
def add_arguments(self, parser):
"""add the arguments for this command"""
parser.add_argument(
"--dry_run",
action="store_true",
help="don't actually merge, only print what would happen",
)
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
"""run deduplications"""
dedupe_model(models.Edition)
dedupe_model(models.Work)
dedupe_model(models.Author)
dedupe_model(models.Edition, dry_run=options["dry_run"])
dedupe_model(models.Work, dry_run=options["dry_run"])
dedupe_model(models.Author, dry_run=options["dry_run"])

View file

@ -0,0 +1,43 @@
""" Erase any data stored about deleted users """
import sys
from django.core.management.base import BaseCommand, CommandError
from bookwyrm import models
from bookwyrm.models.user import erase_user_data
# pylint: disable=missing-function-docstring
class Command(BaseCommand):
"""command-line options"""
help = "Remove Two Factor Authorisation from user"
def add_arguments(self, parser): # pylint: disable=no-self-use
parser.add_argument(
"--dryrun",
action="store_true",
help="Preview users to be cleared without altering the database",
)
def handle(self, *args, **options): # pylint: disable=unused-argument
# Check for anything fishy
bad_state = models.User.objects.filter(is_deleted=True, is_active=True)
if bad_state.exists():
raise CommandError(
f"{bad_state.count()} user(s) marked as both active and deleted"
)
deleted_users = models.User.objects.filter(is_deleted=True)
self.stdout.write(f"Found {deleted_users.count()} deleted users")
if options["dryrun"]:
self.stdout.write("\n".join(u.username for u in deleted_users[:5]))
if deleted_users.count() > 5:
self.stdout.write("... and more")
sys.exit()
self.stdout.write("Erasing user data:")
for user_id in deleted_users.values_list("id", flat=True):
erase_user_data.delay(user_id)
self.stdout.write(".", ending="")
self.stdout.write("")
self.stdout.write("Tasks created successfully")

View file

@ -1,54 +0,0 @@
""" Get your admin code to allow install """
from django.core.management.base import BaseCommand
from bookwyrm import models
from bookwyrm.settings import VERSION
# pylint: disable=no-self-use
class Command(BaseCommand):
"""command-line options"""
help = "What version is this?"
def add_arguments(self, parser):
"""specify which function to run"""
parser.add_argument(
"--current",
action="store_true",
help="Version stored in database",
)
parser.add_argument(
"--target",
action="store_true",
help="Version stored in settings",
)
parser.add_argument(
"--update",
action="store_true",
help="Update database version",
)
# pylint: disable=unused-argument
def handle(self, *args, **options):
"""execute init"""
site = models.SiteSettings.objects.get()
current = site.version or "0.0.1"
target = VERSION
if options.get("current"):
print(current)
return
if options.get("target"):
print(target)
return
if options.get("update"):
site.version = target
site.save()
return
if current != target:
print(f"{current}/{target}")
else:
print(current)

View file

@ -1,50 +0,0 @@
from django.db.models import ManyToManyField
def update_related(canonical, obj):
"""update all the models with fk to the object being removed"""
# move related models to canonical
related_models = [
(r.remote_field.name, r.related_model) for r in canonical._meta.related_objects
]
for (related_field, related_model) in related_models:
# Skip the ManyToMany fields that arent auto-created. These
# should have a corresponding OneToMany field in the model for
# the linking table anyway. If we update it through that model
# instead then we wont lose the extra fields in the linking
# table.
related_field_obj = related_model._meta.get_field(related_field)
if isinstance(related_field_obj, ManyToManyField):
through = related_field_obj.remote_field.through
if not through._meta.auto_created:
continue
related_objs = related_model.objects.filter(**{related_field: obj})
for related_obj in related_objs:
print("replacing in", related_model.__name__, related_field, related_obj.id)
try:
setattr(related_obj, related_field, canonical)
related_obj.save()
except TypeError:
getattr(related_obj, related_field).add(canonical)
getattr(related_obj, related_field).remove(obj)
def copy_data(canonical, obj):
"""try to get the most data possible"""
for data_field in obj._meta.get_fields():
if not hasattr(data_field, "activitypub_field"):
continue
data_value = getattr(obj, data_field.name)
if not data_value:
continue
if not getattr(canonical, data_field.name):
print("setting data field", data_field.name, data_value)
setattr(canonical, data_field.name, data_value)
canonical.save()
def merge_objects(canonical, obj):
copy_data(canonical, obj)
update_related(canonical, obj)
# remove the outdated entry
obj.delete()

View file

@ -1,4 +1,3 @@
from bookwyrm.management.merge import merge_objects
from django.core.management.base import BaseCommand
@ -9,6 +8,11 @@ class MergeCommand(BaseCommand):
"""add the arguments for this command"""
parser.add_argument("--canonical", type=int, required=True)
parser.add_argument("--other", type=int, required=True)
parser.add_argument(
"--dry_run",
action="store_true",
help="don't actually merge, only print what would happen",
)
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
@ -26,4 +30,8 @@ class MergeCommand(BaseCommand):
print("other book doesnt exist!")
return
merge_objects(canonical, other)
absorbed_fields = other.merge_into(canonical, dry_run=options["dry_run"])
action = "would be" if options["dry_run"] else "has been"
print(f"{other.remote_id} {action} merged into {canonical.remote_id}")
print(f"absorbed fields: {absorbed_fields}")

View file

@ -1,3 +1,4 @@
""" look at all this nice middleware! """
from .timezone_middleware import TimezoneMiddleware
from .ip_middleware import IPBlocklistMiddleware
from .file_too_big import FileTooBig

View file

@ -0,0 +1,30 @@
"""Middleware to display a custom 413 error page"""
from django.http import HttpResponse
from django.shortcuts import render
from django.core.exceptions import RequestDataTooBig
class FileTooBig:
"""Middleware to display a custom page when a
RequestDataTooBig exception is thrown"""
def __init__(self, get_response):
"""boilerplate __init__ from Django docs"""
self.get_response = get_response
def __call__(self, request):
"""If RequestDataTooBig is thrown, render the 413 error page"""
try:
body = request.body # pylint: disable=unused-variable
except RequestDataTooBig:
rendered = render(request, "413.html")
response = HttpResponse(rendered)
return response
response = self.get_response(request)
return response

View file

@ -45,5 +45,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(populate_sort_title),
migrations.RunPython(
populate_sort_title, reverse_code=migrations.RunPython.noop
),
]

View file

@ -0,0 +1,130 @@
# Generated by Django 3.2.20 on 2023-10-27 11:22
import bookwyrm.models.activitypub_mixin
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0181_merge_20230806_2302"),
]
operations = [
migrations.AddField(
model_name="user",
name="also_known_as",
field=bookwyrm.models.fields.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name="user",
name="moved_to",
field=bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
migrations.AlterField(
model_name="notification",
name="notification_type",
field=models.CharField(
choices=[
("FAVORITE", "Favorite"),
("REPLY", "Reply"),
("MENTION", "Mention"),
("TAG", "Tag"),
("FOLLOW", "Follow"),
("FOLLOW_REQUEST", "Follow Request"),
("BOOST", "Boost"),
("IMPORT", "Import"),
("ADD", "Add"),
("REPORT", "Report"),
("LINK_DOMAIN", "Link Domain"),
("INVITE", "Invite"),
("ACCEPT", "Accept"),
("JOIN", "Join"),
("LEAVE", "Leave"),
("REMOVE", "Remove"),
("GROUP_PRIVACY", "Group Privacy"),
("GROUP_NAME", "Group Name"),
("GROUP_DESCRIPTION", "Group Description"),
("MOVE", "Move"),
],
max_length=255,
),
),
migrations.CreateModel(
name="Move",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("object", bookwyrm.models.fields.CharField(max_length=255)),
(
"origin",
bookwyrm.models.fields.CharField(
blank=True, default="", max_length=255, null=True
),
),
(
"user",
bookwyrm.models.fields.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
bases=(bookwyrm.models.activitypub_mixin.ActivityMixin, models.Model),
),
migrations.CreateModel(
name="MoveUser",
fields=[
(
"move_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.move",
),
),
(
"target",
bookwyrm.models.fields.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="move_target",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
bases=("bookwyrm.move",),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-11-05 16:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0182_auto_20231027_1122"),
]
operations = [
migrations.AddField(
model_name="user",
name="is_deleted",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,35 @@
# Generated by Django 3.2.20 on 2023-11-06 04:21
from django.db import migrations
from bookwyrm.models import User
def update_deleted_users(apps, schema_editor):
"""Find all the users who are deleted, not just inactive, and set deleted"""
users = apps.get_model("bookwyrm", "User")
db_alias = schema_editor.connection.alias
users.objects.using(db_alias).filter(
is_active=False,
deactivation_reason__in=[
"self_deletion",
"moderator_deletion",
],
).update(is_deleted=True)
# differente rules for remote users
users.objects.using(db_alias).filter(is_active=False, local=False,).exclude(
deactivation_reason="moderator_deactivation",
).update(is_deleted=True)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0183_auto_20231105_1607"),
]
operations = [
migrations.RunPython(
update_deleted_users, reverse_code=migrations.RunPython.noop
),
]

View file

@ -0,0 +1,42 @@
# Generated by Django 3.2.20 on 2023-11-13 22:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0184_auto_20231106_0421"),
]
operations = [
migrations.AlterField(
model_name="notification",
name="notification_type",
field=models.CharField(
choices=[
("FAVORITE", "Favorite"),
("BOOST", "Boost"),
("REPLY", "Reply"),
("MENTION", "Mention"),
("TAG", "Tag"),
("FOLLOW", "Follow"),
("FOLLOW_REQUEST", "Follow Request"),
("IMPORT", "Import"),
("ADD", "Add"),
("REPORT", "Report"),
("LINK_DOMAIN", "Link Domain"),
("INVITE", "Invite"),
("ACCEPT", "Accept"),
("JOIN", "Join"),
("LEAVE", "Leave"),
("REMOVE", "Remove"),
("GROUP_PRIVACY", "Group Privacy"),
("GROUP_NAME", "Group Name"),
("GROUP_DESCRIPTION", "Group Description"),
("MOVE", "Move"),
],
max_length=255,
),
),
]

View file

@ -0,0 +1,212 @@
# Generated by Django 3.2.20 on 2023-11-16 00:48
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0185_alter_notification_notification_type"),
]
operations = [
migrations.CreateModel(
name="ParentJob",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("task_id", models.UUIDField(blank=True, null=True, unique=True)),
(
"created_date",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"updated_date",
models.DateTimeField(default=django.utils.timezone.now),
),
("complete", models.BooleanField(default=False)),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("active", "Active"),
("complete", "Complete"),
("stopped", "Stopped"),
("failed", "Failed"),
],
default="pending",
max_length=50,
null=True,
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="sitesettings",
name="user_import_time_limit",
field=models.IntegerField(default=48),
),
migrations.AlterField(
model_name="notification",
name="notification_type",
field=models.CharField(
choices=[
("FAVORITE", "Favorite"),
("BOOST", "Boost"),
("REPLY", "Reply"),
("MENTION", "Mention"),
("TAG", "Tag"),
("FOLLOW", "Follow"),
("FOLLOW_REQUEST", "Follow Request"),
("IMPORT", "Import"),
("USER_IMPORT", "User Import"),
("USER_EXPORT", "User Export"),
("ADD", "Add"),
("REPORT", "Report"),
("LINK_DOMAIN", "Link Domain"),
("INVITE", "Invite"),
("ACCEPT", "Accept"),
("JOIN", "Join"),
("LEAVE", "Leave"),
("REMOVE", "Remove"),
("GROUP_PRIVACY", "Group Privacy"),
("GROUP_NAME", "Group Name"),
("GROUP_DESCRIPTION", "Group Description"),
("MOVE", "Move"),
],
max_length=255,
),
),
migrations.CreateModel(
name="BookwyrmExportJob",
fields=[
(
"parentjob_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.parentjob",
),
),
("export_data", models.FileField(null=True, upload_to="")),
],
options={
"abstract": False,
},
bases=("bookwyrm.parentjob",),
),
migrations.CreateModel(
name="BookwyrmImportJob",
fields=[
(
"parentjob_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.parentjob",
),
),
("archive_file", models.FileField(blank=True, null=True, upload_to="")),
("import_data", models.JSONField(null=True)),
(
"required",
django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(blank=True, max_length=50),
blank=True,
size=None,
),
),
],
options={
"abstract": False,
},
bases=("bookwyrm.parentjob",),
),
migrations.CreateModel(
name="ChildJob",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("task_id", models.UUIDField(blank=True, null=True, unique=True)),
(
"created_date",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"updated_date",
models.DateTimeField(default=django.utils.timezone.now),
),
("complete", models.BooleanField(default=False)),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("active", "Active"),
("complete", "Complete"),
("stopped", "Stopped"),
("failed", "Failed"),
],
default="pending",
max_length=50,
null=True,
),
),
(
"parent_job",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="child_jobs",
to="bookwyrm.parentjob",
),
),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="notification",
name="related_user_export",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="bookwyrm.bookwyrmexportjob",
),
),
]

View file

@ -0,0 +1,48 @@
# Generated by Django 3.2.20 on 2023-11-14 10:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0185_alter_notification_notification_type"),
]
operations = [
migrations.AddField(
model_name="notification",
name="related_invite_requests",
field=models.ManyToManyField(to="bookwyrm.InviteRequest"),
),
migrations.AlterField(
model_name="notification",
name="notification_type",
field=models.CharField(
choices=[
("FAVORITE", "Favorite"),
("BOOST", "Boost"),
("REPLY", "Reply"),
("MENTION", "Mention"),
("TAG", "Tag"),
("FOLLOW", "Follow"),
("FOLLOW_REQUEST", "Follow Request"),
("IMPORT", "Import"),
("ADD", "Add"),
("REPORT", "Report"),
("LINK_DOMAIN", "Link Domain"),
("INVITE_REQUEST", "Invite Request"),
("INVITE", "Invite"),
("ACCEPT", "Accept"),
("JOIN", "Join"),
("LEAVE", "Leave"),
("REMOVE", "Remove"),
("GROUP_PRIVACY", "Group Privacy"),
("GROUP_NAME", "Group Name"),
("GROUP_DESCRIPTION", "Group Description"),
("MOVE", "Move"),
],
max_length=255,
),
),
]

View file

@ -0,0 +1,54 @@
# Generated by Django 3.2.20 on 2023-11-09 16:57
import bookwyrm.models.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0186_invite_request_notification"),
]
operations = [
migrations.AddField(
model_name="book",
name="first_published_date_precision",
field=models.CharField(
blank=True,
choices=[
("DAY", "Day prec."),
("MONTH", "Month prec."),
("YEAR", "Year prec."),
],
editable=False,
max_length=10,
null=True,
),
),
migrations.AddField(
model_name="book",
name="published_date_precision",
field=models.CharField(
blank=True,
choices=[
("DAY", "Day prec."),
("MONTH", "Month prec."),
("YEAR", "Year prec."),
],
editable=False,
max_length=10,
null=True,
),
),
migrations.AlterField(
model_name="book",
name="first_published_date",
field=bookwyrm.models.fields.PartialDateField(blank=True, null=True),
),
migrations.AlterField(
model_name="book",
name="published_date",
field=bookwyrm.models.fields.PartialDateField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2023-11-20 18:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0187_partial_publication_dates"),
]
operations = [
migrations.AddField(
model_name="theme",
name="loads",
field=models.BooleanField(blank=True, null=True),
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.23 on 2023-12-12 23:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0188_theme_loads"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("ca-es", "Català (Catalan)"),
("de-de", "Deutsch (German)"),
("eo-uy", "Esperanto (Esperanto)"),
("es-es", "Español (Spanish)"),
("eu-es", "Euskara (Basque)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fi-fi", "Suomi (Finnish)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("nl-nl", "Nederlands (Dutch)"),
("no-no", "Norsk (Norwegian)"),
("pl-pl", "Polski (Polish)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("ro-ro", "Română (Romanian)"),
("sv-se", "Svenska (Swedish)"),
("uk-ua", "Українська (Ukrainian)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2023-11-22 10:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0186_auto_20231116_0048"),
("bookwyrm", "0188_theme_loads"),
]
operations = []

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.23 on 2023-11-23 19:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0189_merge_0186_auto_20231116_0048_0188_theme_loads"),
]
operations = [
migrations.AlterField(
model_name="notification",
name="notification_type",
field=models.CharField(
choices=[
("FAVORITE", "Favorite"),
("BOOST", "Boost"),
("REPLY", "Reply"),
("MENTION", "Mention"),
("TAG", "Tag"),
("FOLLOW", "Follow"),
("FOLLOW_REQUEST", "Follow Request"),
("IMPORT", "Import"),
("USER_IMPORT", "User Import"),
("USER_EXPORT", "User Export"),
("ADD", "Add"),
("REPORT", "Report"),
("LINK_DOMAIN", "Link Domain"),
("INVITE_REQUEST", "Invite Request"),
("INVITE", "Invite"),
("ACCEPT", "Accept"),
("JOIN", "Join"),
("LEAVE", "Leave"),
("REMOVE", "Remove"),
("GROUP_PRIVACY", "Group Privacy"),
("GROUP_NAME", "Group Name"),
("GROUP_DESCRIPTION", "Group Description"),
("MOVE", "Move"),
],
max_length=255,
),
),
]

View file

@ -0,0 +1,16 @@
# Generated by Django 3.2.20 on 2023-11-24 17:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0188_theme_loads"),
]
operations = [
migrations.RemoveIndex(
model_name="author",
name="bookwyrm_au_search__b050a8_gin",
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-01-02 03:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0189_alter_user_preferred_language"),
("bookwyrm", "0190_alter_notification_notification_type"),
]
operations = []

View file

@ -0,0 +1,76 @@
# Generated by Django 3.2.20 on 2023-11-25 00:47
from importlib import import_module
import re
from django.db import migrations
import pgtrigger.compiler
import pgtrigger.migrations
trigger_migration = import_module("bookwyrm.migrations.0077_auto_20210623_2155")
# it's _very_ convenient for development that this migration be reversible
search_vector_trigger = trigger_migration.Migration.operations[4]
author_search_vector_trigger = trigger_migration.Migration.operations[5]
assert re.search(r"\bCREATE TRIGGER search_vector_trigger\b", search_vector_trigger.sql)
assert re.search(
r"\bCREATE TRIGGER author_search_vector_trigger\b",
author_search_vector_trigger.sql,
)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0190_book_search_updates"),
]
operations = [
pgtrigger.migrations.AddTrigger(
model_name="book",
trigger=pgtrigger.compiler.Trigger(
name="update_search_vector_on_book_edit",
sql=pgtrigger.compiler.UpsertTriggerSql(
func="new.search_vector := 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(bookwyrm_author.name), ' '), '')), 'C') 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 ) || setweight(to_tsvector('english', coalesce(new.series, '')), 'D');RETURN NEW;",
hash="77d6399497c0a89b0bf09d296e33c396da63705c",
operation='INSERT OR UPDATE OF "title", "subtitle", "series", "search_vector"',
pgid="pgtrigger_update_search_vector_on_book_edit_bec58",
table="bookwyrm_book",
when="BEFORE",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="author",
trigger=pgtrigger.compiler.Trigger(
name="reset_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="e7bbf08711ff3724c58f4d92fb7a082ffb3d7826",
operation='UPDATE OF "name"',
pgid="pgtrigger_reset_search_vector_on_author_edit_a447c",
table="bookwyrm_author",
when="AFTER",
),
),
),
migrations.RunSQL(
sql="""DROP TRIGGER IF EXISTS search_vector_trigger ON bookwyrm_book;
DROP FUNCTION IF EXISTS book_trigger;
""",
reverse_sql=search_vector_trigger.sql,
),
migrations.RunSQL(
sql="""DROP TRIGGER IF EXISTS author_search_vector_trigger ON bookwyrm_author;
DROP FUNCTION IF EXISTS author_trigger;
""",
reverse_sql=author_search_vector_trigger.sql,
),
migrations.RunSQL(
# Recalculate book search vector for any missed author name changes
# due to bug in JOIN in the old trigger.
sql="UPDATE bookwyrm_book SET search_vector = NULL;",
reverse_sql=migrations.RunSQL.noop,
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.23 on 2024-01-04 23:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.AlterField(
model_name="quotation",
name="endposition",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="quotation",
name="position",
field=models.TextField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-02 19:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.RenameField(
model_name="sitesettings",
old_name="version",
new_name="available_version",
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-16 10:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="user_exports_enabled",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,92 @@
# Generated by Django 3.2.23 on 2024-01-28 02:49
import bookwyrm.storage_backends
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0192_sitesettings_user_exports_enabled"),
]
operations = [
migrations.AddField(
model_name="bookwyrmexportjob",
name="export_json",
field=models.JSONField(
encoder=django.core.serializers.json.DjangoJSONEncoder, null=True
),
),
migrations.AddField(
model_name="bookwyrmexportjob",
name="json_completed",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="bookwyrmexportjob",
name="export_data",
field=models.FileField(
null=True,
storage=bookwyrm.storage_backends.ExportsFileStorage,
upload_to="",
),
),
migrations.CreateModel(
name="AddFileToTar",
fields=[
(
"childjob_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.childjob",
),
),
(
"parent_export_job",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="child_edition_export_jobs",
to="bookwyrm.bookwyrmexportjob",
),
),
],
options={
"abstract": False,
},
bases=("bookwyrm.childjob",),
),
migrations.CreateModel(
name="AddBookToUserExportJob",
fields=[
(
"childjob_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.childjob",
),
),
(
"edition",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="bookwyrm.edition",
),
),
],
options={
"abstract": False,
},
bases=("bookwyrm.childjob",),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-02-03 15:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0192_make_page_positions_text"),
("bookwyrm", "0192_sitesettings_user_exports_enabled"),
]
operations = []

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-02-03 16:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0192_rename_version_sitesettings_available_version"),
("bookwyrm", "0193_merge_20240203_1539"),
]
operations = []

View file

@ -0,0 +1,46 @@
# Generated by Django 3.2.23 on 2024-02-21 00:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0194_merge_20240203_1619"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("ca-es", "Català (Catalan)"),
("de-de", "Deutsch (German)"),
("eo-uy", "Esperanto (Esperanto)"),
("es-es", "Español (Spanish)"),
("eu-es", "Euskara (Basque)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("ko-kr", "한국어 (Korean)"),
("fi-fi", "Suomi (Finnish)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("nl-nl", "Nederlands (Dutch)"),
("no-no", "Norsk (Norwegian)"),
("pl-pl", "Polski (Polish)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("ro-ro", "Română (Romanian)"),
("sv-se", "Svenska (Swedish)"),
("uk-ua", "Українська (Ukrainian)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-03-18 17:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0193_auto_20240128_0249"),
("bookwyrm", "0195_alter_user_preferred_language"),
]
operations = []

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-03-18 00:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_migrate_search_vec_triggers_to_pgtriggers"),
("bookwyrm", "0195_alter_user_preferred_language"),
]
operations = []

View file

@ -0,0 +1,41 @@
# Generated by Django 3.2.25 on 2024-03-20 15:15
import django.contrib.postgres.indexes
from django.db import migrations
import pgtrigger.compiler
import pgtrigger.migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0196_merge_pr3134_into_main"),
]
operations = [
migrations.AddIndex(
model_name="author",
index=django.contrib.postgres.indexes.GinIndex(
fields=["search_vector"], name="bookwyrm_au_search__b050a8_gin"
),
),
pgtrigger.migrations.AddTrigger(
model_name="author",
trigger=pgtrigger.compiler.Trigger(
name="update_search_vector_on_author_edit",
sql=pgtrigger.compiler.UpsertTriggerSql(
func="new.search_vector := setweight(to_tsvector('simple', new.name), 'A') || setweight(to_tsvector('simple', coalesce(array_to_string(new.aliases, ' '), '')), 'B');RETURN NEW;",
hash="b97919016236d74d0ade51a0769a173ea269da64",
operation='INSERT OR UPDATE OF "name", "aliases", "search_vector"',
pgid="pgtrigger_update_search_vector_on_author_edit_c61cb",
table="bookwyrm_author",
when="BEFORE",
),
),
),
migrations.RunSQL(
# Calculate search vector for all Authors.
sql="UPDATE bookwyrm_author SET search_vector = NULL;",
reverse_sql="UPDATE bookwyrm_author SET search_vector = NULL;",
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.25 on 2024-03-24 02:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0196_merge_20240318_1737"),
("bookwyrm", "0196_merge_pr3134_into_main"),
]
operations = []

View file

@ -0,0 +1,48 @@
# Generated by Django 3.2.24 on 2024-02-28 21:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0196_merge_pr3134_into_main"),
]
operations = [
migrations.CreateModel(
name="MergedBook",
fields=[
("deleted_id", models.IntegerField(primary_key=True, serialize=False)),
(
"merged_into",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="absorbed",
to="bookwyrm.book",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="MergedAuthor",
fields=[
("deleted_id", models.IntegerField(primary_key=True, serialize=False)),
(
"merged_into",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="absorbed",
to="bookwyrm.author",
),
),
],
options={
"abstract": False,
},
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2024-03-26 11:37
import bookwyrm.models.bookwyrm_export_job
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0197_merge_20240324_0235"),
]
operations = [
migrations.AlterField(
model_name="bookwyrmexportjob",
name="export_data",
field=models.FileField(
null=True,
storage=bookwyrm.models.bookwyrm_export_job.select_exports_storage,
upload_to="",
),
),
]

View file

@ -0,0 +1,57 @@
# Generated by Django 3.2.25 on 2024-03-20 15:52
from django.db import migrations
import pgtrigger.compiler
import pgtrigger.migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0197_author_search_vector"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="author",
name="reset_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="68422c0f29879c5802b82159dde45297eff53e73",
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="9324f5ca76a6f5e63931881d62d11da11f595b2c",
operation='INSERT OR UPDATE OF "title", "subtitle", "series", "search_vector"',
pgid="pgtrigger_update_search_vector_on_book_edit_bec58",
table="bookwyrm_book",
when="BEFORE",
),
),
),
migrations.RunSQL(
# Recalculate search vector for all Books because it now includes
# Author aliases.
sql="UPDATE bookwyrm_book SET search_vector = NULL;",
reverse_sql="UPDATE bookwyrm_book SET search_vector = NULL;",
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.25 on 2024-03-26 12:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0198_alter_bookwyrmexportjob_export_data"),
("bookwyrm", "0198_book_search_vector_author_aliases"),
]
operations = []

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2024-04-02 19:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0198_book_search_vector_author_aliases"),
]
operations = [
migrations.AddIndex(
model_name="status",
index=models.Index(
fields=["remote_id"], name="bookwyrm_st_remote__06aeba_idx"
),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 3.2.25 on 2024-03-27 19:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0199_merge_20240326_1217"),
]
operations = [
migrations.RemoveField(
model_name="addfiletotar",
name="childjob_ptr",
),
migrations.RemoveField(
model_name="addfiletotar",
name="parent_export_job",
),
migrations.DeleteModel(
name="AddBookToUserExportJob",
),
migrations.DeleteModel(
name="AddFileToTar",
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2024-04-03 19:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0199_status_bookwyrm_st_remote__06aeba_idx"),
]
operations = [
migrations.AddIndex(
model_name="status",
index=models.Index(
fields=["thread_id"], name="bookwyrm_st_thread__cf064f_idx"
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2024-04-03 19:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0200_status_bookwyrm_st_thread__cf064f_idx"),
]
operations = [
migrations.AddIndex(
model_name="keypair",
index=models.Index(
fields=["remote_id"], name="bookwyrm_ke_remote__472927_idx"
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2024-04-03 19:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0201_keypair_bookwyrm_ke_remote__472927_idx"),
]
operations = [
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["username"], name="bookwyrm_us_usernam_b2546d_idx"
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2024-04-03 19:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0202_user_bookwyrm_us_usernam_b2546d_idx"),
]
operations = [
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["is_active", "local"], name="bookwyrm_us_is_acti_972dc4_idx"
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.25 on 2024-04-09 10:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0197_mergedauthor_mergedbook"),
("bookwyrm", "0203_user_bookwyrm_us_is_acti_972dc4_idx"),
]
operations = []

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.25 on 2024-04-13 02:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0200_auto_20240327_1914"),
("bookwyrm", "0204_merge_20240409_1042"),
]
operations = []

View file

@ -26,13 +26,17 @@ from .federated_server import FederatedServer
from .group import Group, GroupMember, GroupMemberInvitation
from .import_job import ImportJob, ImportItem
from .bookwyrm_import_job import BookwyrmImportJob
from .bookwyrm_export_job import BookwyrmExportJob
from .move import MoveUser
from .site import SiteSettings, Theme, SiteInvite
from .site import PasswordReset, InviteRequest
from .announcement import Announcement
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
from .notification import Notification
from .notification import Notification, NotificationType
from .hashtag import Hashtag

View file

@ -152,8 +152,9 @@ class ActivitypubMixin:
# find anyone who's tagged in a status, for example
mentions = self.recipients if hasattr(self, "recipients") else []
# we always send activities to explicitly mentioned users' inboxes
recipients = [u.inbox for u in mentions or [] if not u.local]
# we always send activities to explicitly mentioned users (using shared inboxes
# where available to avoid duplicate submissions to a given instance)
recipients = {u.shared_inbox or u.inbox for u in mentions if not u.local}
# unless it's a dm, all the followers should receive the activity
if privacy != "direct":
@ -173,18 +174,18 @@ class ActivitypubMixin:
if user:
queryset = queryset.filter(following=user)
# ideally, we will send to shared inboxes for efficiency
shared_inboxes = (
queryset.filter(shared_inbox__isnull=False)
.values_list("shared_inbox", flat=True)
.distinct()
# as above, we prefer shared inboxes if available
recipients.update(
queryset.filter(shared_inbox__isnull=False).values_list(
"shared_inbox", flat=True
)
)
# but not everyone has a shared inbox
inboxes = queryset.filter(shared_inbox__isnull=True).values_list(
"inbox", flat=True
recipients.update(
queryset.filter(shared_inbox__isnull=True).values_list(
"inbox", flat=True
)
)
recipients += list(shared_inboxes) + list(inboxes)
return list(set(recipients))
return list(recipients)
def to_activity_dataclass(self):
"""convert from a model to an activity"""
@ -602,7 +603,7 @@ def to_ordered_collection_page(
if activity_page.has_next():
next_page = f"{remote_id}?page={activity_page.next_page_number()}"
if activity_page.has_previous():
prev_page = f"{remote_id}?page=%d{activity_page.previous_page_number()}"
prev_page = f"{remote_id}?page={activity_page.previous_page_number()}"
return activitypub.OrderedCollectionPage(
id=f"{remote_id}?page={page}",
partOf=remote_id,

View file

@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
from bookwyrm.tasks import app, MISC
from .base_model import BookWyrmModel
from .notification import NotificationType
from .user import User
@ -80,7 +81,7 @@ def automod_task():
with transaction.atomic():
for admin in admins:
notification, _ = notification_model.objects.get_or_create(
user=admin, notification_type=notification_model.REPORT, read=False
user=admin, notification_type=NotificationType.REPORT, read=False
)
notification.related_reports.set(reports)

View file

@ -1,18 +1,25 @@
""" database schema for info about authors """
import re
from django.contrib.postgres.indexes import GinIndex
from typing import Tuple, Any
from django.db import models
from django.contrib.postgres.indexes import GinIndex
import pgtrigger
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from bookwyrm.utils.db import format_trigger
from .book import BookDataModel
from .book import BookDataModel, MergedAuthor
from . import fields
class Author(BookDataModel):
"""basic biographic info"""
merged_model = MergedAuthor
wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
@ -38,7 +45,7 @@ class Author(BookDataModel):
)
bio = fields.HtmlField(null=True, blank=True)
def save(self, *args, **kwargs):
def save(self, *args: Tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
"""normalize isni format"""
if self.isni:
self.isni = re.sub(r"\s", "", self.isni)
@ -63,11 +70,48 @@ class Author(BookDataModel):
def get_remote_id(self):
"""editions and works both use "book" instead of model_name"""
return f"https://{DOMAIN}/author/{self.id}"
activity_serializer = activitypub.Author
return f"{BASE_URL}/author/{self.id}"
class Meta:
"""sets up postgres GIN index field"""
"""sets up indexes and triggers"""
# pylint: disable=line-too-long
indexes = (GinIndex(fields=["search_vector"]),)
triggers = [
pgtrigger.Trigger(
name="update_search_vector_on_author_edit",
when=pgtrigger.Before,
operation=pgtrigger.Insert
| pgtrigger.UpdateOf("name", "aliases", "search_vector"),
func=format_trigger(
"""new.search_vector :=
-- author name, with priority A
setweight(to_tsvector('simple', new.name), 'A') ||
-- author aliases, with priority B
setweight(to_tsvector('simple', coalesce(array_to_string(new.aliases, ' '), '')), 'B');
RETURN new;
"""
),
),
pgtrigger.Trigger(
name="reset_book_search_vector_on_author_edit",
when=pgtrigger.After,
operation=pgtrigger.UpdateOf("name", "aliases"),
func=format_trigger(
"""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;
"""
),
),
]
activity_serializer = activitypub.Author

View file

@ -10,7 +10,7 @@ from django.http import Http404
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .fields import RemoteIdField
@ -38,7 +38,7 @@ class BookWyrmModel(models.Model):
def get_remote_id(self):
"""generate the url that resolves to the local object, without a slug"""
base_path = f"https://{DOMAIN}"
base_path = BASE_URL
if hasattr(self, "user"):
base_path = f"{base_path}{self.user.local_path}"
@ -53,7 +53,7 @@ class BookWyrmModel(models.Model):
@property
def local_path(self):
"""how to link to this object in the local app, with a slug"""
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
local = self.get_remote_id().replace(BASE_URL, "")
name = None
if hasattr(self, "name_field"):

View file

@ -1,29 +1,33 @@
""" database schema for books and shelves """
from itertools import chain
import re
from typing import Any
from typing import Any, Dict
from typing_extensions import Self
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.core.cache import cache
from django.db import models, transaction
from django.db.models import Prefetch
from django.db.models import Prefetch, ManyToManyField
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager
from imagekit.models import ImageSpecField
import pgtrigger
from bookwyrm import activitypub
from bookwyrm.isbn.isbn import hyphenator_singleton as hyphenator
from bookwyrm.preview_images import generate_edition_preview_image_task
from bookwyrm.settings import (
DOMAIN,
BASE_URL,
DEFAULT_LANGUAGE,
LANGUAGE_ARTICLES,
ENABLE_PREVIEW_IMAGES,
ENABLE_THUMBNAIL_GENERATION,
)
from bookwyrm.utils.db import format_trigger
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel
@ -106,10 +110,115 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
"""only send book data updates to other bookwyrm instances"""
super().broadcast(activity, sender, software=software, **kwargs)
def merge_into(self, canonical: Self, dry_run=False) -> Dict[str, Any]:
"""merge this entity into another entity"""
if canonical.id == self.id:
raise ValueError(f"Cannot merge {self} into itself")
absorbed_fields = canonical.absorb_data_from(self, dry_run=dry_run)
if dry_run:
return absorbed_fields
canonical.save()
self.merged_model.objects.create(deleted_id=self.id, merged_into=canonical)
# move related models to canonical
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 arent auto-created. These
# should have a corresponding OneToMany field in the model for
# the linking table anyway. If we update it through that model
# instead then we wont lose the extra fields in the linking
# table.
# pylint: disable=protected-access
related_field_obj = related_model._meta.get_field(related_field)
if isinstance(related_field_obj, ManyToManyField):
through = related_field_obj.remote_field.through
if not through._meta.auto_created:
continue
related_objs = related_model.objects.filter(**{related_field: self})
for related_obj in related_objs:
try:
setattr(related_obj, related_field, canonical)
related_obj.save()
except TypeError:
getattr(related_obj, related_field).add(canonical)
getattr(related_obj, related_field).remove(self)
self.delete()
return absorbed_fields
def absorb_data_from(self, other: Self, dry_run=False) -> Dict[str, Any]:
"""fill empty fields with values from another entity"""
absorbed_fields = {}
for data_field in self._meta.get_fields():
if not hasattr(data_field, "activitypub_field"):
continue
canonical_value = getattr(self, data_field.name)
other_value = getattr(other, data_field.name)
if not other_value:
continue
if isinstance(data_field, fields.ArrayField):
if new_values := list(set(other_value) - set(canonical_value)):
# append at the end (in no particular order)
if not dry_run:
setattr(self, data_field.name, canonical_value + new_values)
absorbed_fields[data_field.name] = new_values
elif isinstance(data_field, fields.PartialDateField):
if (
(not canonical_value)
or (other_value.has_day and not canonical_value.has_day)
or (other_value.has_month and not canonical_value.has_month)
):
if not dry_run:
setattr(self, data_field.name, other_value)
absorbed_fields[data_field.name] = other_value
else:
if not canonical_value:
if not dry_run:
setattr(self, data_field.name, other_value)
absorbed_fields[data_field.name] = other_value
return absorbed_fields
class MergedBookDataModel(models.Model):
"""a BookDataModel instance that has been merged into another instance. kept
to be able to redirect old URLs"""
deleted_id = models.IntegerField(primary_key=True)
class Meta:
"""abstract just like BookDataModel"""
abstract = True
class MergedBook(MergedBookDataModel):
"""an Book that has been merged into another one"""
merged_into = models.ForeignKey(
"Book", on_delete=models.PROTECT, related_name="absorbed"
)
class MergedAuthor(MergedBookDataModel):
"""an Author that has been merged into another one"""
merged_into = models.ForeignKey(
"Author", on_delete=models.PROTECT, related_name="absorbed"
)
class Book(BookDataModel):
"""a generic book, which can mean either an edition or a work"""
merged_model = MergedBook
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
# book/work metadata
@ -135,8 +244,8 @@ class Book(BookDataModel):
preview_image = models.ImageField(
upload_to="previews/covers/", blank=True, null=True
)
first_published_date = fields.DateTimeField(blank=True, null=True)
published_date = fields.DateTimeField(blank=True, null=True)
first_published_date = fields.PartialDateField(blank=True, null=True)
published_date = fields.PartialDateField(blank=True, null=True)
objects = InheritanceManager()
field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"])
@ -190,9 +299,13 @@ class Book(BookDataModel):
"""properties of this edition, as a string"""
items = [
self.physical_format if hasattr(self, "physical_format") else None,
f"{self.languages[0]} language"
if self.languages and self.languages[0] and self.languages[0] != "English"
else None,
(
f"{self.languages[0]} language"
if self.languages
and self.languages[0]
and self.languages[0] != "English"
else None
),
str(self.published_date.year) if self.published_date else None,
", ".join(self.publishers) if hasattr(self, "publishers") else None,
]
@ -201,21 +314,20 @@ class Book(BookDataModel):
@property
def alt_text(self):
"""image alt test"""
text = self.title
if self.edition_info:
text += f" ({self.edition_info})"
return text
author = f"{name}: " if (name := self.author_text) else ""
edition = f" ({info})" if (info := self.edition_info) else ""
return f"{author}{self.title}{edition}"
def save(self, *args: Any, **kwargs: Any) -> None:
"""can't be abstract for query reasons, but you shouldn't USE it"""
if not isinstance(self, Edition) and not isinstance(self, Work):
if not isinstance(self, (Edition, Work)):
raise ValueError("Books should be added as Editions or Works")
return super().save(*args, **kwargs)
def get_remote_id(self):
"""editions and works both use "book" instead of model_name"""
return f"https://{DOMAIN}/book/{self.id}"
return f"{BASE_URL}/book/{self.id}"
def guess_sort_title(self):
"""Get a best-guess sort title for the current book"""
@ -233,9 +345,49 @@ class Book(BookDataModel):
)
class Meta:
"""sets up postgres GIN index field"""
"""set up indexes and triggers"""
# pylint: disable=line-too-long
indexes = (GinIndex(fields=["search_vector"]),)
triggers = [
pgtrigger.Trigger(
name="update_search_vector_on_book_edit",
when=pgtrigger.Before,
operation=pgtrigger.Insert
| pgtrigger.UpdateOf("title", "subtitle", "series", "search_vector"),
func=format_trigger(
"""
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
-- title, with priority A (parse in English, default to simple if empty)
setweight(COALESCE(nullif(
to_tsvector('english', new.title), ''),
to_tsvector('simple', new.title)), 'A') ||
-- subtitle, with priority B (always in English?)
setweight(to_tsvector('english', COALESCE(new.subtitle, '')), 'B') ||
-- list of authors names and aliases (with priority C)
(SELECT setweight(to_tsvector('simple', COALESCE(array_to_string(ARRAY_AGG(name_and_aliases), ' '), '')), 'C')
FROM author_names
) ||
--- last: series name, with lowest priority
setweight(to_tsvector('english', COALESCE(new.series, '')), 'D')
INTO new.search_vector;
RETURN new;
"""
),
)
]
class Work(OrderedCollectionPageMixin, Book):
@ -367,9 +519,9 @@ class Edition(Book):
# normalize isbn format
if self.isbn_10:
self.isbn_10 = re.sub(r"[^0-9X]", "", self.isbn_10)
self.isbn_10 = normalize_isbn(self.isbn_10)
if self.isbn_13:
self.isbn_13 = re.sub(r"[^0-9X]", "", self.isbn_13)
self.isbn_13 = normalize_isbn(self.isbn_13)
# set rank
self.edition_rank = self.get_rank()
@ -464,6 +616,11 @@ def isbn_13_to_10(isbn_13):
return converted + str(checkdigit)
def normalize_isbn(isbn):
"""Remove unexpected characters from ISBN 10 or 13"""
return re.sub(r"[^0-9X]", "", isbn)
# pylint: disable=unused-argument
@receiver(models.signals.post_save, sender=Edition)
def preview_image(instance, *args, **kwargs):

View file

@ -0,0 +1,334 @@
"""Export user account to tar.gz file for import into another Bookwyrm instance"""
import logging
import os
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.utils.module_loading import import_string
from bookwyrm import settings, storage_backends
from bookwyrm.models import AnnualGoal, ReadThrough, ShelfBook, ListItem
from bookwyrm.models import Review, Comment, Quotation
from bookwyrm.models import Edition
from bookwyrm.models import UserFollows, User, UserBlocks
from bookwyrm.models.job import ParentJob
from bookwyrm.tasks import app, IMPORTS
from bookwyrm.utils.tar import BookwyrmTarFile
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
kwargs["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
return super().client("s3", *args, **kwargs)
def select_exports_storage():
"""callable to allow for dependency on runtime configuration"""
cls = import_string(settings.EXPORTS_STORAGE)
return cls()
class BookwyrmExportJob(ParentJob):
"""entry for a specific request to export a bookwyrm user"""
export_data = FileField(null=True, storage=select_exports_storage)
export_json = JSONField(null=True, encoder=DjangoJSONEncoder)
json_completed = BooleanField(default=False)
def start_job(self):
"""schedule the first task"""
task = create_export_json_task.delay(job_id=self.id)
self.task_id = task.id
self.save(update_fields=["task_id"])
@app.task(queue=IMPORTS)
def create_export_json_task(job_id):
"""create the JSON data for the export"""
job = BookwyrmExportJob.objects.get(id=job_id)
# don't start the job if it was stopped from the UI
if job.complete:
return
try:
job.set_status("active")
# generate JSON structure
job.export_json = export_json(job.user)
job.save(update_fields=["export_json"])
# create archive in separate task
create_archive_task.delay(job_id=job.id)
except Exception as err: # pylint: disable=broad-except
logger.exception(
"create_export_json_task for %s failed with error: %s", job, err
)
job.set_status("failed")
def archive_file_location(file, directory="") -> str:
"""get the relative location of a file inside the archive"""
return os.path.join(directory, file.name)
def add_file_to_s3_tar(s3_tar: S3Tar, storage, file, directory=""):
"""
add file to S3Tar inside directory, keeping any directories under its
storage location
"""
s3_tar.add_file(
os.path.join(storage.location, file.name),
folder=os.path.dirname(archive_file_location(file, directory=directory)),
)
@app.task(queue=IMPORTS)
def create_archive_task(job_id):
"""create the archive containing the JSON file and additional files"""
job = BookwyrmExportJob.objects.get(id=job_id)
# don't start the job if it was stopped from the UI
if job.complete:
return
try:
export_task_id = str(job.task_id)
archive_filename = f"{export_task_id}.tar.gz"
export_json_bytes = DjangoJSONEncoder().encode(job.export_json).encode("utf-8")
user = job.user
editions = get_books_for_user(user)
if settings.USE_S3:
# Storage for writing temporary files
exports_storage = storage_backends.ExportsS3Storage()
# Handle for creating the final archive
s3_tar = S3Tar(
exports_storage.bucket_name,
os.path.join(exports_storage.location, archive_filename),
session=BookwyrmAwsSession(),
)
# Save JSON file to a temporary location
export_json_tmp_file = os.path.join(export_task_id, "archive.json")
exports_storage.save(
export_json_tmp_file,
ContentFile(export_json_bytes),
)
s3_tar.add_file(
os.path.join(exports_storage.location, export_json_tmp_file)
)
# Add images to TAR
images_storage = storage_backends.ImagesStorage()
if user.avatar:
add_file_to_s3_tar(s3_tar, images_storage, user.avatar)
for edition in editions:
if edition.cover:
add_file_to_s3_tar(
s3_tar, images_storage, edition.cover, directory="images"
)
# Create archive and store file name
s3_tar.tar()
job.export_data = archive_filename
job.save(update_fields=["export_data"])
# Delete temporary files
exports_storage.delete(export_json_tmp_file)
else:
job.export_data = archive_filename
with job.export_data.open("wb") as tar_file:
with BookwyrmTarFile.open(mode="w:gz", fileobj=tar_file) as tar:
# save json file
tar.write_bytes(export_json_bytes)
# Add avatar image if present
if user.avatar:
tar.add_image(user.avatar)
for edition in editions:
if edition.cover:
tar.add_image(edition.cover, directory="images")
job.save(update_fields=["export_data"])
job.set_status("completed")
except Exception as err: # pylint: disable=broad-except
logger.exception("create_archive_task for %s failed with error: %s", job, err)
job.set_status("failed")
def export_json(user: User):
"""create export JSON"""
data = export_user(user) # in the root of the JSON structure
data["settings"] = export_settings(user)
data["goals"] = export_goals(user)
data["books"] = export_books(user)
data["saved_lists"] = export_saved_lists(user)
data["follows"] = export_follows(user)
data["blocks"] = export_blocks(user)
return data
def export_user(user: User):
"""export user data"""
data = user.to_activity()
if user.avatar:
data["icon"]["url"] = archive_file_location(user.avatar)
else:
data["icon"] = {}
return data
def export_settings(user: User):
"""Additional settings - can't be serialized as AP"""
vals = [
"show_goal",
"preferred_timezone",
"default_post_privacy",
"show_suggested_users",
]
return {k: getattr(user, k) for k in vals}
def export_saved_lists(user: User):
"""add user saved lists to export JSON"""
return [l.remote_id for l in user.saved_lists.all()]
def export_follows(user: User):
"""add user follows to export JSON"""
follows = UserFollows.objects.filter(user_subject=user).distinct()
following = User.objects.filter(userfollows_user_object__in=follows).distinct()
return [f.remote_id for f in following]
def export_blocks(user: User):
"""add user blocks to export JSON"""
blocks = UserBlocks.objects.filter(user_subject=user).distinct()
blocking = User.objects.filter(userblocks_user_object__in=blocks).distinct()
return [b.remote_id for b in blocking]
def export_goals(user: User):
"""add user reading goals to export JSON"""
reading_goals = AnnualGoal.objects.filter(user=user).distinct()
return [
{"goal": goal.goal, "year": goal.year, "privacy": goal.privacy}
for goal in reading_goals
]
def export_books(user: User):
"""add books to export JSON"""
editions = get_books_for_user(user)
return [export_book(user, edition) for edition in editions]
def export_book(user: User, edition: Edition):
"""add book to export JSON"""
data = {}
data["work"] = edition.parent_work.to_activity()
data["edition"] = edition.to_activity()
if edition.cover:
data["edition"]["cover"]["url"] = archive_file_location(
edition.cover, directory="images"
)
# authors
data["authors"] = [author.to_activity() for author in edition.authors.all()]
# Shelves this book is on
# Every ShelfItem is this book so we don't other serializing
shelf_books = (
ShelfBook.objects.select_related("shelf")
.filter(user=user, book=edition)
.distinct()
)
data["shelves"] = [shelfbook.shelf.to_activity() for shelfbook in shelf_books]
# Lists and ListItems
# ListItems include "notes" and "approved" so we need them
# even though we know it's this book
list_items = ListItem.objects.filter(book=edition, user=user).distinct()
data["lists"] = []
for item in list_items:
list_info = item.book_list.to_activity()
list_info[
"privacy"
] = item.book_list.privacy # this isn't serialized so we add it
list_info["list_item"] = item.to_activity()
data["lists"].append(list_info)
# Statuses
# Can't use select_subclasses here because
# we need to filter on the "book" value,
# which is not available on an ordinary Status
for status in ["comments", "quotations", "reviews"]:
data[status] = []
comments = Comment.objects.filter(user=user, book=edition).all()
for status in comments:
obj = status.to_activity()
obj["progress"] = status.progress
obj["progress_mode"] = status.progress_mode
data["comments"].append(obj)
quotes = Quotation.objects.filter(user=user, book=edition).all()
for status in quotes:
obj = status.to_activity()
obj["position"] = status.position
obj["endposition"] = status.endposition
obj["position_mode"] = status.position_mode
data["quotations"].append(obj)
reviews = Review.objects.filter(user=user, book=edition).all()
data["reviews"] = [status.to_activity() for status in reviews]
# readthroughs can't be serialized to activity
book_readthroughs = (
ReadThrough.objects.filter(user=user, book=edition).distinct().values()
)
data["readthroughs"] = list(book_readthroughs)
return data
def get_books_for_user(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()
)
return editions

View file

@ -0,0 +1,462 @@
"""Import a user from another Bookwyrm instance"""
import json
import logging
from django.db.models import FileField, JSONField, CharField
from django.utils import timezone
from django.utils.html import strip_tags
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
from bookwyrm import activitypub
from bookwyrm import models
from bookwyrm.tasks import app, IMPORTS
from bookwyrm.models.job import ParentJob, ParentTask, SubTask
from bookwyrm.utils.tar import BookwyrmTarFile
logger = logging.getLogger(__name__)
class BookwyrmImportJob(ParentJob):
"""entry for a specific request for importing a bookwyrm user backup"""
archive_file = FileField(null=True, blank=True)
import_data = JSONField(null=True)
required = DjangoArrayField(CharField(max_length=50, blank=True), blank=True)
def start_job(self):
"""Start the job"""
start_import_task.delay(job_id=self.id, no_children=True)
@app.task(queue=IMPORTS, base=ParentTask)
def start_import_task(**kwargs):
"""trigger the child import tasks for each user data"""
job = BookwyrmImportJob.objects.get(id=kwargs["job_id"])
archive_file = job.archive_file
# don't start the job if it was stopped from the UI
if job.complete:
return
try:
archive_file.open("rb")
with BookwyrmTarFile.open(mode="r:gz", fileobj=archive_file) as tar:
json_filename = next(
filter(lambda n: n.startswith("archive"), tar.getnames())
)
job.import_data = json.loads(tar.read(json_filename).decode("utf-8"))
if "include_user_profile" in job.required:
update_user_profile(job.user, tar, job.import_data)
if "include_user_settings" in job.required:
update_user_settings(job.user, job.import_data)
if "include_goals" in job.required:
update_goals(job.user, job.import_data.get("goals", []))
if "include_saved_lists" in job.required:
upsert_saved_lists(job.user, job.import_data.get("saved_lists", []))
if "include_follows" in job.required:
upsert_follows(job.user, job.import_data.get("follows", []))
if "include_blocks" in job.required:
upsert_user_blocks(job.user, job.import_data.get("blocks", []))
process_books(job, tar)
job.set_status("complete")
archive_file.close()
except Exception as err: # pylint: disable=broad-except
logger.exception("User Import Job %s Failed with error: %s", job.id, err)
job.set_status("failed")
def process_books(job, tar):
"""
Process user import data related to books
We always import the books even if not assigning
them to shelves, lists etc
"""
books = job.import_data.get("books")
for data in books:
book = get_or_create_edition(data, tar)
if "include_shelves" in job.required:
upsert_shelves(book, job.user, data)
if "include_readthroughs" in job.required:
upsert_readthroughs(data.get("readthroughs"), job.user, book.id)
if "include_comments" in job.required:
upsert_statuses(
job.user, models.Comment, data.get("comments"), book.remote_id
)
if "include_quotations" in job.required:
upsert_statuses(
job.user, models.Quotation, data.get("quotations"), book.remote_id
)
if "include_reviews" in job.required:
upsert_statuses(
job.user, models.Review, data.get("reviews"), book.remote_id
)
if "include_lists" in job.required:
upsert_lists(job.user, data.get("lists"), book.id)
def get_or_create_edition(book_data, tar):
"""Take a JSON string of work and edition data,
find or create the edition and work in the database and
return an edition instance"""
edition = book_data.get("edition")
existing = models.Edition.find_existing(edition)
if existing:
return existing
# make sure we have the authors in the local DB
# replace the old author ids in the edition JSON
edition["authors"] = []
for author in book_data.get("authors"):
parsed_author = activitypub.parse(author)
instance = parsed_author.to_model(
model=models.Author, save=True, overwrite=True
)
edition["authors"].append(instance.remote_id)
# we will add the cover later from the tar
# don't try to load it from the old server
cover = edition.get("cover", {})
cover_path = cover.get("url", None)
edition["cover"] = {}
# first we need the parent work to exist
work = book_data.get("work")
work["editions"] = []
parsed_work = activitypub.parse(work)
work_instance = parsed_work.to_model(model=models.Work, save=True, overwrite=True)
# now we have a work we can add it to the edition
# and create the edition model instance
edition["work"] = work_instance.remote_id
parsed_edition = activitypub.parse(edition)
book = parsed_edition.to_model(model=models.Edition, save=True, overwrite=True)
# set the cover image from the tar
if cover_path:
tar.write_image_to_file(cover_path, book.cover)
return book
def upsert_readthroughs(data, user, book_id):
"""Take a JSON string of readthroughs and
find or create the instances in the database"""
for read_through in data:
obj = {}
keys = [
"progress_mode",
"start_date",
"finish_date",
"stopped_date",
"is_active",
]
for key in keys:
obj[key] = read_through[key]
obj["user_id"] = user.id
obj["book_id"] = book_id
existing = models.ReadThrough.objects.filter(**obj).first()
if not existing:
models.ReadThrough.objects.create(**obj)
def upsert_statuses(user, cls, data, book_remote_id):
"""Take a JSON string of a status and
find or create the instances in the database"""
for status in data:
if is_alias(
user, status["attributedTo"]
): # don't let l33t hax0rs steal other people's posts
# update ids and remove replies
status["attributedTo"] = user.remote_id
status["to"] = update_followers_address(user, status["to"])
status["cc"] = update_followers_address(user, status["cc"])
status[
"replies"
] = (
{}
) # this parses incorrectly but we can't set it without knowing the new id
status["inReplyToBook"] = book_remote_id
parsed = activitypub.parse(status)
if not status_already_exists(
user, parsed
): # don't duplicate posts on multiple import
instance = parsed.to_model(model=cls, save=True, overwrite=True)
for val in [
"progress",
"progress_mode",
"position",
"endposition",
"position_mode",
]:
if status.get(val):
instance.val = status[val]
instance.remote_id = instance.get_remote_id() # update the remote_id
instance.save() # save and broadcast
else:
logger.warning("User does not have permission to import statuses")
def upsert_lists(user, lists, book_id):
"""Take a list of objects each containing
a list and list item as AP objects
Because we are creating new IDs we can't assume the id
will exist or be accurate, so we only use to_model for
adding new items after checking whether they exist .
"""
book = models.Edition.objects.get(id=book_id)
for blist in lists:
booklist = models.List.objects.filter(name=blist["name"], user=user).first()
if not booklist:
blist["owner"] = user.remote_id
parsed = activitypub.parse(blist)
booklist = parsed.to_model(model=models.List, save=True, overwrite=True)
booklist.privacy = blist["privacy"]
booklist.save()
item = models.ListItem.objects.filter(book=book, book_list=booklist).exists()
if not item:
count = booklist.books.count()
models.ListItem.objects.create(
book=book,
book_list=booklist,
user=user,
notes=blist["list_item"]["notes"],
approved=blist["list_item"]["approved"],
order=count + 1,
)
def upsert_shelves(book, user, book_data):
"""Take shelf JSON objects and create
DB entries if they don't already exist"""
shelves = book_data["shelves"]
for shelf in shelves:
book_shelf = models.Shelf.objects.filter(name=shelf["name"], user=user).first()
if not book_shelf:
book_shelf = models.Shelf.objects.create(name=shelf["name"], user=user)
# add the book as a ShelfBook if needed
if not models.ShelfBook.objects.filter(
book=book, shelf=book_shelf, user=user
).exists():
models.ShelfBook.objects.create(
book=book, shelf=book_shelf, user=user, shelved_date=timezone.now()
)
def update_user_profile(user, tar, data):
"""update the user's profile from import data"""
name = data.get("name", None)
username = data.get("preferredUsername")
user.name = name if name else username
user.summary = strip_tags(data.get("summary", None))
user.save(update_fields=["name", "summary"])
if data["icon"].get("url"):
avatar_filename = next(filter(lambda n: n.startswith("avatar"), tar.getnames()))
tar.write_image_to_file(avatar_filename, user.avatar)
def update_user_settings(user, data):
"""update the user's settings from import data"""
update_fields = ["manually_approves_followers", "hide_follows", "discoverable"]
ap_fields = [
("manuallyApprovesFollowers", "manually_approves_followers"),
("hideFollows", "hide_follows"),
("discoverable", "discoverable"),
]
for (ap_field, bw_field) in ap_fields:
setattr(user, bw_field, data[ap_field])
bw_fields = [
"show_goal",
"show_suggested_users",
"default_post_privacy",
"preferred_timezone",
]
for field in bw_fields:
update_fields.append(field)
setattr(user, field, data["settings"][field])
user.save(update_fields=update_fields)
@app.task(queue=IMPORTS, base=SubTask)
def update_user_settings_task(job_id):
"""wrapper task for user's settings import"""
parent_job = BookwyrmImportJob.objects.get(id=job_id)
return update_user_settings(parent_job.user, parent_job.import_data.get("user"))
def update_goals(user, data):
"""update the user's goals from import data"""
for goal in data:
# edit the existing goal if there is one
existing = models.AnnualGoal.objects.filter(
year=goal["year"], user=user
).first()
if existing:
for k in goal.keys():
setattr(existing, k, goal[k])
existing.save()
else:
goal["user"] = user
models.AnnualGoal.objects.create(**goal)
@app.task(queue=IMPORTS, base=SubTask)
def update_goals_task(job_id):
"""wrapper task for user's goals import"""
parent_job = BookwyrmImportJob.objects.get(id=job_id)
return update_goals(parent_job.user, parent_job.import_data.get("goals"))
def upsert_saved_lists(user, values):
"""Take a list of remote ids and add as saved lists"""
for remote_id in values:
book_list = activitypub.resolve_remote_id(remote_id, models.List)
if book_list:
user.saved_lists.add(book_list)
@app.task(queue=IMPORTS, base=SubTask)
def upsert_saved_lists_task(job_id):
"""wrapper task for user's saved lists import"""
parent_job = BookwyrmImportJob.objects.get(id=job_id)
return upsert_saved_lists(
parent_job.user, parent_job.import_data.get("saved_lists")
)
def upsert_follows(user, values):
"""Take a list of remote ids and add as follows"""
for remote_id in values:
followee = activitypub.resolve_remote_id(remote_id, models.User)
if followee:
(follow_request, created,) = models.UserFollowRequest.objects.get_or_create(
user_subject=user,
user_object=followee,
)
if not created:
# this request probably failed to connect with the remote
# and should save to trigger a re-broadcast
follow_request.save()
@app.task(queue=IMPORTS, base=SubTask)
def upsert_follows_task(job_id):
"""wrapper task for user's follows import"""
parent_job = BookwyrmImportJob.objects.get(id=job_id)
return upsert_follows(parent_job.user, parent_job.import_data.get("follows"))
def upsert_user_blocks(user, user_ids):
"""block users"""
for user_id in user_ids:
user_object = activitypub.resolve_remote_id(user_id, models.User)
if user_object:
exists = models.UserBlocks.objects.filter(
user_subject=user, user_object=user_object
).exists()
if not exists:
models.UserBlocks.objects.create(
user_subject=user, user_object=user_object
)
# remove the blocked users's lists from the groups
models.List.remove_from_group(user, user_object)
# remove the blocked user from all blocker's owned groups
models.GroupMember.remove(user, user_object)
@app.task(queue=IMPORTS, base=SubTask)
def upsert_user_blocks_task(job_id):
"""wrapper task for user's blocks import"""
parent_job = BookwyrmImportJob.objects.get(id=job_id)
return upsert_user_blocks(
parent_job.user, parent_job.import_data.get("blocked_users")
)
def update_followers_address(user, field):
"""statuses to or cc followers need to have the followers
address updated to the new local user"""
for i, audience in enumerate(field):
if audience.rsplit("/")[-1] == "followers":
field[i] = user.followers_url
return field
def is_alias(user, remote_id):
"""check that the user is listed as movedTo or also_known_as
in the remote user's profile"""
remote_user = activitypub.resolve_remote_id(
remote_id=remote_id, model=models.User, save=False
)
if remote_user:
if remote_user.moved_to:
return user.remote_id == remote_user.moved_to
if remote_user.also_known_as:
return user in remote_user.also_known_as.all()
return False
def status_already_exists(user, status):
"""check whether this status has already been published
by this user. We can't rely on to_model() because it
only matches on remote_id, which we have to change
*after* saving because it needs the primary key (id)"""
return models.Status.objects.filter(
user=user, content=status.content, published_date=status.published
).exists()

View file

@ -11,7 +11,7 @@ ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
class Connector(BookWyrmModel):
"""book data source connectors"""
identifier = models.CharField(max_length=255, unique=True)
identifier = models.CharField(max_length=255, unique=True) # domain
priority = models.IntegerField(default=2)
name = models.CharField(max_length=255, null=True, blank=True)
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)

View file

@ -16,7 +16,7 @@ FederationStatus = [
class FederatedServer(BookWyrmModel):
"""store which servers we federate with"""
server_name = models.CharField(max_length=255, unique=True)
server_name = models.CharField(max_length=255, unique=True) # domain
status = models.CharField(
max_length=255, default="federated", choices=FederationStatus
)
@ -64,5 +64,4 @@ class FederatedServer(BookWyrmModel):
def is_blocked(cls, url: str) -> bool:
"""look up if a domain is blocked"""
url = urlparse(url)
domain = url.netloc
return cls.objects.filter(server_name=domain, status="blocked").exists()
return cls.objects.filter(server_name=url.hostname, status="blocked").exists()

View file

@ -1,5 +1,6 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
from datetime import datetime
import re
from uuid import uuid4
from urllib.parse import urljoin
@ -19,6 +20,11 @@ from markdown import markdown
from bookwyrm import activitypub
from bookwyrm.connectors import get_image
from bookwyrm.utils.sanitizer import clean
from bookwyrm.utils.partial_date import (
PartialDate,
PartialDateModel,
from_partial_isoformat,
)
from bookwyrm.settings import MEDIA_FULL_URL
@ -254,12 +260,12 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
if to == [self.public]:
setattr(instance, self.name, "public")
elif self.public in cc:
setattr(instance, self.name, "unlisted")
elif to == [user.followers_url]:
setattr(instance, self.name, "followers")
elif cc == []:
setattr(instance, self.name, "direct")
elif self.public in cc:
setattr(instance, self.name, "unlisted")
else:
setattr(instance, self.name, "followers")
return original == getattr(instance, self.name)
@ -476,16 +482,18 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
if not url:
return None
return activitypub.Document(url=url, name=alt)
return activitypub.Image(url=url, name=alt)
def field_from_activity(self, value, allow_external_connections=True):
image_slug = value
# when it's an inline image (User avatar/icon, Book cover), it's a json
# blob, but when it's an attached image, it's just a url
if hasattr(image_slug, "url"):
url = image_slug.url
elif isinstance(image_slug, str):
if isinstance(image_slug, str):
url = image_slug
elif isinstance(image_slug, dict):
url = image_slug.get("url")
elif hasattr(image_slug, "url"): # Serialized to Image/Document object?
url = image_slug.url
else:
return None
@ -534,8 +542,9 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
return value.isoformat()
def field_from_activity(self, value, allow_external_connections=True):
missing_fields = datetime(1970, 1, 1) # "2022-10" => "2022-10-01"
try:
date_value = dateutil.parser.parse(value)
date_value = dateutil.parser.parse(value, default=missing_fields)
try:
return timezone.make_aware(date_value)
except ValueError:
@ -544,6 +553,37 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
return None
class PartialDateField(ActivitypubFieldMixin, PartialDateModel):
"""activitypub-aware partial date field"""
def field_to_activity(self, value) -> str:
return value.partial_isoformat() if value else None
def field_from_activity(self, value, allow_external_connections=True):
# pylint: disable=no-else-return
try:
return from_partial_isoformat(value)
except ValueError:
pass
# fallback to full ISO-8601 parsing
try:
parsed = dateutil.parser.isoparse(value)
except (ValueError, ParserError):
return None
if timezone.is_aware(parsed):
return PartialDate.from_datetime(parsed)
else:
# Should not happen on the wire, but truncate down to date parts.
return PartialDate.from_date_parts(parsed.year, parsed.month, parsed.day)
# FIXME: decide whether to fix timestamps like "2023-09-30T21:00:00-03":
# clearly Oct 1st, not Sep 30th (an unwanted side-effect of USE_TZ). It's
# basically the remnants of #3028; there is a data migration pending (see …)
# but over the wire we might get these for an indeterminate amount of time.
class HtmlField(ActivitypubFieldMixin, models.TextField):
"""a text field for storing html"""

View file

@ -1,8 +1,7 @@
""" do book related things with other users """
from django.apps import apps
from django.db import models, IntegrityError, transaction
from django.db.models import Q
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .base_model import BookWyrmModel
from . import fields
from .relationship import UserBlocks
@ -18,7 +17,7 @@ class Group(BookWyrmModel):
def get_remote_id(self):
"""don't want the user to be in there in this case"""
return f"https://{DOMAIN}/group/{self.id}"
return f"{BASE_URL}/group/{self.id}"
@classmethod
def followers_filter(cls, queryset, viewer):
@ -143,26 +142,28 @@ class GroupMemberInvitation(models.Model):
@transaction.atomic
def accept(self):
"""turn this request into the real deal"""
# pylint: disable-next=import-outside-toplevel
from .notification import Notification, NotificationType # circular dependency
GroupMember.from_request(self)
model = apps.get_model("bookwyrm.Notification", require_ready=True)
# tell the group owner
model.notify(
Notification.notify(
self.group.user,
self.user,
related_group=self.group,
notification_type=model.ACCEPT,
notification_type=NotificationType.ACCEPT,
)
# let the other members know about it
for membership in self.group.memberships.all():
member = membership.user
if member not in (self.user, self.group.user):
model.notify(
Notification.notify(
member,
self.user,
related_group=self.group,
notification_type=model.JOIN,
notification_type=NotificationType.JOIN,
)
def reject(self):

View file

@ -1,4 +1,5 @@
""" track progress of goodreads imports """
from datetime import datetime
import math
import re
import dateutil.parser
@ -259,38 +260,30 @@ class ImportItem(models.Model):
except ValueError:
return None
def _parse_datefield(self, field, /):
if not (date := self.normalized_data.get(field)):
return None
defaults = datetime(1970, 1, 1) # "2022-10" => "2022-10-01"
parsed = dateutil.parser.parse(date, default=defaults)
# Keep timezone if import already had one, else use default.
return parsed if timezone.is_aware(parsed) else timezone.make_aware(parsed)
@property
def date_added(self):
"""when the book was added to this dataset"""
if self.normalized_data.get("date_added"):
parsed_date_added = dateutil.parser.parse(
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 self._parse_datefield("date_added")
@property
def date_started(self):
"""when the book was started"""
if self.normalized_data.get("date_started"):
return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_started"))
)
return None
return self._parse_datefield("date_started")
@property
def date_read(self):
"""the date a book was completed"""
if self.normalized_data.get("date_finished"):
return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_finished"))
)
return None
return self._parse_datefield("date_finished")
@property
def reads(self):

307
bookwyrm/models/job.py Normal file
View file

@ -0,0 +1,307 @@
"""Everything needed for Celery to multi-thread complex tasks."""
from django.db import models
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from bookwyrm.models.user import User
from bookwyrm.tasks import app
class Job(models.Model):
"""Abstract model to store the state of a Task."""
class Status(models.TextChoices):
"""Possible job states."""
PENDING = "pending", _("Pending")
ACTIVE = "active", _("Active")
COMPLETE = "complete", _("Complete")
STOPPED = "stopped", _("Stopped")
FAILED = "failed", _("Failed")
task_id = models.UUIDField(unique=True, null=True, blank=True)
created_date = models.DateTimeField(default=timezone.now)
updated_date = models.DateTimeField(default=timezone.now)
complete = models.BooleanField(default=False)
status = models.CharField(
max_length=50, choices=Status.choices, default=Status.PENDING, null=True
)
class Meta:
"""Make it abstract"""
abstract = True
def complete_job(self):
"""Report that the job has completed"""
if self.complete:
return
self.status = self.Status.COMPLETE
self.complete = True
self.updated_date = timezone.now()
self.save(update_fields=["status", "complete", "updated_date"])
def stop_job(self, reason=None):
"""Stop the job"""
if self.complete:
return
self.__terminate_job()
if reason and reason == "failed":
self.status = self.Status.FAILED
else:
self.status = self.Status.STOPPED
self.complete = True
self.updated_date = timezone.now()
self.save(update_fields=["status", "complete", "updated_date"])
def set_status(self, status):
"""Set job status"""
if self.complete:
return
if self.status == status:
return
if status == self.Status.COMPLETE:
self.complete_job()
return
if status == self.Status.STOPPED:
self.stop_job()
return
if status == self.Status.FAILED:
self.stop_job(reason="failed")
return
self.updated_date = timezone.now()
self.status = status
self.save(update_fields=["status", "updated_date"])
def __terminate_job(self):
"""Tell workers to ignore and not execute this task."""
app.control.revoke(self.task_id, terminate=True)
class ParentJob(Job):
"""Store the state of a Task which can spawn many :model:`ChildJob`s to spread
resource load.
Intended to be sub-classed if necessary via proxy or
multi-table inheritance.
Extends :model:`Job`.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
def complete_job(self):
"""Report that the job has completed and stop pending
children. Extend.
"""
super().complete_job()
self.__terminate_pending_child_jobs()
def notify_child_job_complete(self):
"""let the job know when the items get work done"""
if self.complete:
return
self.updated_date = timezone.now()
self.save(update_fields=["updated_date"])
if not self.complete and self.has_completed:
self.complete_job()
def __terminate_job(self): # pylint: disable=unused-private-member
"""Tell workers to ignore and not execute this task
& pending child tasks. Extend.
"""
super().__terminate_job()
self.__terminate_pending_child_jobs()
def __terminate_pending_child_jobs(self):
"""Tell workers to ignore and not execute any pending child tasks."""
tasks = self.pending_child_jobs.filter(task_id__isnull=False).values_list(
"task_id", flat=True
)
app.control.revoke(list(tasks))
self.pending_child_jobs.update(status=self.Status.STOPPED)
@property
def has_completed(self):
"""has this job finished"""
return not self.pending_child_jobs.exists()
@property
def pending_child_jobs(self):
"""items that haven't been processed yet"""
return self.child_jobs.filter(complete=False)
class ChildJob(Job):
"""Stores the state of a Task for the related :model:`ParentJob`.
Intended to be sub-classed if necessary via proxy or
multi-table inheritance.
Extends :model:`Job`.
"""
parent_job = models.ForeignKey(
ParentJob, on_delete=models.CASCADE, related_name="child_jobs"
)
def set_status(self, status):
"""Set job and parent_job status. Extend."""
super().set_status(status)
if (
status == self.Status.ACTIVE
and self.parent_job.status == self.Status.PENDING
):
self.parent_job.set_status(self.Status.ACTIVE)
def complete_job(self):
"""Report to parent_job that the job has completed. Extend."""
super().complete_job()
self.parent_job.notify_child_job_complete()
class ParentTask(app.Task):
"""Used with ParentJob, Abstract Tasks execute code at specific points in
a Task's lifecycle, applying to all Tasks with the same 'base'.
All status & ParentJob.task_id assignment is managed here for you.
Usage e.g. @app.task(base=ParentTask)
"""
def before_start(
self, task_id, args, kwargs
): # pylint: disable=no-self-use, unused-argument
"""Handler called before the task starts. Override.
Prepare ParentJob before the task starts.
Arguments:
task_id (str): Unique id of the task to execute.
args (Tuple): Original arguments for the task to execute.
kwargs (Dict): Original keyword arguments for the task to execute.
Keyword Arguments:
job_id (int): Unique 'id' of the ParentJob.
no_children (bool): If 'True' this is the only Task expected to run
for the given ParentJob.
Returns:
None: The return value of this handler is ignored.
"""
job = ParentJob.objects.get(id=kwargs["job_id"])
job.task_id = task_id
job.save(update_fields=["task_id"])
if kwargs["no_children"]:
job.set_status(ChildJob.Status.ACTIVE)
def on_success(
self, retval, task_id, args, kwargs
): # pylint: disable=no-self-use, unused-argument
"""Run by the worker if the task executes successfully. Override.
Update ParentJob on Task complete.
Arguments:
retval (Any): The return value of the task.
task_id (str): Unique id of the executed task.
args (Tuple): Original arguments for the executed task.
kwargs (Dict): Original keyword arguments for the executed task.
Keyword Arguments:
job_id (int): Unique 'id' of the ParentJob.
no_children (bool): If 'True' this is the only Task expected to run
for the given ParentJob.
Returns:
None: The return value of this handler is ignored.
"""
if kwargs["no_children"]:
job = ParentJob.objects.get(id=kwargs["job_id"])
job.complete_job()
class SubTask(app.Task):
"""Used with ChildJob, Abstract Tasks execute code at specific points in
a Task's lifecycle, applying to all Tasks with the same 'base'.
All status & ChildJob.task_id assignment is managed here for you.
Usage e.g. @app.task(base=SubTask)
"""
def before_start(
self, task_id, *args, **kwargs
): # pylint: disable=no-self-use, unused-argument
"""Handler called before the task starts. Override.
Prepare ChildJob before the task starts.
Arguments:
task_id (str): Unique id of the task to execute.
args (Tuple): Original arguments for the task to execute.
kwargs (Dict): Original keyword arguments for the task to execute.
Keyword Arguments:
job_id (int): Unique 'id' of the ParentJob.
child_id (int): Unique 'id' of the ChildJob.
Returns:
None: The return value of this handler is ignored.
"""
child_job = ChildJob.objects.get(id=kwargs["child_id"])
child_job.task_id = task_id
child_job.save(update_fields=["task_id"])
child_job.set_status(ChildJob.Status.ACTIVE)
def on_success(
self, retval, task_id, *args, **kwargs
): # pylint: disable=no-self-use, unused-argument
"""Run by the worker if the task executes successfully. Override.
Notify ChildJob of task completion.
Arguments:
retval (Any): The return value of the task.
task_id (str): Unique id of the executed task.
args (Tuple): Original arguments for the executed task.
kwargs (Dict): Original keyword arguments for the executed task.
Keyword Arguments:
job_id (int): Unique 'id' of the ParentJob.
child_id (int): Unique 'id' of the ChildJob.
Returns:
None: The return value of this handler is ignored.
"""
subtask = ChildJob.objects.get(id=kwargs["child_id"])
subtask.complete_job()
@transaction.atomic
def create_child_job(parent_job, task_callback):
"""Utility method for creating a ChildJob
and running a task to avoid DB race conditions
"""
child_job = ChildJob.objects.create(parent_job=parent_job)
transaction.on_commit(
lambda: task_callback.delay(job_id=parent_job.id, child_id=child_job.id)
)
return child_job

View file

@ -38,7 +38,7 @@ class Link(ActivitypubMixin, BookWyrmModel):
"""create a link"""
# get or create the associated domain
if not self.domain:
domain = urlparse(self.url).netloc
domain = urlparse(self.url).hostname
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
# this is never broadcast, the owning model broadcasts an update

View file

@ -7,7 +7,7 @@ from django.db.models import Q
from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
@ -50,7 +50,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
def get_remote_id(self):
"""don't want the user to be in there in this case"""
return f"https://{DOMAIN}/list/{self.id}"
return f"{BASE_URL}/list/{self.id}"
@property
def collection_queryset(self):

71
bookwyrm/models/move.py Normal file
View file

@ -0,0 +1,71 @@
""" move an object including migrating a user account """
from django.core.exceptions import PermissionDenied
from django.db import models
from bookwyrm import activitypub
from .activitypub_mixin import ActivityMixin
from .base_model import BookWyrmModel
from . import fields
from .notification import Notification, NotificationType
class Move(ActivityMixin, BookWyrmModel):
"""migrating an activitypub object"""
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
)
object = fields.CharField(
max_length=255,
blank=False,
null=False,
activitypub_field="object",
)
origin = fields.CharField(
max_length=255,
blank=True,
null=True,
default="",
activitypub_field="origin",
)
activity_serializer = activitypub.Move
class MoveUser(Move):
"""migrating an activitypub user account"""
target = fields.ForeignKey(
"User",
on_delete=models.PROTECT,
related_name="move_target",
activitypub_field="target",
)
def save(self, *args, **kwargs):
"""update user info and broadcast it"""
# only allow if the source is listed in the target's alsoKnownAs
if self.user in self.target.also_known_as.all():
self.user.also_known_as.add(self.target.id)
self.user.update_active_date()
self.user.moved_to = self.target.remote_id
self.user.save(update_fields=["moved_to"])
if self.user.local:
kwargs[
"broadcast"
] = True # Only broadcast if we are initiating the Move
super().save(*args, **kwargs)
for follower in self.user.followers.all():
if follower.local:
Notification.notify(
follower, self.user, notification_type=NotificationType.MOVE
)
else:
raise PermissionDenied()

View file

@ -1,12 +1,21 @@
""" alert a user to activity """
from django.db import models, transaction
from django.dispatch import receiver
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
from .base_model import BookWyrmModel
from . import Boost, Favorite, GroupMemberInvitation, ImportJob, LinkDomain
from . import (
Boost,
Favorite,
GroupMemberInvitation,
ImportJob,
BookwyrmImportJob,
LinkDomain,
)
from . import ListItem, Report, Status, User, UserFollowRequest
from .site import InviteRequest
class Notification(BookWyrmModel):
class NotificationType(models.TextChoices):
"""you've been tagged, liked, followed, etc"""
# Status interactions
@ -22,6 +31,8 @@ class Notification(BookWyrmModel):
# Imports
IMPORT = "IMPORT"
USER_IMPORT = "USER_IMPORT"
USER_EXPORT = "USER_EXPORT"
# List activity
ADD = "ADD"
@ -29,6 +40,7 @@ class Notification(BookWyrmModel):
# Admin
REPORT = "REPORT"
LINK_DOMAIN = "LINK_DOMAIN"
INVITE_REQUEST = "INVITE_REQUEST"
# Groups
INVITE = "INVITE"
@ -40,12 +52,12 @@ class Notification(BookWyrmModel):
GROUP_NAME = "GROUP_NAME"
GROUP_DESCRIPTION = "GROUP_DESCRIPTION"
# pylint: disable=line-too-long
NotificationType = models.TextChoices(
# there has got be a better way to do this
"NotificationType",
f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {LINK_DOMAIN} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}",
)
# Migrations
MOVE = "MOVE"
class Notification(BookWyrmModel):
"""a notification object"""
user = models.ForeignKey("User", on_delete=models.CASCADE)
read = models.BooleanField(default=False)
@ -61,11 +73,15 @@ class Notification(BookWyrmModel):
)
related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True)
related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
related_user_export = models.ForeignKey(
"BookwyrmExportJob", on_delete=models.CASCADE, null=True
)
related_list_items = models.ManyToManyField(
"ListItem", symmetrical=False, related_name="notifications"
)
related_reports = models.ManyToManyField("Report", symmetrical=False)
related_link_domains = models.ManyToManyField("LinkDomain", symmetrical=False)
related_reports = models.ManyToManyField("Report")
related_link_domains = models.ManyToManyField("LinkDomain")
related_invite_requests = models.ManyToManyField("InviteRequest")
@classmethod
@transaction.atomic
@ -90,11 +106,11 @@ class Notification(BookWyrmModel):
user=user,
related_users=related_user,
related_list_items__book_list=list_item.book_list,
notification_type=Notification.ADD,
notification_type=NotificationType.ADD,
).first()
if not notification:
notification = cls.objects.create(
user=user, notification_type=Notification.ADD
user=user, notification_type=NotificationType.ADD
)
notification.related_users.add(related_user)
notification.related_list_items.add(list_item)
@ -121,7 +137,7 @@ def notify_on_fav(sender, instance, *args, **kwargs):
instance.status.user,
instance.user,
related_status=instance.status,
notification_type=Notification.FAVORITE,
notification_type=NotificationType.FAVORITE,
)
@ -135,7 +151,7 @@ def notify_on_unfav(sender, instance, *args, **kwargs):
instance.status.user,
instance.user,
related_status=instance.status,
notification_type=Notification.FAVORITE,
notification_type=NotificationType.FAVORITE,
)
@ -160,7 +176,7 @@ def notify_user_on_mention(sender, instance, *args, **kwargs):
instance.reply_parent.user,
instance.user,
related_status=instance,
notification_type=Notification.REPLY,
notification_type=NotificationType.REPLY,
)
for mention_user in instance.mention_users.all():
@ -172,7 +188,7 @@ def notify_user_on_mention(sender, instance, *args, **kwargs):
Notification.notify(
mention_user,
instance.user,
notification_type=Notification.MENTION,
notification_type=NotificationType.MENTION,
related_status=instance,
)
@ -191,7 +207,7 @@ def notify_user_on_boost(sender, instance, *args, **kwargs):
instance.boosted_status.user,
instance.user,
related_status=instance.boosted_status,
notification_type=Notification.BOOST,
notification_type=NotificationType.BOOST,
)
@ -203,7 +219,7 @@ def notify_user_on_unboost(sender, instance, *args, **kwargs):
instance.boosted_status.user,
instance.user,
related_status=instance.boosted_status,
notification_type=Notification.BOOST,
notification_type=NotificationType.BOOST,
)
@ -218,11 +234,41 @@ def notify_user_on_import_complete(
return
Notification.objects.get_or_create(
user=instance.user,
notification_type=Notification.IMPORT,
notification_type=NotificationType.IMPORT,
related_import=instance,
)
@receiver(models.signals.post_save, sender=BookwyrmImportJob)
# pylint: disable=unused-argument
def notify_user_on_user_import_complete(
sender, instance, *args, update_fields=None, **kwargs
):
"""we imported your user details! aren't you proud of us"""
update_fields = update_fields or []
if not instance.complete or "complete" not in update_fields:
return
Notification.objects.create(
user=instance.user, notification_type=NotificationType.USER_IMPORT
)
@receiver(models.signals.post_save, sender=BookwyrmExportJob)
# pylint: disable=unused-argument
def notify_user_on_user_export_complete(
sender, instance, *args, update_fields=None, **kwargs
):
"""we exported your user details! aren't you proud of us"""
update_fields = update_fields or []
if not instance.complete or "complete" not in update_fields:
return
Notification.objects.create(
user=instance.user,
notification_type=NotificationType.USER_EXPORT,
related_user_export=instance,
)
@receiver(models.signals.post_save, sender=Report)
@transaction.atomic
# pylint: disable=unused-argument
@ -233,11 +279,10 @@ def notify_admins_on_report(sender, instance, created, *args, **kwargs):
return
# moderators and superusers should be notified
admins = User.admins()
for admin in admins:
for admin in User.admins():
notification, _ = Notification.objects.get_or_create(
user=admin,
notification_type=Notification.REPORT,
notification_type=NotificationType.REPORT,
read=False,
)
notification.related_reports.add(instance)
@ -253,16 +298,33 @@ def notify_admins_on_link_domain(sender, instance, created, *args, **kwargs):
return
# moderators and superusers should be notified
admins = User.admins()
for admin in admins:
for admin in User.admins():
notification, _ = Notification.objects.get_or_create(
user=admin,
notification_type=Notification.LINK_DOMAIN,
notification_type=NotificationType.LINK_DOMAIN,
read=False,
)
notification.related_link_domains.add(instance)
@receiver(models.signals.post_save, sender=InviteRequest)
@transaction.atomic
# pylint: disable=unused-argument
def notify_admins_on_invite_request(sender, instance, created, *args, **kwargs):
"""need to handle a new invite request"""
if not created:
return
# moderators and superusers should be notified
for admin in User.admins():
notification, _ = Notification.objects.get_or_create(
user=admin,
notification_type=NotificationType.INVITE_REQUEST,
read=False,
)
notification.related_invite_requests.add(instance)
@receiver(models.signals.post_save, sender=GroupMemberInvitation)
# pylint: disable=unused-argument
def notify_user_on_group_invite(sender, instance, *args, **kwargs):
@ -271,7 +333,7 @@ def notify_user_on_group_invite(sender, instance, *args, **kwargs):
instance.user,
instance.group.user,
related_group=instance.group,
notification_type=Notification.INVITE,
notification_type=NotificationType.INVITE,
)
@ -309,11 +371,12 @@ def notify_user_on_follow(sender, instance, created, *args, **kwargs):
notification = Notification.objects.filter(
user=instance.user_object,
related_users=instance.user_subject,
notification_type=Notification.FOLLOW_REQUEST,
notification_type=NotificationType.FOLLOW_REQUEST,
).first()
if not notification:
notification = Notification.objects.create(
user=instance.user_object, notification_type=Notification.FOLLOW_REQUEST
user=instance.user_object,
notification_type=NotificationType.FOLLOW_REQUEST,
)
notification.related_users.set([instance.user_subject])
notification.read = False
@ -323,6 +386,6 @@ def notify_user_on_follow(sender, instance, created, *args, **kwargs):
Notification.notify(
instance.user_object,
instance.user_subject,
notification_type=Notification.FOLLOW,
notification_type=NotificationType.FOLLOW,
read=False,
)

View file

@ -65,6 +65,13 @@ class UserRelationship(BookWyrmModel):
base_path = self.user_subject.remote_id
return f"{base_path}#follows/{self.id}"
def get_accept_reject_id(self, status):
"""get id for sending an accept or reject of a local user"""
base_path = self.user_object.remote_id
status_id = self.id or 0
return f"{base_path}#{status}/{status_id}"
class UserFollows(ActivityMixin, UserRelationship):
"""Following a user"""
@ -105,6 +112,20 @@ class UserFollows(ActivityMixin, UserRelationship):
)
return obj
def reject(self):
"""generate a Reject for this follow. This would normally happen
when a user deletes a follow they previously accepted"""
if self.user_object.local:
activity = activitypub.Reject(
id=self.get_accept_reject_id(status="rejects"),
actor=self.user_object.remote_id,
object=self.to_activity(),
).serialize()
self.broadcast(activity, self.user_object)
self.delete()
class UserFollowRequest(ActivitypubMixin, UserRelationship):
"""following a user requires manual or automatic confirmation"""
@ -148,13 +169,6 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
if not manually_approves:
self.accept()
def get_accept_reject_id(self, status):
"""get id for sending an accept or reject of a local user"""
base_path = self.user_object.remote_id
status_id = self.id or 0
return f"{base_path}#{status}/{status_id}"
def accept(self, broadcast_only=False):
"""turn this request into the real deal"""
user = self.user_object

View file

@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .base_model import BookWyrmModel
@ -46,7 +46,7 @@ class Report(BookWyrmModel):
raise PermissionDenied()
def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}"
return f"{BASE_URL}/settings/reports/{self.id}"
def comment(self, user, note):
"""comment on a report"""

Some files were not shown because too many files have changed in this diff Show more