mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-28 23:13:12 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2deecc5c91 | ||
|
|
54cfa13861 | ||
|
|
ee4f99261f | ||
|
|
d2fa0a8f5a | ||
|
|
02a15c9460 | ||
|
|
7a6428c037 | ||
|
|
c6001aa7b8 | ||
|
|
eefbefd714 | ||
|
|
683cf529d7 | ||
|
|
38204c87cf | ||
|
|
96ee4746ad | ||
|
|
d7c1afa2a5 | ||
|
|
16ed6ef200 | ||
|
|
98b9a9c1a0 | ||
|
|
6775633be5 | ||
|
|
150dfecc6f | ||
|
|
81ae55bc1c | ||
|
|
935189ecc2 | ||
|
|
7997f20d89 | ||
|
|
ae27500cde | ||
|
|
71d853999e | ||
|
|
70288d6865 | ||
|
|
e83d519cab | ||
|
|
6355d8dff1 | ||
|
|
227cfdb063 |
@@ -14,7 +14,7 @@
|
||||
"forwardPorts": [8000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "pip3 install --user -r requirements.txt && npm install && mkdir -p data && python3 manage.py migrate",
|
||||
"postCreateCommand": "pip3 install --user -r requirements.txt -r requirements.dev.txt && npm install && mkdir -p data && python3 manage.py migrate",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
!/manage.py
|
||||
!/package.json
|
||||
!/package-lock.json
|
||||
!/requirements.prod.txt
|
||||
!/requirements.dev.txt
|
||||
!/requirements.txt
|
||||
!/rollup.config.js
|
||||
!/supervisord.conf
|
||||
|
||||
6
.github/workflows/main.yaml
vendored
6
.github/workflows/main.yaml
vendored
@@ -1,6 +1,6 @@
|
||||
name: linkding CI
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
unit_tests:
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
- name: Setup Python environment
|
||||
run: pip install -r requirements.txt
|
||||
run: pip install -r requirements.txt -r requirements.dev.txt
|
||||
- name: Run tests
|
||||
run: python manage.py test bookmarks.tests
|
||||
e2e_tests:
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
run: npm install
|
||||
- name: Setup Python environment
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements.txt -r requirements.dev.txt
|
||||
playwright install chromium
|
||||
- name: Run build
|
||||
run: |
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## v1.24.1 (16/03/2024)
|
||||
|
||||
### What's Changed
|
||||
* Bump dependencies by @sissbruecker in https://github.com/sissbruecker/linkding/pull/618
|
||||
* Persist secret key in data folder by @sissbruecker in https://github.com/sissbruecker/linkding/pull/620
|
||||
* Group ideographic characters in tag cloud by @jonathan-s in https://github.com/sissbruecker/linkding/pull/613
|
||||
* Bump django from 5.0.1 to 5.0.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/625
|
||||
* Add k8s setup to community section by @jzck in https://github.com/sissbruecker/linkding/pull/633
|
||||
* Added a new Linkding client to community section by @JGeek00 in https://github.com/sissbruecker/linkding/pull/638
|
||||
|
||||
### New Contributors
|
||||
* @jzck made their first contribution in https://github.com/sissbruecker/linkding/pull/633
|
||||
* @JGeek00 made their first contribution in https://github.com/sissbruecker/linkding/pull/638
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.24.0...v1.24.1
|
||||
|
||||
---
|
||||
|
||||
## v1.24.0 (27/01/2024)
|
||||
|
||||
### What's Changed
|
||||
* Support Open Graph description by @jonathan-s in https://github.com/sissbruecker/linkding/pull/602
|
||||
* Add tooltip to truncated bookmark titles by @jonathan-s in https://github.com/sissbruecker/linkding/pull/607
|
||||
* Improve bulk tag performance by @sissbruecker in https://github.com/sissbruecker/linkding/pull/612
|
||||
* Increase tag limit in tag autocomplete by @hypebeast in https://github.com/sissbruecker/linkding/pull/581
|
||||
* Add CapRover as managed hosting option by @adamshand in https://github.com/sissbruecker/linkding/pull/585
|
||||
* Bump playwright dependencies by @jonathan-s in https://github.com/sissbruecker/linkding/pull/601
|
||||
* Adjust archive.org donation link in general.html by @JnsDornbusch in https://github.com/sissbruecker/linkding/pull/603
|
||||
|
||||
### New Contributors
|
||||
* @hypebeast made their first contribution in https://github.com/sissbruecker/linkding/pull/581
|
||||
* @adamshand made their first contribution in https://github.com/sissbruecker/linkding/pull/585
|
||||
* @jonathan-s made their first contribution in https://github.com/sissbruecker/linkding/pull/601
|
||||
* @JnsDornbusch made their first contribution in https://github.com/sissbruecker/linkding/pull/603
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.23.1...v1.24.0
|
||||
|
||||
---
|
||||
|
||||
## v1.23.1 (08/12/2023)
|
||||
|
||||
### What's Changed
|
||||
* Properly encode search query param by @sissbruecker in https://github.com/sissbruecker/linkding/pull/587
|
||||
|
||||
> [!WARNING]
|
||||
> *This resolves a security vulnerability in linkding. Everyone is encouraged to upgrade to the latest version as soon as possible.*
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.23.0...v1.23.1
|
||||
|
||||
---
|
||||
|
||||
## v1.23.0 (24/11/2023)
|
||||
|
||||
### What's Changed
|
||||
* Add Alpine based Docker image (experimental) by @sissbruecker in https://github.com/sissbruecker/linkding/pull/570
|
||||
* Add backup CLI command by @sissbruecker in https://github.com/sissbruecker/linkding/pull/571
|
||||
* Update browser extension links by @OPerepadia in https://github.com/sissbruecker/linkding/pull/574
|
||||
* Include archived bookmarks in export by @sissbruecker in https://github.com/sissbruecker/linkding/pull/579
|
||||
|
||||
### New Contributors
|
||||
* @OPerepadia made their first contribution in https://github.com/sissbruecker/linkding/pull/574
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.3...v1.23.0
|
||||
|
||||
---
|
||||
|
||||
## v1.22.3 (04/11/2023)
|
||||
|
||||
### What's Changed
|
||||
|
||||
15
Makefile
Normal file
15
Makefile
Normal file
@@ -0,0 +1,15 @@
|
||||
.PHONY: serve
|
||||
|
||||
serve:
|
||||
python manage.py runserver
|
||||
|
||||
tasks:
|
||||
python manage.py process_tasks
|
||||
|
||||
test:
|
||||
pytest
|
||||
|
||||
format:
|
||||
black bookmarks
|
||||
black siteroot
|
||||
npx prettier bookmarks/frontend --write
|
||||
37
README.md
37
README.md
@@ -9,11 +9,11 @@
|
||||
## Overview
|
||||
- [Introduction](#introduction)
|
||||
- [Installation](#installation)
|
||||
- [Using Docker](#using-docker)
|
||||
- [Using Docker Compose](#using-docker-compose)
|
||||
- [User Setup](#user-setup)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Managed Hosting Options](#managed-hosting-options)
|
||||
- [Using Docker](#using-docker)
|
||||
- [Using Docker Compose](#using-docker-compose)
|
||||
- [User Setup](#user-setup)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Managed Hosting Options](#managed-hosting-options)
|
||||
- [Documentation](#documentation)
|
||||
- [Browser Extension](#browser-extension)
|
||||
- [Community](#community)
|
||||
@@ -103,7 +103,7 @@ docker-compose up -d
|
||||
|
||||
To complete the setup, you still have to [create an initial user](#user-setup), so that you can access your installation.
|
||||
|
||||
### User setup
|
||||
### User Setup
|
||||
|
||||
For security reasons, the linkding Docker image does not provide an initial user, so you have to create one after setting up an installation. To do so, replace the credentials in the following command and run it:
|
||||
|
||||
@@ -119,7 +119,7 @@ docker-compose exec linkding python manage.py createsuperuser --username=joe --e
|
||||
|
||||
The command will prompt you for a secure password. After the command has completed you can start using the application by logging into the UI with your credentials.
|
||||
|
||||
Alternatively you can automatically create an initial superuser on startup using the [`LD_SUPERUSER_NAME` option](docs/Options.md#LD_SUPERUSER_NAME).
|
||||
Alternatively you can automatically create an initial superuser on startup using the [`LD_SUPERUSER_NAME` option](docs/Options.md#LD_SUPERUSER_NAME).
|
||||
|
||||
### Reverse Proxy Setup
|
||||
|
||||
@@ -182,6 +182,7 @@ Self-hosting web applications still requires a lot of technical know-how and com
|
||||
|
||||
- [linkding on fly.io](https://github.com/fspoettel/linkding-on-fly) - Guide for hosting a linkding installation on [fly.io](https://fly.io). By [fspoettel](https://github.com/fspoettel)
|
||||
- [PikaPods.com](https://www.pikapods.com/) - Managed hosting for linkding, EU and US regions available. [1-click setup link](https://www.pikapods.com/pods?run=linkding) ([Disclosure](#pikapods))
|
||||
- [CapRover](https://caprover.com/) - Linkding is included as a default one-click app
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -210,11 +211,13 @@ This section lists community projects around using linkding, in alphabetical ord
|
||||
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
|
||||
- [feed2linkding](https://codeberg.org/strubbl/feed2linkding) A commandline utility to add all web feed item links to linkding via API call. By [Strubbl](https://github.com/Strubbl)
|
||||
- [Helm Chart](https://charts.pascaliske.dev/charts/linkding/) Helm Chart for deploying linkding inside a Kubernetes cluster. By [pascaliske](https://github.com/pascaliske)
|
||||
- [iOS Shortcut using API and Tagging](https://gist.github.com/andrewdolphin/a7dff49505e588d940bec55132fab8ad) An iOS shortcut using the Linkding API (no extra logins required) that pulls previously used tags and allows tagging at the time of link creation.
|
||||
- [iOS Shortcut using API and Tagging](https://gist.github.com/andrewdolphin/a7dff49505e588d940bec55132fab8ad) An iOS shortcut using the Linkding API (no extra logins required) that pulls previously used tags and allows tagging at the time of link creation.
|
||||
- [k8s + s3](https://github.com/jzck/linkding-k8s-s3) - Setup for hosting stateless linkding on k8s with sqlite replicated to s3. By [jzck](https://github.com/jzck)
|
||||
- [Linka!](https://github.com/cmsax/linka) Web app (also a PWA) for quickly searching & opening bookmarks in linkding, support multi keywords, exclude mode and other advance options. By [cmsax](https://github.com/cmsax)
|
||||
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
|
||||
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
|
||||
- [linkding-injector](https://github.com/Fivefold/linkding-injector) Injects search results from linkding into the sidebar of search pages like google and duckduckgo. Tested with Firefox and Chrome. By [Fivefold](https://github.com/Fivefold)
|
||||
- [Linkdy](https://github.com/JGeek00/linkdy): An open source mobile and desktop (not yet) client created with Flutter. Available at the [Google Play Store](https://play.google.com/store/apps/details?id=com.jgeek00.linkdy). By [JGeek00](https://github.com/JGeek00).
|
||||
- [LinkThing](https://apps.apple.com/us/app/linkthing/id1666031776) An iOS client for linkding. By [amoscardino](https://github.com/amoscardino)
|
||||
- [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
|
||||
- [Postman collection](https://gist.github.com/gingerbeardman/f0b42502f3bc9344e92ce63afd4360d3) a group of saved request templates for API testing. By [gingerbeardman](https://github.com/gingerbeardman)
|
||||
@@ -223,7 +226,7 @@ This section lists community projects around using linkding, in alphabetical ord
|
||||
|
||||
### PikaPods
|
||||
|
||||
[PikaPods](https://www.pikapods.com/) has a revenue sharing agreement with this project, sharing some of their revenue from hosting linkding instances. I do not intend to profit from this project financially, so I am in turn donating that revenue. Big thanks to PikaPods for making this possible.
|
||||
[PikaPods](https://www.pikapods.com/) has a revenue sharing agreement with this project, sharing some of their revenue from hosting linkding instances. I do not intend to profit from this project financially, so I am in turn donating that revenue. Big thanks to PikaPods for making this possible.
|
||||
|
||||
See the table below for a list of donations.
|
||||
|
||||
@@ -255,7 +258,7 @@ source ~/environments/linkding/bin/activate[.csh|.fish]
|
||||
```
|
||||
Within the active environment install the application dependencies from the application folder:
|
||||
```
|
||||
pip3 install -Ur requirements.txt
|
||||
pip3 install -r requirements.txt -r requirements.dev.txt
|
||||
```
|
||||
Install frontend dependencies:
|
||||
```
|
||||
@@ -280,6 +283,20 @@ python3 manage.py runserver
|
||||
```
|
||||
The frontend is now available under http://localhost:8000
|
||||
|
||||
### Tests
|
||||
|
||||
Run all tests with pytest:
|
||||
```
|
||||
pytest
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
Format Python code with black, and JavaScript code with prettier:
|
||||
```
|
||||
make format
|
||||
```
|
||||
|
||||
### DevContainers
|
||||
|
||||
This repository also supports DevContainers: [](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=git@github.com:sissbruecker/linkding.git)
|
||||
|
||||
@@ -14,80 +14,123 @@ from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
||||
|
||||
|
||||
class LinkdingAdminSite(AdminSite):
|
||||
site_header = 'linkding administration'
|
||||
site_title = 'linkding Admin'
|
||||
site_header = "linkding administration"
|
||||
site_title = "linkding Admin"
|
||||
|
||||
|
||||
class AdminBookmark(admin.ModelAdmin):
|
||||
list_display = ('resolved_title', 'url', 'is_archived', 'owner', 'date_added')
|
||||
search_fields = ('title', 'description', 'website_title', 'website_description', 'url', 'tags__name')
|
||||
list_filter = ('owner__username', 'is_archived', 'unread', 'tags',)
|
||||
ordering = ('-date_added',)
|
||||
actions = ['delete_selected_bookmarks', 'archive_selected_bookmarks', 'unarchive_selected_bookmarks', 'mark_as_read', 'mark_as_unread']
|
||||
list_display = ("resolved_title", "url", "is_archived", "owner", "date_added")
|
||||
search_fields = (
|
||||
"title",
|
||||
"description",
|
||||
"website_title",
|
||||
"website_description",
|
||||
"url",
|
||||
"tags__name",
|
||||
)
|
||||
list_filter = (
|
||||
"owner__username",
|
||||
"is_archived",
|
||||
"unread",
|
||||
"tags",
|
||||
)
|
||||
ordering = ("-date_added",)
|
||||
actions = [
|
||||
"delete_selected_bookmarks",
|
||||
"archive_selected_bookmarks",
|
||||
"unarchive_selected_bookmarks",
|
||||
"mark_as_read",
|
||||
"mark_as_unread",
|
||||
]
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
# Remove default delete action, which gets replaced by delete_selected_bookmarks below
|
||||
# The default action shows a confirmation page which can fail in production when selecting all bookmarks and the
|
||||
# number of objects to delete exceeds the value in DATA_UPLOAD_MAX_NUMBER_FIELDS (1000 by default)
|
||||
del actions['delete_selected']
|
||||
del actions["delete_selected"]
|
||||
return actions
|
||||
|
||||
def delete_selected_bookmarks(self, request, queryset: QuerySet):
|
||||
bookmarks_count = queryset.count()
|
||||
for bookmark in queryset:
|
||||
bookmark.delete()
|
||||
self.message_user(request, ngettext(
|
||||
'%d bookmark was successfully deleted.',
|
||||
'%d bookmarks were successfully deleted.',
|
||||
bookmarks_count,
|
||||
) % bookmarks_count, messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d bookmark was successfully deleted.",
|
||||
"%d bookmarks were successfully deleted.",
|
||||
bookmarks_count,
|
||||
)
|
||||
% bookmarks_count,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
def archive_selected_bookmarks(self, request, queryset: QuerySet):
|
||||
for bookmark in queryset:
|
||||
archive_bookmark(bookmark)
|
||||
bookmarks_count = queryset.count()
|
||||
self.message_user(request, ngettext(
|
||||
'%d bookmark was successfully archived.',
|
||||
'%d bookmarks were successfully archived.',
|
||||
bookmarks_count,
|
||||
) % bookmarks_count, messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d bookmark was successfully archived.",
|
||||
"%d bookmarks were successfully archived.",
|
||||
bookmarks_count,
|
||||
)
|
||||
% bookmarks_count,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
def unarchive_selected_bookmarks(self, request, queryset: QuerySet):
|
||||
for bookmark in queryset:
|
||||
unarchive_bookmark(bookmark)
|
||||
bookmarks_count = queryset.count()
|
||||
self.message_user(request, ngettext(
|
||||
'%d bookmark was successfully unarchived.',
|
||||
'%d bookmarks were successfully unarchived.',
|
||||
bookmarks_count,
|
||||
) % bookmarks_count, messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d bookmark was successfully unarchived.",
|
||||
"%d bookmarks were successfully unarchived.",
|
||||
bookmarks_count,
|
||||
)
|
||||
% bookmarks_count,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
def mark_as_read(self, request, queryset: QuerySet):
|
||||
bookmarks_count = queryset.count()
|
||||
queryset.update(unread=False)
|
||||
self.message_user(request, ngettext(
|
||||
'%d bookmark marked as read.',
|
||||
'%d bookmarks marked as read.',
|
||||
bookmarks_count,
|
||||
) % bookmarks_count, messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d bookmark marked as read.",
|
||||
"%d bookmarks marked as read.",
|
||||
bookmarks_count,
|
||||
)
|
||||
% bookmarks_count,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
def mark_as_unread(self, request, queryset: QuerySet):
|
||||
bookmarks_count = queryset.count()
|
||||
queryset.update(unread=True)
|
||||
self.message_user(request, ngettext(
|
||||
'%d bookmark marked as unread.',
|
||||
'%d bookmarks marked as unread.',
|
||||
bookmarks_count,
|
||||
) % bookmarks_count, messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d bookmark marked as unread.",
|
||||
"%d bookmarks marked as unread.",
|
||||
bookmarks_count,
|
||||
)
|
||||
% bookmarks_count,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
|
||||
class AdminTag(admin.ModelAdmin):
|
||||
list_display = ('name', 'bookmarks_count', 'owner', 'date_added')
|
||||
search_fields = ('name', 'owner__username')
|
||||
list_filter = ('owner__username',)
|
||||
ordering = ('-date_added',)
|
||||
actions = ['delete_unused_tags']
|
||||
list_display = ("name", "bookmarks_count", "owner", "date_added")
|
||||
search_fields = ("name", "owner__username")
|
||||
list_filter = ("owner__username",)
|
||||
ordering = ("-date_added",)
|
||||
actions = ["delete_unused_tags"]
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
@@ -97,7 +140,7 @@ class AdminTag(admin.ModelAdmin):
|
||||
def bookmarks_count(self, obj):
|
||||
return obj.bookmarks_count
|
||||
|
||||
bookmarks_count.admin_order_field = 'bookmarks_count'
|
||||
bookmarks_count.admin_order_field = "bookmarks_count"
|
||||
|
||||
def delete_unused_tags(self, request, queryset: QuerySet):
|
||||
unused_tags = queryset.filter(bookmark__isnull=True)
|
||||
@@ -106,23 +149,33 @@ class AdminTag(admin.ModelAdmin):
|
||||
tag.delete()
|
||||
|
||||
if unused_tags_count > 0:
|
||||
self.message_user(request, ngettext(
|
||||
'%d unused tag was successfully deleted.',
|
||||
'%d unused tags were successfully deleted.',
|
||||
unused_tags_count,
|
||||
) % unused_tags_count, messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d unused tag was successfully deleted.",
|
||||
"%d unused tags were successfully deleted.",
|
||||
unused_tags_count,
|
||||
)
|
||||
% unused_tags_count,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
else:
|
||||
self.message_user(request, gettext(
|
||||
'There were no unused tags in the selection',
|
||||
), messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
gettext(
|
||||
"There were no unused tags in the selection",
|
||||
),
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
|
||||
class AdminUserProfileInline(admin.StackedInline):
|
||||
model = UserProfile
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Profile'
|
||||
fk_name = 'user'
|
||||
readonly_fields = ('search_preferences', )
|
||||
verbose_name_plural = "Profile"
|
||||
fk_name = "user"
|
||||
readonly_fields = ("search_preferences",)
|
||||
|
||||
|
||||
class AdminCustomUser(UserAdmin):
|
||||
inlines = (AdminUserProfileInline,)
|
||||
@@ -134,15 +187,15 @@ class AdminCustomUser(UserAdmin):
|
||||
|
||||
|
||||
class AdminToast(admin.ModelAdmin):
|
||||
list_display = ('key', 'message', 'owner', 'acknowledged')
|
||||
search_fields = ('key', 'message')
|
||||
list_filter = ('owner__username',)
|
||||
list_display = ("key", "message", "owner", "acknowledged")
|
||||
search_fields = ("key", "message")
|
||||
list_filter = ("owner__username",)
|
||||
|
||||
|
||||
class AdminFeedToken(admin.ModelAdmin):
|
||||
list_display = ('key', 'user')
|
||||
search_fields = ['key']
|
||||
list_filter = ('user__username',)
|
||||
list_display = ("key", "user")
|
||||
search_fields = ["key"]
|
||||
list_filter = ("user__username",)
|
||||
|
||||
|
||||
linkding_admin_site = LinkdingAdminSite()
|
||||
|
||||
@@ -5,18 +5,28 @@ from rest_framework.response import Response
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.api.serializers import BookmarkSerializer, TagSerializer, UserProfileSerializer
|
||||
from bookmarks.api.serializers import (
|
||||
BookmarkSerializer,
|
||||
TagSerializer,
|
||||
UserProfileSerializer,
|
||||
)
|
||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, User
|
||||
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark, website_loader
|
||||
from bookmarks.services.bookmarks import (
|
||||
archive_bookmark,
|
||||
unarchive_bookmark,
|
||||
website_loader,
|
||||
)
|
||||
from bookmarks.services.website_loader import WebsiteMetadata
|
||||
|
||||
|
||||
class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin):
|
||||
class BookmarkViewSet(
|
||||
viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
):
|
||||
serializer_class = BookmarkSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
@@ -24,7 +34,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
# The shared action should still filter bookmarks so that
|
||||
# unauthenticated users only see bookmarks from users that have public
|
||||
# sharing explicitly enabled
|
||||
if self.action == 'shared':
|
||||
if self.action == "shared":
|
||||
return [AllowAny()]
|
||||
|
||||
# Otherwise use default permissions which should require authentication
|
||||
@@ -33,7 +43,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
# For list action, use query set that applies search and tag projections
|
||||
if self.action == 'list':
|
||||
if self.action == "list":
|
||||
search = BookmarkSearch.from_request(self.request.GET)
|
||||
return queries.query_bookmarks(user, user.profile, search)
|
||||
|
||||
@@ -41,9 +51,9 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
return Bookmark.objects.all().filter(owner=user)
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {'user': self.request.user}
|
||||
return {"user": self.request.user}
|
||||
|
||||
@action(methods=['get'], detail=False)
|
||||
@action(methods=["get"], detail=False)
|
||||
def archived(self, request):
|
||||
user = request.user
|
||||
search = BookmarkSearch.from_request(request.GET)
|
||||
@@ -53,51 +63,59 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
data = serializer(page, many=True).data
|
||||
return self.get_paginated_response(data)
|
||||
|
||||
@action(methods=['get'], detail=False)
|
||||
@action(methods=["get"], detail=False)
|
||||
def shared(self, request):
|
||||
search = BookmarkSearch.from_request(request.GET)
|
||||
user = User.objects.filter(username=search.user).first()
|
||||
public_only = not request.user.is_authenticated
|
||||
query_set = queries.query_shared_bookmarks(user, request.user_profile, search, public_only)
|
||||
query_set = queries.query_shared_bookmarks(
|
||||
user, request.user_profile, search, public_only
|
||||
)
|
||||
page = self.paginate_queryset(query_set)
|
||||
serializer = self.get_serializer_class()
|
||||
data = serializer(page, many=True).data
|
||||
return self.get_paginated_response(data)
|
||||
|
||||
@action(methods=['post'], detail=True)
|
||||
@action(methods=["post"], detail=True)
|
||||
def archive(self, request, pk):
|
||||
bookmark = self.get_object()
|
||||
archive_bookmark(bookmark)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=['post'], detail=True)
|
||||
@action(methods=["post"], detail=True)
|
||||
def unarchive(self, request, pk):
|
||||
bookmark = self.get_object()
|
||||
unarchive_bookmark(bookmark)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=['get'], detail=False)
|
||||
@action(methods=["get"], detail=False)
|
||||
def check(self, request):
|
||||
url = request.GET.get('url')
|
||||
url = request.GET.get("url")
|
||||
bookmark = Bookmark.objects.filter(owner=request.user, url=url).first()
|
||||
existing_bookmark_data = self.get_serializer(bookmark).data if bookmark else None
|
||||
existing_bookmark_data = (
|
||||
self.get_serializer(bookmark).data if bookmark else None
|
||||
)
|
||||
|
||||
# Either return metadata from existing bookmark, or scrape from URL
|
||||
if bookmark:
|
||||
metadata = WebsiteMetadata(url, bookmark.website_title, bookmark.website_description)
|
||||
metadata = WebsiteMetadata(
|
||||
url, bookmark.website_title, bookmark.website_description
|
||||
)
|
||||
else:
|
||||
metadata = website_loader.load_website_metadata(url)
|
||||
|
||||
return Response({
|
||||
'bookmark': existing_bookmark_data,
|
||||
'metadata': metadata.to_dict()
|
||||
}, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
{"bookmark": existing_bookmark_data, "metadata": metadata.to_dict()},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class TagViewSet(viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.CreateModelMixin):
|
||||
class TagViewSet(
|
||||
viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
):
|
||||
serializer_class = TagSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -105,16 +123,16 @@ class TagViewSet(viewsets.GenericViewSet,
|
||||
return Tag.objects.all().filter(owner=user)
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {'user': self.request.user}
|
||||
return {"user": self.request.user}
|
||||
|
||||
|
||||
class UserViewSet(viewsets.GenericViewSet):
|
||||
@action(methods=['get'], detail=False)
|
||||
@action(methods=["get"], detail=False)
|
||||
def profile(self, request):
|
||||
return Response(UserProfileSerializer(request.user.profile).data)
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'bookmarks', BookmarkViewSet, basename='bookmark')
|
||||
router.register(r'tags', TagViewSet, basename='tag')
|
||||
router.register(r'user', UserViewSet, basename='user')
|
||||
router.register(r"bookmarks", BookmarkViewSet, basename="bookmark")
|
||||
router.register(r"tags", TagViewSet, basename="tag")
|
||||
router.register(r"user", UserViewSet, basename="user")
|
||||
|
||||
@@ -14,7 +14,7 @@ class TagListField(serializers.ListField):
|
||||
class BookmarkListSerializer(ListSerializer):
|
||||
def to_representation(self, data):
|
||||
# Prefetch nested relations to avoid n+1 queries
|
||||
prefetch_related_objects(data, 'tags')
|
||||
prefetch_related_objects(data, "tags")
|
||||
|
||||
return super().to_representation(data)
|
||||
|
||||
@@ -23,32 +23,32 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
fields = [
|
||||
'id',
|
||||
'url',
|
||||
'title',
|
||||
'description',
|
||||
'notes',
|
||||
'website_title',
|
||||
'website_description',
|
||||
'is_archived',
|
||||
'unread',
|
||||
'shared',
|
||||
'tag_names',
|
||||
'date_added',
|
||||
'date_modified'
|
||||
"id",
|
||||
"url",
|
||||
"title",
|
||||
"description",
|
||||
"notes",
|
||||
"website_title",
|
||||
"website_description",
|
||||
"is_archived",
|
||||
"unread",
|
||||
"shared",
|
||||
"tag_names",
|
||||
"date_added",
|
||||
"date_modified",
|
||||
]
|
||||
read_only_fields = [
|
||||
'website_title',
|
||||
'website_description',
|
||||
'date_added',
|
||||
'date_modified'
|
||||
"website_title",
|
||||
"website_description",
|
||||
"date_added",
|
||||
"date_modified",
|
||||
]
|
||||
list_serializer_class = BookmarkListSerializer
|
||||
|
||||
# Override optional char fields to provide default value
|
||||
title = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
description = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
notes = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
title = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
description = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
notes = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
is_archived = serializers.BooleanField(required=False, default=False)
|
||||
unread = serializers.BooleanField(required=False, default=False)
|
||||
shared = serializers.BooleanField(required=False, default=False)
|
||||
@@ -57,38 +57,38 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
bookmark = Bookmark()
|
||||
bookmark.url = validated_data['url']
|
||||
bookmark.title = validated_data['title']
|
||||
bookmark.description = validated_data['description']
|
||||
bookmark.notes = validated_data['notes']
|
||||
bookmark.is_archived = validated_data['is_archived']
|
||||
bookmark.unread = validated_data['unread']
|
||||
bookmark.shared = validated_data['shared']
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
return create_bookmark(bookmark, tag_string, self.context['user'])
|
||||
bookmark.url = validated_data["url"]
|
||||
bookmark.title = validated_data["title"]
|
||||
bookmark.description = validated_data["description"]
|
||||
bookmark.notes = validated_data["notes"]
|
||||
bookmark.is_archived = validated_data["is_archived"]
|
||||
bookmark.unread = validated_data["unread"]
|
||||
bookmark.shared = validated_data["shared"]
|
||||
tag_string = build_tag_string(validated_data["tag_names"])
|
||||
return create_bookmark(bookmark, tag_string, self.context["user"])
|
||||
|
||||
def update(self, instance: Bookmark, validated_data):
|
||||
# Update fields if they were provided in the payload
|
||||
for key in ['url', 'title', 'description', 'notes', 'unread', 'shared']:
|
||||
for key in ["url", "title", "description", "notes", "unread", "shared"]:
|
||||
if key in validated_data:
|
||||
setattr(instance, key, validated_data[key])
|
||||
|
||||
# Use tag string from payload, or use bookmark's current tags as fallback
|
||||
tag_string = build_tag_string(instance.tag_names)
|
||||
if 'tag_names' in validated_data:
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
if "tag_names" in validated_data:
|
||||
tag_string = build_tag_string(validated_data["tag_names"])
|
||||
|
||||
return update_bookmark(instance, tag_string, self.context['user'])
|
||||
return update_bookmark(instance, tag_string, self.context["user"])
|
||||
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'name', 'date_added']
|
||||
read_only_fields = ['date_added']
|
||||
fields = ["id", "name", "date_added"]
|
||||
read_only_fields = ["date_added"]
|
||||
|
||||
def create(self, validated_data):
|
||||
return get_or_create_tag(validated_data['name'], self.context['user'])
|
||||
return get_or_create_tag(validated_data["name"], self.context["user"])
|
||||
|
||||
|
||||
class UserProfileSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class BookmarksConfig(AppConfig):
|
||||
name = 'bookmarks'
|
||||
name = "bookmarks"
|
||||
|
||||
def ready(self):
|
||||
# Register signal handlers
|
||||
|
||||
@@ -5,28 +5,32 @@ from bookmarks import utils
|
||||
|
||||
def toasts(request):
|
||||
user = request.user
|
||||
toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user.is_authenticated else []
|
||||
toast_messages = (
|
||||
Toast.objects.filter(owner=user, acknowledged=False)
|
||||
if user.is_authenticated
|
||||
else []
|
||||
)
|
||||
has_toasts = len(toast_messages) > 0
|
||||
|
||||
return {
|
||||
'has_toasts': has_toasts,
|
||||
'toast_messages': toast_messages,
|
||||
"has_toasts": has_toasts,
|
||||
"toast_messages": toast_messages,
|
||||
}
|
||||
|
||||
|
||||
def public_shares(request):
|
||||
# Only check for public shares for anonymous users
|
||||
if not request.user.is_authenticated:
|
||||
query_set = queries.query_shared_bookmarks(None, request.user_profile, BookmarkSearch(), True)
|
||||
query_set = queries.query_shared_bookmarks(
|
||||
None, request.user_profile, BookmarkSearch(), True
|
||||
)
|
||||
has_public_shares = query_set.count() > 0
|
||||
return {
|
||||
'has_public_shares': has_public_shares,
|
||||
"has_public_shares": has_public_shares,
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def app_version(request):
|
||||
return {
|
||||
'app_version': utils.app_version
|
||||
}
|
||||
return {"app_version": utils.app_version}
|
||||
|
||||
@@ -6,38 +6,54 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
|
||||
class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
||||
def test_create_should_check_for_existing_bookmark(self):
|
||||
existing_bookmark = self.setup_bookmark(title='Existing title',
|
||||
description='Existing description',
|
||||
notes='Existing notes',
|
||||
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')],
|
||||
website_title='Existing website title',
|
||||
website_description='Existing website description',
|
||||
unread=True)
|
||||
tag_names = ' '.join(existing_bookmark.tag_names)
|
||||
existing_bookmark = self.setup_bookmark(
|
||||
title="Existing title",
|
||||
description="Existing description",
|
||||
notes="Existing notes",
|
||||
tags=[self.setup_tag(name="tag1"), self.setup_tag(name="tag2")],
|
||||
website_title="Existing website title",
|
||||
website_description="Existing website description",
|
||||
unread=True,
|
||||
)
|
||||
tag_names = " ".join(existing_bookmark.tag_names)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:new'))
|
||||
page.goto(self.live_server_url + reverse("bookmarks:new"))
|
||||
|
||||
# Enter bookmarked URL
|
||||
page.get_by_label('URL').fill(existing_bookmark.url)
|
||||
page.get_by_label("URL").fill(existing_bookmark.url)
|
||||
# Already bookmarked hint should be visible
|
||||
page.get_by_text('This URL is already bookmarked.').wait_for(timeout=2000)
|
||||
page.get_by_text("This URL is already bookmarked.").wait_for(timeout=2000)
|
||||
# Form should be pre-filled with data from existing bookmark
|
||||
self.assertEqual(existing_bookmark.title, page.get_by_label('Title').input_value())
|
||||
self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value())
|
||||
self.assertEqual(existing_bookmark.notes, page.get_by_label('Notes').input_value())
|
||||
self.assertEqual(existing_bookmark.website_title, page.get_by_label('Title').get_attribute('placeholder'))
|
||||
self.assertEqual(existing_bookmark.website_description,
|
||||
page.get_by_label('Description').get_attribute('placeholder'))
|
||||
self.assertEqual(tag_names, page.get_by_label('Tags').input_value())
|
||||
self.assertTrue(tag_names, page.get_by_label('Mark as unread').is_checked())
|
||||
self.assertEqual(
|
||||
existing_bookmark.title, page.get_by_label("Title").input_value()
|
||||
)
|
||||
self.assertEqual(
|
||||
existing_bookmark.description,
|
||||
page.get_by_label("Description").input_value(),
|
||||
)
|
||||
self.assertEqual(
|
||||
existing_bookmark.notes, page.get_by_label("Notes").input_value()
|
||||
)
|
||||
self.assertEqual(
|
||||
existing_bookmark.website_title,
|
||||
page.get_by_label("Title").get_attribute("placeholder"),
|
||||
)
|
||||
self.assertEqual(
|
||||
existing_bookmark.website_description,
|
||||
page.get_by_label("Description").get_attribute("placeholder"),
|
||||
)
|
||||
self.assertEqual(tag_names, page.get_by_label("Tags").input_value())
|
||||
self.assertTrue(tag_names, page.get_by_label("Mark as unread").is_checked())
|
||||
|
||||
# Enter non-bookmarked URL
|
||||
page.get_by_label('URL').fill('https://example.com/unknown')
|
||||
page.get_by_label("URL").fill("https://example.com/unknown")
|
||||
# Already bookmarked hint should be hidden
|
||||
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden', timeout=2000)
|
||||
page.get_by_text("This URL is already bookmarked.").wait_for(
|
||||
state="hidden", timeout=2000
|
||||
)
|
||||
|
||||
browser.close()
|
||||
|
||||
@@ -47,21 +63,25 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
page.goto(
|
||||
self.live_server_url + reverse("bookmarks:edit", args=[bookmark.id])
|
||||
)
|
||||
|
||||
page.wait_for_timeout(timeout=1000)
|
||||
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden')
|
||||
page.get_by_text("This URL is already bookmarked.").wait_for(state="hidden")
|
||||
|
||||
def test_enter_url_of_existing_bookmark_should_show_notes(self):
|
||||
bookmark = self.setup_bookmark(notes='Existing notes', description='Existing description')
|
||||
bookmark = self.setup_bookmark(
|
||||
notes="Existing notes", description="Existing description"
|
||||
)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:new'))
|
||||
page.goto(self.live_server_url + reverse("bookmarks:new"))
|
||||
|
||||
details = page.locator('details.notes')
|
||||
expect(details).not_to_have_attribute('open', value='')
|
||||
details = page.locator("details.notes")
|
||||
expect(details).not_to_have_attribute("open", value="")
|
||||
|
||||
page.get_by_label('URL').fill(bookmark.url)
|
||||
expect(details).to_have_attribute('open', value='')
|
||||
page.get_by_label("URL").fill(bookmark.url)
|
||||
expect(details).to_have_attribute("open", value="")
|
||||
|
||||
@@ -9,15 +9,15 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
class BookmarkItemE2ETestCase(LinkdingE2ETestCase):
|
||||
@skip("Fails in CI, needs investigation")
|
||||
def test_toggle_notes_should_show_hide_notes(self):
|
||||
bookmark = self.setup_bookmark(notes='Test notes')
|
||||
bookmark = self.setup_bookmark(notes="Test notes")
|
||||
|
||||
with sync_playwright() as p:
|
||||
page = self.open(reverse('bookmarks:index'), p)
|
||||
page = self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
notes = self.locate_bookmark(bookmark.title).locator('.notes')
|
||||
notes = self.locate_bookmark(bookmark.title).locator(".notes")
|
||||
expect(notes).to_be_hidden()
|
||||
|
||||
toggle_notes = page.locator('li button.toggle-notes')
|
||||
toggle_notes = page.locator("li button.toggle-notes")
|
||||
toggle_notes.click()
|
||||
expect(notes).to_be_visible()
|
||||
|
||||
|
||||
@@ -9,100 +9,180 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
def setup_test_data(self):
|
||||
self.setup_numbered_bookmarks(50)
|
||||
self.setup_numbered_bookmarks(50, archived=True)
|
||||
self.setup_numbered_bookmarks(50, prefix='foo')
|
||||
self.setup_numbered_bookmarks(50, archived=True, prefix='foo')
|
||||
self.setup_numbered_bookmarks(50, prefix="foo")
|
||||
self.setup_numbered_bookmarks(50, archived=True, prefix="foo")
|
||||
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=False, title__startswith="Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=True, title__startswith="Archived Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
|
||||
)
|
||||
|
||||
def test_active_bookmarks_bulk_select_across(self):
|
||||
self.setup_test_data()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
||||
self.assertEqual(
|
||||
0,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=False, title__startswith="Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=True, title__startswith="Archived Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
|
||||
)
|
||||
|
||||
def test_archived_bookmarks_bulk_select_across(self):
|
||||
self.setup_test_data()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:archived'), p)
|
||||
self.open(reverse("bookmarks:archived"), p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=False, title__startswith="Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=True, title__startswith="Archived Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
|
||||
)
|
||||
|
||||
def test_active_bookmarks_bulk_select_across_respects_query(self):
|
||||
self.setup_test_data()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index') + '?q=foo', p)
|
||||
self.open(reverse("bookmarks:index") + "?q=foo", p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=False, title__startswith="Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=True, title__startswith="Archived Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
|
||||
)
|
||||
|
||||
def test_archived_bookmarks_bulk_select_across_respects_query(self):
|
||||
self.setup_test_data()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:archived') + '?q=foo', p)
|
||||
self.open(reverse("bookmarks:archived") + "?q=foo", p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=False, title__startswith="Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(
|
||||
is_archived=True, title__startswith="Archived Bookmark"
|
||||
).count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
50,
|
||||
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
|
||||
)
|
||||
|
||||
def test_select_all_toggles_all_checkboxes(self):
|
||||
self.setup_numbered_bookmarks(5)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
page = self.open(url, p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
|
||||
checkboxes = page.locator('label[ld-bulk-edit-checkbox] input')
|
||||
checkboxes = page.locator("label[ld-bulk-edit-checkbox] input")
|
||||
self.assertEqual(6, checkboxes.count())
|
||||
for i in range(checkboxes.count()):
|
||||
expect(checkboxes.nth(i)).not_to_be_checked()
|
||||
@@ -121,7 +201,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
self.setup_numbered_bookmarks(5)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
@@ -138,7 +218,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
self.setup_numbered_bookmarks(5)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
@@ -160,7 +240,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
self.setup_numbered_bookmarks(5)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
@@ -171,18 +251,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
expect(self.locate_bulk_edit_select_across()).to_be_checked()
|
||||
|
||||
# Hide select across by toggling a single bookmark
|
||||
self.locate_bookmark('Bookmark 1').locator('label[ld-bulk-edit-checkbox]').click()
|
||||
self.locate_bookmark("Bookmark 1").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
).click()
|
||||
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
|
||||
|
||||
# Show select across again, verify it is unchecked
|
||||
self.locate_bookmark('Bookmark 1').locator('label[ld-bulk-edit-checkbox]').click()
|
||||
self.locate_bookmark("Bookmark 1").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
).click()
|
||||
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
|
||||
|
||||
def test_execute_resets_all_checkboxes(self):
|
||||
self.setup_numbered_bookmarks(100)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
page = self.open(url, p)
|
||||
|
||||
# Select all bookmarks, enable select across
|
||||
@@ -191,18 +275,18 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
|
||||
# Get reference for bookmark list
|
||||
bookmark_list = page.locator('ul[ld-bookmark-list]')
|
||||
bookmark_list = page.locator("ul[ld-bookmark-list]")
|
||||
|
||||
# Execute bulk action
|
||||
self.select_bulk_action('Mark as unread')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.select_bulk_action("Mark as unread")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
# Verify bulk edit checkboxes are reset
|
||||
checkboxes = page.locator('label[ld-bulk-edit-checkbox] input')
|
||||
checkboxes = page.locator("label[ld-bulk-edit-checkbox] input")
|
||||
self.assertEqual(31, checkboxes.count())
|
||||
for i in range(checkboxes.count()):
|
||||
expect(checkboxes.nth(i)).not_to_be_checked()
|
||||
@@ -215,18 +299,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
self.setup_numbered_bookmarks(100)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
|
||||
expect(self.locate_bulk_edit_bar().get_by_text('All pages (100 bookmarks)')).to_be_visible()
|
||||
expect(
|
||||
self.locate_bulk_edit_bar().get_by_text("All pages (100 bookmarks)")
|
||||
).to_be_visible()
|
||||
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
|
||||
expect(self.locate_bulk_edit_bar().get_by_text('All pages (70 bookmarks)')).to_be_visible()
|
||||
expect(
|
||||
self.locate_bulk_edit_bar().get_by_text("All pages (70 bookmarks)")
|
||||
).to_be_visible()
|
||||
|
||||
@@ -16,13 +16,15 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
# verify correct data is loaded on update
|
||||
self.setup_numbered_bookmarks(3, with_tags=True)
|
||||
self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
||||
self.setup_numbered_bookmarks(3,
|
||||
shared=True,
|
||||
prefix="Joe's Bookmark",
|
||||
user=self.setup_user(enable_sharing=True))
|
||||
self.setup_numbered_bookmarks(
|
||||
3,
|
||||
shared=True,
|
||||
prefix="Joe's Bookmark",
|
||||
user=self.setup_user(enable_sharing=True),
|
||||
)
|
||||
|
||||
def assertVisibleBookmarks(self, titles: List[str]):
|
||||
bookmark_tags = self.page.locator('li[ld-bookmark-item]')
|
||||
bookmark_tags = self.page.locator("li[ld-bookmark-item]")
|
||||
expect(bookmark_tags).to_have_count(len(titles))
|
||||
|
||||
for title in titles:
|
||||
@@ -30,7 +32,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
expect(matching_tag).to_be_visible()
|
||||
|
||||
def assertVisibleTags(self, titles: List[str]):
|
||||
tag_tags = self.page.locator('.tag-cloud .unselected-tags a')
|
||||
tag_tags = self.page.locator(".tag-cloud .unselected-tags a")
|
||||
expect(tag_tags).to_have_count(len(titles))
|
||||
|
||||
for title in titles:
|
||||
@@ -38,65 +40,67 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
expect(matching_tag).to_be_visible()
|
||||
|
||||
def test_partial_update_respects_query(self):
|
||||
self.setup_numbered_bookmarks(5, prefix='foo')
|
||||
self.setup_numbered_bookmarks(5, prefix='bar')
|
||||
self.setup_numbered_bookmarks(5, prefix="foo")
|
||||
self.setup_numbered_bookmarks(5, prefix="bar")
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index') + '?q=foo'
|
||||
url = reverse("bookmarks:index") + "?q=foo"
|
||||
self.open(url, p)
|
||||
|
||||
self.assertVisibleBookmarks(['foo 1', 'foo 2', 'foo 3', 'foo 4', 'foo 5'])
|
||||
self.assertVisibleBookmarks(["foo 1", "foo 2", "foo 3", "foo 4", "foo 5"])
|
||||
|
||||
self.locate_bookmark('foo 2').get_by_text('Archive').click()
|
||||
self.assertVisibleBookmarks(['foo 1', 'foo 3', 'foo 4', 'foo 5'])
|
||||
self.locate_bookmark("foo 2").get_by_text("Archive").click()
|
||||
self.assertVisibleBookmarks(["foo 1", "foo 3", "foo 4", "foo 5"])
|
||||
|
||||
def test_partial_update_respects_sort(self):
|
||||
self.setup_numbered_bookmarks(5, prefix='foo')
|
||||
self.setup_numbered_bookmarks(5, prefix="foo")
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index') + '?sort=title_asc'
|
||||
url = reverse("bookmarks:index") + "?sort=title_asc"
|
||||
page = self.open(url, p)
|
||||
|
||||
first_item = page.locator('li[ld-bookmark-item]').first
|
||||
expect(first_item).to_contain_text('foo 1')
|
||||
first_item = page.locator("li[ld-bookmark-item]").first
|
||||
expect(first_item).to_contain_text("foo 1")
|
||||
|
||||
first_item.get_by_text('Archive').click()
|
||||
first_item.get_by_text("Archive").click()
|
||||
|
||||
first_item = page.locator('li[ld-bookmark-item]').first
|
||||
expect(first_item).to_contain_text('foo 2')
|
||||
first_item = page.locator("li[ld-bookmark-item]").first
|
||||
expect(first_item).to_contain_text("foo 2")
|
||||
|
||||
def test_partial_update_respects_page(self):
|
||||
# add a suffix, otherwise 'foo 1' also matches 'foo 10'
|
||||
self.setup_numbered_bookmarks(50, prefix='foo', suffix='-')
|
||||
self.setup_numbered_bookmarks(50, prefix="foo", suffix="-")
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index') + '?q=foo&page=2'
|
||||
url = reverse("bookmarks:index") + "?q=foo&page=2"
|
||||
self.open(url, p)
|
||||
|
||||
# with descending sort, page two has 'foo 1' to 'foo 20'
|
||||
expected_titles = [f'foo {i}-' for i in range(1, 21)]
|
||||
expected_titles = [f"foo {i}-" for i in range(1, 21)]
|
||||
self.assertVisibleBookmarks(expected_titles)
|
||||
|
||||
self.locate_bookmark('foo 20-').get_by_text('Archive').click()
|
||||
self.locate_bookmark("foo 20-").get_by_text("Archive").click()
|
||||
|
||||
expected_titles = [f'foo {i}-' for i in range(1, 20)]
|
||||
expected_titles = [f"foo {i}-" for i in range(1, 20)]
|
||||
self.assertVisibleBookmarks(expected_titles)
|
||||
|
||||
def test_multiple_partial_updates(self):
|
||||
self.setup_numbered_bookmarks(5)
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
self.locate_bookmark('Bookmark 1').get_by_text('Archive').click()
|
||||
self.assertVisibleBookmarks(['Bookmark 2', 'Bookmark 3', 'Bookmark 4', 'Bookmark 5'])
|
||||
self.locate_bookmark("Bookmark 1").get_by_text("Archive").click()
|
||||
self.assertVisibleBookmarks(
|
||||
["Bookmark 2", "Bookmark 3", "Bookmark 4", "Bookmark 5"]
|
||||
)
|
||||
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Archive').click()
|
||||
self.assertVisibleBookmarks(['Bookmark 3', 'Bookmark 4', 'Bookmark 5'])
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
|
||||
self.assertVisibleBookmarks(["Bookmark 3", "Bookmark 4", "Bookmark 5"])
|
||||
|
||||
self.locate_bookmark('Bookmark 3').get_by_text('Archive').click()
|
||||
self.assertVisibleBookmarks(['Bookmark 4', 'Bookmark 5'])
|
||||
self.locate_bookmark("Bookmark 3").get_by_text("Archive").click()
|
||||
self.assertVisibleBookmarks(["Bookmark 4", "Bookmark 5"])
|
||||
|
||||
self.assertReloads(0)
|
||||
|
||||
@@ -104,185 +108,201 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Archive').click()
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3'])
|
||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_active_bookmarks_partial_update_on_delete(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Remove').click()
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Confirm').click()
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Remove").click()
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3'])
|
||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_active_bookmarks_partial_update_on_mark_as_read(self):
|
||||
self.setup_fixture()
|
||||
bookmark2 = self.get_numbered_bookmark('Bookmark 2')
|
||||
bookmark2 = self.get_numbered_bookmark("Bookmark 2")
|
||||
bookmark2.unread = True
|
||||
bookmark2.save()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
expect(self.locate_bookmark('Bookmark 2')).to_have_class('unread')
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Unread').click()
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Yes').click()
|
||||
expect(self.locate_bookmark("Bookmark 2")).to_have_class("unread")
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Unread").click()
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Yes").click()
|
||||
|
||||
expect(self.locate_bookmark('Bookmark 2')).not_to_have_class('unread')
|
||||
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("unread")
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_active_bookmarks_partial_update_on_unshare(self):
|
||||
self.setup_fixture()
|
||||
bookmark2 = self.get_numbered_bookmark('Bookmark 2')
|
||||
bookmark2 = self.get_numbered_bookmark("Bookmark 2")
|
||||
bookmark2.shared = True
|
||||
bookmark2.save()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
expect(self.locate_bookmark('Bookmark 2')).to_have_class('shared')
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Shared').click()
|
||||
self.locate_bookmark('Bookmark 2').get_by_text('Yes').click()
|
||||
expect(self.locate_bookmark("Bookmark 2")).to_have_class("shared")
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Shared").click()
|
||||
self.locate_bookmark("Bookmark 2").get_by_text("Yes").click()
|
||||
|
||||
expect(self.locate_bookmark('Bookmark 2')).not_to_have_class('shared')
|
||||
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("shared")
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_active_bookmarks_partial_update_on_bulk_archive(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
||||
self.select_bulk_action('Archive')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.locate_bookmark("Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
).click()
|
||||
self.select_bulk_action("Archive")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3'])
|
||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_active_bookmarks_partial_update_on_bulk_delete(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:index'), p)
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.locate_bookmark("Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
).click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3'])
|
||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_archived_bookmarks_partial_update_on_unarchive(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:archived'), p)
|
||||
self.open(reverse("bookmarks:archived"), p)
|
||||
|
||||
self.locate_bookmark('Archived Bookmark 2').get_by_text('Unarchive').click()
|
||||
self.locate_bookmark("Archived Bookmark 2").get_by_text("Unarchive").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3'])
|
||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_archived_bookmarks_partial_update_on_delete(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:archived'), p)
|
||||
self.open(reverse("bookmarks:archived"), p)
|
||||
|
||||
self.locate_bookmark('Archived Bookmark 2').get_by_text('Remove').click()
|
||||
self.locate_bookmark('Archived Bookmark 2').get_by_text('Confirm').click()
|
||||
self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click()
|
||||
self.locate_bookmark("Archived Bookmark 2").get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3'])
|
||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_archived_bookmarks_partial_update_on_bulk_unarchive(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:archived'), p)
|
||||
self.open(reverse("bookmarks:archived"), p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
||||
self.select_bulk_action('Unarchive')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.locate_bookmark("Archived Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
).click()
|
||||
self.select_bulk_action("Unarchive")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3'])
|
||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_archived_bookmarks_partial_update_on_bulk_delete(self):
|
||||
self.setup_fixture()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:archived'), p)
|
||||
self.open(reverse("bookmarks:archived"), p)
|
||||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
||||
self.select_bulk_action('Delete')
|
||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
||||
self.locate_bookmark("Archived Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
).click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3'])
|
||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_shared_bookmarks_partial_update_on_unarchive(self):
|
||||
self.setup_fixture()
|
||||
self.setup_numbered_bookmarks(3, shared=True, prefix="My Bookmark", with_tags=True)
|
||||
self.setup_numbered_bookmarks(
|
||||
3, shared=True, prefix="My Bookmark", with_tags=True
|
||||
)
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:shared'), p)
|
||||
self.open(reverse("bookmarks:shared"), p)
|
||||
|
||||
self.locate_bookmark('My Bookmark 2').get_by_text('Archive').click()
|
||||
self.locate_bookmark("My Bookmark 2").get_by_text("Archive").click()
|
||||
|
||||
# Shared bookmarks page also shows archived bookmarks, though it probably shouldn't
|
||||
self.assertVisibleBookmarks([
|
||||
'My Bookmark 1',
|
||||
'My Bookmark 2',
|
||||
'My Bookmark 3',
|
||||
"Joe's Bookmark 1",
|
||||
"Joe's Bookmark 2",
|
||||
"Joe's Bookmark 3",
|
||||
])
|
||||
self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 2', 'Shared Tag 3'])
|
||||
self.assertVisibleBookmarks(
|
||||
[
|
||||
"My Bookmark 1",
|
||||
"My Bookmark 2",
|
||||
"My Bookmark 3",
|
||||
"Joe's Bookmark 1",
|
||||
"Joe's Bookmark 2",
|
||||
"Joe's Bookmark 3",
|
||||
]
|
||||
)
|
||||
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 2", "Shared Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_shared_bookmarks_partial_update_on_delete(self):
|
||||
self.setup_fixture()
|
||||
self.setup_numbered_bookmarks(3, shared=True, prefix="My Bookmark", with_tags=True)
|
||||
self.setup_numbered_bookmarks(
|
||||
3, shared=True, prefix="My Bookmark", with_tags=True
|
||||
)
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse('bookmarks:shared'), p)
|
||||
self.open(reverse("bookmarks:shared"), p)
|
||||
|
||||
self.locate_bookmark('My Bookmark 2').get_by_text('Remove').click()
|
||||
self.locate_bookmark('My Bookmark 2').get_by_text('Confirm').click()
|
||||
self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click()
|
||||
self.locate_bookmark("My Bookmark 2").get_by_text("Confirm").click()
|
||||
|
||||
self.assertVisibleBookmarks([
|
||||
'My Bookmark 1',
|
||||
'My Bookmark 3',
|
||||
"Joe's Bookmark 1",
|
||||
"Joe's Bookmark 2",
|
||||
"Joe's Bookmark 3",
|
||||
])
|
||||
self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 3'])
|
||||
self.assertVisibleBookmarks(
|
||||
[
|
||||
"My Bookmark 1",
|
||||
"My Bookmark 3",
|
||||
"Joe's Bookmark 1",
|
||||
"Joe's Bookmark 2",
|
||||
"Joe's Bookmark 3",
|
||||
]
|
||||
)
|
||||
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 3"])
|
||||
self.assertReloads(0)
|
||||
|
||||
@@ -9,11 +9,11 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:index'))
|
||||
page.goto(self.live_server_url + reverse("bookmarks:index"))
|
||||
|
||||
page.press('body', 's')
|
||||
page.press("body", "s")
|
||||
|
||||
expect(page.get_by_placeholder('Search for words or #tags')).to_be_focused()
|
||||
expect(page.get_by_placeholder("Search for words or #tags")).to_be_focused()
|
||||
|
||||
browser.close()
|
||||
|
||||
@@ -21,10 +21,10 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:index'))
|
||||
page.goto(self.live_server_url + reverse("bookmarks:index"))
|
||||
|
||||
page.press('body', 'n')
|
||||
page.press("body", "n")
|
||||
|
||||
expect(page).to_have_url(self.live_server_url + reverse('bookmarks:new'))
|
||||
expect(page).to_have_url(self.live_server_url + reverse("bookmarks:new"))
|
||||
|
||||
browser.close()
|
||||
|
||||
@@ -9,12 +9,14 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:settings.general'))
|
||||
page.goto(self.live_server_url + reverse("bookmarks:settings.general"))
|
||||
|
||||
enable_sharing = page.get_by_label('Enable bookmark sharing')
|
||||
enable_sharing_label = page.get_by_text('Enable bookmark sharing')
|
||||
enable_public_sharing = page.get_by_label('Enable public bookmark sharing')
|
||||
enable_public_sharing_label = page.get_by_text('Enable public bookmark sharing')
|
||||
enable_sharing = page.get_by_label("Enable bookmark sharing")
|
||||
enable_sharing_label = page.get_by_text("Enable bookmark sharing")
|
||||
enable_public_sharing = page.get_by_label("Enable public bookmark sharing")
|
||||
enable_public_sharing_label = page.get_by_text(
|
||||
"Enable public bookmark sharing"
|
||||
)
|
||||
|
||||
# Public sharing is disabled by default
|
||||
expect(enable_sharing).not_to_be_checked()
|
||||
|
||||
@@ -7,24 +7,28 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
||||
def setUp(self) -> None:
|
||||
self.client.force_login(self.get_or_create_test_user())
|
||||
self.cookie = self.client.cookies['sessionid']
|
||||
self.cookie = self.client.cookies["sessionid"]
|
||||
|
||||
def setup_browser(self, playwright) -> BrowserContext:
|
||||
browser = playwright.chromium.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
context.add_cookies([{
|
||||
'name': 'sessionid',
|
||||
'value': self.cookie.value,
|
||||
'domain': self.live_server_url.replace('http:', ''),
|
||||
'path': '/'
|
||||
}])
|
||||
context.add_cookies(
|
||||
[
|
||||
{
|
||||
"name": "sessionid",
|
||||
"value": self.cookie.value,
|
||||
"domain": self.live_server_url.replace("http:", ""),
|
||||
"path": "/",
|
||||
}
|
||||
]
|
||||
)
|
||||
return context
|
||||
|
||||
def open(self, url: str, playwright: Playwright) -> Page:
|
||||
browser = self.setup_browser(playwright)
|
||||
self.page = browser.new_page()
|
||||
self.page.goto(self.live_server_url + url)
|
||||
self.page.on('load', self.on_load)
|
||||
self.page.on("load", self.on_load)
|
||||
self.num_loads = 0
|
||||
return self.page
|
||||
|
||||
@@ -35,20 +39,24 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(self.num_loads, count)
|
||||
|
||||
def locate_bookmark(self, title: str):
|
||||
bookmark_tags = self.page.locator('li[ld-bookmark-item]')
|
||||
bookmark_tags = self.page.locator("li[ld-bookmark-item]")
|
||||
return bookmark_tags.filter(has_text=title)
|
||||
|
||||
def locate_bulk_edit_bar(self):
|
||||
return self.page.locator('.bulk-edit-bar')
|
||||
return self.page.locator(".bulk-edit-bar")
|
||||
|
||||
def locate_bulk_edit_select_all(self):
|
||||
return self.locate_bulk_edit_bar().locator('label[ld-bulk-edit-checkbox][all]')
|
||||
return self.locate_bulk_edit_bar().locator("label[ld-bulk-edit-checkbox][all]")
|
||||
|
||||
def locate_bulk_edit_select_across(self):
|
||||
return self.locate_bulk_edit_bar().locator('label.select-across')
|
||||
return self.locate_bulk_edit_bar().locator("label.select-across")
|
||||
|
||||
def locate_bulk_edit_toggle(self):
|
||||
return self.page.get_by_title('Bulk edit')
|
||||
return self.page.get_by_title("Bulk edit")
|
||||
|
||||
def select_bulk_action(self, value: str):
|
||||
return self.locate_bulk_edit_bar().locator('select[name="bulk_action"]').select_option(value)
|
||||
return (
|
||||
self.locate_bulk_edit_bar()
|
||||
.locator('select[name="bulk_action"]')
|
||||
.select_option(value)
|
||||
)
|
||||
|
||||
@@ -17,17 +17,21 @@ class FeedContext:
|
||||
|
||||
def sanitize(text: str):
|
||||
if not text:
|
||||
return ''
|
||||
return ""
|
||||
# remove control characters
|
||||
valid_chars = ['\n', '\r', '\t']
|
||||
return ''.join(ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != 'C')
|
||||
valid_chars = ["\n", "\r", "\t"]
|
||||
return "".join(
|
||||
ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != "C"
|
||||
)
|
||||
|
||||
|
||||
class BaseBookmarksFeed(Feed):
|
||||
def get_object(self, request, feed_key: str):
|
||||
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
||||
search = BookmarkSearch(q=request.GET.get('q', ''))
|
||||
query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, search)
|
||||
search = BookmarkSearch(q=request.GET.get("q", ""))
|
||||
query_set = queries.query_bookmarks(
|
||||
feed_token.user, feed_token.user.profile, search
|
||||
)
|
||||
return FeedContext(feed_token, query_set)
|
||||
|
||||
def item_title(self, item: Bookmark):
|
||||
@@ -44,22 +48,22 @@ class BaseBookmarksFeed(Feed):
|
||||
|
||||
|
||||
class AllBookmarksFeed(BaseBookmarksFeed):
|
||||
title = 'All bookmarks'
|
||||
description = 'All bookmarks'
|
||||
title = "All bookmarks"
|
||||
description = "All bookmarks"
|
||||
|
||||
def link(self, context: FeedContext):
|
||||
return reverse('bookmarks:feeds.all', args=[context.feed_token.key])
|
||||
return reverse("bookmarks:feeds.all", args=[context.feed_token.key])
|
||||
|
||||
def items(self, context: FeedContext):
|
||||
return context.query_set
|
||||
|
||||
|
||||
class UnreadBookmarksFeed(BaseBookmarksFeed):
|
||||
title = 'Unread bookmarks'
|
||||
description = 'All unread bookmarks'
|
||||
title = "Unread bookmarks"
|
||||
description = "All unread bookmarks"
|
||||
|
||||
def link(self, context: FeedContext):
|
||||
return reverse('bookmarks:feeds.unread', args=[context.feed_token.key])
|
||||
return reverse("bookmarks:feeds.unread", args=[context.feed_token.key])
|
||||
|
||||
def items(self, context: FeedContext):
|
||||
return context.query_set.filter(unread=True)
|
||||
|
||||
@@ -59,10 +59,18 @@ class BookmarkItem {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
|
||||
// Toggle notes
|
||||
const notesToggle = element.querySelector(".toggle-notes");
|
||||
if (notesToggle) {
|
||||
notesToggle.addEventListener("click", this.onToggleNotes.bind(this));
|
||||
}
|
||||
|
||||
// Add tooltip to title if it is truncated
|
||||
const titleAnchor = element.querySelector(".title > a");
|
||||
const titleSpan = titleAnchor.querySelector("span");
|
||||
if (titleSpan.offsetWidth > titleAnchor.offsetWidth) {
|
||||
titleAnchor.dataset.tooltip = titleSpan.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
onToggleNotes(event) {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
async function init() {
|
||||
// For now we cache all tags on load as the template did before
|
||||
try {
|
||||
tags = await apiClient.getTags({limit: 1000, offset: 0});
|
||||
tags = await apiClient.getTags({limit: 5000, offset: 0});
|
||||
tags.sort((left, right) => left.name.toLowerCase().localeCompare(right.name.toLowerCase()))
|
||||
} catch (e) {
|
||||
console.warn('TagAutocomplete: Error loading tag list');
|
||||
|
||||
@@ -8,19 +8,19 @@ class Command(BaseCommand):
|
||||
help = "Creates a backup of the linkding database"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('destination', type=str, help='Backup file destination')
|
||||
parser.add_argument("destination", type=str, help="Backup file destination")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
destination = options['destination']
|
||||
destination = options["destination"]
|
||||
|
||||
def progress(status, remaining, total):
|
||||
self.stdout.write(f'Copied {total-remaining} of {total} pages...')
|
||||
self.stdout.write(f"Copied {total-remaining} of {total} pages...")
|
||||
|
||||
source_db = sqlite3.connect(os.path.join('data', 'db.sqlite3'))
|
||||
source_db = sqlite3.connect(os.path.join("data", "db.sqlite3"))
|
||||
backup_db = sqlite3.connect(destination)
|
||||
with backup_db:
|
||||
source_db.backup(backup_db, pages=50, progress=progress)
|
||||
backup_db.close()
|
||||
source_db.close()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'Backup created at {destination}'))
|
||||
self.stdout.write(self.style.SUCCESS(f"Backup created at {destination}"))
|
||||
|
||||
@@ -12,18 +12,20 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
User = get_user_model()
|
||||
superuser_name = os.getenv('LD_SUPERUSER_NAME', None)
|
||||
superuser_password = os.getenv('LD_SUPERUSER_PASSWORD', None)
|
||||
superuser_name = os.getenv("LD_SUPERUSER_NAME", None)
|
||||
superuser_password = os.getenv("LD_SUPERUSER_PASSWORD", None)
|
||||
|
||||
# Skip if option is undefined
|
||||
if not superuser_name:
|
||||
logger.info('Skip creating initial superuser, LD_SUPERUSER_NAME option is not defined')
|
||||
logger.info(
|
||||
"Skip creating initial superuser, LD_SUPERUSER_NAME option is not defined"
|
||||
)
|
||||
return
|
||||
|
||||
# Skip if user already exists
|
||||
user_exists = User.objects.filter(username=superuser_name).exists()
|
||||
if user_exists:
|
||||
logger.info('Skip creating initial superuser, user already exists')
|
||||
logger.info("Skip creating initial superuser, user already exists")
|
||||
return
|
||||
|
||||
user = User(username=superuser_name, is_superuser=True, is_staff=True)
|
||||
@@ -34,4 +36,4 @@ class Command(BaseCommand):
|
||||
user.set_unusable_password()
|
||||
|
||||
user.save()
|
||||
logger.info('Created initial superuser')
|
||||
logger.info("Created initial superuser")
|
||||
|
||||
@@ -14,11 +14,11 @@ class Command(BaseCommand):
|
||||
if not settings.USE_SQLITE:
|
||||
return
|
||||
|
||||
connection = connections['default']
|
||||
connection = connections["default"]
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("PRAGMA journal_mode")
|
||||
current_mode = cursor.fetchone()[0]
|
||||
logger.info(f'Current journal mode: {current_mode}')
|
||||
if current_mode != 'wal':
|
||||
logger.info(f"Current journal mode: {current_mode}")
|
||||
if current_mode != "wal":
|
||||
cursor.execute("PRAGMA journal_mode=wal;")
|
||||
logger.info('Switched to WAL journal mode')
|
||||
logger.info("Switched to WAL journal mode")
|
||||
|
||||
@@ -6,13 +6,15 @@ class Command(BaseCommand):
|
||||
help = "Creates an admin user non-interactively if it doesn't exist"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--username', help="Admin's username")
|
||||
parser.add_argument('--email', help="Admin's email")
|
||||
parser.add_argument('--password', help="Admin's password")
|
||||
parser.add_argument("--username", help="Admin's username")
|
||||
parser.add_argument("--email", help="Admin's email")
|
||||
parser.add_argument("--password", help="Admin's password")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
User = get_user_model()
|
||||
if not User.objects.filter(username=options['username']).exists():
|
||||
User.objects.create_superuser(username=options['username'],
|
||||
email=options['email'],
|
||||
password=options['password'])
|
||||
if not User.objects.filter(username=options["username"]).exists():
|
||||
User.objects.create_superuser(
|
||||
username=options["username"],
|
||||
email=options["email"],
|
||||
password=options["password"],
|
||||
)
|
||||
|
||||
24
bookmarks/management/commands/generate_secret_key.py
Normal file
24
bookmarks/management/commands/generate_secret_key.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.utils import get_random_secret_key
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate secret key file if it does not exist"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
secret_key_file = os.path.join("data", "secretkey.txt")
|
||||
|
||||
if os.path.exists(secret_key_file):
|
||||
logger.info(f"Secret key file already exists")
|
||||
return
|
||||
|
||||
secret_key = get_random_secret_key()
|
||||
with open(secret_key_file, "w") as f:
|
||||
f.write(secret_key)
|
||||
logger.info(f"Generated secret key file")
|
||||
@@ -5,15 +5,17 @@ from bookmarks.services.importer import import_netscape_html
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Import Netscape HTML bookmark file'
|
||||
help = "Import Netscape HTML bookmark file"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('file', type=str, help='Path to file')
|
||||
parser.add_argument('user', type=str, help='Name of the user for which to import')
|
||||
parser.add_argument("file", type=str, help="Path to file")
|
||||
parser.add_argument(
|
||||
"user", type=str, help="Name of the user for which to import"
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
filepath = kwargs['file']
|
||||
username = kwargs['user']
|
||||
filepath = kwargs["file"]
|
||||
username = kwargs["user"]
|
||||
with open(filepath) as html_file:
|
||||
html = html_file.read()
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
@@ -15,19 +15,36 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Bookmark',
|
||||
name="Bookmark",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField()),
|
||||
('title', models.CharField(max_length=512)),
|
||||
('description', models.TextField()),
|
||||
('website_title', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('website_description', models.TextField(blank=True, null=True)),
|
||||
('unread', models.BooleanField(default=True)),
|
||||
('date_added', models.DateTimeField()),
|
||||
('date_modified', models.DateTimeField()),
|
||||
('date_accessed', models.DateTimeField(blank=True, null=True)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("url", models.URLField()),
|
||||
("title", models.CharField(max_length=512)),
|
||||
("description", models.TextField()),
|
||||
(
|
||||
"website_title",
|
||||
models.CharField(blank=True, max_length=512, null=True),
|
||||
),
|
||||
("website_description", models.TextField(blank=True, null=True)),
|
||||
("unread", models.BooleanField(default=True)),
|
||||
("date_added", models.DateTimeField()),
|
||||
("date_modified", models.DateTimeField()),
|
||||
("date_accessed", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,22 +9,36 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('bookmarks', '0001_initial'),
|
||||
("bookmarks", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
name="Tag",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('date_added', models.DateTimeField()),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=64)),
|
||||
("date_added", models.DateTimeField()),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(to='bookmarks.Tag'),
|
||||
model_name="bookmark",
|
||||
name="tags",
|
||||
field=models.ManyToManyField(to="bookmarks.Tag"),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0002_auto_20190629_2303'),
|
||||
("bookmarks", "0002_auto_20190629_2303"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='url',
|
||||
model_name="bookmark",
|
||||
name="url",
|
||||
field=models.URLField(max_length=2048),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,18 +6,18 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0003_auto_20200913_0656'),
|
||||
("bookmarks", "0003_auto_20200913_0656"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='description',
|
||||
model_name="bookmark",
|
||||
name="description",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='title',
|
||||
model_name="bookmark",
|
||||
name="title",
|
||||
field=models.CharField(blank=True, max_length=512),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -7,13 +7,16 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0004_auto_20200926_1028'),
|
||||
("bookmarks", "0004_auto_20200926_1028"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='url',
|
||||
field=models.CharField(max_length=2048, validators=[bookmarks.validators.BookmarkURLValidator()]),
|
||||
model_name="bookmark",
|
||||
name="url",
|
||||
field=models.CharField(
|
||||
max_length=2048,
|
||||
validators=[bookmarks.validators.BookmarkURLValidator()],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0005_auto_20210103_1212'),
|
||||
("bookmarks", "0005_auto_20210103_1212"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='is_archived',
|
||||
model_name="bookmark",
|
||||
name="is_archived",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,8 +6,8 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
User = apps.get_model('auth', 'User')
|
||||
UserProfile = apps.get_model('bookmarks', 'UserProfile')
|
||||
User = apps.get_model("auth", "User")
|
||||
UserProfile = apps.get_model("bookmarks", "UserProfile")
|
||||
for user in User.objects.all():
|
||||
try:
|
||||
if user.profile:
|
||||
@@ -24,19 +24,42 @@ def reverse(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('bookmarks', '0006_bookmark_is_archived'),
|
||||
("bookmarks", "0006_bookmark_is_archived"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserProfile',
|
||||
name="UserProfile",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('theme',
|
||||
models.CharField(choices=[('auto', 'Auto'), ('light', 'Light'), ('dark', 'Dark')], default='auto',
|
||||
max_length=10)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile',
|
||||
to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"theme",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("auto", "Auto"),
|
||||
("light", "Light"),
|
||||
("dark", "Dark"),
|
||||
],
|
||||
default="auto",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="profile",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(forwards, reverse),
|
||||
|
||||
@@ -6,13 +6,21 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0007_userprofile'),
|
||||
("bookmarks", "0007_userprofile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='bookmark_date_display',
|
||||
field=models.CharField(choices=[('relative', 'Relative'), ('absolute', 'Absolute'), ('hidden', 'Hidden')], default='relative', max_length=10),
|
||||
model_name="userprofile",
|
||||
name="bookmark_date_display",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("relative", "Relative"),
|
||||
("absolute", "Absolute"),
|
||||
("hidden", "Hidden"),
|
||||
],
|
||||
default="relative",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0008_userprofile_bookmark_date_display'),
|
||||
("bookmarks", "0008_userprofile_bookmark_date_display"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='web_archive_snapshot_url',
|
||||
model_name="bookmark",
|
||||
name="web_archive_snapshot_url",
|
||||
field=models.CharField(blank=True, max_length=2048),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,17 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0009_bookmark_web_archive_snapshot_url'),
|
||||
("bookmarks", "0009_bookmark_web_archive_snapshot_url"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='bookmark_link_target',
|
||||
field=models.CharField(choices=[('_blank', 'New page'), ('_self', 'Same page')], default='_blank', max_length=10),
|
||||
model_name="userprofile",
|
||||
name="bookmark_link_target",
|
||||
field=models.CharField(
|
||||
choices=[("_blank", "New page"), ("_self", "Same page")],
|
||||
default="_blank",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,17 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0010_userprofile_bookmark_link_target'),
|
||||
("bookmarks", "0010_userprofile_bookmark_link_target"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='web_archive_integration',
|
||||
field=models.CharField(choices=[('disabled', 'Disabled'), ('enabled', 'Enabled')], default='disabled', max_length=10),
|
||||
model_name="userprofile",
|
||||
name="web_archive_integration",
|
||||
field=models.CharField(
|
||||
choices=[("disabled", "Disabled"), ("enabled", "Enabled")],
|
||||
default="disabled",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,18 +9,32 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('bookmarks', '0011_userprofile_web_archive_integration'),
|
||||
("bookmarks", "0011_userprofile_web_archive_integration"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Toast',
|
||||
name="Toast",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=50)),
|
||||
('message', models.TextField()),
|
||||
('acknowledged', models.BooleanField(default=False)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("key", models.CharField(max_length=50)),
|
||||
("message", models.TextField()),
|
||||
("acknowledged", models.BooleanField(default=False)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -10,19 +10,21 @@ User = get_user_model()
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
for user in User.objects.all():
|
||||
toast = Toast(key='web_archive_opt_in_hint',
|
||||
message='The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.',
|
||||
owner=user)
|
||||
toast = Toast(
|
||||
key="web_archive_opt_in_hint",
|
||||
message="The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.",
|
||||
owner=user,
|
||||
)
|
||||
toast.save()
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
Toast.objects.filter(key='web_archive_opt_in_hint').delete()
|
||||
Toast.objects.filter(key="web_archive_opt_in_hint").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('bookmarks', '0012_toast'),
|
||||
("bookmarks", "0012_toast"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
Bookmark = apps.get_model('bookmarks', 'Bookmark')
|
||||
Bookmark = apps.get_model("bookmarks", "Bookmark")
|
||||
Bookmark.objects.update(unread=False)
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ def reverse(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('bookmarks', '0013_web_archive_optin_toast'),
|
||||
("bookmarks", "0013_web_archive_optin_toast"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='unread',
|
||||
model_name="bookmark",
|
||||
name="unread",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(forwards, reverse),
|
||||
|
||||
@@ -9,16 +9,26 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('bookmarks', '0014_alter_bookmark_unread'),
|
||||
("bookmarks", "0014_alter_bookmark_unread"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FeedToken',
|
||||
name="FeedToken",
|
||||
fields=[
|
||||
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feed_token', to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"key",
|
||||
models.CharField(max_length=40, primary_key=True, serialize=False),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="feed_token",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0015_feedtoken'),
|
||||
("bookmarks", "0015_feedtoken"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='shared',
|
||||
model_name="bookmark",
|
||||
name="shared",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0016_bookmark_shared'),
|
||||
("bookmarks", "0016_bookmark_shared"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='enable_sharing',
|
||||
model_name="userprofile",
|
||||
name="enable_sharing",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0017_userprofile_enable_sharing'),
|
||||
("bookmarks", "0017_userprofile_enable_sharing"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='favicon_file',
|
||||
model_name="bookmark",
|
||||
name="favicon_file",
|
||||
field=models.CharField(blank=True, max_length=512),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0018_bookmark_favicon_file'),
|
||||
("bookmarks", "0018_bookmark_favicon_file"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='enable_favicons',
|
||||
model_name="userprofile",
|
||||
name="enable_favicons",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,17 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0019_userprofile_enable_favicons'),
|
||||
("bookmarks", "0019_userprofile_enable_favicons"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='tag_search',
|
||||
field=models.CharField(choices=[('strict', 'Strict'), ('lax', 'Lax')], default='strict', max_length=10),
|
||||
model_name="userprofile",
|
||||
name="tag_search",
|
||||
field=models.CharField(
|
||||
choices=[("strict", "Strict"), ("lax", "Lax")],
|
||||
default="strict",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0020_userprofile_tag_search'),
|
||||
("bookmarks", "0020_userprofile_tag_search"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='display_url',
|
||||
model_name="userprofile",
|
||||
name="display_url",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0021_userprofile_display_url'),
|
||||
("bookmarks", "0021_userprofile_display_url"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='notes',
|
||||
model_name="bookmark",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0022_bookmark_notes'),
|
||||
("bookmarks", "0022_bookmark_notes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='permanent_notes',
|
||||
model_name="userprofile",
|
||||
name="permanent_notes",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0023_userprofile_permanent_notes'),
|
||||
("bookmarks", "0023_userprofile_permanent_notes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='enable_public_sharing',
|
||||
model_name="userprofile",
|
||||
name="enable_public_sharing",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0024_userprofile_enable_public_sharing'),
|
||||
("bookmarks", "0024_userprofile_enable_public_sharing"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='search_preferences',
|
||||
model_name="userprofile",
|
||||
name="search_preferences",
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -26,10 +26,10 @@ class Tag(models.Model):
|
||||
def sanitize_tag_name(tag_name: str):
|
||||
# strip leading/trailing spaces
|
||||
# replace inner spaces with replacement char
|
||||
return tag_name.strip().replace(' ', '-')
|
||||
return tag_name.strip().replace(" ", "-")
|
||||
|
||||
|
||||
def parse_tag_string(tag_string: str, delimiter: str = ','):
|
||||
def parse_tag_string(tag_string: str, delimiter: str = ","):
|
||||
if not tag_string:
|
||||
return []
|
||||
names = tag_string.strip().split(delimiter)
|
||||
@@ -42,7 +42,7 @@ def parse_tag_string(tag_string: str, delimiter: str = ','):
|
||||
return names
|
||||
|
||||
|
||||
def build_tag_string(tag_names: List[str], delimiter: str = ','):
|
||||
def build_tag_string(tag_names: List[str], delimiter: str = ","):
|
||||
return delimiter.join(tag_names)
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class Bookmark(models.Model):
|
||||
return [tag.name for tag in self.tags.all()]
|
||||
|
||||
def __str__(self):
|
||||
return self.resolved_title + ' (' + self.url[:30] + '...)'
|
||||
return self.resolved_title + " (" + self.url[:30] + "...)"
|
||||
|
||||
|
||||
class BookmarkForm(forms.ModelForm):
|
||||
@@ -90,15 +90,13 @@ class BookmarkForm(forms.ModelForm):
|
||||
url = forms.CharField(validators=[BookmarkURLValidator()])
|
||||
tag_string = forms.CharField(required=False)
|
||||
# Do not require title and description in form as we fill these automatically if they are empty
|
||||
title = forms.CharField(max_length=512,
|
||||
required=False)
|
||||
description = forms.CharField(required=False,
|
||||
widget=forms.Textarea())
|
||||
title = forms.CharField(max_length=512, required=False)
|
||||
description = forms.CharField(required=False, widget=forms.Textarea())
|
||||
# Include website title and description as hidden field as they only provide info when editing bookmarks
|
||||
website_title = forms.CharField(max_length=512,
|
||||
required=False, widget=forms.HiddenInput())
|
||||
website_description = forms.CharField(required=False,
|
||||
widget=forms.HiddenInput())
|
||||
website_title = forms.CharField(
|
||||
max_length=512, required=False, widget=forms.HiddenInput()
|
||||
)
|
||||
website_description = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
unread = forms.BooleanField(required=False)
|
||||
shared = forms.BooleanField(required=False)
|
||||
# Hidden field that determines whether to close window/tab after saving the bookmark
|
||||
@@ -107,16 +105,16 @@ class BookmarkForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
fields = [
|
||||
'url',
|
||||
'tag_string',
|
||||
'title',
|
||||
'description',
|
||||
'notes',
|
||||
'website_title',
|
||||
'website_description',
|
||||
'unread',
|
||||
'shared',
|
||||
'auto_close',
|
||||
"url",
|
||||
"tag_string",
|
||||
"title",
|
||||
"description",
|
||||
"notes",
|
||||
"website_title",
|
||||
"website_description",
|
||||
"unread",
|
||||
"shared",
|
||||
"auto_close",
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -125,45 +123,47 @@ class BookmarkForm(forms.ModelForm):
|
||||
|
||||
|
||||
class BookmarkSearch:
|
||||
SORT_ADDED_ASC = 'added_asc'
|
||||
SORT_ADDED_DESC = 'added_desc'
|
||||
SORT_TITLE_ASC = 'title_asc'
|
||||
SORT_TITLE_DESC = 'title_desc'
|
||||
SORT_ADDED_ASC = "added_asc"
|
||||
SORT_ADDED_DESC = "added_desc"
|
||||
SORT_TITLE_ASC = "title_asc"
|
||||
SORT_TITLE_DESC = "title_desc"
|
||||
|
||||
FILTER_SHARED_OFF = 'off'
|
||||
FILTER_SHARED_SHARED = 'yes'
|
||||
FILTER_SHARED_UNSHARED = 'no'
|
||||
FILTER_SHARED_OFF = "off"
|
||||
FILTER_SHARED_SHARED = "yes"
|
||||
FILTER_SHARED_UNSHARED = "no"
|
||||
|
||||
FILTER_UNREAD_OFF = 'off'
|
||||
FILTER_UNREAD_YES = 'yes'
|
||||
FILTER_UNREAD_NO = 'no'
|
||||
FILTER_UNREAD_OFF = "off"
|
||||
FILTER_UNREAD_YES = "yes"
|
||||
FILTER_UNREAD_NO = "no"
|
||||
|
||||
params = ['q', 'user', 'sort', 'shared', 'unread']
|
||||
preferences = ['sort', 'shared', 'unread']
|
||||
params = ["q", "user", "sort", "shared", "unread"]
|
||||
preferences = ["sort", "shared", "unread"]
|
||||
defaults = {
|
||||
'q': '',
|
||||
'user': '',
|
||||
'sort': SORT_ADDED_DESC,
|
||||
'shared': FILTER_SHARED_OFF,
|
||||
'unread': FILTER_UNREAD_OFF,
|
||||
"q": "",
|
||||
"user": "",
|
||||
"sort": SORT_ADDED_DESC,
|
||||
"shared": FILTER_SHARED_OFF,
|
||||
"unread": FILTER_UNREAD_OFF,
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
q: str = None,
|
||||
user: str = None,
|
||||
sort: str = None,
|
||||
shared: str = None,
|
||||
unread: str = None,
|
||||
preferences: dict = None):
|
||||
def __init__(
|
||||
self,
|
||||
q: str = None,
|
||||
user: str = None,
|
||||
sort: str = None,
|
||||
shared: str = None,
|
||||
unread: str = None,
|
||||
preferences: dict = None,
|
||||
):
|
||||
if not preferences:
|
||||
preferences = {}
|
||||
self.defaults = {**BookmarkSearch.defaults, **preferences}
|
||||
|
||||
self.q = q or self.defaults['q']
|
||||
self.user = user or self.defaults['user']
|
||||
self.sort = sort or self.defaults['sort']
|
||||
self.shared = shared or self.defaults['shared']
|
||||
self.unread = unread or self.defaults['unread']
|
||||
self.q = q or self.defaults["q"]
|
||||
self.user = user or self.defaults["user"]
|
||||
self.sort = sort or self.defaults["sort"]
|
||||
self.shared = shared or self.defaults["shared"]
|
||||
self.unread = unread or self.defaults["unread"]
|
||||
|
||||
def is_modified(self, param):
|
||||
value = self.__dict__[param]
|
||||
@@ -175,7 +175,11 @@ class BookmarkSearch:
|
||||
|
||||
@property
|
||||
def modified_preferences(self):
|
||||
return [preference for preference in self.preferences if self.is_modified(preference)]
|
||||
return [
|
||||
preference
|
||||
for preference in self.preferences
|
||||
if self.is_modified(preference)
|
||||
]
|
||||
|
||||
@property
|
||||
def has_modifications(self):
|
||||
@@ -191,7 +195,9 @@ class BookmarkSearch:
|
||||
|
||||
@property
|
||||
def preferences_dict(self):
|
||||
return {preference: self.__dict__[preference] for preference in self.preferences}
|
||||
return {
|
||||
preference: self.__dict__[preference] for preference in self.preferences
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_request(query_dict: QueryDict, preferences: dict = None):
|
||||
@@ -206,20 +212,20 @@ class BookmarkSearch:
|
||||
|
||||
class BookmarkSearchForm(forms.Form):
|
||||
SORT_CHOICES = [
|
||||
(BookmarkSearch.SORT_ADDED_ASC, 'Added ↑'),
|
||||
(BookmarkSearch.SORT_ADDED_DESC, 'Added ↓'),
|
||||
(BookmarkSearch.SORT_TITLE_ASC, 'Title ↑'),
|
||||
(BookmarkSearch.SORT_TITLE_DESC, 'Title ↓'),
|
||||
(BookmarkSearch.SORT_ADDED_ASC, "Added ↑"),
|
||||
(BookmarkSearch.SORT_ADDED_DESC, "Added ↓"),
|
||||
(BookmarkSearch.SORT_TITLE_ASC, "Title ↑"),
|
||||
(BookmarkSearch.SORT_TITLE_DESC, "Title ↓"),
|
||||
]
|
||||
FILTER_SHARED_CHOICES = [
|
||||
(BookmarkSearch.FILTER_SHARED_OFF, 'Off'),
|
||||
(BookmarkSearch.FILTER_SHARED_SHARED, 'Shared'),
|
||||
(BookmarkSearch.FILTER_SHARED_UNSHARED, 'Unshared'),
|
||||
(BookmarkSearch.FILTER_SHARED_OFF, "Off"),
|
||||
(BookmarkSearch.FILTER_SHARED_SHARED, "Shared"),
|
||||
(BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"),
|
||||
]
|
||||
FILTER_UNREAD_CHOICES = [
|
||||
(BookmarkSearch.FILTER_UNREAD_OFF, 'Off'),
|
||||
(BookmarkSearch.FILTER_UNREAD_YES, 'Unread'),
|
||||
(BookmarkSearch.FILTER_UNREAD_NO, 'Read'),
|
||||
(BookmarkSearch.FILTER_UNREAD_OFF, "Off"),
|
||||
(BookmarkSearch.FILTER_UNREAD_YES, "Unread"),
|
||||
(BookmarkSearch.FILTER_UNREAD_NO, "Read"),
|
||||
]
|
||||
|
||||
q = forms.CharField()
|
||||
@@ -228,7 +234,12 @@ class BookmarkSearchForm(forms.Form):
|
||||
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
|
||||
unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect)
|
||||
|
||||
def __init__(self, search: BookmarkSearch, editable_fields: List[str] = None, users: List[User] = None):
|
||||
def __init__(
|
||||
self,
|
||||
search: BookmarkSearch,
|
||||
editable_fields: List[str] = None,
|
||||
users: List[User] = None,
|
||||
):
|
||||
super().__init__()
|
||||
editable_fields = editable_fields or []
|
||||
self.editable_fields = editable_fields
|
||||
@@ -236,8 +247,8 @@ class BookmarkSearchForm(forms.Form):
|
||||
# set choices for user field if users are provided
|
||||
if users:
|
||||
user_choices = [(user.username, user.username) for user in users]
|
||||
user_choices.insert(0, ('', 'Everyone'))
|
||||
self.fields['user'].choices = user_choices
|
||||
user_choices.insert(0, ("", "Everyone"))
|
||||
self.fields["user"].choices = user_choices
|
||||
|
||||
for param in search.params:
|
||||
# set initial values for modified params
|
||||
@@ -251,50 +262,70 @@ class BookmarkSearchForm(forms.Form):
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
THEME_AUTO = 'auto'
|
||||
THEME_LIGHT = 'light'
|
||||
THEME_DARK = 'dark'
|
||||
THEME_AUTO = "auto"
|
||||
THEME_LIGHT = "light"
|
||||
THEME_DARK = "dark"
|
||||
THEME_CHOICES = [
|
||||
(THEME_AUTO, 'Auto'),
|
||||
(THEME_LIGHT, 'Light'),
|
||||
(THEME_DARK, 'Dark'),
|
||||
(THEME_AUTO, "Auto"),
|
||||
(THEME_LIGHT, "Light"),
|
||||
(THEME_DARK, "Dark"),
|
||||
]
|
||||
BOOKMARK_DATE_DISPLAY_RELATIVE = 'relative'
|
||||
BOOKMARK_DATE_DISPLAY_ABSOLUTE = 'absolute'
|
||||
BOOKMARK_DATE_DISPLAY_HIDDEN = 'hidden'
|
||||
BOOKMARK_DATE_DISPLAY_RELATIVE = "relative"
|
||||
BOOKMARK_DATE_DISPLAY_ABSOLUTE = "absolute"
|
||||
BOOKMARK_DATE_DISPLAY_HIDDEN = "hidden"
|
||||
BOOKMARK_DATE_DISPLAY_CHOICES = [
|
||||
(BOOKMARK_DATE_DISPLAY_RELATIVE, 'Relative'),
|
||||
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, 'Absolute'),
|
||||
(BOOKMARK_DATE_DISPLAY_HIDDEN, 'Hidden'),
|
||||
(BOOKMARK_DATE_DISPLAY_RELATIVE, "Relative"),
|
||||
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, "Absolute"),
|
||||
(BOOKMARK_DATE_DISPLAY_HIDDEN, "Hidden"),
|
||||
]
|
||||
BOOKMARK_LINK_TARGET_BLANK = '_blank'
|
||||
BOOKMARK_LINK_TARGET_SELF = '_self'
|
||||
BOOKMARK_LINK_TARGET_BLANK = "_blank"
|
||||
BOOKMARK_LINK_TARGET_SELF = "_self"
|
||||
BOOKMARK_LINK_TARGET_CHOICES = [
|
||||
(BOOKMARK_LINK_TARGET_BLANK, 'New page'),
|
||||
(BOOKMARK_LINK_TARGET_SELF, 'Same page'),
|
||||
(BOOKMARK_LINK_TARGET_BLANK, "New page"),
|
||||
(BOOKMARK_LINK_TARGET_SELF, "Same page"),
|
||||
]
|
||||
WEB_ARCHIVE_INTEGRATION_DISABLED = 'disabled'
|
||||
WEB_ARCHIVE_INTEGRATION_ENABLED = 'enabled'
|
||||
WEB_ARCHIVE_INTEGRATION_DISABLED = "disabled"
|
||||
WEB_ARCHIVE_INTEGRATION_ENABLED = "enabled"
|
||||
WEB_ARCHIVE_INTEGRATION_CHOICES = [
|
||||
(WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'),
|
||||
(WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'),
|
||||
(WEB_ARCHIVE_INTEGRATION_DISABLED, "Disabled"),
|
||||
(WEB_ARCHIVE_INTEGRATION_ENABLED, "Enabled"),
|
||||
]
|
||||
TAG_SEARCH_STRICT = 'strict'
|
||||
TAG_SEARCH_LAX = 'lax'
|
||||
TAG_SEARCH_STRICT = "strict"
|
||||
TAG_SEARCH_LAX = "lax"
|
||||
TAG_SEARCH_CHOICES = [
|
||||
(TAG_SEARCH_STRICT, 'Strict'),
|
||||
(TAG_SEARCH_LAX, 'Lax'),
|
||||
(TAG_SEARCH_STRICT, "Strict"),
|
||||
(TAG_SEARCH_LAX, "Lax"),
|
||||
]
|
||||
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
||||
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
||||
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False,
|
||||
default=BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||
bookmark_link_target = models.CharField(max_length=10, choices=BOOKMARK_LINK_TARGET_CHOICES, blank=False,
|
||||
default=BOOKMARK_LINK_TARGET_BLANK)
|
||||
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
|
||||
default=WEB_ARCHIVE_INTEGRATION_DISABLED)
|
||||
tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False,
|
||||
default=TAG_SEARCH_STRICT)
|
||||
user = models.OneToOneField(
|
||||
get_user_model(), related_name="profile", on_delete=models.CASCADE
|
||||
)
|
||||
theme = models.CharField(
|
||||
max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO
|
||||
)
|
||||
bookmark_date_display = models.CharField(
|
||||
max_length=10,
|
||||
choices=BOOKMARK_DATE_DISPLAY_CHOICES,
|
||||
blank=False,
|
||||
default=BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
)
|
||||
bookmark_link_target = models.CharField(
|
||||
max_length=10,
|
||||
choices=BOOKMARK_LINK_TARGET_CHOICES,
|
||||
blank=False,
|
||||
default=BOOKMARK_LINK_TARGET_BLANK,
|
||||
)
|
||||
web_archive_integration = models.CharField(
|
||||
max_length=10,
|
||||
choices=WEB_ARCHIVE_INTEGRATION_CHOICES,
|
||||
blank=False,
|
||||
default=WEB_ARCHIVE_INTEGRATION_DISABLED,
|
||||
)
|
||||
tag_search = models.CharField(
|
||||
max_length=10,
|
||||
choices=TAG_SEARCH_CHOICES,
|
||||
blank=False,
|
||||
default=TAG_SEARCH_STRICT,
|
||||
)
|
||||
enable_sharing = models.BooleanField(default=False, null=False)
|
||||
enable_public_sharing = models.BooleanField(default=False, null=False)
|
||||
enable_favicons = models.BooleanField(default=False, null=False)
|
||||
@@ -306,8 +337,18 @@ class UserProfile(models.Model):
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search',
|
||||
'enable_sharing', 'enable_public_sharing', 'enable_favicons', 'display_url', 'permanent_notes']
|
||||
fields = [
|
||||
"theme",
|
||||
"bookmark_date_display",
|
||||
"bookmark_link_target",
|
||||
"web_archive_integration",
|
||||
"tag_search",
|
||||
"enable_sharing",
|
||||
"enable_public_sharing",
|
||||
"enable_favicons",
|
||||
"display_url",
|
||||
"permanent_notes",
|
||||
]
|
||||
|
||||
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
@@ -332,11 +373,13 @@ class FeedToken(models.Model):
|
||||
"""
|
||||
Adapted from authtoken.models.Token
|
||||
"""
|
||||
|
||||
key = models.CharField(max_length=40, primary_key=True)
|
||||
user = models.OneToOneField(get_user_model(),
|
||||
related_name='feed_token',
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
user = models.OneToOneField(
|
||||
get_user_model(),
|
||||
related_name="feed_token",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -10,18 +10,24 @@ from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
|
||||
from bookmarks.utils import unique
|
||||
|
||||
|
||||
def query_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, search) \
|
||||
.filter(is_archived=False)
|
||||
def query_bookmarks(
|
||||
user: User, profile: UserProfile, search: BookmarkSearch
|
||||
) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, search).filter(is_archived=False)
|
||||
|
||||
|
||||
def query_archived_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, search) \
|
||||
.filter(is_archived=True)
|
||||
def query_archived_bookmarks(
|
||||
user: User, profile: UserProfile, search: BookmarkSearch
|
||||
) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, search).filter(is_archived=True)
|
||||
|
||||
|
||||
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, search: BookmarkSearch,
|
||||
public_only: bool) -> QuerySet:
|
||||
def query_shared_bookmarks(
|
||||
user: Optional[User],
|
||||
profile: UserProfile,
|
||||
search: BookmarkSearch,
|
||||
public_only: bool,
|
||||
) -> QuerySet:
|
||||
conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True)
|
||||
if public_only:
|
||||
conditions = conditions & Q(owner__profile__enable_public_sharing=True)
|
||||
@@ -29,7 +35,9 @@ def query_shared_bookmarks(user: Optional[User], profile: UserProfile, search: B
|
||||
return _base_bookmarks_query(user, profile, search).filter(conditions)
|
||||
|
||||
|
||||
def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
||||
def _base_bookmarks_query(
|
||||
user: Optional[User], profile: UserProfile, search: BookmarkSearch
|
||||
) -> QuerySet:
|
||||
query_set = Bookmark.objects
|
||||
|
||||
# Filter for user
|
||||
@@ -40,34 +48,32 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
|
||||
query = parse_query_string(search.q)
|
||||
|
||||
# Filter for search terms and tags
|
||||
for term in query['search_terms']:
|
||||
conditions = Q(title__icontains=term) \
|
||||
| Q(description__icontains=term) \
|
||||
| Q(notes__icontains=term) \
|
||||
| Q(website_title__icontains=term) \
|
||||
| Q(website_description__icontains=term) \
|
||||
| Q(url__icontains=term)
|
||||
for term in query["search_terms"]:
|
||||
conditions = (
|
||||
Q(title__icontains=term)
|
||||
| Q(description__icontains=term)
|
||||
| Q(notes__icontains=term)
|
||||
| Q(website_title__icontains=term)
|
||||
| Q(website_description__icontains=term)
|
||||
| Q(url__icontains=term)
|
||||
)
|
||||
|
||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
||||
conditions = conditions | Exists(Bookmark.objects.filter(id=OuterRef('id'), tags__name__iexact=term))
|
||||
conditions = conditions | Exists(
|
||||
Bookmark.objects.filter(id=OuterRef("id"), tags__name__iexact=term)
|
||||
)
|
||||
|
||||
query_set = query_set.filter(conditions)
|
||||
|
||||
for tag_name in query['tag_names']:
|
||||
query_set = query_set.filter(
|
||||
tags__name__iexact=tag_name
|
||||
)
|
||||
for tag_name in query["tag_names"]:
|
||||
query_set = query_set.filter(tags__name__iexact=tag_name)
|
||||
|
||||
# Untagged bookmarks
|
||||
if query['untagged']:
|
||||
query_set = query_set.filter(
|
||||
tags=None
|
||||
)
|
||||
if query["untagged"]:
|
||||
query_set = query_set.filter(tags=None)
|
||||
# Legacy unread bookmarks filter from query
|
||||
if query['unread']:
|
||||
query_set = query_set.filter(
|
||||
unread=True
|
||||
)
|
||||
if query["unread"]:
|
||||
query_set = query_set.filter(unread=True)
|
||||
|
||||
# Unread filter from bookmark search
|
||||
if search.unread == BookmarkSearch.FILTER_UNREAD_YES:
|
||||
@@ -83,29 +89,36 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
|
||||
|
||||
# Sort by date added
|
||||
if search.sort == BookmarkSearch.SORT_ADDED_ASC:
|
||||
query_set = query_set.order_by('date_added')
|
||||
query_set = query_set.order_by("date_added")
|
||||
elif search.sort == BookmarkSearch.SORT_ADDED_DESC:
|
||||
query_set = query_set.order_by('-date_added')
|
||||
query_set = query_set.order_by("-date_added")
|
||||
|
||||
# Sort by title
|
||||
if search.sort == BookmarkSearch.SORT_TITLE_ASC or search.sort == BookmarkSearch.SORT_TITLE_DESC:
|
||||
if (
|
||||
search.sort == BookmarkSearch.SORT_TITLE_ASC
|
||||
or search.sort == BookmarkSearch.SORT_TITLE_DESC
|
||||
):
|
||||
# For the title, the resolved_title logic from the Bookmark entity needs
|
||||
# to be replicated as there is no corresponding database field
|
||||
query_set = query_set.annotate(
|
||||
effective_title=Case(
|
||||
When(Q(title__isnull=False) & ~Q(title__exact=''), then=Lower('title')),
|
||||
When(Q(website_title__isnull=False) & ~Q(website_title__exact=''), then=Lower('website_title')),
|
||||
default=Lower('url'),
|
||||
output_field=CharField()
|
||||
))
|
||||
When(Q(title__isnull=False) & ~Q(title__exact=""), then=Lower("title")),
|
||||
When(
|
||||
Q(website_title__isnull=False) & ~Q(website_title__exact=""),
|
||||
then=Lower("website_title"),
|
||||
),
|
||||
default=Lower("url"),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
|
||||
# For SQLite, if the ICU extension is loaded, use the custom collation
|
||||
# loaded into the connection. This results in an improved sort order for
|
||||
# unicode characters (umlauts, etc.)
|
||||
if settings.USE_SQLITE and settings.USE_SQLITE_ICU_EXTENSION:
|
||||
order_field = RawSQL('effective_title COLLATE ICU', ())
|
||||
order_field = RawSQL("effective_title COLLATE ICU", ())
|
||||
else:
|
||||
order_field = 'effective_title'
|
||||
order_field = "effective_title"
|
||||
|
||||
if search.sort == BookmarkSearch.SORT_TITLE_ASC:
|
||||
query_set = query_set.order_by(order_field)
|
||||
@@ -115,7 +128,9 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
|
||||
return query_set
|
||||
|
||||
|
||||
def query_bookmark_tags(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
||||
def query_bookmark_tags(
|
||||
user: User, profile: UserProfile, search: BookmarkSearch
|
||||
) -> QuerySet:
|
||||
bookmarks_query = query_bookmarks(user, profile, search)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
@@ -123,7 +138,9 @@ def query_bookmark_tags(user: User, profile: UserProfile, search: BookmarkSearch
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_archived_bookmark_tags(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
||||
def query_archived_bookmark_tags(
|
||||
user: User, profile: UserProfile, search: BookmarkSearch
|
||||
) -> QuerySet:
|
||||
bookmarks_query = query_archived_bookmarks(user, profile, search)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
@@ -131,8 +148,12 @@ def query_archived_bookmark_tags(user: User, profile: UserProfile, search: Bookm
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, search: BookmarkSearch,
|
||||
public_only: bool) -> QuerySet:
|
||||
def query_shared_bookmark_tags(
|
||||
user: Optional[User],
|
||||
profile: UserProfile,
|
||||
search: BookmarkSearch,
|
||||
public_only: bool,
|
||||
) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(user, profile, search, public_only)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
@@ -140,7 +161,9 @@ def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, searc
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_shared_bookmark_users(profile: UserProfile, search: BookmarkSearch, public_only: bool) -> QuerySet:
|
||||
def query_shared_bookmark_users(
|
||||
profile: UserProfile, search: BookmarkSearch, public_only: bool
|
||||
) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(None, profile, search, public_only)
|
||||
|
||||
query_set = User.objects.filter(bookmark__in=bookmarks_query)
|
||||
@@ -155,23 +178,23 @@ def get_user_tags(user: User):
|
||||
def parse_query_string(query_string):
|
||||
# Sanitize query params
|
||||
if not query_string:
|
||||
query_string = ''
|
||||
query_string = ""
|
||||
|
||||
# Split query into search terms and tags
|
||||
keywords = query_string.strip().split(' ')
|
||||
keywords = query_string.strip().split(" ")
|
||||
keywords = [word for word in keywords if word]
|
||||
|
||||
search_terms = [word for word in keywords if word[0] != '#' and word[0] != '!']
|
||||
tag_names = [word[1:] for word in keywords if word[0] == '#']
|
||||
search_terms = [word for word in keywords if word[0] != "#" and word[0] != "!"]
|
||||
tag_names = [word[1:] for word in keywords if word[0] == "#"]
|
||||
tag_names = unique(tag_names, str.lower)
|
||||
|
||||
# Special search commands
|
||||
untagged = '!untagged' in keywords
|
||||
unread = '!unread' in keywords
|
||||
untagged = "!untagged" in keywords
|
||||
unread = "!unread" in keywords
|
||||
|
||||
return {
|
||||
'search_terms': search_terms,
|
||||
'tag_names': tag_names,
|
||||
'untagged': untagged,
|
||||
'unread': unread,
|
||||
"search_terms": search_terms,
|
||||
"tag_names": tag_names,
|
||||
"untagged": untagged,
|
||||
"unread": unread,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ from bookmarks.services import tasks
|
||||
|
||||
def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User):
|
||||
# If URL is already bookmarked, then update it
|
||||
existing_bookmark: Bookmark = Bookmark.objects.filter(owner=current_user, url=bookmark.url).first()
|
||||
existing_bookmark: Bookmark = Bookmark.objects.filter(
|
||||
owner=current_user, url=bookmark.url
|
||||
).first()
|
||||
|
||||
if existing_bookmark is not None:
|
||||
_merge_bookmark_data(bookmark, existing_bookmark)
|
||||
@@ -67,9 +69,10 @@ def archive_bookmark(bookmark: Bookmark):
|
||||
|
||||
def archive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(is_archived=True, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
|
||||
is_archived=True, date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def unarchive_bookmark(bookmark: Bookmark):
|
||||
@@ -81,70 +84,93 @@ def unarchive_bookmark(bookmark: Bookmark):
|
||||
|
||||
def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(is_archived=False, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
|
||||
is_archived=False, date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.delete()
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).delete()
|
||||
|
||||
|
||||
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
owned_bookmark_ids = Bookmark.objects.filter(
|
||||
owner=current_user, id__in=sanitized_bookmark_ids
|
||||
).values_list("id", flat=True)
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, current_user)
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark.tags.add(*tags)
|
||||
bookmark.date_modified = timezone.now()
|
||||
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||
relationships = []
|
||||
for tag in tags:
|
||||
for bookmark_id in owned_bookmark_ids:
|
||||
relationships.append(
|
||||
BookmarkToTagRelationShip(bookmark_id=bookmark_id, tag=tag)
|
||||
)
|
||||
|
||||
Bookmark.objects.bulk_update(bookmarks, ['date_modified'])
|
||||
# Insert all bookmark -> tag associations at once, should ignore errors if association already exists
|
||||
BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True)
|
||||
Bookmark.objects.filter(id__in=owned_bookmark_ids).update(
|
||||
date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def untag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||
def untag_bookmarks(
|
||||
bookmark_ids: [Union[int, str]], tag_string: str, current_user: User
|
||||
):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
owned_bookmark_ids = Bookmark.objects.filter(
|
||||
owner=current_user, id__in=sanitized_bookmark_ids
|
||||
).values_list("id", flat=True)
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, current_user)
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark.tags.remove(*tags)
|
||||
bookmark.date_modified = timezone.now()
|
||||
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||
for tag in tags:
|
||||
# Remove all bookmark -> tag associations for the owned bookmarks and the current tag
|
||||
BookmarkToTagRelationShip.objects.filter(
|
||||
bookmark_id__in=owned_bookmark_ids, tag=tag
|
||||
).delete()
|
||||
|
||||
Bookmark.objects.bulk_update(bookmarks, ['date_modified'])
|
||||
Bookmark.objects.filter(id__in=owned_bookmark_ids).update(
|
||||
date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(unread=False, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
|
||||
unread=False, date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(unread=True, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
|
||||
unread=True, date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(shared=True, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
|
||||
shared=True, date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(shared=False, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
|
||||
shared=False, date_modified=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
||||
|
||||
@@ -13,40 +13,41 @@ def export_netscape_html(bookmarks: List[Bookmark]):
|
||||
[append_bookmark(doc, bookmark) for bookmark in bookmarks]
|
||||
append_list_end(doc)
|
||||
|
||||
return '\n\r'.join(doc)
|
||||
return "\n\r".join(doc)
|
||||
|
||||
|
||||
def append_header(doc: BookmarkDocument):
|
||||
doc.append('<!DOCTYPE NETSCAPE-Bookmark-file-1>')
|
||||
doc.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>")
|
||||
doc.append('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">')
|
||||
doc.append('<TITLE>Bookmarks</TITLE>')
|
||||
doc.append('<H1>Bookmarks</H1>')
|
||||
doc.append("<TITLE>Bookmarks</TITLE>")
|
||||
doc.append("<H1>Bookmarks</H1>")
|
||||
|
||||
|
||||
def append_list_start(doc: BookmarkDocument):
|
||||
doc.append('<DL><p>')
|
||||
doc.append("<DL><p>")
|
||||
|
||||
|
||||
def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
|
||||
url = bookmark.url
|
||||
title = html.escape(bookmark.resolved_title or '')
|
||||
desc = html.escape(bookmark.resolved_description or '')
|
||||
title = html.escape(bookmark.resolved_title or "")
|
||||
desc = html.escape(bookmark.resolved_description or "")
|
||||
if bookmark.notes:
|
||||
desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]'
|
||||
desc += f"[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]"
|
||||
tag_names = bookmark.tag_names
|
||||
if bookmark.is_archived:
|
||||
tag_names.append('linkding:archived')
|
||||
tags = ','.join(tag_names)
|
||||
toread = '1' if bookmark.unread else '0'
|
||||
private = '0' if bookmark.shared else '1'
|
||||
tag_names.append("linkding:archived")
|
||||
tags = ",".join(tag_names)
|
||||
toread = "1" if bookmark.unread else "0"
|
||||
private = "0" if bookmark.shared else "1"
|
||||
added = int(bookmark.date_added.timestamp())
|
||||
|
||||
doc.append(
|
||||
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>')
|
||||
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
|
||||
)
|
||||
|
||||
if desc:
|
||||
doc.append(f'<DD>{desc}')
|
||||
doc.append(f"<DD>{desc}")
|
||||
|
||||
|
||||
def append_list_end(doc: BookmarkDocument):
|
||||
doc.append('</DL><p>')
|
||||
doc.append("</DL><p>")
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# register mime type for .ico files, which is not included in the default
|
||||
# mimetypes of the Docker image
|
||||
mimetypes.add_type('image/x-icon', '.ico')
|
||||
mimetypes.add_type("image/x-icon", ".ico")
|
||||
|
||||
|
||||
def _ensure_favicon_folder():
|
||||
@@ -23,16 +23,16 @@ def _ensure_favicon_folder():
|
||||
|
||||
|
||||
def _url_to_filename(url: str) -> str:
|
||||
return re.sub(r'\W+', '_', url)
|
||||
return re.sub(r"\W+", "_", url)
|
||||
|
||||
|
||||
def _get_url_parameters(url: str) -> dict:
|
||||
parsed_uri = urlparse(url)
|
||||
return {
|
||||
# https://example.com/foo?bar -> https://example.com
|
||||
'url': f'{parsed_uri.scheme}://{parsed_uri.hostname}',
|
||||
"url": f"{parsed_uri.scheme}://{parsed_uri.hostname}",
|
||||
# https://example.com/foo?bar -> example.com
|
||||
'domain': parsed_uri.hostname,
|
||||
"domain": parsed_uri.hostname,
|
||||
}
|
||||
|
||||
|
||||
@@ -63,21 +63,21 @@ def load_favicon(url: str) -> str:
|
||||
# Create favicon folder if not exists
|
||||
_ensure_favicon_folder()
|
||||
# Use scheme+hostname as favicon filename to reuse icon for all pages on the same domain
|
||||
favicon_name = _url_to_filename(url_parameters['url'])
|
||||
favicon_name = _url_to_filename(url_parameters["url"])
|
||||
favicon_file = _check_existing_favicon(favicon_name)
|
||||
|
||||
if not favicon_file:
|
||||
# Load favicon from provider, save to file
|
||||
favicon_url = settings.LD_FAVICON_PROVIDER.format(**url_parameters)
|
||||
logger.debug(f'Loading favicon from: {favicon_url}')
|
||||
logger.debug(f"Loading favicon from: {favicon_url}")
|
||||
with requests.get(favicon_url, stream=True) as response:
|
||||
content_type = response.headers['Content-Type']
|
||||
content_type = response.headers["Content-Type"]
|
||||
file_extension = mimetypes.guess_extension(content_type)
|
||||
favicon_file = f'{favicon_name}{file_extension}'
|
||||
favicon_file = f"{favicon_name}{file_extension}"
|
||||
favicon_path = _get_favicon_path(favicon_file)
|
||||
with open(favicon_path, 'wb') as file:
|
||||
with open(favicon_path, "wb") as file:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
file.write(chunk)
|
||||
logger.debug(f'Saved favicon as: {favicon_path}')
|
||||
logger.debug(f"Saved favicon as: {favicon_path}")
|
||||
|
||||
return favicon_file
|
||||
|
||||
@@ -55,18 +55,20 @@ class TagCache:
|
||||
self.cache[tag.name.lower()] = tag
|
||||
|
||||
|
||||
def import_netscape_html(html: str, user: User, options: ImportOptions = ImportOptions()) -> ImportResult:
|
||||
def import_netscape_html(
|
||||
html: str, user: User, options: ImportOptions = ImportOptions()
|
||||
) -> ImportResult:
|
||||
result = ImportResult()
|
||||
import_start = timezone.now()
|
||||
|
||||
try:
|
||||
netscape_bookmarks = parse(html)
|
||||
except:
|
||||
logging.exception('Could not read bookmarks file.')
|
||||
logging.exception("Could not read bookmarks file.")
|
||||
raise
|
||||
|
||||
parse_end = timezone.now()
|
||||
logger.debug(f'Parse duration: {parse_end - import_start}')
|
||||
logger.debug(f"Parse duration: {parse_end - import_start}")
|
||||
|
||||
# Create and cache all tags beforehand
|
||||
_create_missing_tags(netscape_bookmarks, user)
|
||||
@@ -83,7 +85,7 @@ def import_netscape_html(html: str, user: User, options: ImportOptions = ImportO
|
||||
tasks.schedule_bookmarks_without_favicons(user)
|
||||
|
||||
end = timezone.now()
|
||||
logger.debug(f'Import duration: {end - import_start}')
|
||||
logger.debug(f"Import duration: {end - import_start}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -110,7 +112,7 @@ def _get_batches(items: List, batch_size: int):
|
||||
num_items = len(items)
|
||||
|
||||
while offset < num_items:
|
||||
batch = items[offset:min(offset + batch_size, num_items)]
|
||||
batch = items[offset : min(offset + batch_size, num_items)]
|
||||
if len(batch) > 0:
|
||||
batches.append(batch)
|
||||
offset = offset + batch_size
|
||||
@@ -118,11 +120,13 @@ def _get_batches(items: List, batch_size: int):
|
||||
return batches
|
||||
|
||||
|
||||
def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
||||
user: User,
|
||||
options: ImportOptions,
|
||||
tag_cache: TagCache,
|
||||
result: ImportResult):
|
||||
def _import_batch(
|
||||
netscape_bookmarks: List[NetscapeBookmark],
|
||||
user: User,
|
||||
options: ImportOptions,
|
||||
tag_cache: TagCache,
|
||||
result: ImportResult,
|
||||
):
|
||||
# Query existing bookmarks
|
||||
batch_urls = [bookmark.href for bookmark in netscape_bookmarks]
|
||||
existing_bookmarks = Bookmark.objects.filter(owner=user, url__in=batch_urls)
|
||||
@@ -136,7 +140,13 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
||||
try:
|
||||
# Lookup existing bookmark by URL, or create new bookmark if there is no bookmark for that URL yet
|
||||
bookmark = next(
|
||||
(bookmark for bookmark in existing_bookmarks if bookmark.url == netscape_bookmark.href), None)
|
||||
(
|
||||
bookmark
|
||||
for bookmark in existing_bookmarks
|
||||
if bookmark.url == netscape_bookmark.href
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not bookmark:
|
||||
bookmark = Bookmark(owner=user)
|
||||
is_update = False
|
||||
@@ -146,7 +156,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
||||
_copy_bookmark_data(netscape_bookmark, bookmark, options)
|
||||
# Validate bookmark fields, exclude owner to prevent n+1 database query,
|
||||
# also there is no specific validation on owner
|
||||
bookmark.clean_fields(exclude=['owner'])
|
||||
bookmark.clean_fields(exclude=["owner"])
|
||||
# Schedule for update or insert
|
||||
if is_update:
|
||||
bookmarks_to_update.append(bookmark)
|
||||
@@ -155,20 +165,25 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
||||
|
||||
result.success = result.success + 1
|
||||
except:
|
||||
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + '...'
|
||||
logging.exception('Error importing bookmark: ' + shortened_bookmark_tag_str)
|
||||
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..."
|
||||
logging.exception("Error importing bookmark: " + shortened_bookmark_tag_str)
|
||||
result.failed = result.failed + 1
|
||||
|
||||
# Bulk update bookmarks in DB
|
||||
Bookmark.objects.bulk_update(bookmarks_to_update, ['url',
|
||||
'date_added',
|
||||
'date_modified',
|
||||
'unread',
|
||||
'shared',
|
||||
'title',
|
||||
'description',
|
||||
'notes',
|
||||
'owner'])
|
||||
Bookmark.objects.bulk_update(
|
||||
bookmarks_to_update,
|
||||
[
|
||||
"url",
|
||||
"date_added",
|
||||
"date_modified",
|
||||
"unread",
|
||||
"shared",
|
||||
"title",
|
||||
"description",
|
||||
"notes",
|
||||
"owner",
|
||||
],
|
||||
)
|
||||
# Bulk insert new bookmarks into DB
|
||||
Bookmark.objects.bulk_create(bookmarks_to_create)
|
||||
|
||||
@@ -183,13 +198,20 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
||||
for netscape_bookmark in netscape_bookmarks:
|
||||
# Lookup bookmark by URL again
|
||||
bookmark = next(
|
||||
(bookmark for bookmark in existing_bookmarks if bookmark.url == netscape_bookmark.href), None)
|
||||
(
|
||||
bookmark
|
||||
for bookmark in existing_bookmarks
|
||||
if bookmark.url == netscape_bookmark.href
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not bookmark:
|
||||
# Something is wrong, we should have just created this bookmark
|
||||
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + '...'
|
||||
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..."
|
||||
logging.warning(
|
||||
f'Failed to assign tags to the bookmark: {shortened_bookmark_tag_str}. Could not find bookmark by URL.')
|
||||
f"Failed to assign tags to the bookmark: {shortened_bookmark_tag_str}. Could not find bookmark by URL."
|
||||
)
|
||||
continue
|
||||
|
||||
# Get tag models by string, schedule inserts for bookmark -> tag associations
|
||||
@@ -201,7 +223,9 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
||||
BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True)
|
||||
|
||||
|
||||
def _copy_bookmark_data(netscape_bookmark: NetscapeBookmark, bookmark: Bookmark, options: ImportOptions):
|
||||
def _copy_bookmark_data(
|
||||
netscape_bookmark: NetscapeBookmark, bookmark: Bookmark, options: ImportOptions
|
||||
):
|
||||
bookmark.url = netscape_bookmark.href
|
||||
if netscape_bookmark.date_added:
|
||||
bookmark.date_added = parse_timestamp(netscape_bookmark.date_added)
|
||||
|
||||
@@ -25,29 +25,29 @@ class BookmarkParser(HTMLParser):
|
||||
|
||||
self.current_tag = None
|
||||
self.bookmark = None
|
||||
self.href = ''
|
||||
self.add_date = ''
|
||||
self.tags = ''
|
||||
self.title = ''
|
||||
self.description = ''
|
||||
self.notes = ''
|
||||
self.toread = ''
|
||||
self.private = ''
|
||||
self.href = ""
|
||||
self.add_date = ""
|
||||
self.tags = ""
|
||||
self.title = ""
|
||||
self.description = ""
|
||||
self.notes = ""
|
||||
self.toread = ""
|
||||
self.private = ""
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list):
|
||||
name = 'handle_start_' + tag.lower()
|
||||
name = "handle_start_" + tag.lower()
|
||||
if name in dir(self):
|
||||
getattr(self, name)({k.lower(): v for k, v in attrs})
|
||||
self.current_tag = tag
|
||||
|
||||
def handle_endtag(self, tag: str):
|
||||
name = 'handle_end_' + tag.lower()
|
||||
name = "handle_end_" + tag.lower()
|
||||
if name in dir(self):
|
||||
getattr(self, name)()
|
||||
self.current_tag = None
|
||||
|
||||
def handle_data(self, data):
|
||||
name = f'handle_{self.current_tag}_data'
|
||||
name = f"handle_{self.current_tag}_data"
|
||||
if name in dir(self):
|
||||
getattr(self, name)(data)
|
||||
|
||||
@@ -60,22 +60,22 @@ class BookmarkParser(HTMLParser):
|
||||
def handle_start_a(self, attrs: Dict[str, str]):
|
||||
vars(self).update(attrs)
|
||||
tag_names = parse_tag_string(self.tags)
|
||||
archived = 'linkding:archived' in self.tags
|
||||
archived = "linkding:archived" in self.tags
|
||||
try:
|
||||
tag_names.remove('linkding:archived')
|
||||
tag_names.remove("linkding:archived")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.bookmark = NetscapeBookmark(
|
||||
href=self.href,
|
||||
title='',
|
||||
description='',
|
||||
notes='',
|
||||
title="",
|
||||
description="",
|
||||
notes="",
|
||||
date_added=self.add_date,
|
||||
tag_names=tag_names,
|
||||
to_read=self.toread == '1',
|
||||
to_read=self.toread == "1",
|
||||
# Mark as private by default, also when attribute is not specified
|
||||
private=self.private != '0',
|
||||
private=self.private != "0",
|
||||
archived=archived,
|
||||
)
|
||||
|
||||
@@ -84,9 +84,9 @@ class BookmarkParser(HTMLParser):
|
||||
|
||||
def handle_dd_data(self, data):
|
||||
desc = data.strip()
|
||||
if '[linkding-notes]' in desc:
|
||||
self.notes = desc.split('[linkding-notes]')[1].split('[/linkding-notes]')[0]
|
||||
self.description = desc.split('[linkding-notes]')[0]
|
||||
if "[linkding-notes]" in desc:
|
||||
self.notes = desc.split("[linkding-notes]")[1].split("[/linkding-notes]")[0]
|
||||
self.description = desc.split("[linkding-notes]")[0]
|
||||
|
||||
def add_bookmark(self):
|
||||
if self.bookmark:
|
||||
@@ -95,14 +95,14 @@ class BookmarkParser(HTMLParser):
|
||||
self.bookmark.notes = self.notes
|
||||
self.bookmarks.append(self.bookmark)
|
||||
self.bookmark = None
|
||||
self.href = ''
|
||||
self.add_date = ''
|
||||
self.tags = ''
|
||||
self.title = ''
|
||||
self.description = ''
|
||||
self.notes = ''
|
||||
self.toread = ''
|
||||
self.private = ''
|
||||
self.href = ""
|
||||
self.add_date = ""
|
||||
self.tags = ""
|
||||
self.title = ""
|
||||
self.description = ""
|
||||
self.notes = ""
|
||||
self.toread = ""
|
||||
self.private = ""
|
||||
|
||||
|
||||
def parse(html: str) -> List[NetscapeBookmark]:
|
||||
|
||||
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def get_or_create_tags(tag_names: List[str], user: User):
|
||||
tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names]
|
||||
return unique(tags, operator.attrgetter('id'))
|
||||
return unique(tags, operator.attrgetter("id"))
|
||||
|
||||
|
||||
def get_or_create_tag(name: str, user: User):
|
||||
|
||||
@@ -18,8 +18,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def is_web_archive_integration_active(user: User) -> bool:
|
||||
background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS
|
||||
web_archive_integration_enabled = \
|
||||
user.profile.web_archive_integration == UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
|
||||
web_archive_integration_enabled = (
|
||||
user.profile.web_archive_integration
|
||||
== UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
|
||||
)
|
||||
|
||||
return background_tasks_enabled and web_archive_integration_enabled
|
||||
|
||||
@@ -31,28 +33,36 @@ def create_web_archive_snapshot(user: User, bookmark: Bookmark, force_update: bo
|
||||
|
||||
def _load_newest_snapshot(bookmark: Bookmark):
|
||||
try:
|
||||
logger.info(f'Load existing snapshot for bookmark. url={bookmark.url}')
|
||||
cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(bookmark.url)
|
||||
logger.info(f"Load existing snapshot for bookmark. url={bookmark.url}")
|
||||
cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(
|
||||
bookmark.url
|
||||
)
|
||||
existing_snapshot = cdx_api.newest()
|
||||
|
||||
if existing_snapshot:
|
||||
bookmark.web_archive_snapshot_url = existing_snapshot.archive_url
|
||||
bookmark.save(update_fields=['web_archive_snapshot_url'])
|
||||
logger.info(f'Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}')
|
||||
bookmark.save(update_fields=["web_archive_snapshot_url"])
|
||||
logger.info(
|
||||
f"Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}"
|
||||
)
|
||||
|
||||
except NoCDXRecordFound:
|
||||
logger.info(f'Could not find any snapshots for bookmark. url={bookmark.url}')
|
||||
logger.info(f"Could not find any snapshots for bookmark. url={bookmark.url}")
|
||||
except WaybackError as error:
|
||||
logger.error(f'Failed to load existing snapshot. url={bookmark.url}', exc_info=error)
|
||||
logger.error(
|
||||
f"Failed to load existing snapshot. url={bookmark.url}", exc_info=error
|
||||
)
|
||||
|
||||
|
||||
def _create_snapshot(bookmark: Bookmark):
|
||||
logger.info(f'Create new snapshot for bookmark. url={bookmark.url}...')
|
||||
archive = waybackpy.WaybackMachineSaveAPI(bookmark.url, DEFAULT_USER_AGENT, max_tries=1)
|
||||
logger.info(f"Create new snapshot for bookmark. url={bookmark.url}...")
|
||||
archive = waybackpy.WaybackMachineSaveAPI(
|
||||
bookmark.url, DEFAULT_USER_AGENT, max_tries=1
|
||||
)
|
||||
archive.save()
|
||||
bookmark.web_archive_snapshot_url = archive.archive_url
|
||||
bookmark.save(update_fields=['web_archive_snapshot_url'])
|
||||
logger.info(f'Successfully created new snapshot for bookmark:. url={bookmark.url}')
|
||||
bookmark.save(update_fields=["web_archive_snapshot_url"])
|
||||
logger.info(f"Successfully created new snapshot for bookmark:. url={bookmark.url}")
|
||||
|
||||
|
||||
@background()
|
||||
@@ -72,10 +82,13 @@ def _create_web_archive_snapshot_task(bookmark_id: int, force_update: bool):
|
||||
return
|
||||
except TooManyRequestsError:
|
||||
logger.error(
|
||||
f'Failed to create snapshot due to rate limiting, trying to load newest snapshot as fallback. url={bookmark.url}')
|
||||
f"Failed to create snapshot due to rate limiting, trying to load newest snapshot as fallback. url={bookmark.url}"
|
||||
)
|
||||
except WaybackError as error:
|
||||
logger.error(f'Failed to create snapshot, trying to load newest snapshot as fallback. url={bookmark.url}',
|
||||
exc_info=error)
|
||||
logger.error(
|
||||
f"Failed to create snapshot, trying to load newest snapshot as fallback. url={bookmark.url}",
|
||||
exc_info=error,
|
||||
)
|
||||
|
||||
# Load the newest snapshot as fallback
|
||||
_load_newest_snapshot(bookmark)
|
||||
@@ -102,7 +115,9 @@ def schedule_bookmarks_without_snapshots(user: User):
|
||||
@background()
|
||||
def _schedule_bookmarks_without_snapshots_task(user_id: int):
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
bookmarks_without_snapshots = Bookmark.objects.filter(web_archive_snapshot_url__exact='', owner=user)
|
||||
bookmarks_without_snapshots = Bookmark.objects.filter(
|
||||
web_archive_snapshot_url__exact="", owner=user
|
||||
)
|
||||
|
||||
for bookmark in bookmarks_without_snapshots:
|
||||
# To prevent rate limit errors from the Wayback API only try to load the latest snapshots instead of creating
|
||||
@@ -128,14 +143,16 @@ def _load_favicon_task(bookmark_id: int):
|
||||
except Bookmark.DoesNotExist:
|
||||
return
|
||||
|
||||
logger.info(f'Load favicon for bookmark. url={bookmark.url}')
|
||||
logger.info(f"Load favicon for bookmark. url={bookmark.url}")
|
||||
|
||||
new_favicon_file = favicon_loader.load_favicon(bookmark.url)
|
||||
|
||||
if new_favicon_file != bookmark.favicon_file:
|
||||
bookmark.favicon_file = new_favicon_file
|
||||
bookmark.save(update_fields=['favicon_file'])
|
||||
logger.info(f'Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}')
|
||||
bookmark.save(update_fields=["favicon_file"])
|
||||
logger.info(
|
||||
f"Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}"
|
||||
)
|
||||
|
||||
|
||||
def schedule_bookmarks_without_favicons(user: User):
|
||||
@@ -146,11 +163,13 @@ def schedule_bookmarks_without_favicons(user: User):
|
||||
@background()
|
||||
def _schedule_bookmarks_without_favicons_task(user_id: int):
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
bookmarks = Bookmark.objects.filter(favicon_file__exact='', owner=user)
|
||||
bookmarks = Bookmark.objects.filter(favicon_file__exact="", owner=user)
|
||||
tasks = []
|
||||
|
||||
for bookmark in bookmarks:
|
||||
task = Task.objects.new_task(task_name='bookmarks.services.tasks._load_favicon_task', args=(bookmark.id,))
|
||||
task = Task.objects.new_task(
|
||||
task_name="bookmarks.services.tasks._load_favicon_task", args=(bookmark.id,)
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
Task.objects.bulk_create(tasks)
|
||||
@@ -168,7 +187,9 @@ def _schedule_refresh_favicons_task(user_id: int):
|
||||
tasks = []
|
||||
|
||||
for bookmark in bookmarks:
|
||||
task = Task.objects.new_task(task_name='bookmarks.services.tasks._load_favicon_task', args=(bookmark.id,))
|
||||
task = Task.objects.new_task(
|
||||
task_name="bookmarks.services.tasks._load_favicon_task", args=(bookmark.id,)
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
Task.objects.bulk_create(tasks)
|
||||
|
||||
@@ -14,8 +14,10 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI):
|
||||
|
||||
def newest(self):
|
||||
unix_timestamp = int(time.time())
|
||||
self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(unix_timestamp)
|
||||
self.sort = 'closest'
|
||||
self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(
|
||||
unix_timestamp
|
||||
)
|
||||
self.sort = "closest"
|
||||
self.limit = -5
|
||||
|
||||
newest_snapshot = None
|
||||
@@ -37,4 +39,4 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI):
|
||||
super().add_payload(payload)
|
||||
# Set fastLatest query param, as we are only using this API to get the latest snapshot and using fastLatest
|
||||
# makes searching for latest snapshots faster
|
||||
payload['fastLatest'] = 'true'
|
||||
payload["fastLatest"] = "true"
|
||||
|
||||
@@ -18,9 +18,9 @@ class WebsiteMetadata:
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'url': self.url,
|
||||
'title': self.title,
|
||||
'description': self.description,
|
||||
"url": self.url,
|
||||
"title": self.title,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
|
||||
@@ -34,17 +34,29 @@ def load_website_metadata(url: str):
|
||||
start = timezone.now()
|
||||
page_text = load_page(url)
|
||||
end = timezone.now()
|
||||
logger.debug(f'Load duration: {end - start}')
|
||||
logger.debug(f"Load duration: {end - start}")
|
||||
|
||||
start = timezone.now()
|
||||
soup = BeautifulSoup(page_text, 'html.parser')
|
||||
soup = BeautifulSoup(page_text, "html.parser")
|
||||
|
||||
title = soup.title.string.strip() if soup.title is not None else None
|
||||
description_tag = soup.find('meta', attrs={'name': 'description'})
|
||||
description = description = description_tag['content'].strip() if description_tag and description_tag[
|
||||
'content'] else None
|
||||
description_tag = soup.find("meta", attrs={"name": "description"})
|
||||
description = (
|
||||
description_tag["content"].strip()
|
||||
if description_tag and description_tag["content"]
|
||||
else None
|
||||
)
|
||||
|
||||
if not description:
|
||||
description_tag = soup.find("meta", attrs={"property": "og:description"})
|
||||
description = (
|
||||
description_tag["content"].strip()
|
||||
if description_tag and description_tag["content"]
|
||||
else None
|
||||
)
|
||||
|
||||
end = timezone.now()
|
||||
logger.debug(f'Parsing duration: {end - start}')
|
||||
logger.debug(f"Parsing duration: {end - start}")
|
||||
finally:
|
||||
return WebsiteMetadata(url=url, title=title, description=description)
|
||||
|
||||
@@ -68,30 +80,30 @@ def load_page(url: str):
|
||||
else:
|
||||
content = content + chunk
|
||||
|
||||
logger.debug(f'Loaded chunk (iteration={iteration}, total={size / 1024})')
|
||||
logger.debug(f"Loaded chunk (iteration={iteration}, total={size / 1024})")
|
||||
|
||||
# Stop reading if we have parsed end of head tag
|
||||
end_of_head = '</head>'.encode('utf-8')
|
||||
end_of_head = "</head>".encode("utf-8")
|
||||
if end_of_head in content:
|
||||
logger.debug(f'Found closing head tag after {size} bytes')
|
||||
logger.debug(f"Found closing head tag after {size} bytes")
|
||||
content = content.split(end_of_head)[0] + end_of_head
|
||||
break
|
||||
# Stop reading if we exceed limit
|
||||
if size > MAX_CONTENT_LIMIT:
|
||||
logger.debug(f'Cancel reading document after {size} bytes')
|
||||
logger.debug(f"Cancel reading document after {size} bytes")
|
||||
break
|
||||
if hasattr(r, '_content_consumed'):
|
||||
logger.debug(f'Request consumed: {r._content_consumed}')
|
||||
if hasattr(r, "_content_consumed"):
|
||||
logger.debug(f"Request consumed: {r._content_consumed}")
|
||||
|
||||
# Use charset_normalizer to determine encoding that best matches the response content
|
||||
# Several sites seem to specify the response encoding incorrectly, so we ignore it and use custom logic instead
|
||||
# This is different from Response.text which does respect the encoding specified in the response first,
|
||||
# before trying to determine one
|
||||
results = from_bytes(content or '')
|
||||
results = from_bytes(content or "")
|
||||
return str(results.best())
|
||||
|
||||
|
||||
DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36'
|
||||
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36"
|
||||
|
||||
|
||||
def fake_request_headers():
|
||||
|
||||
@@ -15,9 +15,11 @@ def user_logged_in(sender, request, user, **kwargs):
|
||||
def extend_sqlite(connection=None, **kwargs):
|
||||
# Load ICU extension into Sqlite connection to support case-insensitive
|
||||
# comparisons with unicode characters
|
||||
if connection.vendor == 'sqlite' and settings.USE_SQLITE_ICU_EXTENSION:
|
||||
if connection.vendor == "sqlite" and settings.USE_SQLITE_ICU_EXTENSION:
|
||||
connection.connection.enable_load_extension(True)
|
||||
connection.connection.load_extension(settings.SQLITE_ICU_EXTENSION_PATH.rstrip('.so'))
|
||||
connection.connection.load_extension(
|
||||
settings.SQLITE_ICU_EXTENSION_PATH.rstrip(".so")
|
||||
)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Load an ICU collation for case-insensitive ordering.
|
||||
|
||||
@@ -107,6 +107,18 @@ ul.bookmark-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bookmarks */
|
||||
li[ld-bookmark-item] {
|
||||
position: relative;
|
||||
@@ -122,6 +134,27 @@ li[ld-bookmark-item] {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&[data-tooltip]:hover::after, &[data-tooltip]:focus::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
height: fit-content;
|
||||
background-color: #292f62;
|
||||
color: #fff;
|
||||
padding: $unit-1;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid #424a8c;
|
||||
font-size: $font-size-sm;
|
||||
font-style: normal;
|
||||
white-space: normal;
|
||||
animation: 0.3s ease 0s appear;
|
||||
}
|
||||
}
|
||||
|
||||
&.unread .title a {
|
||||
|
||||
@@ -43,6 +43,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
<div class="title">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener" >
|
||||
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
|
||||
<img src="{% static bookmark_item.favicon_file %}" alt="">
|
||||
{% endif %}
|
||||
{{ bookmark_item.title }}
|
||||
<span>{{ bookmark_item.title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if bookmark_list.show_url %}
|
||||
|
||||
@@ -115,8 +115,6 @@
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn">Nevermind</a>
|
||||
</div>
|
||||
|
||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
||||
<script type="application/javascript">
|
||||
/**
|
||||
* - Pre-fill title and description placeholders with metadata from website as soon as URL changes
|
||||
@@ -129,7 +127,6 @@
|
||||
const descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
|
||||
const notesDetails = document.querySelector('form details.notes');
|
||||
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
|
||||
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
|
||||
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
||||
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
||||
const websiteTitleInput = document.getElementById('{{ form.website_title.id_for_label }}');
|
||||
@@ -185,6 +182,10 @@
|
||||
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
|
||||
|
||||
if (existingBookmark && !editedBookmarkId) {
|
||||
// Workaround: tag input will be replaced by tag autocomplete, so
|
||||
// defer getting the input until we need it
|
||||
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
|
||||
|
||||
bookmarkExistsHint.style['display'] = 'block';
|
||||
notesDetails.open = !!existingBookmark.notes;
|
||||
updateInput(titleInput, existingBookmark.title);
|
||||
|
||||
@@ -43,6 +43,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -122,5 +122,6 @@
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -34,7 +34,10 @@
|
||||
</ul>
|
||||
</div>
|
||||
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a>
|
||||
<a href="{% url 'logout' %}" class="btn btn-link">Logout</a>
|
||||
<form class="d-inline" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
{# Menu drop-down for smaller devices #}
|
||||
<div class="show-md">
|
||||
@@ -74,7 +77,10 @@
|
||||
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'logout' %}" class="btn btn-link">Logout</a>
|
||||
<form class="d-inline" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
props: {
|
||||
name: 'q',
|
||||
placeholder: 'Search for words or #tags',
|
||||
value: '{{ search.q|safe }}',
|
||||
value: input.value,
|
||||
tags: uniqueTags,
|
||||
mode: '{{ mode }}',
|
||||
linkTarget: '{{ request.user_profile.bookmark_link_target }}',
|
||||
|
||||
@@ -46,6 +46,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
Machine</a>.
|
||||
This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in
|
||||
case it goes offline or its content is modified.
|
||||
Please consider donating to the <a href="https://archive.org/donate/index.php" target="_blank"
|
||||
Please consider donating to the <a href="https://archive.org/donate" target="_blank"
|
||||
rel="noopener">Internet Archive</a> if you make use of this feature.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,48 +2,67 @@ from typing import List
|
||||
|
||||
from django import template
|
||||
|
||||
from bookmarks.models import BookmarkForm, BookmarkSearch, BookmarkSearchForm, Tag, build_tag_string, User
|
||||
from bookmarks.models import (
|
||||
BookmarkForm,
|
||||
BookmarkSearch,
|
||||
BookmarkSearchForm,
|
||||
Tag,
|
||||
build_tag_string,
|
||||
User,
|
||||
)
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/form.html', name='bookmark_form', takes_context=True)
|
||||
def bookmark_form(context, form: BookmarkForm, cancel_url: str, bookmark_id: int = 0, auto_close: bool = False):
|
||||
@register.inclusion_tag("bookmarks/form.html", name="bookmark_form", takes_context=True)
|
||||
def bookmark_form(
|
||||
context,
|
||||
form: BookmarkForm,
|
||||
cancel_url: str,
|
||||
bookmark_id: int = 0,
|
||||
auto_close: bool = False,
|
||||
):
|
||||
return {
|
||||
'request': context['request'],
|
||||
'form': form,
|
||||
'auto_close': auto_close,
|
||||
'bookmark_id': bookmark_id,
|
||||
'cancel_url': cancel_url
|
||||
"request": context["request"],
|
||||
"form": form,
|
||||
"auto_close": auto_close,
|
||||
"bookmark_id": bookmark_id,
|
||||
"cancel_url": cancel_url,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/search.html', name='bookmark_search', takes_context=True)
|
||||
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''):
|
||||
@register.inclusion_tag(
|
||||
"bookmarks/search.html", name="bookmark_search", takes_context=True
|
||||
)
|
||||
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ""):
|
||||
tag_names = [tag.name for tag in tags]
|
||||
tags_string = build_tag_string(tag_names, ' ')
|
||||
search_form = BookmarkSearchForm(search, editable_fields=['q'])
|
||||
tags_string = build_tag_string(tag_names, " ")
|
||||
search_form = BookmarkSearchForm(search, editable_fields=["q"])
|
||||
|
||||
if mode == 'shared':
|
||||
preferences_form = BookmarkSearchForm(search, editable_fields=['sort'])
|
||||
if mode == "shared":
|
||||
preferences_form = BookmarkSearchForm(search, editable_fields=["sort"])
|
||||
else:
|
||||
preferences_form = BookmarkSearchForm(search, editable_fields=['sort', 'shared', 'unread'])
|
||||
preferences_form = BookmarkSearchForm(
|
||||
search, editable_fields=["sort", "shared", "unread"]
|
||||
)
|
||||
return {
|
||||
'request': context['request'],
|
||||
'search': search,
|
||||
'search_form': search_form,
|
||||
'preferences_form': preferences_form,
|
||||
'tags_string': tags_string,
|
||||
'mode': mode,
|
||||
"request": context["request"],
|
||||
"search": search,
|
||||
"search_form": search_form,
|
||||
"preferences_form": preferences_form,
|
||||
"tags_string": tags_string,
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/user_select.html', name='user_select', takes_context=True)
|
||||
@register.inclusion_tag(
|
||||
"bookmarks/user_select.html", name="user_select", takes_context=True
|
||||
)
|
||||
def user_select(context, search: BookmarkSearch, users: List[User]):
|
||||
sorted_users = sorted(users, key=lambda x: str.lower(x.username))
|
||||
form = BookmarkSearchForm(search, editable_fields=['user'], users=sorted_users)
|
||||
form = BookmarkSearchForm(search, editable_fields=["user"], users=sorted_users)
|
||||
return {
|
||||
'search': search,
|
||||
'users': sorted_users,
|
||||
'form': form,
|
||||
"search": search,
|
||||
"users": sorted_users,
|
||||
"form": form,
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ NUM_ADJACENT_PAGES = 2
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/pagination.html', name='pagination', takes_context=True)
|
||||
@register.inclusion_tag(
|
||||
"bookmarks/pagination.html", name="pagination", takes_context=True
|
||||
)
|
||||
def pagination(context, page: Page):
|
||||
visible_page_numbers = get_visible_page_numbers(page.number, page.paginator.num_pages)
|
||||
visible_page_numbers = get_visible_page_numbers(
|
||||
page.number, page.paginator.num_pages
|
||||
)
|
||||
|
||||
return {
|
||||
'page': page,
|
||||
'visible_page_numbers': visible_page_numbers
|
||||
}
|
||||
return {"page": page, "visible_page_numbers": visible_page_numbers}
|
||||
|
||||
|
||||
def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]:
|
||||
@@ -29,10 +30,12 @@ def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]:
|
||||
visible_pages = set()
|
||||
|
||||
# Add adjacent pages around current page
|
||||
visible_pages |= set(range(
|
||||
max(1, current_page_number - NUM_ADJACENT_PAGES),
|
||||
min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1
|
||||
))
|
||||
visible_pages |= set(
|
||||
range(
|
||||
max(1, current_page_number - NUM_ADJACENT_PAGES),
|
||||
min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1,
|
||||
)
|
||||
)
|
||||
|
||||
# Add first page
|
||||
visible_pages.add(1)
|
||||
|
||||
@@ -28,12 +28,12 @@ def add_tag_to_query(context, tag_name: str):
|
||||
params = context.request.GET.copy()
|
||||
|
||||
# Append to or create query string
|
||||
if params.__contains__('q'):
|
||||
query_string = params.__getitem__('q') + ' '
|
||||
if params.__contains__("q"):
|
||||
query_string = params.__getitem__("q") + " "
|
||||
else:
|
||||
query_string = ''
|
||||
query_string = query_string + '#' + tag_name
|
||||
params.__setitem__('q', query_string)
|
||||
query_string = ""
|
||||
query_string = query_string + "#" + tag_name
|
||||
params.__setitem__("q", query_string)
|
||||
|
||||
return params.urlencode()
|
||||
|
||||
@@ -41,20 +41,26 @@ def add_tag_to_query(context, tag_name: str):
|
||||
@register.simple_tag(takes_context=True)
|
||||
def remove_tag_from_query(context, tag_name: str):
|
||||
params = context.request.GET.copy()
|
||||
if params.__contains__('q'):
|
||||
if params.__contains__("q"):
|
||||
# Split query string into parts
|
||||
query_string = params.__getitem__('q')
|
||||
query_string = params.__getitem__("q")
|
||||
query_parts = query_string.split()
|
||||
# Remove tag with hash
|
||||
tag_name_with_hash = '#' + tag_name
|
||||
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)]
|
||||
tag_name_with_hash = "#" + tag_name
|
||||
query_parts = [
|
||||
part
|
||||
for part in query_parts
|
||||
if str.lower(part) != str.lower(tag_name_with_hash)
|
||||
]
|
||||
# When using lax tag search, also remove tag without hash
|
||||
profile = context.request.user_profile
|
||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
||||
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name)]
|
||||
query_parts = [
|
||||
part for part in query_parts if str.lower(part) != str.lower(tag_name)
|
||||
]
|
||||
# Rebuild query string
|
||||
query_string = ' '.join(query_parts)
|
||||
params.__setitem__('q', query_string)
|
||||
query_string = " ".join(query_parts)
|
||||
params.__setitem__("q", query_string)
|
||||
|
||||
return params.urlencode()
|
||||
|
||||
@@ -71,38 +77,38 @@ def replace_query_param(context, **kwargs):
|
||||
return query.urlencode()
|
||||
|
||||
|
||||
@register.filter(name='hash_tag')
|
||||
@register.filter(name="hash_tag")
|
||||
def hash_tag(tag_name):
|
||||
return '#' + tag_name
|
||||
return "#" + tag_name
|
||||
|
||||
|
||||
@register.filter(name='first_char')
|
||||
@register.filter(name="first_char")
|
||||
def first_char(text):
|
||||
return text[0]
|
||||
|
||||
|
||||
@register.filter(name='remaining_chars')
|
||||
@register.filter(name="remaining_chars")
|
||||
def remaining_chars(text, index):
|
||||
return text[index:]
|
||||
|
||||
|
||||
@register.filter(name='humanize_absolute_date')
|
||||
@register.filter(name="humanize_absolute_date")
|
||||
def humanize_absolute_date(value):
|
||||
if value in (None, ''):
|
||||
return ''
|
||||
if value in (None, ""):
|
||||
return ""
|
||||
return utils.humanize_absolute_date(value)
|
||||
|
||||
|
||||
@register.filter(name='humanize_relative_date')
|
||||
@register.filter(name="humanize_relative_date")
|
||||
def humanize_relative_date(value):
|
||||
if value in (None, ''):
|
||||
return ''
|
||||
if value in (None, ""):
|
||||
return ""
|
||||
return utils.humanize_relative_date(value)
|
||||
|
||||
|
||||
@register.tag
|
||||
def htmlmin(parser, token):
|
||||
nodelist = parser.parse(('endhtmlmin',))
|
||||
nodelist = parser.parse(("endhtmlmin",))
|
||||
parser.delete_first_token()
|
||||
return HtmlMinNode(nodelist)
|
||||
|
||||
@@ -114,7 +120,7 @@ class HtmlMinNode(template.Node):
|
||||
def render(self, context):
|
||||
output = self.nodelist.render(context)
|
||||
|
||||
output = re.sub(r'\s+', ' ', output)
|
||||
output = re.sub(r"\s+", " ", output)
|
||||
|
||||
return output
|
||||
|
||||
@@ -123,11 +129,11 @@ class HtmlMinNode(template.Node):
|
||||
def render_markdown(context, markdown_text):
|
||||
# naive approach to reusing the renderer for a single request
|
||||
# works for bookmark list for now
|
||||
if not ('markdown_renderer' in context):
|
||||
renderer = markdown.Markdown(extensions=['fenced_code', 'nl2br'])
|
||||
context['markdown_renderer'] = renderer
|
||||
if not ("markdown_renderer" in context):
|
||||
renderer = markdown.Markdown(extensions=["fenced_code", "nl2br"])
|
||||
context["markdown_renderer"] = renderer
|
||||
else:
|
||||
renderer = context['markdown_renderer']
|
||||
renderer = context["markdown_renderer"]
|
||||
|
||||
as_html = renderer.convert(markdown_text)
|
||||
sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs)
|
||||
|
||||
@@ -18,26 +18,29 @@ class BookmarkFactoryMixin:
|
||||
|
||||
def get_or_create_test_user(self):
|
||||
if self.user is None:
|
||||
self.user = User.objects.create_user('testuser', 'test@example.com', 'password123')
|
||||
self.user = User.objects.create_user(
|
||||
"testuser", "test@example.com", "password123"
|
||||
)
|
||||
|
||||
return self.user
|
||||
|
||||
def setup_bookmark(self,
|
||||
is_archived: bool = False,
|
||||
unread: bool = False,
|
||||
shared: bool = False,
|
||||
tags=None,
|
||||
user: User = None,
|
||||
url: str = '',
|
||||
title: str = None,
|
||||
description: str = '',
|
||||
notes: str = '',
|
||||
website_title: str = '',
|
||||
website_description: str = '',
|
||||
web_archive_snapshot_url: str = '',
|
||||
favicon_file: str = '',
|
||||
added: datetime = None,
|
||||
):
|
||||
def setup_bookmark(
|
||||
self,
|
||||
is_archived: bool = False,
|
||||
unread: bool = False,
|
||||
shared: bool = False,
|
||||
tags=None,
|
||||
user: User = None,
|
||||
url: str = "",
|
||||
title: str = None,
|
||||
description: str = "",
|
||||
notes: str = "",
|
||||
website_title: str = "",
|
||||
website_description: str = "",
|
||||
web_archive_snapshot_url: str = "",
|
||||
favicon_file: str = "",
|
||||
added: datetime = None,
|
||||
):
|
||||
if title is None:
|
||||
title = get_random_string(length=32)
|
||||
if tags is None:
|
||||
@@ -46,7 +49,7 @@ class BookmarkFactoryMixin:
|
||||
user = self.get_or_create_test_user()
|
||||
if not url:
|
||||
unique_id = get_random_string(length=32)
|
||||
url = 'https://example.com/' + unique_id
|
||||
url = "https://example.com/" + unique_id
|
||||
if added is None:
|
||||
added = timezone.now()
|
||||
bookmark = Bookmark(
|
||||
@@ -71,49 +74,53 @@ class BookmarkFactoryMixin:
|
||||
bookmark.save()
|
||||
return bookmark
|
||||
|
||||
def setup_numbered_bookmarks(self,
|
||||
count: int,
|
||||
prefix: str = '',
|
||||
suffix: str = '',
|
||||
tag_prefix: str = '',
|
||||
archived: bool = False,
|
||||
unread: bool = False,
|
||||
shared: bool = False,
|
||||
with_tags: bool = False,
|
||||
user: User = None):
|
||||
def setup_numbered_bookmarks(
|
||||
self,
|
||||
count: int,
|
||||
prefix: str = "",
|
||||
suffix: str = "",
|
||||
tag_prefix: str = "",
|
||||
archived: bool = False,
|
||||
unread: bool = False,
|
||||
shared: bool = False,
|
||||
with_tags: bool = False,
|
||||
user: User = None,
|
||||
):
|
||||
user = user or self.get_or_create_test_user()
|
||||
bookmarks = []
|
||||
|
||||
if not prefix:
|
||||
if archived:
|
||||
prefix = 'Archived Bookmark'
|
||||
prefix = "Archived Bookmark"
|
||||
elif shared:
|
||||
prefix = 'Shared Bookmark'
|
||||
prefix = "Shared Bookmark"
|
||||
else:
|
||||
prefix = 'Bookmark'
|
||||
prefix = "Bookmark"
|
||||
|
||||
if not tag_prefix:
|
||||
if archived:
|
||||
tag_prefix = 'Archived Tag'
|
||||
tag_prefix = "Archived Tag"
|
||||
elif shared:
|
||||
tag_prefix = 'Shared Tag'
|
||||
tag_prefix = "Shared Tag"
|
||||
else:
|
||||
tag_prefix = 'Tag'
|
||||
tag_prefix = "Tag"
|
||||
|
||||
for i in range(1, count + 1):
|
||||
title = f'{prefix} {i}{suffix}'
|
||||
url = f'https://example.com/{prefix}/{i}'
|
||||
title = f"{prefix} {i}{suffix}"
|
||||
url = f"https://example.com/{prefix}/{i}"
|
||||
tags = []
|
||||
if with_tags:
|
||||
tag_name = f'{tag_prefix} {i}{suffix}'
|
||||
tag_name = f"{tag_prefix} {i}{suffix}"
|
||||
tags = [self.setup_tag(name=tag_name, user=user)]
|
||||
bookmark = self.setup_bookmark(url=url,
|
||||
title=title,
|
||||
is_archived=archived,
|
||||
unread=unread,
|
||||
shared=shared,
|
||||
tags=tags,
|
||||
user=user)
|
||||
bookmark = self.setup_bookmark(
|
||||
url=url,
|
||||
title=title,
|
||||
is_archived=archived,
|
||||
unread=unread,
|
||||
shared=shared,
|
||||
tags=tags,
|
||||
user=user,
|
||||
)
|
||||
bookmarks.append(bookmark)
|
||||
|
||||
return bookmarks
|
||||
@@ -121,7 +128,7 @@ class BookmarkFactoryMixin:
|
||||
def get_numbered_bookmark(self, title: str):
|
||||
return Bookmark.objects.get(title=title)
|
||||
|
||||
def setup_tag(self, user: User = None, name: str = ''):
|
||||
def setup_tag(self, user: User = None, name: str = ""):
|
||||
if user is None:
|
||||
user = self.get_or_create_test_user()
|
||||
if not name:
|
||||
@@ -130,10 +137,15 @@ class BookmarkFactoryMixin:
|
||||
tag.save()
|
||||
return tag
|
||||
|
||||
def setup_user(self, name: str = None, enable_sharing: bool = False, enable_public_sharing: bool = False):
|
||||
def setup_user(
|
||||
self,
|
||||
name: str = None,
|
||||
enable_sharing: bool = False,
|
||||
enable_public_sharing: bool = False,
|
||||
):
|
||||
if not name:
|
||||
name = get_random_string(length=32)
|
||||
user = User.objects.create_user(name, 'user@example.com', 'password123')
|
||||
user = User.objects.create_user(name, "user@example.com", "password123")
|
||||
user.profile.enable_sharing = enable_sharing
|
||||
user.profile.enable_public_sharing = enable_public_sharing
|
||||
user.profile.save()
|
||||
@@ -161,17 +173,17 @@ class LinkdingApiTestCase(APITestCase):
|
||||
return response
|
||||
|
||||
def post(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
||||
response = self.client.post(url, data, format='json')
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
return response
|
||||
|
||||
def put(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
||||
response = self.client.put(url, data, format='json')
|
||||
response = self.client.put(url, data, format="json")
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
return response
|
||||
|
||||
def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
||||
response = self.client.patch(url, data, format='json')
|
||||
response = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
return response
|
||||
|
||||
@@ -182,14 +194,16 @@ class LinkdingApiTestCase(APITestCase):
|
||||
|
||||
|
||||
class BookmarkHtmlTag:
|
||||
def __init__(self,
|
||||
href: str = '',
|
||||
title: str = '',
|
||||
description: str = '',
|
||||
add_date: str = '',
|
||||
tags: str = '',
|
||||
to_read: bool = False,
|
||||
private: bool = True):
|
||||
def __init__(
|
||||
self,
|
||||
href: str = "",
|
||||
title: str = "",
|
||||
description: str = "",
|
||||
add_date: str = "",
|
||||
tags: str = "",
|
||||
to_read: bool = False,
|
||||
private: bool = True,
|
||||
):
|
||||
self.href = href
|
||||
self.title = title
|
||||
self.description = description
|
||||
@@ -201,7 +215,7 @@ class BookmarkHtmlTag:
|
||||
|
||||
class ImportTestMixin:
|
||||
def render_tag(self, tag: BookmarkHtmlTag):
|
||||
return f'''
|
||||
return f"""
|
||||
<DT>
|
||||
<A {f'HREF="{tag.href}"' if tag.href else ''}
|
||||
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
|
||||
@@ -211,13 +225,13 @@ class ImportTestMixin:
|
||||
{tag.title if tag.title else ''}
|
||||
</A>
|
||||
{f'<DD>{tag.description}' if tag.description else ''}
|
||||
'''
|
||||
"""
|
||||
|
||||
def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ''):
|
||||
def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ""):
|
||||
if tags:
|
||||
rendered_tags = [self.render_tag(tag) for tag in tags]
|
||||
tags_html = '\n'.join(rendered_tags)
|
||||
return f'''
|
||||
tags_html = "\n".join(rendered_tags)
|
||||
return f"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
@@ -225,34 +239,34 @@ class ImportTestMixin:
|
||||
<DL><p>
|
||||
{tags_html}
|
||||
</DL><p>
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
_words = [
|
||||
'quasi',
|
||||
'consequatur',
|
||||
'necessitatibus',
|
||||
'debitis',
|
||||
'quod',
|
||||
'vero',
|
||||
'qui',
|
||||
'commodi',
|
||||
'quod',
|
||||
'odio',
|
||||
'aliquam',
|
||||
'veniam',
|
||||
'architecto',
|
||||
'consequatur',
|
||||
'autem',
|
||||
'qui',
|
||||
'iste',
|
||||
'asperiores',
|
||||
'soluta',
|
||||
'et',
|
||||
"quasi",
|
||||
"consequatur",
|
||||
"necessitatibus",
|
||||
"debitis",
|
||||
"quod",
|
||||
"vero",
|
||||
"qui",
|
||||
"commodi",
|
||||
"quod",
|
||||
"odio",
|
||||
"aliquam",
|
||||
"veniam",
|
||||
"architecto",
|
||||
"consequatur",
|
||||
"autem",
|
||||
"qui",
|
||||
"iste",
|
||||
"asperiores",
|
||||
"soluta",
|
||||
"et",
|
||||
]
|
||||
|
||||
|
||||
def random_sentence(num_words: int = None, including_word: str = ''):
|
||||
def random_sentence(num_words: int = None, including_word: str = ""):
|
||||
if num_words is None:
|
||||
num_words = random.randint(5, 10)
|
||||
selected_words = random.choices(_words, k=num_words)
|
||||
@@ -260,7 +274,7 @@ def random_sentence(num_words: int = None, including_word: str = ''):
|
||||
selected_words.append(including_word)
|
||||
random.shuffle(selected_words)
|
||||
|
||||
return ' '.join(selected_words)
|
||||
return " ".join(selected_words)
|
||||
|
||||
|
||||
def disable_logging(f):
|
||||
@@ -275,5 +289,5 @@ def disable_logging(f):
|
||||
|
||||
|
||||
def collapse_whitespace(text: str):
|
||||
text = text.replace('\n', '').replace('\r', '')
|
||||
return ' '.join(text.split())
|
||||
text = text.replace("\n", "").replace("\r", "")
|
||||
return " ".join(text.split())
|
||||
|
||||
@@ -6,21 +6,24 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def assertSharedBookmarksLinkCount(self, response, count):
|
||||
url = reverse('bookmarks:shared')
|
||||
self.assertContains(response, f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
|
||||
count=count)
|
||||
url = reverse("bookmarks:shared")
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
|
||||
count=count,
|
||||
)
|
||||
|
||||
def test_publicly_shared_bookmarks_link(self):
|
||||
# should not render link if no public shares exist
|
||||
user = self.setup_user(enable_sharing=True)
|
||||
self.setup_bookmark(user=user, shared=True)
|
||||
|
||||
response = self.client.get(reverse('login'))
|
||||
response = self.client.get(reverse("login"))
|
||||
self.assertSharedBookmarksLinkCount(response, 0)
|
||||
|
||||
# should render link if public shares exist
|
||||
user.profile.enable_public_sharing = True
|
||||
user.profile.save()
|
||||
|
||||
response = self.client.get(reverse('login'))
|
||||
response = self.client.get(reverse("login"))
|
||||
self.assertSharedBookmarksLinkCount(response, 1)
|
||||
|
||||
@@ -7,23 +7,35 @@ from django.test import TestCase
|
||||
|
||||
class AppOptionsTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.settings_module = importlib.import_module('siteroot.settings.base')
|
||||
self.settings_module = importlib.import_module("siteroot.settings.base")
|
||||
|
||||
def test_empty_csrf_trusted_origins(self):
|
||||
module = importlib.reload(self.settings_module)
|
||||
|
||||
self.assertFalse(hasattr(module, 'CSRF_TRUSTED_ORIGINS'))
|
||||
self.assertFalse(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
|
||||
|
||||
@mock.patch.dict(os.environ, {'LD_CSRF_TRUSTED_ORIGINS': 'https://linkding.example.com'})
|
||||
@mock.patch.dict(
|
||||
os.environ, {"LD_CSRF_TRUSTED_ORIGINS": "https://linkding.example.com"}
|
||||
)
|
||||
def test_single_csrf_trusted_origin(self):
|
||||
module = importlib.reload(self.settings_module)
|
||||
|
||||
self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS'))
|
||||
self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com'])
|
||||
self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
|
||||
self.assertCountEqual(
|
||||
module.CSRF_TRUSTED_ORIGINS, ["https://linkding.example.com"]
|
||||
)
|
||||
|
||||
@mock.patch.dict(os.environ, {'LD_CSRF_TRUSTED_ORIGINS': 'https://linkding.example.com,http://linkding.example.com'})
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"LD_CSRF_TRUSTED_ORIGINS": "https://linkding.example.com,http://linkding.example.com"
|
||||
},
|
||||
)
|
||||
def test_multiple_csrf_trusted_origin(self):
|
||||
module = importlib.reload(self.settings_module)
|
||||
|
||||
self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS'))
|
||||
self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com', 'http://linkding.example.com'])
|
||||
self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
|
||||
self.assertCountEqual(
|
||||
module.CSRF_TRUSTED_ORIGINS,
|
||||
["https://linkding.example.com", "http://linkding.example.com"],
|
||||
)
|
||||
|
||||
@@ -10,37 +10,49 @@ class AuthProxySupportTest(TestCase):
|
||||
# Reproducing configuration from the settings logic here
|
||||
# ideally this test would just override the respective options
|
||||
@modify_settings(
|
||||
MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'},
|
||||
AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'}
|
||||
MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"},
|
||||
AUTHENTICATION_BACKENDS={
|
||||
"prepend": "django.contrib.auth.backends.RemoteUserBackend"
|
||||
},
|
||||
)
|
||||
def test_auth_proxy_authentication(self):
|
||||
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123')
|
||||
user = User.objects.create_user(
|
||||
"auth_proxy_user", "user@example.com", "password123"
|
||||
)
|
||||
|
||||
headers = {'REMOTE_USER': user.username}
|
||||
response = self.client.get(reverse('bookmarks:index'), **headers)
|
||||
headers = {"REMOTE_USER": user.username}
|
||||
response = self.client.get(reverse("bookmarks:index"), **headers)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Reproducing configuration from the settings logic here
|
||||
# ideally this test would just override the respective options
|
||||
@modify_settings(
|
||||
MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'},
|
||||
AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'}
|
||||
MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"},
|
||||
AUTHENTICATION_BACKENDS={
|
||||
"prepend": "django.contrib.auth.backends.RemoteUserBackend"
|
||||
},
|
||||
)
|
||||
def test_auth_proxy_with_custom_header(self):
|
||||
with patch.object(CustomRemoteUserMiddleware, 'header', new_callable=PropertyMock) as mock:
|
||||
mock.return_value = 'Custom-User'
|
||||
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123')
|
||||
with patch.object(
|
||||
CustomRemoteUserMiddleware, "header", new_callable=PropertyMock
|
||||
) as mock:
|
||||
mock.return_value = "Custom-User"
|
||||
user = User.objects.create_user(
|
||||
"auth_proxy_user", "user@example.com", "password123"
|
||||
)
|
||||
|
||||
headers = {'Custom-User': user.username}
|
||||
response = self.client.get(reverse('bookmarks:index'), **headers)
|
||||
headers = {"Custom-User": user.username}
|
||||
response = self.client.get(reverse("bookmarks:index"), **headers)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_auth_proxy_is_disabled_by_default(self):
|
||||
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123')
|
||||
user = User.objects.create_user(
|
||||
"auth_proxy_user", "user@example.com", "password123"
|
||||
)
|
||||
|
||||
headers = {'REMOTE_USER': user.username}
|
||||
response = self.client.get(reverse('bookmarks:index'), **headers, follow=True)
|
||||
headers = {"REMOTE_USER": user.username}
|
||||
response = self.client.get(reverse("bookmarks:index"), **headers, follow=True)
|
||||
|
||||
self.assertRedirects(response, '/login/?next=%2Fbookmarks')
|
||||
self.assertRedirects(response, "/login/?next=%2Fbookmarks")
|
||||
|
||||
@@ -17,26 +17,37 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(len(bookmarks), Bookmark.objects.count())
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertEqual(model_to_dict(bookmark), model_to_dict(Bookmark.objects.get(id=bookmark.id)))
|
||||
self.assertEqual(
|
||||
model_to_dict(bookmark),
|
||||
model_to_dict(Bookmark.objects.get(id=bookmark.id)),
|
||||
)
|
||||
|
||||
def test_archive_should_archive_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'archive': [bookmark.id],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"archive": [bookmark.id],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertTrue(bookmark.is_archived)
|
||||
|
||||
def test_can_only_archive_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
|
||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
||||
'archive': [bookmark.id],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"archive": [bookmark.id],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
@@ -46,20 +57,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_unarchive_should_unarchive_bookmark(self):
|
||||
bookmark = self.setup_bookmark(is_archived=True)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'unarchive': [bookmark.id],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"unarchive": [bookmark.id],
|
||||
},
|
||||
)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertFalse(bookmark.is_archived)
|
||||
|
||||
def test_unarchive_can_only_archive_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark = self.setup_bookmark(is_archived=True, user=other_user)
|
||||
|
||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
||||
'unarchive': [bookmark.id],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"unarchive": [bookmark.id],
|
||||
},
|
||||
)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
@@ -68,28 +87,39 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_delete_should_delete_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'remove': [bookmark.id],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"remove": [bookmark.id],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 0)
|
||||
|
||||
def test_delete_can_only_delete_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
|
||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
||||
'remove': [bookmark.id],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"remove": [bookmark.id],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists())
|
||||
|
||||
def test_mark_as_read(self):
|
||||
bookmark = self.setup_bookmark(unread=True)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'mark_as_read': [bookmark.id],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"mark_as_read": [bookmark.id],
|
||||
},
|
||||
)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertFalse(bookmark.unread)
|
||||
@@ -97,21 +127,29 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_unshare_should_unshare_bookmark(self):
|
||||
bookmark = self.setup_bookmark(shared=True)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'unshare': [bookmark.id],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"unshare": [bookmark.id],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertFalse(bookmark.shared)
|
||||
|
||||
def test_can_only_unshare_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
|
||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
||||
'unshare': [bookmark.id],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"unshare": [bookmark.id],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
@@ -123,27 +161,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||
|
||||
def test_can_only_bulk_archive_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(user=other_user)
|
||||
bookmark2 = self.setup_bookmark(user=other_user)
|
||||
bookmark3 = self.setup_bookmark(user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -154,27 +208,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||
bookmark3 = self.setup_bookmark(is_archived=True)
|
||||
|
||||
self.client.post(reverse('bookmarks:archived.action'), {
|
||||
'bulk_action': ['bulk_unarchive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_unarchive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||
|
||||
def test_can_only_bulk_unarchive_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||
bookmark2 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||
bookmark3 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:archived.action'), {
|
||||
'bulk_action': ['bulk_unarchive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_unarchive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -185,27 +255,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
||||
|
||||
def test_can_only_bulk_delete_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(user=other_user)
|
||||
bookmark2 = self.setup_bookmark(user=other_user)
|
||||
bookmark3 = self.setup_bookmark(user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||
@@ -218,12 +304,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_tag'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_tag"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_tag_string": [f"{tag1.name} {tag2.name}"],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -234,19 +327,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||
|
||||
def test_can_only_bulk_tag_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(user=other_user)
|
||||
bookmark2 = self.setup_bookmark(user=other_user)
|
||||
bookmark3 = self.setup_bookmark(user=other_user)
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_tag'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_tag"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_tag_string": [f"{tag1.name} {tag2.name}"],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -263,12 +365,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_untag'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_untag"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_tag_string": [f"{tag1.name} {tag2.name}"],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -279,19 +388,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertCountEqual(bookmark3.tags.all(), [])
|
||||
|
||||
def test_can_only_bulk_untag_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
bookmark1 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_untag'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_untag"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_tag_string": [f"{tag1.name} {tag2.name}"],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -306,27 +424,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=True)
|
||||
bookmark3 = self.setup_bookmark(unread=True)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_read'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_read"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||
|
||||
def test_can_only_bulk_mark_as_read_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(unread=True, user=other_user)
|
||||
bookmark2 = self.setup_bookmark(unread=True, user=other_user)
|
||||
bookmark3 = self.setup_bookmark(unread=True, user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_read'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_read"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -337,27 +471,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=False)
|
||||
bookmark3 = self.setup_bookmark(unread=False)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_unread'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_unread"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||
|
||||
def test_can_only_bulk_mark_as_unread_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(unread=False, user=other_user)
|
||||
bookmark2 = self.setup_bookmark(unread=False, user=other_user)
|
||||
bookmark3 = self.setup_bookmark(unread=False, user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_unread'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_unread"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -368,27 +518,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(shared=False)
|
||||
bookmark3 = self.setup_bookmark(shared=False)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_share'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_share"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||
|
||||
def test_can_only_bulk_share_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(shared=False, user=other_user)
|
||||
bookmark2 = self.setup_bookmark(shared=False, user=other_user)
|
||||
bookmark3 = self.setup_bookmark(shared=False, user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_share'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_share"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -399,27 +565,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(shared=True)
|
||||
bookmark3 = self.setup_bookmark(shared=True)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_unshare'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_unshare"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||
|
||||
def test_can_only_bulk_unshare_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(shared=True, user=other_user)
|
||||
bookmark2 = self.setup_bookmark(shared=True, user=other_user)
|
||||
bookmark3 = self.setup_bookmark(shared=True, user=other_user)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_unshare'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_unshare"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -430,11 +612,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -443,11 +628,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_bulk_select_across_ignores_page(self):
|
||||
self.setup_numbered_bookmarks(100)
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action') + '?page=2', {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action") + "?page=2",
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(0, Bookmark.objects.count())
|
||||
|
||||
@@ -455,85 +643,108 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
# create a number of bookmarks with different states / visibility
|
||||
self.setup_numbered_bookmarks(3, with_tags=True)
|
||||
self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
||||
self.setup_numbered_bookmarks(3,
|
||||
shared=True,
|
||||
prefix="Joe's Bookmark",
|
||||
user=self.setup_user(enable_sharing=True))
|
||||
self.setup_numbered_bookmarks(
|
||||
3,
|
||||
shared=True,
|
||||
prefix="Joe's Bookmark",
|
||||
user=self.setup_user(enable_sharing=True),
|
||||
)
|
||||
|
||||
def test_index_action_bulk_select_across_only_affects_active_bookmarks(self):
|
||||
self.setup_bulk_edit_scope_test_data()
|
||||
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 1').first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 2').first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 3').first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 1").first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 2").first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 3").first())
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(6, Bookmark.objects.count())
|
||||
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 1').first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 2').first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 3').first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title="Bookmark 1").first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title="Bookmark 2").first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title="Bookmark 3").first())
|
||||
|
||||
def test_index_action_bulk_select_across_respects_query(self):
|
||||
self.setup_numbered_bookmarks(3, prefix='foo')
|
||||
self.setup_numbered_bookmarks(3, prefix='bar')
|
||||
self.setup_numbered_bookmarks(3, prefix="foo")
|
||||
self.setup_numbered_bookmarks(3, prefix="bar")
|
||||
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count())
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith="foo").count())
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action') + '?q=foo', {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action") + "?q=foo",
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count())
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count())
|
||||
|
||||
def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self):
|
||||
self.setup_bulk_edit_scope_test_data()
|
||||
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 1').first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 2').first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 3').first())
|
||||
self.assertIsNotNone(
|
||||
Bookmark.objects.filter(title="Archived Bookmark 1").first()
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
Bookmark.objects.filter(title="Archived Bookmark 2").first()
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
Bookmark.objects.filter(title="Archived Bookmark 3").first()
|
||||
)
|
||||
|
||||
self.client.post(reverse('bookmarks:archived.action'), {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(6, Bookmark.objects.count())
|
||||
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 1').first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 2').first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 3').first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 1").first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 2").first())
|
||||
self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 3").first())
|
||||
|
||||
def test_archived_action_bulk_select_across_respects_query(self):
|
||||
self.setup_numbered_bookmarks(3, prefix='foo', archived=True)
|
||||
self.setup_numbered_bookmarks(3, prefix='bar', archived=True)
|
||||
self.setup_numbered_bookmarks(3, prefix="foo", archived=True)
|
||||
self.setup_numbered_bookmarks(3, prefix="bar", archived=True)
|
||||
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count())
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith="foo").count())
|
||||
|
||||
self.client.post(reverse('bookmarks:archived.action') + '?q=foo', {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived.action") + "?q=foo",
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
|
||||
self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count())
|
||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count())
|
||||
|
||||
def test_shared_action_bulk_select_across_not_supported(self):
|
||||
self.setup_bulk_edit_scope_test_data()
|
||||
|
||||
response = self.client.post(reverse('bookmarks:shared.action'), {
|
||||
'bulk_action': ['bulk_delete'],
|
||||
'bulk_execute': [''],
|
||||
'bulk_select_across': ['on'],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:shared.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_delete"],
|
||||
"bulk_execute": [""],
|
||||
"bulk_select_across": ["on"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_handles_empty_bookmark_id(self):
|
||||
@@ -541,17 +752,23 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [],
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
||||
@@ -561,9 +778,16 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
self.client.post(reverse('bookmarks:index.action'), {
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index.action"),
|
||||
{
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
||||
|
||||
@@ -572,14 +796,25 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
url = reverse('bookmarks:index.action') + '?return_url=' + reverse('bookmarks:settings.index')
|
||||
response = self.client.post(url, {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
})
|
||||
url = (
|
||||
reverse("bookmarks:index.action")
|
||||
+ "?return_url="
|
||||
+ reverse("bookmarks:settings.index")
|
||||
)
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:settings.index'))
|
||||
self.assertRedirects(response, reverse("bookmarks:settings.index"))
|
||||
|
||||
def test_should_not_redirect_to_external_url(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
@@ -587,19 +822,27 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
def post_with(return_url, follow=None):
|
||||
url = reverse('bookmarks:index.action') + f'?return_url={return_url}'
|
||||
return self.client.post(url, {
|
||||
'bulk_action': ['bulk_archive'],
|
||||
'bulk_execute': [''],
|
||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||
}, follow=follow)
|
||||
url = reverse("bookmarks:index.action") + f"?return_url={return_url}"
|
||||
return self.client.post(
|
||||
url,
|
||||
{
|
||||
"bulk_action": ["bulk_archive"],
|
||||
"bulk_execute": [""],
|
||||
"bookmark_id": [
|
||||
str(bookmark1.id),
|
||||
str(bookmark2.id),
|
||||
str(bookmark3.id),
|
||||
],
|
||||
},
|
||||
follow=follow,
|
||||
)
|
||||
|
||||
response = post_with('https://example.com')
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
response = post_with('//example.com')
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
response = post_with('://example.com')
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
response = post_with("https://example.com")
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
response = post_with("//example.com")
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
response = post_with("://example.com")
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
|
||||
response = post_with('/foo//example.com', follow=True)
|
||||
response = post_with("/foo//example.com", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@@ -6,7 +6,11 @@ from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin, collapse_whitespace
|
||||
from bookmarks.tests.helpers import (
|
||||
BookmarkFactoryMixin,
|
||||
HtmlTestMixin,
|
||||
collapse_whitespace,
|
||||
)
|
||||
|
||||
|
||||
class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
@@ -15,33 +19,41 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
def assertVisibleBookmarks(
|
||||
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
|
||||
):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]')
|
||||
bookmark_list = soup.select_one(
|
||||
f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]'
|
||||
)
|
||||
self.assertIsNotNone(bookmark_list)
|
||||
|
||||
bookmark_items = bookmark_list.select('li[ld-bookmark-item]')
|
||||
bookmark_items = bookmark_list.select("li[ld-bookmark-item]")
|
||||
self.assertEqual(len(bookmark_items), len(bookmarks))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark_item = bookmark_list.select_one(
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
|
||||
)
|
||||
self.assertIsNotNone(bookmark_item)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
def assertInvisibleBookmarks(
|
||||
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
|
||||
):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark_item = soup.select_one(
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
|
||||
)
|
||||
self.assertIsNone(bookmark_item)
|
||||
|
||||
def assertVisibleTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
tag_cloud = soup.select_one('div.tag-cloud')
|
||||
tag_cloud = soup.select_one("div.tag-cloud")
|
||||
self.assertIsNotNone(tag_cloud)
|
||||
|
||||
tag_items = tag_cloud.select('a[data-is-tag-item]')
|
||||
tag_items = tag_cloud.select("a[data-is-tag-item]")
|
||||
self.assertEqual(len(tag_items), len(tags))
|
||||
|
||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
||||
@@ -51,7 +63,7 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
def assertInvisibleTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
tag_items = soup.select('a[data-is-tag-item]')
|
||||
tag_items = soup.select("a[data-is-tag-item]")
|
||||
|
||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
||||
|
||||
@@ -60,78 +72,103 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
def assertSelectedTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
selected_tags = soup.select_one('p.selected-tags')
|
||||
selected_tags = soup.select_one("p.selected-tags")
|
||||
self.assertIsNotNone(selected_tags)
|
||||
|
||||
tag_list = selected_tags.select('a')
|
||||
tag_list = selected_tags.select("a")
|
||||
self.assertEqual(len(tag_list), len(tags))
|
||||
|
||||
for tag in tags:
|
||||
self.assertTrue(tag.name in selected_tags.text, msg=f'Selected tags do not contain: {tag.name}')
|
||||
self.assertTrue(
|
||||
tag.name in selected_tags.text,
|
||||
msg=f"Selected tags do not contain: {tag.name}",
|
||||
)
|
||||
|
||||
def assertEditLink(self, response, url):
|
||||
html = response.content.decode()
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a href="{url}">Edit</a>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def assertBulkActionForm(self, response, url: str):
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
needle = collapse_whitespace(f'''
|
||||
needle = collapse_whitespace(
|
||||
f"""
|
||||
<form class="bookmark-actions"
|
||||
action="{url}"
|
||||
method="post" autocomplete="off">
|
||||
''')
|
||||
"""
|
||||
)
|
||||
self.assertIn(needle, html)
|
||||
|
||||
def test_should_list_archived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=False),
|
||||
self.setup_bookmark(is_archived=True, user=other_user),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_bookmarks_matching_query(self):
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo', archived=True)
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar', archived=True)
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, prefix="foo", archived=True
|
||||
)
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, prefix="bar", archived=True
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + '?q=foo')
|
||||
response = self.client.get(reverse("bookmarks:archived") + "?q=foo")
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
||||
unarchived_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=False, tag_prefix='unarchived')
|
||||
other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, user=other_user,
|
||||
tag_prefix='otheruser')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, archived=True
|
||||
)
|
||||
unarchived_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, archived=False, tag_prefix="unarchived"
|
||||
)
|
||||
other_user_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, archived=True, user=other_user, tag_prefix="otheruser"
|
||||
)
|
||||
|
||||
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
|
||||
invisible_tags = self.get_tags_from_bookmarks(unarchived_bookmarks + other_user_bookmarks)
|
||||
invisible_tags = self.get_tags_from_bookmarks(
|
||||
unarchived_bookmarks + other_user_bookmarks
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='foo',
|
||||
tag_prefix='foo')
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='bar',
|
||||
tag_prefix='bar')
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, archived=True, prefix="foo", tag_prefix="foo"
|
||||
)
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, archived=True, prefix="bar", tag_prefix="bar"
|
||||
)
|
||||
|
||||
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
|
||||
invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + '?q=foo')
|
||||
response = self.client.get(reverse("bookmarks:archived") + "?q=foo")
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
@@ -139,19 +176,31 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
|
||||
user_profile = self.user.profile
|
||||
user_profile.search_preferences = {
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
user_profile.save()
|
||||
|
||||
unread_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=True, with_tags=True, prefix='unread',
|
||||
tag_prefix='unread')
|
||||
read_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=False, with_tags=True, prefix='read',
|
||||
tag_prefix='read')
|
||||
unread_bookmarks = self.setup_numbered_bookmarks(
|
||||
3,
|
||||
archived=True,
|
||||
unread=True,
|
||||
with_tags=True,
|
||||
prefix="unread",
|
||||
tag_prefix="unread",
|
||||
)
|
||||
read_bookmarks = self.setup_numbered_bookmarks(
|
||||
3,
|
||||
archived=True,
|
||||
unread=False,
|
||||
with_tags=True,
|
||||
prefix="read",
|
||||
tag_prefix="read",
|
||||
)
|
||||
|
||||
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
|
||||
read_tags = self.get_tags_from_bookmarks(read_bookmarks)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
self.assertVisibleBookmarks(response, unread_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, read_bookmarks)
|
||||
self.assertVisibleTags(response, unread_tags)
|
||||
@@ -167,11 +216,15 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
]
|
||||
self.setup_bookmark(is_archived=True, tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + f'?q=%23{tags[0].name}+%23{tags[1].name}')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:archived") + f"?q=%23{tags[0].name}+%23{tags[1].name}"
|
||||
)
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(self):
|
||||
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(
|
||||
self,
|
||||
):
|
||||
tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
@@ -181,7 +234,10 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
]
|
||||
self.setup_bookmark(title=tags[0].name, tags=tags, is_archived=True)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:archived")
|
||||
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
|
||||
)
|
||||
|
||||
self.assertSelectedTags(response, [tags[1]])
|
||||
|
||||
@@ -198,16 +254,19 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
]
|
||||
self.setup_bookmark(tags=tags, is_archived=True)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:archived")
|
||||
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
|
||||
)
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
|
||||
|
||||
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||
user = self.get_or_create_test_user()
|
||||
@@ -216,71 +275,72 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
|
||||
|
||||
def test_edit_link_return_url_respects_search_options(self):
|
||||
bookmark = self.setup_bookmark(title='foo', is_archived=True)
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
base_url = reverse('bookmarks:archived')
|
||||
bookmark = self.setup_bookmark(title="foo", is_archived=True)
|
||||
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||
base_url = reverse("bookmarks:archived")
|
||||
|
||||
# without query params
|
||||
return_url = urllib.parse.quote(base_url)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query
|
||||
url_params = '?q=foo'
|
||||
url_params = "?q=foo"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query and sort and page
|
||||
url_params = '?q=foo&sort=title_asc&page=2'
|
||||
url_params = "?q=foo&sort=title_asc&page=2"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
def test_bulk_edit_respects_search_options(self):
|
||||
action_url = reverse('bookmarks:archived.action')
|
||||
base_url = reverse('bookmarks:archived')
|
||||
action_url = reverse("bookmarks:archived.action")
|
||||
base_url = reverse("bookmarks:archived")
|
||||
|
||||
# without params
|
||||
return_url = urllib.parse.quote_plus(base_url)
|
||||
url = f'{action_url}?return_url={return_url}'
|
||||
url = f"{action_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url)
|
||||
self.assertBulkActionForm(response, url)
|
||||
|
||||
# with query
|
||||
url_params = '?q=foo'
|
||||
url_params = "?q=foo"
|
||||
return_url = urllib.parse.quote_plus(base_url + url_params)
|
||||
url = f'{action_url}?q=foo&return_url={return_url}'
|
||||
url = f"{action_url}?q=foo&return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertBulkActionForm(response, url)
|
||||
|
||||
# with query and sort
|
||||
url_params = '?q=foo&sort=title_asc'
|
||||
url_params = "?q=foo&sort=title_asc"
|
||||
return_url = urllib.parse.quote_plus(base_url + url_params)
|
||||
url = f'{action_url}?q=foo&sort=title_asc&return_url={return_url}'
|
||||
url = f"{action_url}?q=foo&sort=title_asc&return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertBulkActionForm(response, url)
|
||||
|
||||
def test_allowed_bulk_actions(self):
|
||||
url = reverse('bookmarks:archived')
|
||||
url = reverse("bookmarks:archived")
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<select name="bulk_action" class="form-select select-sm">
|
||||
<option value="bulk_unarchive">Unarchive</option>
|
||||
<option value="bulk_delete">Delete</option>
|
||||
@@ -289,18 +349,21 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
<option value="bulk_read">Mark as read</option>
|
||||
<option value="bulk_unread">Mark as unread</option>
|
||||
</select>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
||||
user_profile = self.user.profile
|
||||
user_profile.enable_sharing = True
|
||||
user_profile.save()
|
||||
|
||||
url = reverse('bookmarks:archived')
|
||||
url = reverse("bookmarks:archived")
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<select name="bulk_action" class="form-select select-sm">
|
||||
<option value="bulk_unarchive">Unarchive</option>
|
||||
<option value="bulk_delete">Delete</option>
|
||||
@@ -311,114 +374,191 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
<option value="bulk_share">Share</option>
|
||||
<option value="bulk_unshare">Unshare</option>
|
||||
</select>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_apply_search_preferences(self):
|
||||
# no params
|
||||
response = self.client.post(reverse('bookmarks:archived'))
|
||||
response = self.client.post(reverse("bookmarks:archived"))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:archived'))
|
||||
self.assertEqual(response.url, reverse("bookmarks:archived"))
|
||||
|
||||
# some params
|
||||
response = self.client.post(reverse('bookmarks:archived'), {
|
||||
'q': 'foo',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"q": "foo",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&sort=title_asc')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:archived") + "?q=foo&sort=title_asc"
|
||||
)
|
||||
|
||||
# params with default value are removed
|
||||
response = self.client.post(reverse('bookmarks:archived'), {
|
||||
'q': 'foo',
|
||||
'user': '',
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"q": "foo",
|
||||
"user": "",
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&unread=yes')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:archived") + "?q=foo&unread=yes"
|
||||
)
|
||||
|
||||
# page is removed
|
||||
response = self.client.post(reverse('bookmarks:archived'), {
|
||||
'q': 'foo',
|
||||
'page': '2',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"q": "foo",
|
||||
"page": "2",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&sort=title_asc')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:archived") + "?q=foo&sort=title_asc"
|
||||
)
|
||||
|
||||
def test_save_search_preferences(self):
|
||||
user_profile = self.user.profile
|
||||
|
||||
# no params
|
||||
self.client.post(reverse('bookmarks:archived'), {
|
||||
'save': '',
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"save": "",
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# with param
|
||||
self.client.post(reverse('bookmarks:archived'), {
|
||||
'save': '',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"save": "",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# add a param
|
||||
self.client.post(reverse('bookmarks:archived'), {
|
||||
'save': '',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"save": "",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# remove a param
|
||||
self.client.post(reverse('bookmarks:archived'), {
|
||||
'save': '',
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"save": "",
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# ignores non-preferences
|
||||
self.client.post(reverse('bookmarks:archived'), {
|
||||
'save': '',
|
||||
'q': 'foo',
|
||||
'user': 'john',
|
||||
'page': '3',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:archived"),
|
||||
{
|
||||
"save": "",
|
||||
"q": "foo",
|
||||
"user": "john",
|
||||
"page": "3",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
def test_url_encode_bookmark_actions_url(self):
|
||||
url = reverse('bookmarks:archived') + '?q=%23foo'
|
||||
url = reverse("bookmarks:archived") + "?q=%23foo"
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
soup = self.make_soup(html)
|
||||
actions_form = soup.select('form.bookmark-actions')[0]
|
||||
actions_form = soup.select("form.bookmark-actions")[0]
|
||||
|
||||
self.assertEqual(actions_form.attrs['action'],
|
||||
'/bookmarks/archived/action?q=%23foo&return_url=%2Fbookmarks%2Farchived%3Fq%3D%2523foo')
|
||||
self.assertEqual(
|
||||
actions_form.attrs["action"],
|
||||
"/bookmarks/archived/action?q=%23foo&return_url=%2Fbookmarks%2Farchived%3Fq%3D%2523foo",
|
||||
)
|
||||
|
||||
def test_encode_search_params(self):
|
||||
bookmark = self.setup_bookmark(description="alert('xss')", is_archived=True)
|
||||
|
||||
url = reverse("bookmarks:archived") + "?q=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
self.assertContains(response, bookmark.url)
|
||||
|
||||
url = reverse("bookmarks:archived") + "?sort=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:archived") + "?unread=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:archived") + "?shared=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:archived") + "?user=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:archived") + "?page=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
@@ -8,7 +8,9 @@ from django.db.utils import DEFAULT_DB_ALIAS
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryMixin):
|
||||
class BookmarkArchivedViewPerformanceTestCase(
|
||||
TransactionTestCase, BookmarkFactoryMixin
|
||||
):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
@@ -26,8 +28,10 @@ class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFacto
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks)
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
self.assertContains(
|
||||
response, "<li ld-bookmark-item>", num_initial_bookmarks
|
||||
)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
@@ -38,5 +42,9 @@ class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFacto
|
||||
|
||||
# assert num queries doesn't increase
|
||||
with self.assertNumQueries(number_of_queries):
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks + num_additional_bookmarks)
|
||||
response = self.client.get(reverse("bookmarks:archived"))
|
||||
self.assertContains(
|
||||
response,
|
||||
"<li ld-bookmark-item>",
|
||||
num_initial_bookmarks + num_additional_bookmarks,
|
||||
)
|
||||
|
||||
@@ -16,153 +16,192 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
form_data = {
|
||||
'url': 'http://example.com/edited',
|
||||
'tag_string': 'editedtag1 editedtag2',
|
||||
'title': 'edited title',
|
||||
'description': 'edited description',
|
||||
'notes': 'edited notes',
|
||||
'unread': False,
|
||||
'shared': False,
|
||||
"url": "http://example.com/edited",
|
||||
"tag_string": "editedtag1 editedtag2",
|
||||
"title": "edited title",
|
||||
"description": "edited description",
|
||||
"notes": "edited notes",
|
||||
"unread": False,
|
||||
"shared": False,
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
||||
def test_should_edit_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data({'id': bookmark.id})
|
||||
form_data = self.create_form_data({"id": bookmark.id})
|
||||
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual(bookmark.owner, self.user)
|
||||
self.assertEqual(bookmark.url, form_data['url'])
|
||||
self.assertEqual(bookmark.title, form_data['title'])
|
||||
self.assertEqual(bookmark.description, form_data['description'])
|
||||
self.assertEqual(bookmark.notes, form_data['notes'])
|
||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
||||
self.assertEqual(bookmark.url, form_data["url"])
|
||||
self.assertEqual(bookmark.title, form_data["title"])
|
||||
self.assertEqual(bookmark.description, form_data["description"])
|
||||
self.assertEqual(bookmark.notes, form_data["notes"])
|
||||
self.assertEqual(bookmark.unread, form_data["unread"])
|
||||
self.assertEqual(bookmark.shared, form_data["shared"])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
tags = bookmark.tags.order_by('name').all()
|
||||
self.assertEqual(tags[0].name, 'editedtag1')
|
||||
self.assertEqual(tags[1].name, 'editedtag2')
|
||||
tags = bookmark.tags.order_by("name").all()
|
||||
self.assertEqual(tags[0].name, "editedtag1")
|
||||
self.assertEqual(tags[1].name, "editedtag2")
|
||||
|
||||
def test_should_edit_unread_state(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'unread': True})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
form_data = self.create_form_data({"id": bookmark.id, "unread": True})
|
||||
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertTrue(bookmark.unread)
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'unread': False})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
form_data = self.create_form_data({"id": bookmark.id, "unread": False})
|
||||
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertFalse(bookmark.unread)
|
||||
|
||||
def test_should_edit_shared_state(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'shared': True})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
form_data = self.create_form_data({"id": bookmark.id, "shared": True})
|
||||
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertTrue(bookmark.shared)
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'shared': False})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
form_data = self.create_form_data({"id": bookmark.id, "shared": False})
|
||||
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertFalse(bookmark.shared)
|
||||
|
||||
def test_should_prefill_bookmark_form_fields(self):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
bookmark = self.setup_bookmark(tags=[tag1, tag2], title='edited title', description='edited description',
|
||||
notes='edited notes', website_title='website title',
|
||||
website_description='website description')
|
||||
bookmark = self.setup_bookmark(
|
||||
tags=[tag1, tag2],
|
||||
title="edited title",
|
||||
description="edited description",
|
||||
notes="edited notes",
|
||||
website_title="website title",
|
||||
website_description="website description",
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="text" name="url" value="{bookmark.url}" placeholder=" "
|
||||
autofocus class="form-input" required id="id_url">
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
tag_string = build_tag_string(bookmark.tag_names, ' ')
|
||||
self.assertInHTML(f'''
|
||||
tag_string = build_tag_string(bookmark.tag_names, " ")
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input ld-tag-autocomplete type="text" name="tag_string" value="{tag_string}"
|
||||
autocomplete="off" autocapitalize="off" class="form-input" id="id_tag_string">
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="text" name="title" value="{bookmark.title}" maxlength="512" autocomplete="off"
|
||||
class="form-input" id="id_title">
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<textarea name="description" cols="40" rows="2" class="form-input" id="id_description">
|
||||
{bookmark.description}
|
||||
</textarea>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">
|
||||
{bookmark.notes}
|
||||
</textarea>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="hidden" name="website_title" id="id_website_title"
|
||||
value="{bookmark.website_title}">
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="hidden" name="website_description" id="id_website_description"
|
||||
value="{bookmark.website_description}">
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_redirect_to_return_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data()
|
||||
|
||||
url = reverse('bookmarks:edit', args=[bookmark.id]) + '?return_url=' + reverse('bookmarks:close')
|
||||
url = (
|
||||
reverse("bookmarks:edit", args=[bookmark.id])
|
||||
+ "?return_url="
|
||||
+ reverse("bookmarks:close")
|
||||
)
|
||||
response = self.client.post(url, form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||
self.assertRedirects(response, reverse("bookmarks:close"))
|
||||
|
||||
def test_should_redirect_to_bookmark_index_by_default(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data()
|
||||
|
||||
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:edit", args=[bookmark.id]), form_data
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
|
||||
def test_should_not_redirect_to_external_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
def post_with(return_url, follow=None):
|
||||
form_data = self.create_form_data()
|
||||
url = reverse('bookmarks:edit', args=[bookmark.id]) + f'?return_url={return_url}'
|
||||
url = (
|
||||
reverse("bookmarks:edit", args=[bookmark.id])
|
||||
+ f"?return_url={return_url}"
|
||||
)
|
||||
return self.client.post(url, form_data, follow=follow)
|
||||
|
||||
response = post_with('https://example.com')
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
response = post_with('//example.com')
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
response = post_with('://example.com')
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
response = post_with("https://example.com")
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
response = post_with("//example.com")
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
response = post_with("://example.com")
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
|
||||
response = post_with('/foo//example.com', follow=True)
|
||||
response = post_with("/foo//example.com", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_can_only_edit_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
form_data = self.create_form_data({'id': bookmark.id})
|
||||
form_data = self.create_form_data({"id": bookmark.id})
|
||||
|
||||
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:edit", args=[bookmark.id]), form_data
|
||||
)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertNotEqual(bookmark.url, form_data['url'])
|
||||
self.assertNotEqual(bookmark.url, form_data["url"])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_should_respect_share_profile_setting(self):
|
||||
@@ -170,38 +209,46 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.user.profile.enable_sharing = False
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=0)
|
||||
""",
|
||||
html,
|
||||
count=0,
|
||||
)
|
||||
|
||||
self.user.profile.enable_sharing = True
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=1)
|
||||
""",
|
||||
html,
|
||||
count=1,
|
||||
)
|
||||
|
||||
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
|
||||
self.assertContains(response, '<details class="notes">', count=1)
|
||||
|
||||
def test_should_show_notes_if_there_are_notes(self):
|
||||
bookmark = self.setup_bookmark(notes='test notes')
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
bookmark = self.setup_bookmark(notes="test notes")
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
|
||||
self.assertContains(response, '<details class="notes" open>', count=1)
|
||||
|
||||
@@ -6,7 +6,11 @@ from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin, collapse_whitespace
|
||||
from bookmarks.tests.helpers import (
|
||||
BookmarkFactoryMixin,
|
||||
HtmlTestMixin,
|
||||
collapse_whitespace,
|
||||
)
|
||||
|
||||
|
||||
class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
@@ -15,33 +19,41 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
def assertVisibleBookmarks(
|
||||
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
|
||||
):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]')
|
||||
bookmark_list = soup.select_one(
|
||||
f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]'
|
||||
)
|
||||
self.assertIsNotNone(bookmark_list)
|
||||
|
||||
bookmark_items = bookmark_list.select('li[ld-bookmark-item]')
|
||||
bookmark_items = bookmark_list.select("li[ld-bookmark-item]")
|
||||
self.assertEqual(len(bookmark_items), len(bookmarks))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark_item = bookmark_list.select_one(
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
|
||||
)
|
||||
self.assertIsNotNone(bookmark_item)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
def assertInvisibleBookmarks(
|
||||
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
|
||||
):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark_item = soup.select_one(
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
|
||||
)
|
||||
self.assertIsNone(bookmark_item)
|
||||
|
||||
def assertVisibleTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
tag_cloud = soup.select_one('div.tag-cloud')
|
||||
tag_cloud = soup.select_one("div.tag-cloud")
|
||||
self.assertIsNotNone(tag_cloud)
|
||||
|
||||
tag_items = tag_cloud.select('a[data-is-tag-item]')
|
||||
tag_items = tag_cloud.select("a[data-is-tag-item]")
|
||||
self.assertEqual(len(tag_items), len(tags))
|
||||
|
||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
||||
@@ -51,7 +63,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def assertInvisibleTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
tag_items = soup.select('a[data-is-tag-item]')
|
||||
tag_items = soup.select("a[data-is-tag-item]")
|
||||
|
||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
||||
|
||||
@@ -60,74 +72,96 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def assertSelectedTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
selected_tags = soup.select_one('p.selected-tags')
|
||||
selected_tags = soup.select_one("p.selected-tags")
|
||||
self.assertIsNotNone(selected_tags)
|
||||
|
||||
tag_list = selected_tags.select('a')
|
||||
tag_list = selected_tags.select("a")
|
||||
self.assertEqual(len(tag_list), len(tags))
|
||||
|
||||
for tag in tags:
|
||||
self.assertTrue(tag.name in selected_tags.text, msg=f'Selected tags do not contain: {tag.name}')
|
||||
self.assertTrue(
|
||||
tag.name in selected_tags.text,
|
||||
msg=f"Selected tags do not contain: {tag.name}",
|
||||
)
|
||||
|
||||
def assertEditLink(self, response, url):
|
||||
html = response.content.decode()
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a href="{url}">Edit</a>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def assertBulkActionForm(self, response, url: str):
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
needle = collapse_whitespace(f'''
|
||||
needle = collapse_whitespace(
|
||||
f"""
|
||||
<form class="bookmark-actions"
|
||||
action="{url}"
|
||||
method="post" autocomplete="off">
|
||||
''')
|
||||
"""
|
||||
)
|
||||
self.assertIn(needle, html)
|
||||
|
||||
def test_should_list_unarchived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3)
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(user=other_user),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_bookmarks_matching_query(self):
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo')
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar')
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix="foo")
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix="bar")
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + '?q=foo')
|
||||
response = self.client.get(reverse("bookmarks:index") + "?q=foo")
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True)
|
||||
archived_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, tag_prefix='archived')
|
||||
other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, user=other_user, tag_prefix='otheruser')
|
||||
archived_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, archived=True, tag_prefix="archived"
|
||||
)
|
||||
other_user_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, user=other_user, tag_prefix="otheruser"
|
||||
)
|
||||
|
||||
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
|
||||
invisible_tags = self.get_tags_from_bookmarks(archived_bookmarks + other_user_bookmarks)
|
||||
invisible_tags = self.get_tags_from_bookmarks(
|
||||
archived_bookmarks + other_user_bookmarks
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='foo', tag_prefix='foo')
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='bar', tag_prefix='bar')
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, prefix="foo", tag_prefix="foo"
|
||||
)
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, with_tags=True, prefix="bar", tag_prefix="bar"
|
||||
)
|
||||
|
||||
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
|
||||
invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + '?q=foo')
|
||||
response = self.client.get(reverse("bookmarks:index") + "?q=foo")
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
@@ -135,19 +169,21 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
|
||||
user_profile = self.user.profile
|
||||
user_profile.search_preferences = {
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
user_profile.save()
|
||||
|
||||
unread_bookmarks = self.setup_numbered_bookmarks(3, unread=True, with_tags=True, prefix='unread',
|
||||
tag_prefix='unread')
|
||||
read_bookmarks = self.setup_numbered_bookmarks(3, unread=False, with_tags=True, prefix='read',
|
||||
tag_prefix='read')
|
||||
unread_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, unread=True, with_tags=True, prefix="unread", tag_prefix="unread"
|
||||
)
|
||||
read_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, unread=False, with_tags=True, prefix="read", tag_prefix="read"
|
||||
)
|
||||
|
||||
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
|
||||
read_tags = self.get_tags_from_bookmarks(read_bookmarks)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
self.assertVisibleBookmarks(response, unread_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, read_bookmarks)
|
||||
self.assertVisibleTags(response, unread_tags)
|
||||
@@ -163,11 +199,16 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
]
|
||||
self.setup_bookmark(tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q=%23{tags[0].name}+%23{tags[1].name.upper()}')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:index")
|
||||
+ f"?q=%23{tags[0].name}+%23{tags[1].name.upper()}"
|
||||
)
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(self):
|
||||
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(
|
||||
self,
|
||||
):
|
||||
tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
@@ -177,7 +218,9 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
]
|
||||
self.setup_bookmark(title=tags[0].name, tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:index") + f"?q={tags[0].name}+%23{tags[1].name.upper()}"
|
||||
)
|
||||
|
||||
self.assertSelectedTags(response, [tags[1]])
|
||||
|
||||
@@ -194,16 +237,18 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
]
|
||||
self.setup_bookmark(tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:index") + f"?q={tags[0].name}+%23{tags[1].name.upper()}"
|
||||
)
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
|
||||
|
||||
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||
user = self.get_or_create_test_user()
|
||||
@@ -212,71 +257,72 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
|
||||
|
||||
def test_edit_link_return_url_respects_search_options(self):
|
||||
bookmark = self.setup_bookmark(title='foo')
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
base_url = reverse('bookmarks:index')
|
||||
bookmark = self.setup_bookmark(title="foo")
|
||||
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||
base_url = reverse("bookmarks:index")
|
||||
|
||||
# without query params
|
||||
return_url = urllib.parse.quote(base_url)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query
|
||||
url_params = '?q=foo'
|
||||
url_params = "?q=foo"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query and sort and page
|
||||
url_params = '?q=foo&sort=title_asc&page=2'
|
||||
url_params = "?q=foo&sort=title_asc&page=2"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
def test_bulk_edit_respects_search_options(self):
|
||||
action_url = reverse('bookmarks:index.action')
|
||||
base_url = reverse('bookmarks:index')
|
||||
action_url = reverse("bookmarks:index.action")
|
||||
base_url = reverse("bookmarks:index")
|
||||
|
||||
# without params
|
||||
return_url = urllib.parse.quote_plus(base_url)
|
||||
url = f'{action_url}?return_url={return_url}'
|
||||
url = f"{action_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url)
|
||||
self.assertBulkActionForm(response, url)
|
||||
|
||||
# with query
|
||||
url_params = '?q=foo'
|
||||
url_params = "?q=foo"
|
||||
return_url = urllib.parse.quote_plus(base_url + url_params)
|
||||
url = f'{action_url}?q=foo&return_url={return_url}'
|
||||
url = f"{action_url}?q=foo&return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertBulkActionForm(response, url)
|
||||
|
||||
# with query and sort
|
||||
url_params = '?q=foo&sort=title_asc'
|
||||
url_params = "?q=foo&sort=title_asc"
|
||||
return_url = urllib.parse.quote_plus(base_url + url_params)
|
||||
url = f'{action_url}?q=foo&sort=title_asc&return_url={return_url}'
|
||||
url = f"{action_url}?q=foo&sort=title_asc&return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertBulkActionForm(response, url)
|
||||
|
||||
def test_allowed_bulk_actions(self):
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<select name="bulk_action" class="form-select select-sm">
|
||||
<option value="bulk_archive">Archive</option>
|
||||
<option value="bulk_delete">Delete</option>
|
||||
@@ -285,18 +331,21 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
<option value="bulk_read">Mark as read</option>
|
||||
<option value="bulk_unread">Mark as unread</option>
|
||||
</select>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
||||
user_profile = self.user.profile
|
||||
user_profile.enable_sharing = True
|
||||
user_profile.save()
|
||||
|
||||
url = reverse('bookmarks:index')
|
||||
url = reverse("bookmarks:index")
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<select name="bulk_action" class="form-select select-sm">
|
||||
<option value="bulk_archive">Archive</option>
|
||||
<option value="bulk_delete">Delete</option>
|
||||
@@ -307,114 +356,189 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
<option value="bulk_share">Share</option>
|
||||
<option value="bulk_unshare">Unshare</option>
|
||||
</select>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_apply_search_preferences(self):
|
||||
# no params
|
||||
response = self.client.post(reverse('bookmarks:index'))
|
||||
response = self.client.post(reverse("bookmarks:index"))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:index'))
|
||||
self.assertEqual(response.url, reverse("bookmarks:index"))
|
||||
|
||||
# some params
|
||||
response = self.client.post(reverse('bookmarks:index'), {
|
||||
'q': 'foo',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"q": "foo",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&sort=title_asc')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:index") + "?q=foo&sort=title_asc"
|
||||
)
|
||||
|
||||
# params with default value are removed
|
||||
response = self.client.post(reverse('bookmarks:index'), {
|
||||
'q': 'foo',
|
||||
'user': '',
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"q": "foo",
|
||||
"user": "",
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&unread=yes')
|
||||
self.assertEqual(response.url, reverse("bookmarks:index") + "?q=foo&unread=yes")
|
||||
|
||||
# page is removed
|
||||
response = self.client.post(reverse('bookmarks:index'), {
|
||||
'q': 'foo',
|
||||
'page': '2',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"q": "foo",
|
||||
"page": "2",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&sort=title_asc')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:index") + "?q=foo&sort=title_asc"
|
||||
)
|
||||
|
||||
def test_save_search_preferences(self):
|
||||
user_profile = self.user.profile
|
||||
|
||||
# no params
|
||||
self.client.post(reverse('bookmarks:index'), {
|
||||
'save': '',
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"save": "",
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# with param
|
||||
self.client.post(reverse('bookmarks:index'), {
|
||||
'save': '',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"save": "",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# add a param
|
||||
self.client.post(reverse('bookmarks:index'), {
|
||||
'save': '',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"save": "",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# remove a param
|
||||
self.client.post(reverse('bookmarks:index'), {
|
||||
'save': '',
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"save": "",
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# ignores non-preferences
|
||||
self.client.post(reverse('bookmarks:index'), {
|
||||
'save': '',
|
||||
'q': 'foo',
|
||||
'user': 'john',
|
||||
'page': '3',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:index"),
|
||||
{
|
||||
"save": "",
|
||||
"q": "foo",
|
||||
"user": "john",
|
||||
"page": "3",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
def test_url_encode_bookmark_actions_url(self):
|
||||
url = reverse('bookmarks:index') + '?q=%23foo'
|
||||
url = reverse("bookmarks:index") + "?q=%23foo"
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
soup = self.make_soup(html)
|
||||
actions_form = soup.select('form.bookmark-actions')[0]
|
||||
actions_form = soup.select("form.bookmark-actions")[0]
|
||||
|
||||
self.assertEqual(actions_form.attrs['action'],
|
||||
'/bookmarks/action?q=%23foo&return_url=%2Fbookmarks%3Fq%3D%2523foo')
|
||||
self.assertEqual(
|
||||
actions_form.attrs["action"],
|
||||
"/bookmarks/action?q=%23foo&return_url=%2Fbookmarks%3Fq%3D%2523foo",
|
||||
)
|
||||
|
||||
def test_encode_search_params(self):
|
||||
bookmark = self.setup_bookmark(description="alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:index") + "?q=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
self.assertContains(response, bookmark.url)
|
||||
|
||||
url = reverse("bookmarks:index") + "?sort=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:index") + "?unread=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:index") + "?shared=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:index") + "?user=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:index") + "?page=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
@@ -26,8 +26,10 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks)
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
self.assertContains(
|
||||
response, "<li ld-bookmark-item>", num_initial_bookmarks
|
||||
)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
@@ -38,5 +40,9 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
|
||||
|
||||
# assert num queries doesn't increase
|
||||
with self.assertNumQueries(number_of_queries):
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks + num_additional_bookmarks)
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
self.assertContains(
|
||||
response,
|
||||
"<li ld-bookmark-item>",
|
||||
num_initial_bookmarks + num_additional_bookmarks,
|
||||
)
|
||||
|
||||
@@ -15,41 +15,41 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
form_data = {
|
||||
'url': 'http://example.com',
|
||||
'tag_string': 'tag1 tag2',
|
||||
'title': 'test title',
|
||||
'description': 'test description',
|
||||
'notes': 'test notes',
|
||||
'unread': False,
|
||||
'shared': False,
|
||||
'auto_close': '',
|
||||
"url": "http://example.com",
|
||||
"tag_string": "tag1 tag2",
|
||||
"title": "test title",
|
||||
"description": "test description",
|
||||
"notes": "test notes",
|
||||
"unread": False,
|
||||
"shared": False,
|
||||
"auto_close": "",
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
||||
def test_should_create_new_bookmark(self):
|
||||
form_data = self.create_form_data()
|
||||
|
||||
self.client.post(reverse('bookmarks:new'), form_data)
|
||||
self.client.post(reverse("bookmarks:new"), form_data)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
|
||||
bookmark = Bookmark.objects.first()
|
||||
self.assertEqual(bookmark.owner, self.user)
|
||||
self.assertEqual(bookmark.url, form_data['url'])
|
||||
self.assertEqual(bookmark.title, form_data['title'])
|
||||
self.assertEqual(bookmark.description, form_data['description'])
|
||||
self.assertEqual(bookmark.notes, form_data['notes'])
|
||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
||||
self.assertEqual(bookmark.url, form_data["url"])
|
||||
self.assertEqual(bookmark.title, form_data["title"])
|
||||
self.assertEqual(bookmark.description, form_data["description"])
|
||||
self.assertEqual(bookmark.notes, form_data["notes"])
|
||||
self.assertEqual(bookmark.unread, form_data["unread"])
|
||||
self.assertEqual(bookmark.shared, form_data["shared"])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
tags = bookmark.tags.order_by('name').all()
|
||||
self.assertEqual(tags[0].name, 'tag1')
|
||||
self.assertEqual(tags[1].name, 'tag2')
|
||||
tags = bookmark.tags.order_by("name").all()
|
||||
self.assertEqual(tags[0].name, "tag1")
|
||||
self.assertEqual(tags[1].name, "tag2")
|
||||
|
||||
def test_should_create_new_unread_bookmark(self):
|
||||
form_data = self.create_form_data({'unread': True})
|
||||
form_data = self.create_form_data({"unread": True})
|
||||
|
||||
self.client.post(reverse('bookmarks:new'), form_data)
|
||||
self.client.post(reverse("bookmarks:new"), form_data)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
|
||||
@@ -57,9 +57,9 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertTrue(bookmark.unread)
|
||||
|
||||
def test_should_create_new_shared_bookmark(self):
|
||||
form_data = self.create_form_data({'shared': True})
|
||||
form_data = self.create_form_data({"shared": True})
|
||||
|
||||
self.client.post(reverse('bookmarks:new'), form_data)
|
||||
self.client.post(reverse("bookmarks:new"), form_data)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
|
||||
@@ -67,125 +67,146 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertTrue(bookmark.shared)
|
||||
|
||||
def test_should_prefill_url_from_url_parameter(self):
|
||||
response = self.client.get(reverse('bookmarks:new') + '?url=http://example.com')
|
||||
response = self.client.get(reverse("bookmarks:new") + "?url=http://example.com")
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(
|
||||
'<input type="text" name="url" value="http://example.com" '
|
||||
'placeholder=" " autofocus class="form-input" required '
|
||||
'id="id_url">',
|
||||
html)
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_prefill_title_from_url_parameter(self):
|
||||
response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title')
|
||||
response = self.client.get(reverse("bookmarks:new") + "?title=Example%20Title")
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(
|
||||
'<input type="text" name="title" value="Example Title" '
|
||||
'class="form-input" maxlength="512" autocomplete="off" '
|
||||
'id="id_title">',
|
||||
html)
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_prefill_description_from_url_parameter(self):
|
||||
response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description')
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:new") + "?description=Example%20Site%20Description"
|
||||
)
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(
|
||||
'<textarea name="description" class="form-input" cols="40" '
|
||||
'rows="2" id="id_description">Example Site Description</textarea>',
|
||||
html)
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_enable_auto_close_when_specified_in_url_parameter(self):
|
||||
response = self.client.get(
|
||||
reverse('bookmarks:new') + '?auto_close')
|
||||
response = self.client.get(reverse("bookmarks:new") + "?auto_close")
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(
|
||||
'<input type="hidden" name="auto_close" value="true" '
|
||||
'id="id_auto_close">',
|
||||
html)
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(
|
||||
self):
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(self):
|
||||
response = self.client.get(reverse("bookmarks:new"))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('<input type="hidden" name="auto_close" id="id_auto_close">', html)
|
||||
self.assertInHTML(
|
||||
'<input type="hidden" name="auto_close" id="id_auto_close">', html
|
||||
)
|
||||
|
||||
def test_should_redirect_to_index_view(self):
|
||||
form_data = self.create_form_data()
|
||||
|
||||
response = self.client.post(reverse('bookmarks:new'), form_data)
|
||||
response = self.client.post(reverse("bookmarks:new"), form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
|
||||
def test_should_not_redirect_to_external_url(self):
|
||||
form_data = self.create_form_data()
|
||||
|
||||
response = self.client.post(reverse('bookmarks:new') + '?return_url=https://example.com', form_data)
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:new") + "?return_url=https://example.com", form_data
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||
|
||||
def test_auto_close_should_redirect_to_close_view(self):
|
||||
form_data = self.create_form_data({'auto_close': 'true'})
|
||||
form_data = self.create_form_data({"auto_close": "true"})
|
||||
|
||||
response = self.client.post(reverse('bookmarks:new'), form_data)
|
||||
response = self.client.post(reverse("bookmarks:new"), form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||
self.assertRedirects(response, reverse("bookmarks:close"))
|
||||
|
||||
def test_should_respect_share_profile_setting(self):
|
||||
self.user.profile.enable_sharing = False
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
response = self.client.get(reverse("bookmarks:new"))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=0)
|
||||
""",
|
||||
html,
|
||||
count=0,
|
||||
)
|
||||
|
||||
self.user.profile.enable_sharing = True
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
response = self.client.get(reverse("bookmarks:new"))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=1)
|
||||
""",
|
||||
html,
|
||||
count=1,
|
||||
)
|
||||
|
||||
def test_should_show_respective_share_hint(self):
|
||||
self.user.profile.enable_sharing = True
|
||||
self.user.profile.save()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
response = self.client.get(reverse("bookmarks:new"))
|
||||
html = response.content.decode()
|
||||
self.assertInHTML('''
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<div class="form-input-hint">
|
||||
Share this bookmark with other registered users.
|
||||
</div>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
self.user.profile.enable_public_sharing = True
|
||||
self.user.profile.save()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
response = self.client.get(reverse("bookmarks:new"))
|
||||
html = response.content.decode()
|
||||
self.assertInHTML('''
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<div class="form-input-hint">
|
||||
Share this bookmark with other registered users and anonymous users.
|
||||
</div>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
|
||||
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
|
||||
self.assertContains(response, '<details class="notes">', count=1)
|
||||
self.assertContains(response, '<details class="notes">', count=1)
|
||||
|
||||
@@ -9,40 +9,45 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
|
||||
# no params
|
||||
search = BookmarkSearch()
|
||||
form = BookmarkSearchForm(search)
|
||||
self.assertEqual(form['q'].initial, '')
|
||||
self.assertEqual(form['user'].initial, '')
|
||||
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_OFF)
|
||||
self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||
self.assertEqual(form["q"].initial, "")
|
||||
self.assertEqual(form["user"].initial, "")
|
||||
self.assertEqual(form["sort"].initial, BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertEqual(form["shared"].initial, BookmarkSearch.FILTER_SHARED_OFF)
|
||||
self.assertEqual(form["unread"].initial, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||
|
||||
# with params
|
||||
search = BookmarkSearch(q='search query',
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user='user123',
|
||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES)
|
||||
search = BookmarkSearch(
|
||||
q="search query",
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user="user123",
|
||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||
)
|
||||
form = BookmarkSearchForm(search)
|
||||
self.assertEqual(form['q'].initial, 'search query')
|
||||
self.assertEqual(form['user'].initial, 'user123')
|
||||
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC)
|
||||
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_SHARED)
|
||||
self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_YES)
|
||||
self.assertEqual(form["q"].initial, "search query")
|
||||
self.assertEqual(form["user"].initial, "user123")
|
||||
self.assertEqual(form["sort"].initial, BookmarkSearch.SORT_ADDED_ASC)
|
||||
self.assertEqual(form["shared"].initial, BookmarkSearch.FILTER_SHARED_SHARED)
|
||||
self.assertEqual(form["unread"].initial, BookmarkSearch.FILTER_UNREAD_YES)
|
||||
|
||||
def test_user_options(self):
|
||||
users = [
|
||||
self.setup_user('user1'),
|
||||
self.setup_user('user2'),
|
||||
self.setup_user('user3'),
|
||||
self.setup_user("user1"),
|
||||
self.setup_user("user2"),
|
||||
self.setup_user("user3"),
|
||||
]
|
||||
search = BookmarkSearch()
|
||||
form = BookmarkSearchForm(search, users=users)
|
||||
|
||||
self.assertCountEqual(form['user'].field.choices, [
|
||||
('', 'Everyone'),
|
||||
('user1', 'user1'),
|
||||
('user2', 'user2'),
|
||||
('user3', 'user3'),
|
||||
])
|
||||
self.assertCountEqual(
|
||||
form["user"].field.choices,
|
||||
[
|
||||
("", "Everyone"),
|
||||
("user1", "user1"),
|
||||
("user2", "user2"),
|
||||
("user3", "user3"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_hidden_fields(self):
|
||||
# no modified params
|
||||
@@ -51,24 +56,27 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(len(form.hidden_fields()), 0)
|
||||
|
||||
# some modified params
|
||||
search = BookmarkSearch(q='search query',
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC)
|
||||
search = BookmarkSearch(q="search query", sort=BookmarkSearch.SORT_ADDED_ASC)
|
||||
form = BookmarkSearchForm(search)
|
||||
self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort']])
|
||||
self.assertCountEqual(form.hidden_fields(), [form["q"], form["sort"]])
|
||||
|
||||
# all modified params
|
||||
search = BookmarkSearch(q='search query',
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user='user123',
|
||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES)
|
||||
search = BookmarkSearch(
|
||||
q="search query",
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user="user123",
|
||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||
)
|
||||
form = BookmarkSearchForm(search)
|
||||
self.assertCountEqual(form.hidden_fields(),
|
||||
[form['q'], form['sort'], form['user'], form['shared'], form['unread']])
|
||||
self.assertCountEqual(
|
||||
form.hidden_fields(),
|
||||
[form["q"], form["sort"], form["user"], form["shared"], form["unread"]],
|
||||
)
|
||||
|
||||
# some modified params are editable fields
|
||||
search = BookmarkSearch(q='search query',
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user='user123')
|
||||
form = BookmarkSearchForm(search, editable_fields=['q', 'user'])
|
||||
self.assertCountEqual(form.hidden_fields(), [form['sort']])
|
||||
search = BookmarkSearch(
|
||||
q="search query", sort=BookmarkSearch.SORT_ADDED_ASC, user="user123"
|
||||
)
|
||||
form = BookmarkSearchForm(search, editable_fields=["q", "user"])
|
||||
self.assertCountEqual(form.hidden_fields(), [form["sort"]])
|
||||
|
||||
@@ -10,57 +10,59 @@ class BookmarkSearchModelTest(TestCase):
|
||||
query_dict = QueryDict()
|
||||
|
||||
search = BookmarkSearch.from_request(query_dict)
|
||||
self.assertEqual(search.q, '')
|
||||
self.assertEqual(search.user, '')
|
||||
self.assertEqual(search.q, "")
|
||||
self.assertEqual(search.user, "")
|
||||
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||
|
||||
# some params
|
||||
query_dict = QueryDict('q=search query&user=user123')
|
||||
query_dict = QueryDict("q=search query&user=user123")
|
||||
|
||||
bookmark_search = BookmarkSearch.from_request(query_dict)
|
||||
self.assertEqual(bookmark_search.q, 'search query')
|
||||
self.assertEqual(bookmark_search.user, 'user123')
|
||||
self.assertEqual(bookmark_search.q, "search query")
|
||||
self.assertEqual(bookmark_search.user, "user123")
|
||||
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||
|
||||
# all params
|
||||
query_dict = QueryDict('q=search query&sort=title_asc&user=user123&shared=yes&unread=yes')
|
||||
query_dict = QueryDict(
|
||||
"q=search query&sort=title_asc&user=user123&shared=yes&unread=yes"
|
||||
)
|
||||
|
||||
search = BookmarkSearch.from_request(query_dict)
|
||||
self.assertEqual(search.q, 'search query')
|
||||
self.assertEqual(search.user, 'user123')
|
||||
self.assertEqual(search.q, "search query")
|
||||
self.assertEqual(search.user, "user123")
|
||||
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
|
||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
|
||||
|
||||
# respects preferences
|
||||
preferences = {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
query_dict = QueryDict('q=search query')
|
||||
query_dict = QueryDict("q=search query")
|
||||
|
||||
search = BookmarkSearch.from_request(query_dict, preferences)
|
||||
self.assertEqual(search.q, 'search query')
|
||||
self.assertEqual(search.user, '')
|
||||
self.assertEqual(search.q, "search query")
|
||||
self.assertEqual(search.user, "")
|
||||
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
|
||||
|
||||
# query overrides preferences
|
||||
preferences = {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
query_dict = QueryDict('sort=title_desc&shared=no&unread=off')
|
||||
query_dict = QueryDict("sort=title_desc&shared=no&unread=off")
|
||||
|
||||
search = BookmarkSearch.from_request(query_dict, preferences)
|
||||
self.assertEqual(search.q, '')
|
||||
self.assertEqual(search.user, '')
|
||||
self.assertEqual(search.q, "")
|
||||
self.assertEqual(search.user, "")
|
||||
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC)
|
||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED)
|
||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||
@@ -72,28 +74,36 @@ class BookmarkSearchModelTest(TestCase):
|
||||
self.assertEqual(len(modified_params), 0)
|
||||
|
||||
# params are default values
|
||||
bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='', shared='')
|
||||
bookmark_search = BookmarkSearch(
|
||||
q="", sort=BookmarkSearch.SORT_ADDED_DESC, user="", shared=""
|
||||
)
|
||||
modified_params = bookmark_search.modified_params
|
||||
self.assertEqual(len(modified_params), 0)
|
||||
|
||||
# some modified params
|
||||
bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC)
|
||||
bookmark_search = BookmarkSearch(
|
||||
q="search query", sort=BookmarkSearch.SORT_ADDED_ASC
|
||||
)
|
||||
modified_params = bookmark_search.modified_params
|
||||
self.assertCountEqual(modified_params, ['q', 'sort'])
|
||||
self.assertCountEqual(modified_params, ["q", "sort"])
|
||||
|
||||
# all modified params
|
||||
bookmark_search = BookmarkSearch(q='search query',
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user='user123',
|
||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES)
|
||||
bookmark_search = BookmarkSearch(
|
||||
q="search query",
|
||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||
user="user123",
|
||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||
)
|
||||
modified_params = bookmark_search.modified_params
|
||||
self.assertCountEqual(modified_params, ['q', 'sort', 'user', 'shared', 'unread'])
|
||||
self.assertCountEqual(
|
||||
modified_params, ["q", "sort", "user", "shared", "unread"]
|
||||
)
|
||||
|
||||
# preferences are not modified params
|
||||
preferences = {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
bookmark_search = BookmarkSearch(preferences=preferences)
|
||||
modified_params = bookmark_search.modified_params
|
||||
@@ -101,27 +111,31 @@ class BookmarkSearchModelTest(TestCase):
|
||||
|
||||
# param is not modified if it matches the preference
|
||||
preferences = {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_ASC,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||
preferences=preferences)
|
||||
bookmark_search = BookmarkSearch(
|
||||
sort=BookmarkSearch.SORT_TITLE_ASC,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||
preferences=preferences,
|
||||
)
|
||||
modified_params = bookmark_search.modified_params
|
||||
self.assertEqual(len(modified_params), 0)
|
||||
|
||||
# overriding preferences is a modified param
|
||||
preferences = {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_SHARED,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC,
|
||||
shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
preferences=preferences)
|
||||
bookmark_search = BookmarkSearch(
|
||||
sort=BookmarkSearch.SORT_TITLE_DESC,
|
||||
shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
|
||||
unread=BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
preferences=preferences,
|
||||
)
|
||||
modified_params = bookmark_search.modified_params
|
||||
self.assertCountEqual(modified_params, ['sort', 'shared', 'unread'])
|
||||
self.assertCountEqual(modified_params, ["sort", "shared", "unread"])
|
||||
|
||||
def test_has_modifications(self):
|
||||
# no params
|
||||
@@ -129,34 +143,49 @@ class BookmarkSearchModelTest(TestCase):
|
||||
self.assertFalse(bookmark_search.has_modifications)
|
||||
|
||||
# params are default values
|
||||
bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='', shared='')
|
||||
bookmark_search = BookmarkSearch(
|
||||
q="", sort=BookmarkSearch.SORT_ADDED_DESC, user="", shared=""
|
||||
)
|
||||
self.assertFalse(bookmark_search.has_modifications)
|
||||
|
||||
# modified params
|
||||
bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC)
|
||||
bookmark_search = BookmarkSearch(
|
||||
q="search query", sort=BookmarkSearch.SORT_ADDED_ASC
|
||||
)
|
||||
self.assertTrue(bookmark_search.has_modifications)
|
||||
|
||||
def test_preferences_dict(self):
|
||||
# no params
|
||||
bookmark_search = BookmarkSearch()
|
||||
self.assertEqual(bookmark_search.preferences_dict, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
bookmark_search.preferences_dict,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# with params
|
||||
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES)
|
||||
self.assertEqual(bookmark_search.preferences_dict, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
bookmark_search = BookmarkSearch(
|
||||
sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES
|
||||
)
|
||||
self.assertEqual(
|
||||
bookmark_search.preferences_dict,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# only returns preferences
|
||||
bookmark_search = BookmarkSearch(q='search query', user='user123')
|
||||
self.assertEqual(bookmark_search.preferences_dict, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
bookmark_search = BookmarkSearch(q="search query", user="user123")
|
||||
self.assertEqual(
|
||||
bookmark_search.preferences_dict,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,21 +8,25 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
|
||||
class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all(), mode: str = ''):
|
||||
def render_template(
|
||||
self, url: str, tags: QuerySet[Tag] = Tag.objects.all(), mode: str = ""
|
||||
):
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
request.user = self.get_or_create_test_user()
|
||||
request.user_profile = self.get_or_create_test_user().profile
|
||||
search = BookmarkSearch.from_request(request.GET)
|
||||
context = RequestContext(request, {
|
||||
'request': request,
|
||||
'search': search,
|
||||
'tags': tags,
|
||||
'mode': mode,
|
||||
})
|
||||
context = RequestContext(
|
||||
request,
|
||||
{
|
||||
"request": request,
|
||||
"search": search,
|
||||
"tags": tags,
|
||||
"mode": mode,
|
||||
},
|
||||
)
|
||||
template_to_render = Template(
|
||||
'{% load bookmarks %}'
|
||||
'{% bookmark_search search tags mode %}'
|
||||
"{% load bookmarks %}" "{% bookmark_search search tags mode %}"
|
||||
)
|
||||
return template_to_render.render(context)
|
||||
|
||||
@@ -31,7 +35,7 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.assertIsNotNone(input)
|
||||
|
||||
if value is not None:
|
||||
self.assertEqual(input['value'], value)
|
||||
self.assertEqual(input["value"], value)
|
||||
|
||||
def assertNoHiddenInput(self, form: BeautifulSoup, name: str):
|
||||
input = form.select_one(f'input[name="{name}"][type="hidden"]')
|
||||
@@ -42,19 +46,19 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.assertIsNotNone(input)
|
||||
|
||||
if value is not None:
|
||||
self.assertEqual(input['value'], value)
|
||||
self.assertEqual(input["value"], value)
|
||||
|
||||
def assertSelect(self, form: BeautifulSoup, name: str, value: str = None):
|
||||
select = form.select_one(f'select[name="{name}"]')
|
||||
self.assertIsNotNone(select)
|
||||
|
||||
if value is not None:
|
||||
options = select.select('option')
|
||||
options = select.select("option")
|
||||
for option in options:
|
||||
if option['value'] == value:
|
||||
self.assertTrue(option.has_attr('selected'))
|
||||
if option["value"] == value:
|
||||
self.assertTrue(option.has_attr("selected"))
|
||||
else:
|
||||
self.assertFalse(option.has_attr('selected'))
|
||||
self.assertFalse(option.has_attr("selected"))
|
||||
|
||||
def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str = None):
|
||||
radios = form.select(f'input[name="{name}"][type="radio"]')
|
||||
@@ -62,165 +66,182 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
if value is not None:
|
||||
for radio in radios:
|
||||
if radio['value'] == value:
|
||||
self.assertTrue(radio.has_attr('checked'))
|
||||
if radio["value"] == value:
|
||||
self.assertTrue(radio.has_attr("checked"))
|
||||
else:
|
||||
self.assertFalse(radio.has_attr('checked'))
|
||||
self.assertFalse(radio.has_attr("checked"))
|
||||
|
||||
def assertNoRadioGroup(self, form: BeautifulSoup, name: str):
|
||||
radios = form.select(f'input[name="{name}"][type="radio"]')
|
||||
self.assertTrue(len(radios) == 0)
|
||||
|
||||
def assertUnmodifiedLabel(self, html: str, text: str, id: str = ''):
|
||||
id_attr = f'for="{id}"' if id else ''
|
||||
tag = 'label' if id else 'div'
|
||||
def assertUnmodifiedLabel(self, html: str, text: str, id: str = ""):
|
||||
id_attr = f'for="{id}"' if id else ""
|
||||
tag = "label" if id else "div"
|
||||
needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>'
|
||||
|
||||
self.assertInHTML(needle, html)
|
||||
|
||||
def assertModifiedLabel(self, html: str, text: str, id: str = ''):
|
||||
id_attr = f'for="{id}"' if id else ''
|
||||
tag = 'label' if id else 'div'
|
||||
def assertModifiedLabel(self, html: str, text: str, id: str = ""):
|
||||
id_attr = f'for="{id}"' if id else ""
|
||||
tag = "label" if id else "div"
|
||||
needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>'
|
||||
|
||||
self.assertInHTML(needle, html)
|
||||
|
||||
def test_search_form_inputs(self):
|
||||
# Without params
|
||||
url = '/test'
|
||||
url = "/test"
|
||||
rendered_template = self.render_template(url)
|
||||
soup = self.make_soup(rendered_template)
|
||||
search_form = soup.select_one('form#search')
|
||||
search_form = soup.select_one("form#search")
|
||||
|
||||
self.assertSearchInput(search_form, 'q')
|
||||
self.assertNoHiddenInput(search_form, 'user')
|
||||
self.assertNoHiddenInput(search_form, 'sort')
|
||||
self.assertNoHiddenInput(search_form, 'shared')
|
||||
self.assertNoHiddenInput(search_form, 'unread')
|
||||
self.assertSearchInput(search_form, "q")
|
||||
self.assertNoHiddenInput(search_form, "user")
|
||||
self.assertNoHiddenInput(search_form, "sort")
|
||||
self.assertNoHiddenInput(search_form, "shared")
|
||||
self.assertNoHiddenInput(search_form, "unread")
|
||||
|
||||
# With params
|
||||
url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes'
|
||||
url = "/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes"
|
||||
rendered_template = self.render_template(url)
|
||||
soup = self.make_soup(rendered_template)
|
||||
search_form = soup.select_one('form#search')
|
||||
search_form = soup.select_one("form#search")
|
||||
|
||||
self.assertSearchInput(search_form, 'q', 'foo')
|
||||
self.assertHiddenInput(search_form, 'user', 'john')
|
||||
self.assertHiddenInput(search_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertHiddenInput(search_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED)
|
||||
self.assertHiddenInput(search_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES)
|
||||
self.assertSearchInput(search_form, "q", "foo")
|
||||
self.assertHiddenInput(search_form, "user", "john")
|
||||
self.assertHiddenInput(search_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertHiddenInput(
|
||||
search_form, "shared", BookmarkSearch.FILTER_SHARED_SHARED
|
||||
)
|
||||
self.assertHiddenInput(search_form, "unread", BookmarkSearch.FILTER_UNREAD_YES)
|
||||
|
||||
def test_preferences_form_inputs(self):
|
||||
# Without params
|
||||
url = '/test'
|
||||
url = "/test"
|
||||
rendered_template = self.render_template(url)
|
||||
soup = self.make_soup(rendered_template)
|
||||
preferences_form = soup.select_one('form#search_preferences')
|
||||
preferences_form = soup.select_one("form#search_preferences")
|
||||
|
||||
self.assertNoHiddenInput(preferences_form, 'q')
|
||||
self.assertNoHiddenInput(preferences_form, 'user')
|
||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
||||
self.assertNoHiddenInput(preferences_form, "q")
|
||||
self.assertNoHiddenInput(preferences_form, "user")
|
||||
self.assertNoHiddenInput(preferences_form, "sort")
|
||||
self.assertNoHiddenInput(preferences_form, "shared")
|
||||
self.assertNoHiddenInput(preferences_form, "unread")
|
||||
|
||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_OFF)
|
||||
self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_OFF)
|
||||
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertRadioGroup(
|
||||
preferences_form, "shared", BookmarkSearch.FILTER_SHARED_OFF
|
||||
)
|
||||
self.assertRadioGroup(
|
||||
preferences_form, "unread", BookmarkSearch.FILTER_UNREAD_OFF
|
||||
)
|
||||
|
||||
# With params
|
||||
url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes'
|
||||
url = "/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes"
|
||||
rendered_template = self.render_template(url)
|
||||
soup = self.make_soup(rendered_template)
|
||||
preferences_form = soup.select_one('form#search_preferences')
|
||||
preferences_form = soup.select_one("form#search_preferences")
|
||||
|
||||
self.assertHiddenInput(preferences_form, 'q', 'foo')
|
||||
self.assertHiddenInput(preferences_form, 'user', 'john')
|
||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
||||
self.assertHiddenInput(preferences_form, "q", "foo")
|
||||
self.assertHiddenInput(preferences_form, "user", "john")
|
||||
self.assertNoHiddenInput(preferences_form, "sort")
|
||||
self.assertNoHiddenInput(preferences_form, "shared")
|
||||
self.assertNoHiddenInput(preferences_form, "unread")
|
||||
|
||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED)
|
||||
self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES)
|
||||
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertRadioGroup(
|
||||
preferences_form, "shared", BookmarkSearch.FILTER_SHARED_SHARED
|
||||
)
|
||||
self.assertRadioGroup(
|
||||
preferences_form, "unread", BookmarkSearch.FILTER_UNREAD_YES
|
||||
)
|
||||
|
||||
def test_preferences_form_inputs_shared_mode(self):
|
||||
# Without params
|
||||
url = '/test'
|
||||
rendered_template = self.render_template(url, mode='shared')
|
||||
url = "/test"
|
||||
rendered_template = self.render_template(url, mode="shared")
|
||||
soup = self.make_soup(rendered_template)
|
||||
preferences_form = soup.select_one('form#search_preferences')
|
||||
preferences_form = soup.select_one("form#search_preferences")
|
||||
|
||||
self.assertNoHiddenInput(preferences_form, 'q')
|
||||
self.assertNoHiddenInput(preferences_form, 'user')
|
||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
||||
self.assertNoHiddenInput(preferences_form, "q")
|
||||
self.assertNoHiddenInput(preferences_form, "user")
|
||||
self.assertNoHiddenInput(preferences_form, "sort")
|
||||
self.assertNoHiddenInput(preferences_form, "shared")
|
||||
self.assertNoHiddenInput(preferences_form, "unread")
|
||||
|
||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertNoRadioGroup(preferences_form, 'shared')
|
||||
self.assertNoRadioGroup(preferences_form, 'unread')
|
||||
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_ADDED_DESC)
|
||||
self.assertNoRadioGroup(preferences_form, "shared")
|
||||
self.assertNoRadioGroup(preferences_form, "unread")
|
||||
|
||||
# With params
|
||||
url = '/test?q=foo&user=john&sort=title_asc'
|
||||
rendered_template = self.render_template(url, mode='shared')
|
||||
url = "/test?q=foo&user=john&sort=title_asc"
|
||||
rendered_template = self.render_template(url, mode="shared")
|
||||
soup = self.make_soup(rendered_template)
|
||||
preferences_form = soup.select_one('form#search_preferences')
|
||||
preferences_form = soup.select_one("form#search_preferences")
|
||||
|
||||
self.assertHiddenInput(preferences_form, 'q', 'foo')
|
||||
self.assertHiddenInput(preferences_form, 'user', 'john')
|
||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
||||
self.assertHiddenInput(preferences_form, "q", "foo")
|
||||
self.assertHiddenInput(preferences_form, "user", "john")
|
||||
self.assertNoHiddenInput(preferences_form, "sort")
|
||||
self.assertNoHiddenInput(preferences_form, "shared")
|
||||
self.assertNoHiddenInput(preferences_form, "unread")
|
||||
|
||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertNoRadioGroup(preferences_form, 'shared')
|
||||
self.assertNoRadioGroup(preferences_form, 'unread')
|
||||
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
|
||||
self.assertNoRadioGroup(preferences_form, "shared")
|
||||
self.assertNoRadioGroup(preferences_form, "unread")
|
||||
|
||||
def test_modified_indicator(self):
|
||||
# Without modifications
|
||||
url = '/test'
|
||||
url = "/test"
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertIn('<button type="button" class="btn dropdown-toggle">', rendered_template)
|
||||
self.assertIn(
|
||||
'<button type="button" class="btn dropdown-toggle">', rendered_template
|
||||
)
|
||||
|
||||
# With modifications
|
||||
url = '/test?sort=title_asc'
|
||||
url = "/test?sort=title_asc"
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertIn('<button type="button" class="btn dropdown-toggle badge">', rendered_template)
|
||||
self.assertIn(
|
||||
'<button type="button" class="btn dropdown-toggle badge">',
|
||||
rendered_template,
|
||||
)
|
||||
|
||||
# Ignores non-preferences modifications
|
||||
url = '/test?q=foo&user=john'
|
||||
url = "/test?q=foo&user=john"
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertIn('<button type="button" class="btn dropdown-toggle">', rendered_template)
|
||||
self.assertIn(
|
||||
'<button type="button" class="btn dropdown-toggle">', rendered_template
|
||||
)
|
||||
|
||||
def test_modified_labels(self):
|
||||
# Without modifications
|
||||
url = '/test'
|
||||
url = "/test"
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
|
||||
self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||
self.assertUnmodifiedLabel(rendered_template, "Shared filter")
|
||||
self.assertUnmodifiedLabel(rendered_template, "Unread filter")
|
||||
|
||||
# Modified sort
|
||||
url = '/test?sort=title_asc'
|
||||
url = "/test?sort=title_asc"
|
||||
rendered_template = self.render_template(url)
|
||||
self.assertModifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
|
||||
self.assertModifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||
self.assertUnmodifiedLabel(rendered_template, "Shared filter")
|
||||
self.assertUnmodifiedLabel(rendered_template, "Unread filter")
|
||||
|
||||
# Modified shared
|
||||
url = '/test?shared=yes'
|
||||
url = "/test?shared=yes"
|
||||
rendered_template = self.render_template(url)
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
||||
self.assertModifiedLabel(rendered_template, 'Shared filter')
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
|
||||
self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||
self.assertModifiedLabel(rendered_template, "Shared filter")
|
||||
self.assertUnmodifiedLabel(rendered_template, "Unread filter")
|
||||
|
||||
# Modified unread
|
||||
url = '/test?unread=yes'
|
||||
url = "/test?unread=yes"
|
||||
rendered_template = self.render_template(url)
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
||||
self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
|
||||
self.assertModifiedLabel(rendered_template, 'Unread filter')
|
||||
self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||
self.assertUnmodifiedLabel(rendered_template, "Shared filter")
|
||||
self.assertModifiedLabel(rendered_template, "Unread filter")
|
||||
|
||||
@@ -15,39 +15,50 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertBookmarkCount(self, html: str, bookmark: Bookmark, count: int, link_target: str = '_blank'):
|
||||
def assertBookmarkCount(
|
||||
self, html: str, bookmark: Bookmark, count: int, link_target: str = "_blank"
|
||||
):
|
||||
self.assertInHTML(
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
||||
html, count=count
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
def assertVisibleBookmarks(
|
||||
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
|
||||
):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]')
|
||||
bookmark_list = soup.select_one(
|
||||
f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]'
|
||||
)
|
||||
self.assertIsNotNone(bookmark_list)
|
||||
|
||||
bookmark_items = bookmark_list.select('li[ld-bookmark-item]')
|
||||
bookmark_items = bookmark_list.select("li[ld-bookmark-item]")
|
||||
self.assertEqual(len(bookmark_items), len(bookmarks))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark_item = bookmark_list.select_one(
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
|
||||
)
|
||||
self.assertIsNotNone(bookmark_item)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
def assertInvisibleBookmarks(
|
||||
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
|
||||
):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark_item = soup.select_one(
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
|
||||
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
|
||||
)
|
||||
self.assertIsNone(bookmark_item)
|
||||
|
||||
def assertVisibleTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
tag_cloud = soup.select_one('div.tag-cloud')
|
||||
tag_cloud = soup.select_one("div.tag-cloud")
|
||||
self.assertIsNotNone(tag_cloud)
|
||||
|
||||
tag_items = tag_cloud.select('a[data-is-tag-item]')
|
||||
tag_items = tag_cloud.select("a[data-is-tag-item]")
|
||||
self.assertEqual(len(tag_items), len(tags))
|
||||
|
||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
||||
@@ -57,7 +68,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def assertInvisibleTags(self, response, tags: List[Tag]):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
tag_items = soup.select('a[data-is-tag-item]')
|
||||
tag_items = soup.select("a[data-is-tag-item]")
|
||||
|
||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
||||
|
||||
@@ -67,26 +78,31 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
def assertVisibleUserOptions(self, response, users: List[User]):
|
||||
html = response.content.decode()
|
||||
|
||||
user_options = [
|
||||
'<option value="" selected="">Everyone</option>'
|
||||
]
|
||||
user_options = ['<option value="" selected="">Everyone</option>']
|
||||
for user in users:
|
||||
user_options.append(f'<option value="{user.username}">{user.username}</option>')
|
||||
user_select_html = f'''
|
||||
user_options.append(
|
||||
f'<option value="{user.username}">{user.username}</option>'
|
||||
)
|
||||
user_select_html = f"""
|
||||
<select name="user" class="form-select" required="" id="id_user">
|
||||
{''.join(user_options)}
|
||||
</select>
|
||||
'''
|
||||
"""
|
||||
|
||||
self.assertInHTML(user_select_html, html)
|
||||
|
||||
def assertEditLink(self, response, url):
|
||||
html = response.content.decode()
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a href="{url}">Edit</a>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
|
||||
def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(
|
||||
self,
|
||||
):
|
||||
self.authenticate()
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
@@ -105,7 +121,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=True, user=user4),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
@@ -124,7 +140,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=True, user=user3),
|
||||
]
|
||||
|
||||
url = reverse('bookmarks:shared') + '?user=' + user1.username
|
||||
url = reverse("bookmarks:shared") + "?user=" + user1.username
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
@@ -134,10 +150,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.authenticate()
|
||||
user = self.setup_user(enable_sharing=True)
|
||||
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user, prefix='foo')
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, shared=True, user=user, prefix="foo"
|
||||
)
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared') + '?q=foo')
|
||||
response = self.client.get(reverse("bookmarks:shared") + "?q=foo")
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
@@ -146,15 +164,21 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user1, prefix='user1')
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user2, prefix='user2')
|
||||
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, shared=True, user=user1, prefix="user1"
|
||||
)
|
||||
invisible_bookmarks = self.setup_numbered_bookmarks(
|
||||
3, shared=True, user=user2, prefix="user2"
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
|
||||
def test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled(
|
||||
self,
|
||||
):
|
||||
self.authenticate()
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
@@ -181,7 +205,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=False, user=user3, tags=[invisible_tags[2]])
|
||||
self.setup_bookmark(shared=True, user=user4, tags=[invisible_tags[3]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
@@ -203,7 +227,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[1]])
|
||||
|
||||
url = reverse('bookmarks:shared') + '?user=' + user1.username
|
||||
url = reverse("bookmarks:shared") + "?user=" + user1.username
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
@@ -225,15 +249,21 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_tag(user=user3),
|
||||
]
|
||||
|
||||
self.setup_bookmark(shared=True, user=user1, title='searchvalue', tags=[visible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, title='searchvalue', tags=[visible_tags[1]])
|
||||
self.setup_bookmark(shared=True, user=user3, title='searchvalue', tags=[visible_tags[2]])
|
||||
self.setup_bookmark(
|
||||
shared=True, user=user1, title="searchvalue", tags=[visible_tags[0]]
|
||||
)
|
||||
self.setup_bookmark(
|
||||
shared=True, user=user2, title="searchvalue", tags=[visible_tags[1]]
|
||||
)
|
||||
self.setup_bookmark(
|
||||
shared=True, user=user3, title="searchvalue", tags=[visible_tags[2]]
|
||||
)
|
||||
|
||||
self.setup_bookmark(shared=True, user=user1, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
|
||||
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[2]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared') + '?q=searchvalue')
|
||||
response = self.client.get(reverse("bookmarks:shared") + "?q=searchvalue")
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
@@ -257,7 +287,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
@@ -265,8 +295,8 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
|
||||
self.authenticate()
|
||||
expected_visible_users = [
|
||||
self.setup_user(name='user_a', enable_sharing=True),
|
||||
self.setup_user(name='user_b', enable_sharing=True),
|
||||
self.setup_user(name="user_a", enable_sharing=True),
|
||||
self.setup_user(name="user_b", enable_sharing=True),
|
||||
]
|
||||
self.setup_bookmark(shared=True, user=expected_visible_users[0])
|
||||
self.setup_bookmark(shared=True, user=expected_visible_users[1])
|
||||
@@ -274,14 +304,18 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=False, user=self.setup_user(enable_sharing=True))
|
||||
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=False))
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
self.assertVisibleUserOptions(response, expected_visible_users)
|
||||
|
||||
def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
|
||||
# users with public sharing enabled
|
||||
expected_visible_users = [
|
||||
self.setup_user(name='user_a', enable_sharing=True, enable_public_sharing=True),
|
||||
self.setup_user(name='user_b', enable_sharing=True, enable_public_sharing=True),
|
||||
self.setup_user(
|
||||
name="user_a", enable_sharing=True, enable_public_sharing=True
|
||||
),
|
||||
self.setup_user(
|
||||
name="user_b", enable_sharing=True, enable_public_sharing=True
|
||||
),
|
||||
]
|
||||
self.setup_bookmark(shared=True, user=expected_visible_users[0])
|
||||
self.setup_bookmark(shared=True, user=expected_visible_users[1])
|
||||
@@ -290,7 +324,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=True))
|
||||
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=True))
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
self.assertVisibleUserOptions(response, expected_visible_users)
|
||||
|
||||
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
|
||||
@@ -299,19 +333,33 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
user_profile = self.get_or_create_test_user().profile
|
||||
user_profile.search_preferences = {
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
}
|
||||
user_profile.save()
|
||||
|
||||
unread_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=True, with_tags=True, prefix='unread',
|
||||
tag_prefix='unread', user=other_user)
|
||||
read_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=False, with_tags=True, prefix='read',
|
||||
tag_prefix='read', user=other_user)
|
||||
unread_bookmarks = self.setup_numbered_bookmarks(
|
||||
3,
|
||||
shared=True,
|
||||
unread=True,
|
||||
with_tags=True,
|
||||
prefix="unread",
|
||||
tag_prefix="unread",
|
||||
user=other_user,
|
||||
)
|
||||
read_bookmarks = self.setup_numbered_bookmarks(
|
||||
3,
|
||||
shared=True,
|
||||
unread=False,
|
||||
with_tags=True,
|
||||
prefix="read",
|
||||
tag_prefix="read",
|
||||
user=other_user,
|
||||
)
|
||||
|
||||
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
|
||||
read_tags = self.get_tags_from_bookmarks(read_bookmarks)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
self.assertVisibleBookmarks(response, unread_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, read_bookmarks)
|
||||
self.assertVisibleTags(response, unread_tags)
|
||||
@@ -325,12 +373,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True)
|
||||
self.setup_bookmark(shared=True),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
|
||||
|
||||
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||
self.authenticate()
|
||||
@@ -342,12 +390,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True)
|
||||
self.setup_bookmark(shared=True),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
|
||||
|
||||
def test_edit_link_return_url_respects_search_options(self):
|
||||
self.authenticate()
|
||||
@@ -355,148 +403,227 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
user.profile.enable_sharing = True
|
||||
user.profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(title='foo', shared=True, user=user)
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
base_url = reverse('bookmarks:shared')
|
||||
bookmark = self.setup_bookmark(title="foo", shared=True, user=user)
|
||||
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||
base_url = reverse("bookmarks:shared")
|
||||
|
||||
# without query params
|
||||
return_url = urllib.parse.quote(base_url)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query
|
||||
url_params = '?q=foo'
|
||||
url_params = "?q=foo"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query and user
|
||||
url_params = f'?q=foo&user={user.username}'
|
||||
url_params = f"?q=foo&user={user.username}"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
# with query and sort and page
|
||||
url_params = '?q=foo&sort=title_asc&page=2'
|
||||
url_params = "?q=foo&sort=title_asc&page=2"
|
||||
return_url = urllib.parse.quote(base_url + url_params)
|
||||
url = f'{edit_url}?return_url={return_url}'
|
||||
url = f"{edit_url}?return_url={return_url}"
|
||||
|
||||
response = self.client.get(base_url + url_params)
|
||||
self.assertEditLink(response, url)
|
||||
|
||||
def test_apply_search_preferences(self):
|
||||
# no params
|
||||
response = self.client.post(reverse('bookmarks:shared'))
|
||||
response = self.client.post(reverse("bookmarks:shared"))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:shared'))
|
||||
self.assertEqual(response.url, reverse("bookmarks:shared"))
|
||||
|
||||
# some params
|
||||
response = self.client.post(reverse('bookmarks:shared'), {
|
||||
'q': 'foo',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"q": "foo",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&sort=title_asc')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:shared") + "?q=foo&sort=title_asc"
|
||||
)
|
||||
|
||||
# params with default value are removed
|
||||
response = self.client.post(reverse('bookmarks:shared'), {
|
||||
'q': 'foo',
|
||||
'user': '',
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"q": "foo",
|
||||
"user": "",
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&unread=yes')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:shared") + "?q=foo&unread=yes"
|
||||
)
|
||||
|
||||
# page is removed
|
||||
response = self.client.post(reverse('bookmarks:shared'), {
|
||||
'q': 'foo',
|
||||
'page': '2',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"q": "foo",
|
||||
"page": "2",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&sort=title_asc')
|
||||
self.assertEqual(
|
||||
response.url, reverse("bookmarks:shared") + "?q=foo&sort=title_asc"
|
||||
)
|
||||
|
||||
def test_save_search_preferences(self):
|
||||
self.authenticate()
|
||||
user_profile = self.user.profile
|
||||
|
||||
# no params
|
||||
self.client.post(reverse('bookmarks:shared'), {
|
||||
'save': '',
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"save": "",
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# with param
|
||||
self.client.post(reverse('bookmarks:shared'), {
|
||||
'save': '',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"save": "",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
# add a param
|
||||
self.client.post(reverse('bookmarks:shared'), {
|
||||
'save': '',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"save": "",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# remove a param
|
||||
self.client.post(reverse('bookmarks:shared'), {
|
||||
'save': '',
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"save": "",
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||
},
|
||||
)
|
||||
|
||||
# ignores non-preferences
|
||||
self.client.post(reverse('bookmarks:shared'), {
|
||||
'save': '',
|
||||
'q': 'foo',
|
||||
'user': 'john',
|
||||
'page': '3',
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
})
|
||||
self.client.post(
|
||||
reverse("bookmarks:shared"),
|
||||
{
|
||||
"save": "",
|
||||
"q": "foo",
|
||||
"user": "john",
|
||||
"page": "3",
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
},
|
||||
)
|
||||
user_profile.refresh_from_db()
|
||||
self.assertEqual(user_profile.search_preferences, {
|
||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
})
|
||||
self.assertEqual(
|
||||
user_profile.search_preferences,
|
||||
{
|
||||
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||
},
|
||||
)
|
||||
|
||||
def test_url_encode_bookmark_actions_url(self):
|
||||
url = reverse('bookmarks:shared') + '?q=%23foo'
|
||||
url = reverse("bookmarks:shared") + "?q=%23foo"
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
soup = self.make_soup(html)
|
||||
actions_form = soup.select('form.bookmark-actions')[0]
|
||||
actions_form = soup.select("form.bookmark-actions")[0]
|
||||
|
||||
self.assertEqual(actions_form.attrs['action'],
|
||||
'/bookmarks/shared/action?q=%23foo&return_url=%2Fbookmarks%2Fshared%3Fq%3D%2523foo')
|
||||
self.assertEqual(
|
||||
actions_form.attrs["action"],
|
||||
"/bookmarks/shared/action?q=%23foo&return_url=%2Fbookmarks%2Fshared%3Fq%3D%2523foo",
|
||||
)
|
||||
|
||||
def test_encode_search_params(self):
|
||||
self.authenticate()
|
||||
user = self.get_or_create_test_user()
|
||||
user.profile.enable_sharing = True
|
||||
user.profile.save()
|
||||
bookmark = self.setup_bookmark(description="alert('xss')", shared=True)
|
||||
|
||||
url = reverse("bookmarks:shared") + "?q=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
self.assertContains(response, bookmark.url)
|
||||
|
||||
url = reverse("bookmarks:shared") + "?sort=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:shared") + "?unread=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:shared") + "?shared=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:shared") + "?user=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
url = reverse("bookmarks:shared") + "?page=alert(%27xss%27)"
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, "alert('xss')")
|
||||
|
||||
@@ -27,8 +27,10 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
self.assertContains(response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks)
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
self.assertContains(
|
||||
response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks
|
||||
)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
@@ -40,5 +42,9 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
|
||||
|
||||
# assert num queries doesn't increase
|
||||
with self.assertNumQueries(number_of_queries):
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
self.assertContains(response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks + num_additional_bookmarks)
|
||||
response = self.client.get(reverse("bookmarks:shared"))
|
||||
self.assertContains(
|
||||
response,
|
||||
'<li ld-bookmark-item class="shared">',
|
||||
num_initial_bookmarks + num_additional_bookmarks,
|
||||
)
|
||||
|
||||
@@ -9,36 +9,38 @@ from bookmarks.models import BookmarkForm, Bookmark
|
||||
User = get_user_model()
|
||||
|
||||
ENABLED_URL_VALIDATION_TEST_CASES = [
|
||||
('thisisnotavalidurl', False),
|
||||
('http://domain', False),
|
||||
('unknownscheme://domain.com', False),
|
||||
('http://domain.com', True),
|
||||
('http://www.domain.com', True),
|
||||
('https://domain.com', True),
|
||||
('https://www.domain.com', True),
|
||||
("thisisnotavalidurl", False),
|
||||
("http://domain", False),
|
||||
("unknownscheme://domain.com", False),
|
||||
("http://domain.com", True),
|
||||
("http://www.domain.com", True),
|
||||
("https://domain.com", True),
|
||||
("https://www.domain.com", True),
|
||||
]
|
||||
|
||||
DISABLED_URL_VALIDATION_TEST_CASES = [
|
||||
('thisisnotavalidurl', True),
|
||||
('http://domain', True),
|
||||
('unknownscheme://domain.com', True),
|
||||
('http://domain.com', True),
|
||||
('http://www.domain.com', True),
|
||||
('https://domain.com', True),
|
||||
('https://www.domain.com', True),
|
||||
("thisisnotavalidurl", True),
|
||||
("http://domain", True),
|
||||
("unknownscheme://domain.com", True),
|
||||
("http://domain.com", True),
|
||||
("http://www.domain.com", True),
|
||||
("https://domain.com", True),
|
||||
("https://www.domain.com", True),
|
||||
]
|
||||
|
||||
|
||||
class BookmarkValidationTestCase(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_user('testuser', 'test@example.com', 'password123')
|
||||
self.user = User.objects.create_user(
|
||||
"testuser", "test@example.com", "password123"
|
||||
)
|
||||
|
||||
def test_bookmark_model_should_not_allow_missing_url(self):
|
||||
bookmark = Bookmark(
|
||||
date_added=datetime.datetime.now(),
|
||||
date_modified=datetime.datetime.now(),
|
||||
owner=self.user
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
@@ -46,10 +48,10 @@ class BookmarkValidationTestCase(TestCase):
|
||||
|
||||
def test_bookmark_model_should_not_allow_empty_url(self):
|
||||
bookmark = Bookmark(
|
||||
url='',
|
||||
url="",
|
||||
date_added=datetime.datetime.now(),
|
||||
date_modified=datetime.datetime.now(),
|
||||
owner=self.user
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
@@ -64,15 +66,15 @@ class BookmarkValidationTestCase(TestCase):
|
||||
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
|
||||
|
||||
def test_bookmark_form_should_validate_required_fields(self):
|
||||
form = BookmarkForm(data={'url': ''})
|
||||
form = BookmarkForm(data={"url": ""})
|
||||
|
||||
self.assertEqual(len(form.errors), 1)
|
||||
self.assertIn('required', str(form.errors))
|
||||
self.assertIn("required", str(form.errors))
|
||||
|
||||
form = BookmarkForm(data={'url': None})
|
||||
form = BookmarkForm(data={"url": None})
|
||||
|
||||
self.assertEqual(len(form.errors), 1)
|
||||
self.assertIn('required', str(form.errors))
|
||||
self.assertIn("required", str(form.errors))
|
||||
|
||||
@override_settings(LD_DISABLE_URL_VALIDATION=False)
|
||||
def test_bookmark_form_should_validate_url_if_not_disabled_in_settings(self):
|
||||
@@ -89,23 +91,25 @@ class BookmarkValidationTestCase(TestCase):
|
||||
url=url,
|
||||
date_added=datetime.datetime.now(),
|
||||
date_modified=datetime.datetime.now(),
|
||||
owner=self.user
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
try:
|
||||
bookmark.full_clean()
|
||||
self.assertTrue(expectation, 'Did not expect validation error')
|
||||
self.assertTrue(expectation, "Did not expect validation error")
|
||||
except ValidationError as e:
|
||||
self.assertFalse(expectation, 'Expected validation error')
|
||||
self.assertTrue('url' in e.message_dict, 'Expected URL validation to fail')
|
||||
self.assertFalse(expectation, "Expected validation error")
|
||||
self.assertTrue(
|
||||
"url" in e.message_dict, "Expected URL validation to fail"
|
||||
)
|
||||
|
||||
def _run_bookmark_form_url_validity_checks(self, cases):
|
||||
for case in cases:
|
||||
url, expectation = case
|
||||
form = BookmarkForm(data={'url': url})
|
||||
form = BookmarkForm(data={"url": url})
|
||||
|
||||
if expectation:
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
else:
|
||||
self.assertEqual(len(form.errors), 1)
|
||||
self.assertIn('Enter a valid URL', str(form.errors))
|
||||
self.assertIn("Enter a valid URL", str(form.errors))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,10 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
|
||||
class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
|
||||
self.api_token = Token.objects.get_or_create(
|
||||
user=self.get_or_create_test_user()
|
||||
)[0]
|
||||
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key)
|
||||
|
||||
def get_connection(self):
|
||||
return connections[DEFAULT_DB_ALIAS]
|
||||
@@ -26,7 +28,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-list"),
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
@@ -41,7 +46,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-archived"),
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
@@ -57,7 +65,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-shared"),
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
|
||||
@@ -9,47 +9,68 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
|
||||
|
||||
class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
def authenticate(self) -> None:
|
||||
self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
|
||||
self.api_token = Token.objects.get_or_create(
|
||||
user=self.get_or_create_test_user()
|
||||
)[0]
|
||||
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key)
|
||||
|
||||
def test_list_bookmarks_requires_authentication(self):
|
||||
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-list"),
|
||||
expected_status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
self.authenticate()
|
||||
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def test_list_archived_bookmarks_requires_authentication(self):
|
||||
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-archived"),
|
||||
expected_status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
self.authenticate()
|
||||
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-archived"),
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def test_list_shared_bookmarks_does_not_require_authentication(self):
|
||||
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-shared"),
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
self.authenticate()
|
||||
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
|
||||
self.get(
|
||||
reverse("bookmarks:bookmark-shared"),
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def test_create_bookmark_requires_authentication(self):
|
||||
data = {
|
||||
'url': 'https://example.com/',
|
||||
'title': 'Test title',
|
||||
'description': 'Test description',
|
||||
'notes': 'Test notes',
|
||||
'is_archived': False,
|
||||
'unread': False,
|
||||
'shared': False,
|
||||
'tag_names': ['tag1', 'tag2']
|
||||
"url": "https://example.com/",
|
||||
"title": "Test title",
|
||||
"description": "Test description",
|
||||
"notes": "Test notes",
|
||||
"is_archived": False,
|
||||
"unread": False,
|
||||
"shared": False,
|
||||
"tag_names": ["tag1", "tag2"],
|
||||
}
|
||||
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_401_UNAUTHORIZED)
|
||||
self.post(
|
||||
reverse("bookmarks:bookmark-list"), data, status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
self.authenticate()
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
|
||||
|
||||
def test_get_bookmark_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
||||
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -58,8 +79,8 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
|
||||
def test_update_bookmark_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
data = {'url': 'https://example.com/'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
|
||||
data = {"url": "https://example.com/"}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
||||
self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -68,8 +89,8 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
|
||||
def test_patch_bookmark_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
data = {'url': 'https://example.com'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
|
||||
data = {"url": "https://example.com"}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
||||
self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -78,7 +99,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
|
||||
def test_delete_bookmark_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
||||
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -87,7 +108,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
|
||||
def test_archive_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
url = reverse('bookmarks:bookmark-archive', args=[bookmark.id])
|
||||
url = reverse("bookmarks:bookmark-archive", args=[bookmark.id])
|
||||
|
||||
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -96,7 +117,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
|
||||
def test_unarchive_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark(is_archived=True)
|
||||
url = reverse('bookmarks:bookmark-unarchive', args=[bookmark.id])
|
||||
url = reverse("bookmarks:bookmark-unarchive", args=[bookmark.id])
|
||||
|
||||
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -104,16 +125,18 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def test_check_requires_authentication(self):
|
||||
url = reverse('bookmarks:bookmark-check')
|
||||
check_url = urllib.parse.quote_plus('https://example.com')
|
||||
url = reverse("bookmarks:bookmark-check")
|
||||
check_url = urllib.parse.quote_plus("https://example.com")
|
||||
|
||||
self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
self.get(
|
||||
f"{url}?url={check_url}", expected_status_code=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
self.authenticate()
|
||||
self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
|
||||
self.get(f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK)
|
||||
|
||||
def test_user_profile_requires_authentication(self):
|
||||
url = reverse('bookmarks:user-profile')
|
||||
url = reverse("bookmarks:user-profile")
|
||||
|
||||
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
@@ -16,34 +16,48 @@ from bookmarks.views.partials import contexts
|
||||
|
||||
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
|
||||
favicon_img = f'<img src="/static/{bookmark.favicon_file}" alt="">' if bookmark.favicon_file else ''
|
||||
def assertBookmarksLink(
|
||||
self, html: str, bookmark: Bookmark, link_target: str = "_blank"
|
||||
):
|
||||
favicon_img = (
|
||||
f'<img src="/static/{bookmark.favicon_file}" alt="">'
|
||||
if bookmark.favicon_file
|
||||
else ""
|
||||
)
|
||||
self.assertInHTML(
|
||||
f'''
|
||||
f"""
|
||||
<a href="{bookmark.url}"
|
||||
target="{link_target}"
|
||||
rel="noopener">
|
||||
{favicon_img}
|
||||
{bookmark.resolved_title}
|
||||
<span>{bookmark.resolved_title}</span>
|
||||
</a>
|
||||
''',
|
||||
html
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def assertDateLabel(self, html: str, label_content: str):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<span>{label_content}</span>
|
||||
<span class="separator">|</span>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'):
|
||||
self.assertInHTML(f'''
|
||||
def assertWebArchiveLink(
|
||||
self, html: str, label_content: str, url: str, link_target: str = "_blank"
|
||||
):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a href="{url}"
|
||||
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
|
||||
{label_content} ∞
|
||||
</a>
|
||||
<span class="separator">|</span>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=1)
|
||||
@@ -53,20 +67,32 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
# Edit link
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
self.assertInHTML(f'''
|
||||
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a href="{edit_url}?return_url=/bookmarks">Edit</a>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
# Archive link
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button type="submit" name="archive" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm">Archive</button>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
# Delete link
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button ld-confirm-button type="submit" name="remove" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm">Remove</button>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertShareInfo(self, html: str, bookmark: Bookmark):
|
||||
self.assertShareInfoCount(html, bookmark, 1)
|
||||
@@ -75,11 +101,15 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertShareInfoCount(html, bookmark, 0)
|
||||
|
||||
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<span>Shared by
|
||||
<a href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||
</span>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertFaviconVisible(self, html: str, bookmark: Bookmark):
|
||||
self.assertFaviconCount(html, bookmark, 1)
|
||||
@@ -88,47 +118,68 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertFaviconCount(html, bookmark, 0)
|
||||
|
||||
def assertFaviconCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<img src="/static/{bookmark.favicon_file}" alt="">
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertBookmarkURLCount(self, html: str, bookmark: Bookmark, link_target: str = '_blank', count=0):
|
||||
self.assertInHTML(f'''
|
||||
def assertBookmarkURLCount(
|
||||
self, html: str, bookmark: Bookmark, link_target: str = "_blank", count=0
|
||||
):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<div class="url-path truncate">
|
||||
<a href="{bookmark.url}" target="{link_target}" rel="noopener"
|
||||
class="url-display text-sm">
|
||||
{bookmark.url}
|
||||
</a>
|
||||
</div>
|
||||
''', html, count)
|
||||
""",
|
||||
html,
|
||||
count,
|
||||
)
|
||||
|
||||
def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkURLCount(html, bookmark, count=1)
|
||||
|
||||
def assertBookmarkURLHidden(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
|
||||
def assertBookmarkURLHidden(
|
||||
self, html: str, bookmark: Bookmark, link_target: str = "_blank"
|
||||
):
|
||||
self.assertBookmarkURLCount(html, bookmark, count=0)
|
||||
|
||||
def assertNotes(self, html: str, notes_html: str, count=1):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<div class="notes bg-gray text-gray-dark">
|
||||
<div class="notes-content">
|
||||
{notes_html}
|
||||
</div>
|
||||
</div>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertNotesToggle(self, html: str, count=1):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button type="button" class="btn btn-link btn-sm btn-icon toggle-notes">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<use xlink:href="#ld-icon-note"></use>
|
||||
</svg>
|
||||
Notes
|
||||
</button>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button type="submit" name="unshare" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?">
|
||||
@@ -137,10 +188,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
</svg>
|
||||
Shared
|
||||
</button>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertMarkAsReadButton(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button type="submit" name="mark_as_read" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?">
|
||||
@@ -149,12 +204,19 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
</svg>
|
||||
Unread
|
||||
</button>
|
||||
''', html, count=count)
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def render_template(self,
|
||||
url='/bookmarks',
|
||||
context_type: Type[contexts.BookmarkListContext] = contexts.ActiveBookmarkListContext,
|
||||
user: User | AnonymousUser = None) -> str:
|
||||
def render_template(
|
||||
self,
|
||||
url="/bookmarks",
|
||||
context_type: Type[
|
||||
contexts.BookmarkListContext
|
||||
] = contexts.ActiveBookmarkListContext,
|
||||
user: User | AnonymousUser = None,
|
||||
) -> str:
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
request.user = user or self.get_or_create_test_user()
|
||||
@@ -162,14 +224,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
middleware(request)
|
||||
|
||||
bookmark_list_context = context_type(request)
|
||||
context = RequestContext(request, {'bookmark_list': bookmark_list_context})
|
||||
context = RequestContext(request, {"bookmark_list": bookmark_list_context})
|
||||
|
||||
template = Template(
|
||||
"{% include 'bookmarks/bookmark_list.html' %}"
|
||||
)
|
||||
template = Template("{% include 'bookmarks/bookmark_list.html' %}")
|
||||
return template.render(context)
|
||||
|
||||
def setup_date_format_test(self, date_display_setting: str, web_archive_url: str = ''):
|
||||
def setup_date_format_test(
|
||||
self, date_display_setting: str, web_archive_url: str = ""
|
||||
):
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||
bookmark.web_archive_snapshot_url = web_archive_url
|
||||
@@ -180,38 +242,46 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
return bookmark
|
||||
|
||||
def test_should_respect_absolute_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE)
|
||||
bookmark = self.setup_date_format_test(
|
||||
UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE
|
||||
)
|
||||
html = self.render_template()
|
||||
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT')
|
||||
formatted_date = formats.date_format(bookmark.date_added, "SHORT_DATE_FORMAT")
|
||||
|
||||
self.assertDateLabel(html, formatted_date)
|
||||
|
||||
def test_should_render_web_archive_link_with_absolute_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE,
|
||||
'https://web.archive.org/web/20210811214511/https://wanikani.com/')
|
||||
bookmark = self.setup_date_format_test(
|
||||
UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE,
|
||||
"https://web.archive.org/web/20210811214511/https://wanikani.com/",
|
||||
)
|
||||
html = self.render_template()
|
||||
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT')
|
||||
formatted_date = formats.date_format(bookmark.date_added, "SHORT_DATE_FORMAT")
|
||||
|
||||
self.assertWebArchiveLink(html, formatted_date, bookmark.web_archive_snapshot_url)
|
||||
self.assertWebArchiveLink(
|
||||
html, formatted_date, bookmark.web_archive_snapshot_url
|
||||
)
|
||||
|
||||
def test_should_respect_relative_date_setting(self):
|
||||
self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||
html = self.render_template()
|
||||
|
||||
self.assertDateLabel(html, '1 week ago')
|
||||
self.assertDateLabel(html, "1 week ago")
|
||||
|
||||
def test_should_render_web_archive_link_with_relative_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
'https://web.archive.org/web/20210811214511/https://wanikani.com/')
|
||||
bookmark = self.setup_date_format_test(
|
||||
UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
"https://web.archive.org/web/20210811214511/https://wanikani.com/",
|
||||
)
|
||||
html = self.render_template()
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url)
|
||||
self.assertWebArchiveLink(html, "1 week ago", bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_bookmark_link_target_should_be_blank_by_default(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_template()
|
||||
|
||||
self.assertBookmarksLink(html, bookmark, link_target='_blank')
|
||||
self.assertBookmarksLink(html, bookmark, link_target="_blank")
|
||||
|
||||
def test_bookmark_link_target_should_respect_user_profile(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
@@ -221,17 +291,19 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_template()
|
||||
|
||||
self.assertBookmarksLink(html, bookmark, link_target='_self')
|
||||
self.assertBookmarksLink(html, bookmark, link_target="_self")
|
||||
|
||||
def test_web_archive_link_target_should_be_blank_by_default(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||
bookmark.web_archive_snapshot_url = 'https://example.com'
|
||||
bookmark.web_archive_snapshot_url = "https://example.com"
|
||||
bookmark.save()
|
||||
|
||||
html = self.render_template()
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
|
||||
self.assertWebArchiveLink(
|
||||
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_blank"
|
||||
)
|
||||
|
||||
def test_web_archive_link_target_should_respect_user_profile(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
@@ -240,12 +312,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||
bookmark.web_archive_snapshot_url = 'https://example.com'
|
||||
bookmark.web_archive_snapshot_url = "https://example.com"
|
||||
bookmark.save()
|
||||
|
||||
html = self.render_template()
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_self')
|
||||
self.assertWebArchiveLink(
|
||||
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_self"
|
||||
)
|
||||
|
||||
def test_should_reflect_unread_state_as_css_class(self):
|
||||
self.setup_bookmark(unread=True)
|
||||
@@ -281,7 +355,9 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertNoShareInfo(html, bookmark)
|
||||
|
||||
def test_show_share_info_for_non_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
other_user.profile.enable_sharing = True
|
||||
other_user.profile.save()
|
||||
|
||||
@@ -292,25 +368,32 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertShareInfo(html, bookmark)
|
||||
|
||||
def test_share_info_user_link_keeps_query_params(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
other_user.profile.enable_sharing = True
|
||||
other_user.profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True, title='foo')
|
||||
html = self.render_template(url='/bookmarks?q=foo', context_type=contexts.SharedBookmarkListContext)
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True, title="foo")
|
||||
html = self.render_template(
|
||||
url="/bookmarks?q=foo", context_type=contexts.SharedBookmarkListContext
|
||||
)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<span>Shared by
|
||||
<a href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||
</span>
|
||||
''', html)
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_favicon_should_be_visible_when_favicons_enabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
bookmark = self.setup_bookmark(favicon_file="https_example_com.png")
|
||||
html = self.render_template()
|
||||
|
||||
self.assertFaviconVisible(html, bookmark)
|
||||
@@ -320,7 +403,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file='')
|
||||
bookmark = self.setup_bookmark(favicon_file="")
|
||||
html = self.render_template()
|
||||
|
||||
self.assertFaviconHidden(html, bookmark)
|
||||
@@ -330,7 +413,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.enable_favicons = False
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
bookmark = self.setup_bookmark(favicon_file="https_example_com.png")
|
||||
html = self.render_template()
|
||||
|
||||
self.assertFaviconHidden(html, bookmark)
|
||||
@@ -428,21 +511,23 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark()
|
||||
html = self.render_template()
|
||||
|
||||
self.assertNotes(html, '', 0)
|
||||
self.assertNotes(html, "", 0)
|
||||
self.assertNotesToggle(html, 0)
|
||||
|
||||
def test_with_notes(self):
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = self.render_template()
|
||||
|
||||
note_html = '<p>Test note</p>'
|
||||
note_html = "<p>Test note</p>"
|
||||
self.assertNotes(html, note_html, 1)
|
||||
|
||||
def test_note_renders_markdown(self):
|
||||
self.setup_bookmark(notes='**Example:** `print("Hello world!")`')
|
||||
html = self.render_template()
|
||||
|
||||
note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
|
||||
note_html = (
|
||||
'<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
|
||||
)
|
||||
self.assertNotes(html, note_html, 1)
|
||||
|
||||
def test_note_cleans_html(self):
|
||||
@@ -453,7 +538,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertNotes(html, note_html, 1)
|
||||
|
||||
def test_notes_are_hidden_initially_by_default(self):
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = collapse_whitespace(self.render_template())
|
||||
|
||||
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
||||
@@ -463,7 +548,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.permanent_notes = False
|
||||
profile.save()
|
||||
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = collapse_whitespace(self.render_template())
|
||||
|
||||
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
||||
@@ -473,13 +558,15 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.permanent_notes = True
|
||||
profile.save()
|
||||
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = collapse_whitespace(self.render_template())
|
||||
|
||||
self.assertIn('<ul class="bookmark-list show-notes" data-bookmarks-total="1">', html)
|
||||
self.assertIn(
|
||||
'<ul class="bookmark-list show-notes" data-bookmarks-total="1">', html
|
||||
)
|
||||
|
||||
def test_toggle_notes_is_visible_by_default(self):
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = self.render_template()
|
||||
|
||||
self.assertNotesToggle(html, 1)
|
||||
@@ -489,7 +576,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.permanent_notes = False
|
||||
profile.save()
|
||||
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = self.render_template()
|
||||
|
||||
self.assertNotesToggle(html, 1)
|
||||
@@ -499,7 +586,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.permanent_notes = True
|
||||
profile.save()
|
||||
|
||||
self.setup_bookmark(notes='Test note')
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = self.render_template()
|
||||
|
||||
self.assertNotesToggle(html, 0)
|
||||
@@ -512,25 +599,35 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||
bookmark.web_archive_snapshot_url = 'https://web.archive.org/web/20230531200136/https://example.com'
|
||||
bookmark.web_archive_snapshot_url = (
|
||||
"https://web.archive.org/web/20230531200136/https://example.com"
|
||||
)
|
||||
bookmark.notes = '**Example:** `print("Hello world!")`'
|
||||
bookmark.favicon_file = 'https_example_com.png'
|
||||
bookmark.favicon_file = "https_example_com.png"
|
||||
bookmark.shared = True
|
||||
bookmark.unread = True
|
||||
bookmark.save()
|
||||
|
||||
html = self.render_template(context_type=contexts.SharedBookmarkListContext, user=AnonymousUser())
|
||||
self.assertBookmarksLink(html, bookmark, link_target='_blank')
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
|
||||
html = self.render_template(
|
||||
context_type=contexts.SharedBookmarkListContext, user=AnonymousUser()
|
||||
)
|
||||
self.assertBookmarksLink(html, bookmark, link_target="_blank")
|
||||
self.assertWebArchiveLink(
|
||||
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_blank"
|
||||
)
|
||||
self.assertNoBookmarkActions(html, bookmark)
|
||||
self.assertShareInfo(html, bookmark)
|
||||
self.assertMarkAsReadButton(html, bookmark, count=0)
|
||||
self.assertUnshareButton(html, bookmark, count=0)
|
||||
note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
|
||||
note_html = (
|
||||
'<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
|
||||
)
|
||||
self.assertNotes(html, note_html, 1)
|
||||
self.assertFaviconVisible(html, bookmark)
|
||||
|
||||
def test_empty_state(self):
|
||||
html = self.render_template()
|
||||
|
||||
self.assertInHTML('<p class="empty-title h5">You have no bookmarks yet</p>', html)
|
||||
self.assertInHTML(
|
||||
'<p class="empty-title h5">You have no bookmarks yet</p>', html
|
||||
)
|
||||
|
||||
@@ -6,11 +6,17 @@ from bookmarks.models import Bookmark
|
||||
class BookmarkTestCase(TestCase):
|
||||
|
||||
def test_bookmark_resolved_title(self):
|
||||
bookmark = Bookmark(title='Custom title', website_title='Website title', url='https://example.com')
|
||||
self.assertEqual(bookmark.resolved_title, 'Custom title')
|
||||
bookmark = Bookmark(
|
||||
title="Custom title",
|
||||
website_title="Website title",
|
||||
url="https://example.com",
|
||||
)
|
||||
self.assertEqual(bookmark.resolved_title, "Custom title")
|
||||
|
||||
bookmark = Bookmark(title='', website_title='Website title', url='https://example.com')
|
||||
self.assertEqual(bookmark.resolved_title, 'Website title')
|
||||
bookmark = Bookmark(
|
||||
title="", website_title="Website title", url="https://example.com"
|
||||
)
|
||||
self.assertEqual(bookmark.resolved_title, "Website title")
|
||||
|
||||
bookmark = Bookmark(title='', website_title='', url='https://example.com')
|
||||
self.assertEqual(bookmark.resolved_title, 'https://example.com')
|
||||
bookmark = Bookmark(title="", website_title="", url="https://example.com")
|
||||
self.assertEqual(bookmark.resolved_title, "https://example.com")
|
||||
|
||||
@@ -7,9 +7,21 @@ from django.utils import timezone
|
||||
from bookmarks.models import Bookmark, Tag
|
||||
from bookmarks.services import tasks
|
||||
from bookmarks.services import website_loader
|
||||
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \
|
||||
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks, mark_bookmarks_as_read, \
|
||||
mark_bookmarks_as_unread, share_bookmarks, unshare_bookmarks
|
||||
from bookmarks.services.bookmarks import (
|
||||
create_bookmark,
|
||||
update_bookmark,
|
||||
archive_bookmark,
|
||||
archive_bookmarks,
|
||||
unarchive_bookmark,
|
||||
unarchive_bookmarks,
|
||||
delete_bookmarks,
|
||||
tag_bookmarks,
|
||||
untag_bookmarks,
|
||||
mark_bookmarks_as_read,
|
||||
mark_bookmarks_as_unread,
|
||||
share_bookmarks,
|
||||
unshare_bookmarks,
|
||||
)
|
||||
from bookmarks.services.website_loader import WebsiteMetadata
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
@@ -22,36 +34,48 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.get_or_create_test_user()
|
||||
|
||||
def test_create_should_update_website_metadata(self):
|
||||
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
|
||||
with patch.object(
|
||||
website_loader, "load_website_metadata"
|
||||
) as mock_load_website_metadata:
|
||||
expected_metadata = WebsiteMetadata(
|
||||
'https://example.com',
|
||||
'Website title',
|
||||
'Website description'
|
||||
"https://example.com", "Website title", "Website description"
|
||||
)
|
||||
mock_load_website_metadata.return_value = expected_metadata
|
||||
|
||||
bookmark_data = Bookmark(url='https://example.com',
|
||||
title='Updated Title',
|
||||
description='Updated description',
|
||||
unread=True,
|
||||
shared=True,
|
||||
is_archived=True)
|
||||
created_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user())
|
||||
bookmark_data = Bookmark(
|
||||
url="https://example.com",
|
||||
title="Updated Title",
|
||||
description="Updated description",
|
||||
unread=True,
|
||||
shared=True,
|
||||
is_archived=True,
|
||||
)
|
||||
created_bookmark = create_bookmark(
|
||||
bookmark_data, "", self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
created_bookmark.refresh_from_db()
|
||||
self.assertEqual(expected_metadata.title, created_bookmark.website_title)
|
||||
self.assertEqual(expected_metadata.description, created_bookmark.website_description)
|
||||
self.assertEqual(
|
||||
expected_metadata.description, created_bookmark.website_description
|
||||
)
|
||||
|
||||
def test_create_should_update_existing_bookmark_with_same_url(self):
|
||||
original_bookmark = self.setup_bookmark(url='https://example.com', unread=False, shared=False)
|
||||
bookmark_data = Bookmark(url='https://example.com',
|
||||
title='Updated Title',
|
||||
description='Updated description',
|
||||
notes='Updated notes',
|
||||
unread=True,
|
||||
shared=True,
|
||||
is_archived=True)
|
||||
updated_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user())
|
||||
original_bookmark = self.setup_bookmark(
|
||||
url="https://example.com", unread=False, shared=False
|
||||
)
|
||||
bookmark_data = Bookmark(
|
||||
url="https://example.com",
|
||||
title="Updated Title",
|
||||
description="Updated description",
|
||||
notes="Updated notes",
|
||||
unread=True,
|
||||
shared=True,
|
||||
is_archived=True,
|
||||
)
|
||||
updated_bookmark = create_bookmark(
|
||||
bookmark_data, "", self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
self.assertEqual(updated_bookmark.id, original_bookmark.id)
|
||||
@@ -64,75 +88,91 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertFalse(updated_bookmark.is_archived)
|
||||
|
||||
def test_create_should_create_web_archive_snapshot(self):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
bookmark_data = Bookmark(url='https://example.com')
|
||||
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user)
|
||||
with patch.object(
|
||||
tasks, "create_web_archive_snapshot"
|
||||
) as mock_create_web_archive_snapshot:
|
||||
bookmark_data = Bookmark(url="https://example.com")
|
||||
bookmark = create_bookmark(bookmark_data, "tag1,tag2", self.user)
|
||||
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(self.user, bookmark, False)
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(
|
||||
self.user, bookmark, False
|
||||
)
|
||||
|
||||
def test_create_should_load_favicon(self):
|
||||
with patch.object(tasks, 'load_favicon') as mock_load_favicon:
|
||||
bookmark_data = Bookmark(url='https://example.com')
|
||||
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user)
|
||||
with patch.object(tasks, "load_favicon") as mock_load_favicon:
|
||||
bookmark_data = Bookmark(url="https://example.com")
|
||||
bookmark = create_bookmark(bookmark_data, "tag1,tag2", self.user)
|
||||
|
||||
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
||||
|
||||
def test_update_should_create_web_archive_snapshot_if_url_did_change(self):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
with patch.object(
|
||||
tasks, "create_web_archive_snapshot"
|
||||
) as mock_create_web_archive_snapshot:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.url = 'https://example.com/updated'
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
bookmark.url = "https://example.com/updated"
|
||||
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(self.user, bookmark, True)
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(
|
||||
self.user, bookmark, True
|
||||
)
|
||||
|
||||
def test_update_should_not_create_web_archive_snapshot_if_url_did_not_change(self):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
with patch.object(
|
||||
tasks, "create_web_archive_snapshot"
|
||||
) as mock_create_web_archive_snapshot:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.title = 'updated title'
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
bookmark.title = "updated title"
|
||||
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||
|
||||
mock_create_web_archive_snapshot.assert_not_called()
|
||||
|
||||
def test_update_should_update_website_metadata_if_url_did_change(self):
|
||||
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
|
||||
with patch.object(
|
||||
website_loader, "load_website_metadata"
|
||||
) as mock_load_website_metadata:
|
||||
expected_metadata = WebsiteMetadata(
|
||||
'https://example.com/updated',
|
||||
'Updated website title',
|
||||
'Updated website description'
|
||||
"https://example.com/updated",
|
||||
"Updated website title",
|
||||
"Updated website description",
|
||||
)
|
||||
mock_load_website_metadata.return_value = expected_metadata
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.url = 'https://example.com/updated'
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
bookmark.url = "https://example.com/updated"
|
||||
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
mock_load_website_metadata.assert_called_once()
|
||||
self.assertEqual(expected_metadata.title, bookmark.website_title)
|
||||
self.assertEqual(expected_metadata.description, bookmark.website_description)
|
||||
self.assertEqual(
|
||||
expected_metadata.description, bookmark.website_description
|
||||
)
|
||||
|
||||
def test_update_should_not_update_website_metadata_if_url_did_not_change(self):
|
||||
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
|
||||
with patch.object(
|
||||
website_loader, "load_website_metadata"
|
||||
) as mock_load_website_metadata:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.title = 'updated title'
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
bookmark.title = "updated title"
|
||||
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||
|
||||
mock_load_website_metadata.assert_not_called()
|
||||
|
||||
def test_update_should_update_favicon(self):
|
||||
with patch.object(tasks, 'load_favicon') as mock_load_favicon:
|
||||
with patch.object(tasks, "load_favicon") as mock_load_favicon:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.title = 'updated title'
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
bookmark.title = "updated title"
|
||||
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||
|
||||
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
||||
|
||||
def test_archive_bookmark(self):
|
||||
bookmark = Bookmark(
|
||||
url='https://example.com',
|
||||
url="https://example.com",
|
||||
date_added=timezone.now(),
|
||||
date_modified=timezone.now(),
|
||||
owner=self.user
|
||||
owner=self.user,
|
||||
)
|
||||
bookmark.save()
|
||||
|
||||
@@ -146,7 +186,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_unarchive_bookmark(self):
|
||||
bookmark = Bookmark(
|
||||
url='https://example.com',
|
||||
url="https://example.com",
|
||||
date_added=timezone.now(),
|
||||
date_modified=timezone.now(),
|
||||
owner=self.user,
|
||||
@@ -165,7 +205,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
archive_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
archive_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -183,12 +225,17 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||
|
||||
def test_archive_bookmarks_should_only_archive_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
||||
|
||||
archive_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
archive_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -199,7 +246,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
archive_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user())
|
||||
archive_bookmarks(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -210,7 +260,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||
bookmark3 = self.setup_bookmark(is_archived=True)
|
||||
|
||||
unarchive_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
unarchive_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -221,19 +273,26 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||
bookmark3 = self.setup_bookmark(is_archived=True)
|
||||
|
||||
unarchive_bookmarks([bookmark1.id, bookmark3.id], self.get_or_create_test_user())
|
||||
unarchive_bookmarks(
|
||||
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||
|
||||
def test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(is_archived=True)
|
||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||
inaccessible_bookmark = self.setup_bookmark(is_archived=True, user=other_user)
|
||||
|
||||
unarchive_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
unarchive_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -244,7 +303,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||
bookmark3 = self.setup_bookmark(is_archived=True)
|
||||
|
||||
unarchive_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user())
|
||||
unarchive_bookmarks(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||
@@ -255,7 +317,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
delete_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
delete_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||
@@ -273,23 +337,32 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
||||
|
||||
def test_delete_bookmarks_should_only_delete_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
||||
|
||||
delete_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
delete_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||
self.assertIsNotNone(Bookmark.objects.filter(id=inaccessible_bookmark.id).first())
|
||||
self.assertIsNotNone(
|
||||
Bookmark.objects.filter(id=inaccessible_bookmark.id).first()
|
||||
)
|
||||
|
||||
def test_delete_bookmarks_should_accept_mix_of_int_and_string_ids(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
delete_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
delete_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||
@@ -302,8 +375,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
tag_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -318,7 +394,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], 'tag1,tag2', self.get_or_create_test_user())
|
||||
tag_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id],
|
||||
"tag1,tag2",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -326,8 +406,8 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertEqual(2, Tag.objects.count())
|
||||
|
||||
tag1 = Tag.objects.filter(name='tag1').first()
|
||||
tag2 = Tag.objects.filter(name='tag2').first()
|
||||
tag1 = Tag.objects.filter(name="tag1").first()
|
||||
tag2 = Tag.objects.filter(name="tag2").first()
|
||||
|
||||
self.assertIsNotNone(tag1)
|
||||
self.assertIsNotNone(tag2)
|
||||
@@ -336,6 +416,31 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||
|
||||
def test_tag_bookmarks_should_handle_existing_relationships(self):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
bookmark1 = self.setup_bookmark(tags=[tag1])
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1])
|
||||
|
||||
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||
self.assertEqual(3, BookmarkToTagRelationShip.objects.count())
|
||||
|
||||
tag_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
bookmark3.refresh_from_db()
|
||||
|
||||
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||
self.assertEqual(6, BookmarkToTagRelationShip.objects.count())
|
||||
|
||||
def test_tag_bookmarks_should_only_tag_specified_bookmarks(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
@@ -343,7 +448,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name},{tag2.name}', self.get_or_create_test_user())
|
||||
tag_bookmarks(
|
||||
[bookmark1.id, bookmark3.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -354,15 +463,20 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||
|
||||
def test_tag_bookmarks_should_only_tag_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
tag_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -379,8 +493,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
tag_bookmarks(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||
@@ -393,8 +510,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
untag_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -411,7 +531,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name},{tag2.name}', self.get_or_create_test_user())
|
||||
untag_bookmarks(
|
||||
[bookmark1.id, bookmark3.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -422,15 +546,20 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertCountEqual(bookmark3.tags.all(), [])
|
||||
|
||||
def test_untag_bookmarks_should_only_tag_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
bookmark1 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user, tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
untag_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -447,8 +576,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
untag_bookmarks(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
f"{tag1.name},{tag2.name}",
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertCountEqual(bookmark1.tags.all(), [])
|
||||
self.assertCountEqual(bookmark2.tags.all(), [])
|
||||
@@ -459,7 +591,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=True)
|
||||
bookmark3 = self.setup_bookmark(unread=True)
|
||||
|
||||
mark_bookmarks_as_read([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_read(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -470,19 +604,26 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=True)
|
||||
bookmark3 = self.setup_bookmark(unread=True)
|
||||
|
||||
mark_bookmarks_as_read([bookmark1.id, bookmark3.id], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_read(
|
||||
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||
|
||||
def test_mark_bookmarks_as_read_should_only_update_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(unread=True)
|
||||
bookmark2 = self.setup_bookmark(unread=True)
|
||||
inaccessible_bookmark = self.setup_bookmark(unread=True, user=other_user)
|
||||
|
||||
mark_bookmarks_as_read([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_read(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -493,7 +634,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=True)
|
||||
bookmark3 = self.setup_bookmark(unread=True)
|
||||
|
||||
mark_bookmarks_as_read([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_read(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -504,7 +648,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=False)
|
||||
bookmark3 = self.setup_bookmark(unread=False)
|
||||
|
||||
mark_bookmarks_as_unread([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_unread(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -515,19 +661,26 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=False)
|
||||
bookmark3 = self.setup_bookmark(unread=False)
|
||||
|
||||
mark_bookmarks_as_unread([bookmark1.id, bookmark3.id], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_unread(
|
||||
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||
|
||||
def test_mark_bookmarks_as_unread_should_only_update_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(unread=False)
|
||||
bookmark2 = self.setup_bookmark(unread=False)
|
||||
inaccessible_bookmark = self.setup_bookmark(unread=False, user=other_user)
|
||||
|
||||
mark_bookmarks_as_unread([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_unread(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -538,7 +691,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(unread=False)
|
||||
bookmark3 = self.setup_bookmark(unread=False)
|
||||
|
||||
mark_bookmarks_as_unread([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user())
|
||||
mark_bookmarks_as_unread(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||
@@ -549,7 +705,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(shared=False)
|
||||
bookmark3 = self.setup_bookmark(shared=False)
|
||||
|
||||
share_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
share_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -567,12 +725,17 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||
|
||||
def test_share_bookmarks_should_only_update_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(shared=False)
|
||||
bookmark2 = self.setup_bookmark(shared=False)
|
||||
inaccessible_bookmark = self.setup_bookmark(shared=False, user=other_user)
|
||||
|
||||
share_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
share_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -583,7 +746,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(shared=False)
|
||||
bookmark3 = self.setup_bookmark(shared=False)
|
||||
|
||||
share_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user())
|
||||
share_bookmarks(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -594,7 +760,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(shared=True)
|
||||
bookmark3 = self.setup_bookmark(shared=True)
|
||||
|
||||
unshare_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user())
|
||||
unshare_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -612,12 +780,17 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||
|
||||
def test_unshare_bookmarks_should_only_update_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
)
|
||||
bookmark1 = self.setup_bookmark(shared=True)
|
||||
bookmark2 = self.setup_bookmark(shared=True)
|
||||
inaccessible_bookmark = self.setup_bookmark(shared=True, user=other_user)
|
||||
|
||||
unshare_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user())
|
||||
unshare_bookmarks(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
@@ -628,7 +801,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(shared=True)
|
||||
bookmark3 = self.setup_bookmark(shared=True)
|
||||
|
||||
unshare_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user())
|
||||
unshare_bookmarks(
|
||||
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
|
||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user