mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-03-07 02:13:12 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2fa0a8f5a | ||
|
|
02a15c9460 | ||
|
|
7a6428c037 | ||
|
|
c6001aa7b8 | ||
|
|
eefbefd714 | ||
|
|
683cf529d7 | ||
|
|
38204c87cf | ||
|
|
96ee4746ad | ||
|
|
d7c1afa2a5 | ||
|
|
16ed6ef200 | ||
|
|
98b9a9c1a0 | ||
|
|
6775633be5 | ||
|
|
150dfecc6f | ||
|
|
81ae55bc1c | ||
|
|
935189ecc2 | ||
|
|
7997f20d89 | ||
|
|
ae27500cde | ||
|
|
71d853999e | ||
|
|
70288d6865 | ||
|
|
e83d519cab | ||
|
|
6355d8dff1 | ||
|
|
227cfdb063 | ||
|
|
2d4da099c7 | ||
|
|
a9512b2333 | ||
|
|
47e944e6c5 | ||
|
|
6c7ce91d53 | ||
|
|
87020de917 | ||
|
|
a130daa0f0 | ||
|
|
d7c68c2818 |
@@ -14,7 +14,7 @@
|
|||||||
"forwardPorts": [8000],
|
"forwardPorts": [8000],
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// 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.
|
// Configure tool-specific properties.
|
||||||
"customizations": {
|
"customizations": {
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
# Remove project files, data, tmp files, build files
|
# Ignore everything
|
||||||
/.env
|
*
|
||||||
/.idea
|
|
||||||
/data
|
|
||||||
/node_modules
|
|
||||||
/tmp
|
|
||||||
/docs
|
|
||||||
/static
|
|
||||||
/scripts
|
|
||||||
/build
|
|
||||||
/out
|
|
||||||
/.git
|
|
||||||
/.devcontainer
|
|
||||||
|
|
||||||
/.dockerignore
|
# Include files required for build or at runtime
|
||||||
/.gitignore
|
!/bookmarks
|
||||||
/.gitattributes
|
!/siteroot
|
||||||
/Dockerfile
|
|
||||||
/docker-compose.yml
|
|
||||||
/*.sh
|
|
||||||
/*.iml
|
|
||||||
/*.patch
|
|
||||||
/*.md
|
|
||||||
/*.js
|
|
||||||
/*.log
|
|
||||||
/*.pid
|
|
||||||
|
|
||||||
# Whitelist files needed in build or prod image
|
|
||||||
!/rollup.config.js
|
|
||||||
!/bootstrap.sh
|
|
||||||
!/background-tasks-wrapper.sh
|
!/background-tasks-wrapper.sh
|
||||||
|
!/bootstrap.sh
|
||||||
|
!/LICENSE.txt
|
||||||
|
!/manage.py
|
||||||
|
!/package.json
|
||||||
|
!/package-lock.json
|
||||||
|
!/requirements.dev.txt
|
||||||
|
!/requirements.txt
|
||||||
|
!/rollup.config.js
|
||||||
|
!/supervisord.conf
|
||||||
|
!/uwsgi.ini
|
||||||
|
!/version.txt
|
||||||
|
|
||||||
# Remove development settings
|
# Remove dev settings
|
||||||
/siteroot/settings/dev.py
|
/siteroot/settings/dev.py
|
||||||
|
|||||||
6
.github/workflows/main.yaml
vendored
6
.github/workflows/main.yaml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: linkding CI
|
name: linkding CI
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit_tests:
|
unit_tests:
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt -r requirements.dev.txt
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python manage.py test bookmarks.tests
|
run: python manage.py test bookmarks.tests
|
||||||
e2e_tests:
|
e2e_tests:
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
run: npm install
|
run: npm install
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
run: |
|
run: |
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt -r requirements.dev.txt
|
||||||
playwright install chromium
|
playwright install chromium
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -1,5 +1,66 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
* Fix RSS feed not handling None values by @vitormarcal in https://github.com/sissbruecker/linkding/pull/569
|
||||||
|
* Bump django from 4.1.10 to 4.1.13 by @dependabot in https://github.com/sissbruecker/linkding/pull/567
|
||||||
|
|
||||||
|
### New Contributors
|
||||||
|
* @vitormarcal made their first contribution in https://github.com/sissbruecker/linkding/pull/569
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.2...v1.22.3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.22.2 (27/10/2023)
|
## v1.22.2 (27/10/2023)
|
||||||
|
|
||||||
### What's Changed
|
### 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
|
||||||
61
README.md
61
README.md
@@ -9,11 +9,11 @@
|
|||||||
## Overview
|
## Overview
|
||||||
- [Introduction](#introduction)
|
- [Introduction](#introduction)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Using Docker](#using-docker)
|
- [Using Docker](#using-docker)
|
||||||
- [Using Docker Compose](#using-docker-compose)
|
- [Using Docker Compose](#using-docker-compose)
|
||||||
- [User Setup](#user-setup)
|
- [User Setup](#user-setup)
|
||||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||||
- [Managed Hosting Options](#managed-hosting-options)
|
- [Managed Hosting Options](#managed-hosting-options)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- [Browser Extension](#browser-extension)
|
- [Browser Extension](#browser-extension)
|
||||||
- [Community](#community)
|
- [Community](#community)
|
||||||
@@ -40,7 +40,7 @@ The name comes from:
|
|||||||
- Automatically provides titles, descriptions and icons of bookmarked websites
|
- Automatically provides titles, descriptions and icons of bookmarked websites
|
||||||
- Automatically creates snapshots of bookmarked websites on [the Internet Archive Wayback Machine](https://archive.org/web/)
|
- Automatically creates snapshots of bookmarked websites on [the Internet Archive Wayback Machine](https://archive.org/web/)
|
||||||
- Import and export bookmarks in Netscape HTML format
|
- Import and export bookmarks in Netscape HTML format
|
||||||
- Extensions for [Firefox](https://addons.mozilla.org/de/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe), as well as a bookmarklet
|
- Extensions for [Firefox](https://addons.mozilla.org/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe), as well as a bookmarklet
|
||||||
- Light and dark themes
|
- Light and dark themes
|
||||||
- REST API for developing 3rd party apps
|
- REST API for developing 3rd party apps
|
||||||
- Admin panel for user self-service and raw data access
|
- Admin panel for user self-service and raw data access
|
||||||
@@ -58,9 +58,27 @@ The name comes from:
|
|||||||
linkding is designed to be run with container solutions like [Docker](https://docs.docker.com/get-started/).
|
linkding is designed to be run with container solutions like [Docker](https://docs.docker.com/get-started/).
|
||||||
The Docker image is compatible with ARM platforms, so it can be run on a Raspberry Pi.
|
The Docker image is compatible with ARM platforms, so it can be run on a Raspberry Pi.
|
||||||
|
|
||||||
By default, linkding uses SQLite as a database.
|
linkding uses an SQLite database by default.
|
||||||
Alternatively linkding supports PostgreSQL, see the [database options](docs/Options.md#LD_DB_ENGINE) for more information.
|
Alternatively linkding supports PostgreSQL, see the [database options](docs/Options.md#LD_DB_ENGINE) for more information.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>🧪 Alpine-based image</summary>
|
||||||
|
|
||||||
|
The default Docker image (`latest` tag) is based on a slim variant of Debian Linux.
|
||||||
|
Alternatively, there is an image based on Alpine Linux (`latest-alpine` tag) which has a smaller size, resulting in a smaller download and less disk space required.
|
||||||
|
The Alpine image is currently about 45 MB in compressed size, compared to about 130 MB for the Debian image.
|
||||||
|
|
||||||
|
To use it, replace the `latest` tag with `latest-alpine`, either in the CLI command below when using Docker, or in the `docker-compose.yml` file when using docker-compose.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The image is currently considered experimental in order to gather feedback and iron out any issues.
|
||||||
|
> Only use it if you are comfortable running experimental software or want to help out with testing.
|
||||||
|
> While there should be no issues with creating new installations, there might be issues when migrating existing installations.
|
||||||
|
> If you plan to migrate your existing installation, make sure to create proper [backups](https://github.com/sissbruecker/linkding/blob/master/docs/backup.md) first.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Using Docker
|
### Using Docker
|
||||||
|
|
||||||
To install linkding using Docker you can just run the [latest image](https://hub.docker.com/repository/docker/sissbruecker/linkding) from Docker Hub:
|
To install linkding using Docker you can just run the [latest image](https://hub.docker.com/repository/docker/sissbruecker/linkding) from Docker Hub:
|
||||||
@@ -85,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.
|
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:
|
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:
|
||||||
|
|
||||||
@@ -101,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.
|
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
|
### Reverse Proxy Setup
|
||||||
|
|
||||||
@@ -164,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)
|
- [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))
|
- [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
|
## Documentation
|
||||||
|
|
||||||
@@ -180,7 +199,7 @@ Self-hosting web applications still requires a lot of technical know-how and com
|
|||||||
## Browser Extension
|
## Browser Extension
|
||||||
|
|
||||||
linkding comes with an official browser extension that allows to quickly add bookmarks, and search bookmarks through the browser's address bar. You can get the extension here:
|
linkding comes with an official browser extension that allows to quickly add bookmarks, and search bookmarks through the browser's address bar. You can get the extension here:
|
||||||
- [Mozilla Addon Store](https://addons.mozilla.org/de/firefox/addon/linkding-extension/)
|
- [Mozilla Addon Store](https://addons.mozilla.org/firefox/addon/linkding-extension/)
|
||||||
- [Chrome Web Store](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
|
- [Chrome Web Store](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
|
||||||
|
|
||||||
The extension is open-source as well, and can be found [here](https://github.com/sissbruecker/linkding-extension).
|
The extension is open-source as well, and can be found [here](https://github.com/sissbruecker/linkding-extension).
|
||||||
@@ -192,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)
|
- [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)
|
- [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)
|
- [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)
|
- [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-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-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)
|
- [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)
|
- [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)
|
- [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)
|
- [Postman collection](https://gist.github.com/gingerbeardman/f0b42502f3bc9344e92ce63afd4360d3) a group of saved request templates for API testing. By [gingerbeardman](https://github.com/gingerbeardman)
|
||||||
@@ -205,7 +226,7 @@ This section lists community projects around using linkding, in alphabetical ord
|
|||||||
|
|
||||||
### PikaPods
|
### 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.
|
See the table below for a list of donations.
|
||||||
|
|
||||||
@@ -237,7 +258,7 @@ source ~/environments/linkding/bin/activate[.csh|.fish]
|
|||||||
```
|
```
|
||||||
Within the active environment install the application dependencies from the application folder:
|
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:
|
Install frontend dependencies:
|
||||||
```
|
```
|
||||||
@@ -262,6 +283,20 @@ python3 manage.py runserver
|
|||||||
```
|
```
|
||||||
The frontend is now available under http://localhost:8000
|
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
|
### 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)
|
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):
|
class LinkdingAdminSite(AdminSite):
|
||||||
site_header = 'linkding administration'
|
site_header = "linkding administration"
|
||||||
site_title = 'linkding Admin'
|
site_title = "linkding Admin"
|
||||||
|
|
||||||
|
|
||||||
class AdminBookmark(admin.ModelAdmin):
|
class AdminBookmark(admin.ModelAdmin):
|
||||||
list_display = ('resolved_title', 'url', 'is_archived', 'owner', 'date_added')
|
list_display = ("resolved_title", "url", "is_archived", "owner", "date_added")
|
||||||
search_fields = ('title', 'description', 'website_title', 'website_description', 'url', 'tags__name')
|
search_fields = (
|
||||||
list_filter = ('owner__username', 'is_archived', 'unread', 'tags',)
|
"title",
|
||||||
ordering = ('-date_added',)
|
"description",
|
||||||
actions = ['delete_selected_bookmarks', 'archive_selected_bookmarks', 'unarchive_selected_bookmarks', 'mark_as_read', 'mark_as_unread']
|
"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):
|
def get_actions(self, request):
|
||||||
actions = super().get_actions(request)
|
actions = super().get_actions(request)
|
||||||
# Remove default delete action, which gets replaced by delete_selected_bookmarks below
|
# 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
|
# 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)
|
# 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
|
return actions
|
||||||
|
|
||||||
def delete_selected_bookmarks(self, request, queryset: QuerySet):
|
def delete_selected_bookmarks(self, request, queryset: QuerySet):
|
||||||
bookmarks_count = queryset.count()
|
bookmarks_count = queryset.count()
|
||||||
for bookmark in queryset:
|
for bookmark in queryset:
|
||||||
bookmark.delete()
|
bookmark.delete()
|
||||||
self.message_user(request, ngettext(
|
self.message_user(
|
||||||
'%d bookmark was successfully deleted.',
|
request,
|
||||||
'%d bookmarks were successfully deleted.',
|
ngettext(
|
||||||
bookmarks_count,
|
"%d bookmark was successfully deleted.",
|
||||||
) % bookmarks_count, messages.SUCCESS)
|
"%d bookmarks were successfully deleted.",
|
||||||
|
bookmarks_count,
|
||||||
|
)
|
||||||
|
% bookmarks_count,
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
def archive_selected_bookmarks(self, request, queryset: QuerySet):
|
def archive_selected_bookmarks(self, request, queryset: QuerySet):
|
||||||
for bookmark in queryset:
|
for bookmark in queryset:
|
||||||
archive_bookmark(bookmark)
|
archive_bookmark(bookmark)
|
||||||
bookmarks_count = queryset.count()
|
bookmarks_count = queryset.count()
|
||||||
self.message_user(request, ngettext(
|
self.message_user(
|
||||||
'%d bookmark was successfully archived.',
|
request,
|
||||||
'%d bookmarks were successfully archived.',
|
ngettext(
|
||||||
bookmarks_count,
|
"%d bookmark was successfully archived.",
|
||||||
) % bookmarks_count, messages.SUCCESS)
|
"%d bookmarks were successfully archived.",
|
||||||
|
bookmarks_count,
|
||||||
|
)
|
||||||
|
% bookmarks_count,
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
def unarchive_selected_bookmarks(self, request, queryset: QuerySet):
|
def unarchive_selected_bookmarks(self, request, queryset: QuerySet):
|
||||||
for bookmark in queryset:
|
for bookmark in queryset:
|
||||||
unarchive_bookmark(bookmark)
|
unarchive_bookmark(bookmark)
|
||||||
bookmarks_count = queryset.count()
|
bookmarks_count = queryset.count()
|
||||||
self.message_user(request, ngettext(
|
self.message_user(
|
||||||
'%d bookmark was successfully unarchived.',
|
request,
|
||||||
'%d bookmarks were successfully unarchived.',
|
ngettext(
|
||||||
bookmarks_count,
|
"%d bookmark was successfully unarchived.",
|
||||||
) % bookmarks_count, messages.SUCCESS)
|
"%d bookmarks were successfully unarchived.",
|
||||||
|
bookmarks_count,
|
||||||
|
)
|
||||||
|
% bookmarks_count,
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
def mark_as_read(self, request, queryset: QuerySet):
|
def mark_as_read(self, request, queryset: QuerySet):
|
||||||
bookmarks_count = queryset.count()
|
bookmarks_count = queryset.count()
|
||||||
queryset.update(unread=False)
|
queryset.update(unread=False)
|
||||||
self.message_user(request, ngettext(
|
self.message_user(
|
||||||
'%d bookmark marked as read.',
|
request,
|
||||||
'%d bookmarks marked as read.',
|
ngettext(
|
||||||
bookmarks_count,
|
"%d bookmark marked as read.",
|
||||||
) % bookmarks_count, messages.SUCCESS)
|
"%d bookmarks marked as read.",
|
||||||
|
bookmarks_count,
|
||||||
|
)
|
||||||
|
% bookmarks_count,
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
def mark_as_unread(self, request, queryset: QuerySet):
|
def mark_as_unread(self, request, queryset: QuerySet):
|
||||||
bookmarks_count = queryset.count()
|
bookmarks_count = queryset.count()
|
||||||
queryset.update(unread=True)
|
queryset.update(unread=True)
|
||||||
self.message_user(request, ngettext(
|
self.message_user(
|
||||||
'%d bookmark marked as unread.',
|
request,
|
||||||
'%d bookmarks marked as unread.',
|
ngettext(
|
||||||
bookmarks_count,
|
"%d bookmark marked as unread.",
|
||||||
) % bookmarks_count, messages.SUCCESS)
|
"%d bookmarks marked as unread.",
|
||||||
|
bookmarks_count,
|
||||||
|
)
|
||||||
|
% bookmarks_count,
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdminTag(admin.ModelAdmin):
|
class AdminTag(admin.ModelAdmin):
|
||||||
list_display = ('name', 'bookmarks_count', 'owner', 'date_added')
|
list_display = ("name", "bookmarks_count", "owner", "date_added")
|
||||||
search_fields = ('name', 'owner__username')
|
search_fields = ("name", "owner__username")
|
||||||
list_filter = ('owner__username',)
|
list_filter = ("owner__username",)
|
||||||
ordering = ('-date_added',)
|
ordering = ("-date_added",)
|
||||||
actions = ['delete_unused_tags']
|
actions = ["delete_unused_tags"]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = super().get_queryset(request)
|
queryset = super().get_queryset(request)
|
||||||
@@ -97,7 +140,7 @@ class AdminTag(admin.ModelAdmin):
|
|||||||
def bookmarks_count(self, obj):
|
def bookmarks_count(self, obj):
|
||||||
return obj.bookmarks_count
|
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):
|
def delete_unused_tags(self, request, queryset: QuerySet):
|
||||||
unused_tags = queryset.filter(bookmark__isnull=True)
|
unused_tags = queryset.filter(bookmark__isnull=True)
|
||||||
@@ -106,23 +149,33 @@ class AdminTag(admin.ModelAdmin):
|
|||||||
tag.delete()
|
tag.delete()
|
||||||
|
|
||||||
if unused_tags_count > 0:
|
if unused_tags_count > 0:
|
||||||
self.message_user(request, ngettext(
|
self.message_user(
|
||||||
'%d unused tag was successfully deleted.',
|
request,
|
||||||
'%d unused tags were successfully deleted.',
|
ngettext(
|
||||||
unused_tags_count,
|
"%d unused tag was successfully deleted.",
|
||||||
) % unused_tags_count, messages.SUCCESS)
|
"%d unused tags were successfully deleted.",
|
||||||
|
unused_tags_count,
|
||||||
|
)
|
||||||
|
% unused_tags_count,
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.message_user(request, gettext(
|
self.message_user(
|
||||||
'There were no unused tags in the selection',
|
request,
|
||||||
), messages.SUCCESS)
|
gettext(
|
||||||
|
"There were no unused tags in the selection",
|
||||||
|
),
|
||||||
|
messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdminUserProfileInline(admin.StackedInline):
|
class AdminUserProfileInline(admin.StackedInline):
|
||||||
model = UserProfile
|
model = UserProfile
|
||||||
can_delete = False
|
can_delete = False
|
||||||
verbose_name_plural = 'Profile'
|
verbose_name_plural = "Profile"
|
||||||
fk_name = 'user'
|
fk_name = "user"
|
||||||
readonly_fields = ('search_preferences', )
|
readonly_fields = ("search_preferences",)
|
||||||
|
|
||||||
|
|
||||||
class AdminCustomUser(UserAdmin):
|
class AdminCustomUser(UserAdmin):
|
||||||
inlines = (AdminUserProfileInline,)
|
inlines = (AdminUserProfileInline,)
|
||||||
@@ -134,15 +187,15 @@ class AdminCustomUser(UserAdmin):
|
|||||||
|
|
||||||
|
|
||||||
class AdminToast(admin.ModelAdmin):
|
class AdminToast(admin.ModelAdmin):
|
||||||
list_display = ('key', 'message', 'owner', 'acknowledged')
|
list_display = ("key", "message", "owner", "acknowledged")
|
||||||
search_fields = ('key', 'message')
|
search_fields = ("key", "message")
|
||||||
list_filter = ('owner__username',)
|
list_filter = ("owner__username",)
|
||||||
|
|
||||||
|
|
||||||
class AdminFeedToken(admin.ModelAdmin):
|
class AdminFeedToken(admin.ModelAdmin):
|
||||||
list_display = ('key', 'user')
|
list_display = ("key", "user")
|
||||||
search_fields = ['key']
|
search_fields = ["key"]
|
||||||
list_filter = ('user__username',)
|
list_filter = ("user__username",)
|
||||||
|
|
||||||
|
|
||||||
linkding_admin_site = LinkdingAdminSite()
|
linkding_admin_site = LinkdingAdminSite()
|
||||||
|
|||||||
@@ -5,18 +5,28 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from bookmarks import queries
|
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.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
|
from bookmarks.services.website_loader import WebsiteMetadata
|
||||||
|
|
||||||
|
|
||||||
class BookmarkViewSet(viewsets.GenericViewSet,
|
class BookmarkViewSet(
|
||||||
mixins.ListModelMixin,
|
viewsets.GenericViewSet,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.ListModelMixin,
|
||||||
mixins.CreateModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin):
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
):
|
||||||
serializer_class = BookmarkSerializer
|
serializer_class = BookmarkSerializer
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
@@ -24,7 +34,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
# The shared action should still filter bookmarks so that
|
# The shared action should still filter bookmarks so that
|
||||||
# unauthenticated users only see bookmarks from users that have public
|
# unauthenticated users only see bookmarks from users that have public
|
||||||
# sharing explicitly enabled
|
# sharing explicitly enabled
|
||||||
if self.action == 'shared':
|
if self.action == "shared":
|
||||||
return [AllowAny()]
|
return [AllowAny()]
|
||||||
|
|
||||||
# Otherwise use default permissions which should require authentication
|
# Otherwise use default permissions which should require authentication
|
||||||
@@ -33,7 +43,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
# For list action, use query set that applies search and tag projections
|
# 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)
|
search = BookmarkSearch.from_request(self.request.GET)
|
||||||
return queries.query_bookmarks(user, user.profile, search)
|
return queries.query_bookmarks(user, user.profile, search)
|
||||||
|
|
||||||
@@ -41,9 +51,9 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
return Bookmark.objects.all().filter(owner=user)
|
return Bookmark.objects.all().filter(owner=user)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
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):
|
def archived(self, request):
|
||||||
user = request.user
|
user = request.user
|
||||||
search = BookmarkSearch.from_request(request.GET)
|
search = BookmarkSearch.from_request(request.GET)
|
||||||
@@ -53,51 +63,59 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
data = serializer(page, many=True).data
|
data = serializer(page, many=True).data
|
||||||
return self.get_paginated_response(data)
|
return self.get_paginated_response(data)
|
||||||
|
|
||||||
@action(methods=['get'], detail=False)
|
@action(methods=["get"], detail=False)
|
||||||
def shared(self, request):
|
def shared(self, request):
|
||||||
search = BookmarkSearch.from_request(request.GET)
|
search = BookmarkSearch.from_request(request.GET)
|
||||||
user = User.objects.filter(username=search.user).first()
|
user = User.objects.filter(username=search.user).first()
|
||||||
public_only = not request.user.is_authenticated
|
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)
|
page = self.paginate_queryset(query_set)
|
||||||
serializer = self.get_serializer_class()
|
serializer = self.get_serializer_class()
|
||||||
data = serializer(page, many=True).data
|
data = serializer(page, many=True).data
|
||||||
return self.get_paginated_response(data)
|
return self.get_paginated_response(data)
|
||||||
|
|
||||||
@action(methods=['post'], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
def archive(self, request, pk):
|
def archive(self, request, pk):
|
||||||
bookmark = self.get_object()
|
bookmark = self.get_object()
|
||||||
archive_bookmark(bookmark)
|
archive_bookmark(bookmark)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@action(methods=['post'], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
def unarchive(self, request, pk):
|
def unarchive(self, request, pk):
|
||||||
bookmark = self.get_object()
|
bookmark = self.get_object()
|
||||||
unarchive_bookmark(bookmark)
|
unarchive_bookmark(bookmark)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@action(methods=['get'], detail=False)
|
@action(methods=["get"], detail=False)
|
||||||
def check(self, request):
|
def check(self, request):
|
||||||
url = request.GET.get('url')
|
url = request.GET.get("url")
|
||||||
bookmark = Bookmark.objects.filter(owner=request.user, url=url).first()
|
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
|
# Either return metadata from existing bookmark, or scrape from URL
|
||||||
if bookmark:
|
if bookmark:
|
||||||
metadata = WebsiteMetadata(url, bookmark.website_title, bookmark.website_description)
|
metadata = WebsiteMetadata(
|
||||||
|
url, bookmark.website_title, bookmark.website_description
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
metadata = website_loader.load_website_metadata(url)
|
metadata = website_loader.load_website_metadata(url)
|
||||||
|
|
||||||
return Response({
|
return Response(
|
||||||
'bookmark': existing_bookmark_data,
|
{"bookmark": existing_bookmark_data, "metadata": metadata.to_dict()},
|
||||||
'metadata': metadata.to_dict()
|
status=status.HTTP_200_OK,
|
||||||
}, status=status.HTTP_200_OK)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(viewsets.GenericViewSet,
|
class TagViewSet(
|
||||||
mixins.ListModelMixin,
|
viewsets.GenericViewSet,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.ListModelMixin,
|
||||||
mixins.CreateModelMixin):
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
):
|
||||||
serializer_class = TagSerializer
|
serializer_class = TagSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -105,16 +123,16 @@ class TagViewSet(viewsets.GenericViewSet,
|
|||||||
return Tag.objects.all().filter(owner=user)
|
return Tag.objects.all().filter(owner=user)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
return {'user': self.request.user}
|
return {"user": self.request.user}
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(viewsets.GenericViewSet):
|
class UserViewSet(viewsets.GenericViewSet):
|
||||||
@action(methods=['get'], detail=False)
|
@action(methods=["get"], detail=False)
|
||||||
def profile(self, request):
|
def profile(self, request):
|
||||||
return Response(UserProfileSerializer(request.user.profile).data)
|
return Response(UserProfileSerializer(request.user.profile).data)
|
||||||
|
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'bookmarks', BookmarkViewSet, basename='bookmark')
|
router.register(r"bookmarks", BookmarkViewSet, basename="bookmark")
|
||||||
router.register(r'tags', TagViewSet, basename='tag')
|
router.register(r"tags", TagViewSet, basename="tag")
|
||||||
router.register(r'user', UserViewSet, basename='user')
|
router.register(r"user", UserViewSet, basename="user")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TagListField(serializers.ListField):
|
|||||||
class BookmarkListSerializer(ListSerializer):
|
class BookmarkListSerializer(ListSerializer):
|
||||||
def to_representation(self, data):
|
def to_representation(self, data):
|
||||||
# Prefetch nested relations to avoid n+1 queries
|
# Prefetch nested relations to avoid n+1 queries
|
||||||
prefetch_related_objects(data, 'tags')
|
prefetch_related_objects(data, "tags")
|
||||||
|
|
||||||
return super().to_representation(data)
|
return super().to_representation(data)
|
||||||
|
|
||||||
@@ -23,32 +23,32 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Bookmark
|
model = Bookmark
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
"id",
|
||||||
'url',
|
"url",
|
||||||
'title',
|
"title",
|
||||||
'description',
|
"description",
|
||||||
'notes',
|
"notes",
|
||||||
'website_title',
|
"website_title",
|
||||||
'website_description',
|
"website_description",
|
||||||
'is_archived',
|
"is_archived",
|
||||||
'unread',
|
"unread",
|
||||||
'shared',
|
"shared",
|
||||||
'tag_names',
|
"tag_names",
|
||||||
'date_added',
|
"date_added",
|
||||||
'date_modified'
|
"date_modified",
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'website_title',
|
"website_title",
|
||||||
'website_description',
|
"website_description",
|
||||||
'date_added',
|
"date_added",
|
||||||
'date_modified'
|
"date_modified",
|
||||||
]
|
]
|
||||||
list_serializer_class = BookmarkListSerializer
|
list_serializer_class = BookmarkListSerializer
|
||||||
|
|
||||||
# Override optional char fields to provide default value
|
# Override optional char fields to provide default value
|
||||||
title = 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='')
|
description = serializers.CharField(required=False, allow_blank=True, default="")
|
||||||
notes = 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)
|
is_archived = serializers.BooleanField(required=False, default=False)
|
||||||
unread = serializers.BooleanField(required=False, default=False)
|
unread = serializers.BooleanField(required=False, default=False)
|
||||||
shared = 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):
|
def create(self, validated_data):
|
||||||
bookmark = Bookmark()
|
bookmark = Bookmark()
|
||||||
bookmark.url = validated_data['url']
|
bookmark.url = validated_data["url"]
|
||||||
bookmark.title = validated_data['title']
|
bookmark.title = validated_data["title"]
|
||||||
bookmark.description = validated_data['description']
|
bookmark.description = validated_data["description"]
|
||||||
bookmark.notes = validated_data['notes']
|
bookmark.notes = validated_data["notes"]
|
||||||
bookmark.is_archived = validated_data['is_archived']
|
bookmark.is_archived = validated_data["is_archived"]
|
||||||
bookmark.unread = validated_data['unread']
|
bookmark.unread = validated_data["unread"]
|
||||||
bookmark.shared = validated_data['shared']
|
bookmark.shared = validated_data["shared"]
|
||||||
tag_string = build_tag_string(validated_data['tag_names'])
|
tag_string = build_tag_string(validated_data["tag_names"])
|
||||||
return create_bookmark(bookmark, tag_string, self.context['user'])
|
return create_bookmark(bookmark, tag_string, self.context["user"])
|
||||||
|
|
||||||
def update(self, instance: Bookmark, validated_data):
|
def update(self, instance: Bookmark, validated_data):
|
||||||
# Update fields if they were provided in the payload
|
# 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:
|
if key in validated_data:
|
||||||
setattr(instance, key, validated_data[key])
|
setattr(instance, key, validated_data[key])
|
||||||
|
|
||||||
# Use tag string from payload, or use bookmark's current tags as fallback
|
# Use tag string from payload, or use bookmark's current tags as fallback
|
||||||
tag_string = build_tag_string(instance.tag_names)
|
tag_string = build_tag_string(instance.tag_names)
|
||||||
if 'tag_names' in validated_data:
|
if "tag_names" in validated_data:
|
||||||
tag_string = build_tag_string(validated_data['tag_names'])
|
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 TagSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ['id', 'name', 'date_added']
|
fields = ["id", "name", "date_added"]
|
||||||
read_only_fields = ['date_added']
|
read_only_fields = ["date_added"]
|
||||||
|
|
||||||
def create(self, validated_data):
|
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):
|
class UserProfileSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class BookmarksConfig(AppConfig):
|
class BookmarksConfig(AppConfig):
|
||||||
name = 'bookmarks'
|
name = "bookmarks"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# Register signal handlers
|
# Register signal handlers
|
||||||
|
|||||||
@@ -5,28 +5,32 @@ from bookmarks import utils
|
|||||||
|
|
||||||
def toasts(request):
|
def toasts(request):
|
||||||
user = request.user
|
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
|
has_toasts = len(toast_messages) > 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'has_toasts': has_toasts,
|
"has_toasts": has_toasts,
|
||||||
'toast_messages': toast_messages,
|
"toast_messages": toast_messages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def public_shares(request):
|
def public_shares(request):
|
||||||
# Only check for public shares for anonymous users
|
# Only check for public shares for anonymous users
|
||||||
if not request.user.is_authenticated:
|
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
|
has_public_shares = query_set.count() > 0
|
||||||
return {
|
return {
|
||||||
'has_public_shares': has_public_shares,
|
"has_public_shares": has_public_shares,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def app_version(request):
|
def app_version(request):
|
||||||
return {
|
return {"app_version": utils.app_version}
|
||||||
'app_version': utils.app_version
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,38 +6,54 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
|||||||
|
|
||||||
class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
||||||
def test_create_should_check_for_existing_bookmark(self):
|
def test_create_should_check_for_existing_bookmark(self):
|
||||||
existing_bookmark = self.setup_bookmark(title='Existing title',
|
existing_bookmark = self.setup_bookmark(
|
||||||
description='Existing description',
|
title="Existing title",
|
||||||
notes='Existing notes',
|
description="Existing description",
|
||||||
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')],
|
notes="Existing notes",
|
||||||
website_title='Existing website title',
|
tags=[self.setup_tag(name="tag1"), self.setup_tag(name="tag2")],
|
||||||
website_description='Existing website description',
|
website_title="Existing website title",
|
||||||
unread=True)
|
website_description="Existing website description",
|
||||||
tag_names = ' '.join(existing_bookmark.tag_names)
|
unread=True,
|
||||||
|
)
|
||||||
|
tag_names = " ".join(existing_bookmark.tag_names)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
browser = self.setup_browser(p)
|
browser = self.setup_browser(p)
|
||||||
page = browser.new_page()
|
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
|
# 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
|
# 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
|
# Form should be pre-filled with data from existing bookmark
|
||||||
self.assertEqual(existing_bookmark.title, page.get_by_label('Title').input_value())
|
self.assertEqual(
|
||||||
self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value())
|
existing_bookmark.title, page.get_by_label("Title").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(
|
||||||
self.assertEqual(existing_bookmark.website_description,
|
existing_bookmark.description,
|
||||||
page.get_by_label('Description').get_attribute('placeholder'))
|
page.get_by_label("Description").input_value(),
|
||||||
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.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
|
# 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
|
# 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()
|
browser.close()
|
||||||
|
|
||||||
@@ -47,21 +63,25 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
|||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
browser = self.setup_browser(p)
|
browser = self.setup_browser(p)
|
||||||
page = browser.new_page()
|
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.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):
|
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:
|
with sync_playwright() as p:
|
||||||
browser = self.setup_browser(p)
|
browser = self.setup_browser(p)
|
||||||
page = browser.new_page()
|
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')
|
details = page.locator("details.notes")
|
||||||
expect(details).not_to_have_attribute('open', value='')
|
expect(details).not_to_have_attribute("open", value="")
|
||||||
|
|
||||||
page.get_by_label('URL').fill(bookmark.url)
|
page.get_by_label("URL").fill(bookmark.url)
|
||||||
expect(details).to_have_attribute('open', value='')
|
expect(details).to_have_attribute("open", value="")
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
|||||||
class BookmarkItemE2ETestCase(LinkdingE2ETestCase):
|
class BookmarkItemE2ETestCase(LinkdingE2ETestCase):
|
||||||
@skip("Fails in CI, needs investigation")
|
@skip("Fails in CI, needs investigation")
|
||||||
def test_toggle_notes_should_show_hide_notes(self):
|
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:
|
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()
|
expect(notes).to_be_hidden()
|
||||||
|
|
||||||
toggle_notes = page.locator('li button.toggle-notes')
|
toggle_notes = page.locator("li button.toggle-notes")
|
||||||
toggle_notes.click()
|
toggle_notes.click()
|
||||||
expect(notes).to_be_visible()
|
expect(notes).to_be_visible()
|
||||||
|
|
||||||
|
|||||||
@@ -9,100 +9,180 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
def setup_test_data(self):
|
def setup_test_data(self):
|
||||||
self.setup_numbered_bookmarks(50)
|
self.setup_numbered_bookmarks(50)
|
||||||
self.setup_numbered_bookmarks(50, archived=True)
|
self.setup_numbered_bookmarks(50, archived=True)
|
||||||
self.setup_numbered_bookmarks(50, prefix='foo')
|
self.setup_numbered_bookmarks(50, prefix="foo")
|
||||||
self.setup_numbered_bookmarks(50, archived=True, 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(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
50,
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
Bookmark.objects.filter(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
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):
|
def test_active_bookmarks_bulk_select_across(self):
|
||||||
self.setup_test_data()
|
self.setup_test_data()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bulk_edit_select_all().click()
|
self.locate_bulk_edit_select_all().click()
|
||||||
self.locate_bulk_edit_select_across().click()
|
self.locate_bulk_edit_select_across().click()
|
||||||
|
|
||||||
self.select_bulk_action('Delete')
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').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(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
0,
|
||||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
Bookmark.objects.filter(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
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):
|
def test_archived_bookmarks_bulk_select_across(self):
|
||||||
self.setup_test_data()
|
self.setup_test_data()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bulk_edit_select_all().click()
|
self.locate_bulk_edit_select_all().click()
|
||||||
self.locate_bulk_edit_select_across().click()
|
self.locate_bulk_edit_select_across().click()
|
||||||
|
|
||||||
self.select_bulk_action('Delete')
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').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(
|
||||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
50,
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
Bookmark.objects.filter(
|
||||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
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):
|
def test_active_bookmarks_bulk_select_across_respects_query(self):
|
||||||
self.setup_test_data()
|
self.setup_test_data()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bulk_edit_select_all().click()
|
self.locate_bulk_edit_select_all().click()
|
||||||
self.locate_bulk_edit_select_across().click()
|
self.locate_bulk_edit_select_across().click()
|
||||||
|
|
||||||
self.select_bulk_action('Delete')
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').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(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
50,
|
||||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
Bookmark.objects.filter(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
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):
|
def test_archived_bookmarks_bulk_select_across_respects_query(self):
|
||||||
self.setup_test_data()
|
self.setup_test_data()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bulk_edit_select_all().click()
|
self.locate_bulk_edit_select_all().click()
|
||||||
self.locate_bulk_edit_select_across().click()
|
self.locate_bulk_edit_select_across().click()
|
||||||
|
|
||||||
self.select_bulk_action('Delete')
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').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(
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count())
|
50,
|
||||||
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count())
|
Bookmark.objects.filter(
|
||||||
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count())
|
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):
|
def test_select_all_toggles_all_checkboxes(self):
|
||||||
self.setup_numbered_bookmarks(5)
|
self.setup_numbered_bookmarks(5)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
page = self.open(url, p)
|
page = self.open(url, p)
|
||||||
|
|
||||||
self.locate_bulk_edit_toggle().click()
|
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())
|
self.assertEqual(6, checkboxes.count())
|
||||||
for i in range(checkboxes.count()):
|
for i in range(checkboxes.count()):
|
||||||
expect(checkboxes.nth(i)).not_to_be_checked()
|
expect(checkboxes.nth(i)).not_to_be_checked()
|
||||||
@@ -121,7 +201,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.setup_numbered_bookmarks(5)
|
self.setup_numbered_bookmarks(5)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
self.open(url, p)
|
self.open(url, p)
|
||||||
|
|
||||||
self.locate_bulk_edit_toggle().click()
|
self.locate_bulk_edit_toggle().click()
|
||||||
@@ -138,7 +218,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.setup_numbered_bookmarks(5)
|
self.setup_numbered_bookmarks(5)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
self.open(url, p)
|
self.open(url, p)
|
||||||
|
|
||||||
self.locate_bulk_edit_toggle().click()
|
self.locate_bulk_edit_toggle().click()
|
||||||
@@ -160,7 +240,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.setup_numbered_bookmarks(5)
|
self.setup_numbered_bookmarks(5)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
self.open(url, p)
|
self.open(url, p)
|
||||||
|
|
||||||
self.locate_bulk_edit_toggle().click()
|
self.locate_bulk_edit_toggle().click()
|
||||||
@@ -171,18 +251,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
expect(self.locate_bulk_edit_select_across()).to_be_checked()
|
expect(self.locate_bulk_edit_select_across()).to_be_checked()
|
||||||
|
|
||||||
# Hide select across by toggling a single bookmark
|
# 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()
|
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
|
||||||
|
|
||||||
# Show select across again, verify it is unchecked
|
# 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()
|
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
|
||||||
|
|
||||||
def test_execute_resets_all_checkboxes(self):
|
def test_execute_resets_all_checkboxes(self):
|
||||||
self.setup_numbered_bookmarks(100)
|
self.setup_numbered_bookmarks(100)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
page = self.open(url, p)
|
page = self.open(url, p)
|
||||||
|
|
||||||
# Select all bookmarks, enable select across
|
# Select all bookmarks, enable select across
|
||||||
@@ -191,18 +275,18 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.locate_bulk_edit_select_across().click()
|
self.locate_bulk_edit_select_across().click()
|
||||||
|
|
||||||
# Get reference for bookmark list
|
# Get reference for bookmark list
|
||||||
bookmark_list = page.locator('ul[ld-bookmark-list]')
|
bookmark_list = page.locator("ul[ld-bookmark-list]")
|
||||||
|
|
||||||
# Execute bulk action
|
# Execute bulk action
|
||||||
self.select_bulk_action('Mark as unread')
|
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("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||||
|
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
|
|
||||||
# Verify bulk edit checkboxes are reset
|
# 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())
|
self.assertEqual(31, checkboxes.count())
|
||||||
for i in range(checkboxes.count()):
|
for i in range(checkboxes.count()):
|
||||||
expect(checkboxes.nth(i)).not_to_be_checked()
|
expect(checkboxes.nth(i)).not_to_be_checked()
|
||||||
@@ -215,18 +299,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.setup_numbered_bookmarks(100)
|
self.setup_numbered_bookmarks(100)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
self.open(url, p)
|
self.open(url, p)
|
||||||
|
|
||||||
self.locate_bulk_edit_toggle().click()
|
self.locate_bulk_edit_toggle().click()
|
||||||
self.locate_bulk_edit_select_all().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.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').click()
|
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.locate_bulk_edit_select_all().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
|
# verify correct data is loaded on update
|
||||||
self.setup_numbered_bookmarks(3, with_tags=True)
|
self.setup_numbered_bookmarks(3, with_tags=True)
|
||||||
self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
||||||
self.setup_numbered_bookmarks(3,
|
self.setup_numbered_bookmarks(
|
||||||
shared=True,
|
3,
|
||||||
prefix="Joe's Bookmark",
|
shared=True,
|
||||||
user=self.setup_user(enable_sharing=True))
|
prefix="Joe's Bookmark",
|
||||||
|
user=self.setup_user(enable_sharing=True),
|
||||||
|
)
|
||||||
|
|
||||||
def assertVisibleBookmarks(self, titles: List[str]):
|
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))
|
expect(bookmark_tags).to_have_count(len(titles))
|
||||||
|
|
||||||
for title in titles:
|
for title in titles:
|
||||||
@@ -30,7 +32,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
expect(matching_tag).to_be_visible()
|
expect(matching_tag).to_be_visible()
|
||||||
|
|
||||||
def assertVisibleTags(self, titles: List[str]):
|
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))
|
expect(tag_tags).to_have_count(len(titles))
|
||||||
|
|
||||||
for title in titles:
|
for title in titles:
|
||||||
@@ -38,65 +40,67 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
expect(matching_tag).to_be_visible()
|
expect(matching_tag).to_be_visible()
|
||||||
|
|
||||||
def test_partial_update_respects_query(self):
|
def test_partial_update_respects_query(self):
|
||||||
self.setup_numbered_bookmarks(5, prefix='foo')
|
self.setup_numbered_bookmarks(5, prefix="foo")
|
||||||
self.setup_numbered_bookmarks(5, prefix='bar')
|
self.setup_numbered_bookmarks(5, prefix="bar")
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index') + '?q=foo'
|
url = reverse("bookmarks:index") + "?q=foo"
|
||||||
self.open(url, p)
|
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.locate_bookmark("foo 2").get_by_text("Archive").click()
|
||||||
self.assertVisibleBookmarks(['foo 1', 'foo 3', 'foo 4', 'foo 5'])
|
self.assertVisibleBookmarks(["foo 1", "foo 3", "foo 4", "foo 5"])
|
||||||
|
|
||||||
def test_partial_update_respects_sort(self):
|
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:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index') + '?sort=title_asc'
|
url = reverse("bookmarks:index") + "?sort=title_asc"
|
||||||
page = self.open(url, p)
|
page = self.open(url, p)
|
||||||
|
|
||||||
first_item = page.locator('li[ld-bookmark-item]').first
|
first_item = page.locator("li[ld-bookmark-item]").first
|
||||||
expect(first_item).to_contain_text('foo 1')
|
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
|
first_item = page.locator("li[ld-bookmark-item]").first
|
||||||
expect(first_item).to_contain_text('foo 2')
|
expect(first_item).to_contain_text("foo 2")
|
||||||
|
|
||||||
def test_partial_update_respects_page(self):
|
def test_partial_update_respects_page(self):
|
||||||
# add a suffix, otherwise 'foo 1' also matches 'foo 10'
|
# 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:
|
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)
|
self.open(url, p)
|
||||||
|
|
||||||
# with descending sort, page two has 'foo 1' to 'foo 20'
|
# 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.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)
|
self.assertVisibleBookmarks(expected_titles)
|
||||||
|
|
||||||
def test_multiple_partial_updates(self):
|
def test_multiple_partial_updates(self):
|
||||||
self.setup_numbered_bookmarks(5)
|
self.setup_numbered_bookmarks(5)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
self.open(url, p)
|
self.open(url, p)
|
||||||
|
|
||||||
self.locate_bookmark('Bookmark 1').get_by_text('Archive').click()
|
self.locate_bookmark("Bookmark 1").get_by_text("Archive").click()
|
||||||
self.assertVisibleBookmarks(['Bookmark 2', 'Bookmark 3', 'Bookmark 4', 'Bookmark 5'])
|
self.assertVisibleBookmarks(
|
||||||
|
["Bookmark 2", "Bookmark 3", "Bookmark 4", "Bookmark 5"]
|
||||||
|
)
|
||||||
|
|
||||||
self.locate_bookmark('Bookmark 2').get_by_text('Archive').click()
|
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
|
||||||
self.assertVisibleBookmarks(['Bookmark 3', 'Bookmark 4', 'Bookmark 5'])
|
self.assertVisibleBookmarks(["Bookmark 3", "Bookmark 4", "Bookmark 5"])
|
||||||
|
|
||||||
self.locate_bookmark('Bookmark 3').get_by_text('Archive').click()
|
self.locate_bookmark("Bookmark 3").get_by_text("Archive").click()
|
||||||
self.assertVisibleBookmarks(['Bookmark 4', 'Bookmark 5'])
|
self.assertVisibleBookmarks(["Bookmark 4", "Bookmark 5"])
|
||||||
|
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
@@ -104,185 +108,201 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_active_bookmarks_partial_update_on_delete(self):
|
def test_active_bookmarks_partial_update_on_delete(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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("Remove").click()
|
||||||
self.locate_bookmark('Bookmark 2').get_by_text('Confirm').click()
|
self.locate_bookmark("Bookmark 2").get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3'])
|
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_active_bookmarks_partial_update_on_mark_as_read(self):
|
def test_active_bookmarks_partial_update_on_mark_as_read(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
bookmark2 = self.get_numbered_bookmark('Bookmark 2')
|
bookmark2 = self.get_numbered_bookmark("Bookmark 2")
|
||||||
bookmark2.unread = True
|
bookmark2.unread = True
|
||||||
bookmark2.save()
|
bookmark2.save()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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')
|
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("Unread").click()
|
||||||
self.locate_bookmark('Bookmark 2').get_by_text('Yes').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)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_active_bookmarks_partial_update_on_unshare(self):
|
def test_active_bookmarks_partial_update_on_unshare(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
bookmark2 = self.get_numbered_bookmark('Bookmark 2')
|
bookmark2 = self.get_numbered_bookmark("Bookmark 2")
|
||||||
bookmark2.shared = True
|
bookmark2.shared = True
|
||||||
bookmark2.save()
|
bookmark2.save()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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')
|
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("Shared").click()
|
||||||
self.locate_bookmark('Bookmark 2').get_by_text('Yes').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)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_active_bookmarks_partial_update_on_bulk_archive(self):
|
def test_active_bookmarks_partial_update_on_bulk_archive(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
self.locate_bookmark("Bookmark 2").locator(
|
||||||
self.select_bulk_action('Archive')
|
"label[ld-bulk-edit-checkbox]"
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
).click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').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.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_active_bookmarks_partial_update_on_bulk_delete(self):
|
def test_active_bookmarks_partial_update_on_bulk_delete(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
self.locate_bookmark("Bookmark 2").locator(
|
||||||
self.select_bulk_action('Delete')
|
"label[ld-bulk-edit-checkbox]"
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
).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.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3'])
|
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(['Tag 1', 'Tag 3'])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_archived_bookmarks_partial_update_on_unarchive(self):
|
def test_archived_bookmarks_partial_update_on_unarchive(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_archived_bookmarks_partial_update_on_delete(self):
|
def test_archived_bookmarks_partial_update_on_delete(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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("Remove").click()
|
||||||
self.locate_bookmark('Archived Bookmark 2').get_by_text('Confirm').click()
|
self.locate_bookmark("Archived Bookmark 2").get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3'])
|
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_archived_bookmarks_partial_update_on_bulk_unarchive(self):
|
def test_archived_bookmarks_partial_update_on_bulk_unarchive(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
self.locate_bookmark("Archived Bookmark 2").locator(
|
||||||
self.select_bulk_action('Unarchive')
|
"label[ld-bulk-edit-checkbox]"
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
).click()
|
||||||
self.locate_bulk_edit_bar().get_by_text('Confirm').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.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_archived_bookmarks_partial_update_on_bulk_delete(self):
|
def test_archived_bookmarks_partial_update_on_bulk_delete(self):
|
||||||
self.setup_fixture()
|
self.setup_fixture()
|
||||||
|
|
||||||
with sync_playwright() as p:
|
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_toggle().click()
|
||||||
self.locate_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click()
|
self.locate_bookmark("Archived Bookmark 2").locator(
|
||||||
self.select_bulk_action('Delete')
|
"label[ld-bulk-edit-checkbox]"
|
||||||
self.locate_bulk_edit_bar().get_by_text('Execute').click()
|
).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.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3'])
|
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3'])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_shared_bookmarks_partial_update_on_unarchive(self):
|
def test_shared_bookmarks_partial_update_on_unarchive(self):
|
||||||
self.setup_fixture()
|
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:
|
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
|
# Shared bookmarks page also shows archived bookmarks, though it probably shouldn't
|
||||||
self.assertVisibleBookmarks([
|
self.assertVisibleBookmarks(
|
||||||
'My Bookmark 1',
|
[
|
||||||
'My Bookmark 2',
|
"My Bookmark 1",
|
||||||
'My Bookmark 3',
|
"My Bookmark 2",
|
||||||
"Joe's Bookmark 1",
|
"My Bookmark 3",
|
||||||
"Joe's Bookmark 2",
|
"Joe's Bookmark 1",
|
||||||
"Joe's Bookmark 3",
|
"Joe's Bookmark 2",
|
||||||
])
|
"Joe's Bookmark 3",
|
||||||
self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 2', 'Shared Tag 3'])
|
]
|
||||||
|
)
|
||||||
|
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 2", "Shared Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|
||||||
def test_shared_bookmarks_partial_update_on_delete(self):
|
def test_shared_bookmarks_partial_update_on_delete(self):
|
||||||
self.setup_fixture()
|
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:
|
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("Remove").click()
|
||||||
self.locate_bookmark('My Bookmark 2').get_by_text('Confirm').click()
|
self.locate_bookmark("My Bookmark 2").get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks([
|
self.assertVisibleBookmarks(
|
||||||
'My Bookmark 1',
|
[
|
||||||
'My Bookmark 3',
|
"My Bookmark 1",
|
||||||
"Joe's Bookmark 1",
|
"My Bookmark 3",
|
||||||
"Joe's Bookmark 2",
|
"Joe's Bookmark 1",
|
||||||
"Joe's Bookmark 3",
|
"Joe's Bookmark 2",
|
||||||
])
|
"Joe's Bookmark 3",
|
||||||
self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 3'])
|
]
|
||||||
|
)
|
||||||
|
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 3"])
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
|
|||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
browser = self.setup_browser(p)
|
browser = self.setup_browser(p)
|
||||||
page = browser.new_page()
|
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()
|
browser.close()
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
|
|||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
browser = self.setup_browser(p)
|
browser = self.setup_browser(p)
|
||||||
page = browser.new_page()
|
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()
|
browser.close()
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
|
|||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
browser = self.setup_browser(p)
|
browser = self.setup_browser(p)
|
||||||
page = browser.new_page()
|
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 = page.get_by_label("Enable bookmark sharing")
|
||||||
enable_sharing_label = page.get_by_text('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 = page.get_by_label("Enable public bookmark sharing")
|
||||||
enable_public_sharing_label = page.get_by_text('Enable public bookmark sharing')
|
enable_public_sharing_label = page.get_by_text(
|
||||||
|
"Enable public bookmark sharing"
|
||||||
|
)
|
||||||
|
|
||||||
# Public sharing is disabled by default
|
# Public sharing is disabled by default
|
||||||
expect(enable_sharing).not_to_be_checked()
|
expect(enable_sharing).not_to_be_checked()
|
||||||
|
|||||||
@@ -7,24 +7,28 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
|
|||||||
class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.client.force_login(self.get_or_create_test_user())
|
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:
|
def setup_browser(self, playwright) -> BrowserContext:
|
||||||
browser = playwright.chromium.launch(headless=True)
|
browser = playwright.chromium.launch(headless=True)
|
||||||
context = browser.new_context()
|
context = browser.new_context()
|
||||||
context.add_cookies([{
|
context.add_cookies(
|
||||||
'name': 'sessionid',
|
[
|
||||||
'value': self.cookie.value,
|
{
|
||||||
'domain': self.live_server_url.replace('http:', ''),
|
"name": "sessionid",
|
||||||
'path': '/'
|
"value": self.cookie.value,
|
||||||
}])
|
"domain": self.live_server_url.replace("http:", ""),
|
||||||
|
"path": "/",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def open(self, url: str, playwright: Playwright) -> Page:
|
def open(self, url: str, playwright: Playwright) -> Page:
|
||||||
browser = self.setup_browser(playwright)
|
browser = self.setup_browser(playwright)
|
||||||
self.page = browser.new_page()
|
self.page = browser.new_page()
|
||||||
self.page.goto(self.live_server_url + url)
|
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
|
self.num_loads = 0
|
||||||
return self.page
|
return self.page
|
||||||
|
|
||||||
@@ -35,20 +39,24 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(self.num_loads, count)
|
self.assertEqual(self.num_loads, count)
|
||||||
|
|
||||||
def locate_bookmark(self, title: str):
|
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)
|
return bookmark_tags.filter(has_text=title)
|
||||||
|
|
||||||
def locate_bulk_edit_bar(self):
|
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):
|
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):
|
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):
|
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):
|
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):
|
def sanitize(text: str):
|
||||||
if not text:
|
if not text:
|
||||||
return ''
|
return ""
|
||||||
# remove control characters
|
# remove control characters
|
||||||
valid_chars = ['\n', '\r', '\t']
|
valid_chars = ["\n", "\r", "\t"]
|
||||||
return ''.join(ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != 'C')
|
return "".join(
|
||||||
|
ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != "C"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseBookmarksFeed(Feed):
|
class BaseBookmarksFeed(Feed):
|
||||||
def get_object(self, request, feed_key: str):
|
def get_object(self, request, feed_key: str):
|
||||||
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
||||||
search = BookmarkSearch(q=request.GET.get('q', ''))
|
search = BookmarkSearch(q=request.GET.get("q", ""))
|
||||||
query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, search)
|
query_set = queries.query_bookmarks(
|
||||||
|
feed_token.user, feed_token.user.profile, search
|
||||||
|
)
|
||||||
return FeedContext(feed_token, query_set)
|
return FeedContext(feed_token, query_set)
|
||||||
|
|
||||||
def item_title(self, item: Bookmark):
|
def item_title(self, item: Bookmark):
|
||||||
@@ -44,22 +48,22 @@ class BaseBookmarksFeed(Feed):
|
|||||||
|
|
||||||
|
|
||||||
class AllBookmarksFeed(BaseBookmarksFeed):
|
class AllBookmarksFeed(BaseBookmarksFeed):
|
||||||
title = 'All bookmarks'
|
title = "All bookmarks"
|
||||||
description = 'All bookmarks'
|
description = "All bookmarks"
|
||||||
|
|
||||||
def link(self, context: FeedContext):
|
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):
|
def items(self, context: FeedContext):
|
||||||
return context.query_set
|
return context.query_set
|
||||||
|
|
||||||
|
|
||||||
class UnreadBookmarksFeed(BaseBookmarksFeed):
|
class UnreadBookmarksFeed(BaseBookmarksFeed):
|
||||||
title = 'Unread bookmarks'
|
title = "Unread bookmarks"
|
||||||
description = 'All unread bookmarks'
|
description = "All unread bookmarks"
|
||||||
|
|
||||||
def link(self, context: FeedContext):
|
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):
|
def items(self, context: FeedContext):
|
||||||
return context.query_set.filter(unread=True)
|
return context.query_set.filter(unread=True)
|
||||||
|
|||||||
@@ -59,10 +59,18 @@ class BookmarkItem {
|
|||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
|
|
||||||
|
// Toggle notes
|
||||||
const notesToggle = element.querySelector(".toggle-notes");
|
const notesToggle = element.querySelector(".toggle-notes");
|
||||||
if (notesToggle) {
|
if (notesToggle) {
|
||||||
notesToggle.addEventListener("click", this.onToggleNotes.bind(this));
|
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) {
|
onToggleNotes(event) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
async function init() {
|
async function init() {
|
||||||
// For now we cache all tags on load as the template did before
|
// For now we cache all tags on load as the template did before
|
||||||
try {
|
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()))
|
tags.sort((left, right) => left.name.toLowerCase().localeCompare(right.name.toLowerCase()))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('TagAutocomplete: Error loading tag list');
|
console.warn('TagAutocomplete: Error loading tag list');
|
||||||
|
|||||||
26
bookmarks/management/commands/backup.py
Normal file
26
bookmarks/management/commands/backup.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
destination = options["destination"]
|
||||||
|
|
||||||
|
def progress(status, remaining, total):
|
||||||
|
self.stdout.write(f"Copied {total-remaining} of {total} pages...")
|
||||||
|
|
||||||
|
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}"))
|
||||||
@@ -12,18 +12,20 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
superuser_name = os.getenv('LD_SUPERUSER_NAME', None)
|
superuser_name = os.getenv("LD_SUPERUSER_NAME", None)
|
||||||
superuser_password = os.getenv('LD_SUPERUSER_PASSWORD', None)
|
superuser_password = os.getenv("LD_SUPERUSER_PASSWORD", None)
|
||||||
|
|
||||||
# Skip if option is undefined
|
# Skip if option is undefined
|
||||||
if not superuser_name:
|
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
|
return
|
||||||
|
|
||||||
# Skip if user already exists
|
# Skip if user already exists
|
||||||
user_exists = User.objects.filter(username=superuser_name).exists()
|
user_exists = User.objects.filter(username=superuser_name).exists()
|
||||||
if user_exists:
|
if user_exists:
|
||||||
logger.info('Skip creating initial superuser, user already exists')
|
logger.info("Skip creating initial superuser, user already exists")
|
||||||
return
|
return
|
||||||
|
|
||||||
user = User(username=superuser_name, is_superuser=True, is_staff=True)
|
user = User(username=superuser_name, is_superuser=True, is_staff=True)
|
||||||
@@ -34,4 +36,4 @@ class Command(BaseCommand):
|
|||||||
user.set_unusable_password()
|
user.set_unusable_password()
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
logger.info('Created initial superuser')
|
logger.info("Created initial superuser")
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ class Command(BaseCommand):
|
|||||||
if not settings.USE_SQLITE:
|
if not settings.USE_SQLITE:
|
||||||
return
|
return
|
||||||
|
|
||||||
connection = connections['default']
|
connection = connections["default"]
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute("PRAGMA journal_mode")
|
cursor.execute("PRAGMA journal_mode")
|
||||||
current_mode = cursor.fetchone()[0]
|
current_mode = cursor.fetchone()[0]
|
||||||
logger.info(f'Current journal mode: {current_mode}')
|
logger.info(f"Current journal mode: {current_mode}")
|
||||||
if current_mode != 'wal':
|
if current_mode != "wal":
|
||||||
cursor.execute("PRAGMA journal_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"
|
help = "Creates an admin user non-interactively if it doesn't exist"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--username', help="Admin's username")
|
parser.add_argument("--username", help="Admin's username")
|
||||||
parser.add_argument('--email', help="Admin's email")
|
parser.add_argument("--email", help="Admin's email")
|
||||||
parser.add_argument('--password', help="Admin's password")
|
parser.add_argument("--password", help="Admin's password")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
if not User.objects.filter(username=options['username']).exists():
|
if not User.objects.filter(username=options["username"]).exists():
|
||||||
User.objects.create_superuser(username=options['username'],
|
User.objects.create_superuser(
|
||||||
email=options['email'],
|
username=options["username"],
|
||||||
password=options['password'])
|
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):
|
class Command(BaseCommand):
|
||||||
help = 'Import Netscape HTML bookmark file'
|
help = "Import Netscape HTML bookmark file"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('file', type=str, help='Path to file')
|
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(
|
||||||
|
"user", type=str, help="Name of the user for which to import"
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
filepath = kwargs['file']
|
filepath = kwargs["file"]
|
||||||
username = kwargs['user']
|
username = kwargs["user"]
|
||||||
with open(filepath) as html_file:
|
with open(filepath) as html_file:
|
||||||
html = html_file.read()
|
html = html_file.read()
|
||||||
user = User.objects.get(username=username)
|
user = User.objects.get(username=username)
|
||||||
|
|||||||
@@ -15,19 +15,36 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Bookmark',
|
name="Bookmark",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('url', models.URLField()),
|
"id",
|
||||||
('title', models.CharField(max_length=512)),
|
models.AutoField(
|
||||||
('description', models.TextField()),
|
auto_created=True,
|
||||||
('website_title', models.CharField(blank=True, max_length=512, null=True)),
|
primary_key=True,
|
||||||
('website_description', models.TextField(blank=True, null=True)),
|
serialize=False,
|
||||||
('unread', models.BooleanField(default=True)),
|
verbose_name="ID",
|
||||||
('date_added', models.DateTimeField()),
|
),
|
||||||
('date_modified', models.DateTimeField()),
|
),
|
||||||
('date_accessed', models.DateTimeField(blank=True, null=True)),
|
("url", models.URLField()),
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
("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 = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('bookmarks', '0001_initial'),
|
("bookmarks", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Tag',
|
name="Tag",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('name', models.CharField(max_length=64)),
|
"id",
|
||||||
('date_added', models.DateTimeField()),
|
models.AutoField(
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
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(
|
migrations.AddField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='tags',
|
name="tags",
|
||||||
field=models.ManyToManyField(to='bookmarks.Tag'),
|
field=models.ManyToManyField(to="bookmarks.Tag"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0002_auto_20190629_2303'),
|
("bookmarks", "0002_auto_20190629_2303"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='url',
|
name="url",
|
||||||
field=models.URLField(max_length=2048),
|
field=models.URLField(max_length=2048),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0003_auto_20200913_0656'),
|
("bookmarks", "0003_auto_20200913_0656"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='description',
|
name="description",
|
||||||
field=models.TextField(blank=True),
|
field=models.TextField(blank=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='title',
|
name="title",
|
||||||
field=models.CharField(blank=True, max_length=512),
|
field=models.CharField(blank=True, max_length=512),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0004_auto_20200926_1028'),
|
("bookmarks", "0004_auto_20200926_1028"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='url',
|
name="url",
|
||||||
field=models.CharField(max_length=2048, validators=[bookmarks.validators.BookmarkURLValidator()]),
|
field=models.CharField(
|
||||||
|
max_length=2048,
|
||||||
|
validators=[bookmarks.validators.BookmarkURLValidator()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0005_auto_20210103_1212'),
|
("bookmarks", "0005_auto_20210103_1212"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='is_archived',
|
name="is_archived",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
def forwards(apps, schema_editor):
|
def forwards(apps, schema_editor):
|
||||||
User = apps.get_model('auth', 'User')
|
User = apps.get_model("auth", "User")
|
||||||
UserProfile = apps.get_model('bookmarks', 'UserProfile')
|
UserProfile = apps.get_model("bookmarks", "UserProfile")
|
||||||
for user in User.objects.all():
|
for user in User.objects.all():
|
||||||
try:
|
try:
|
||||||
if user.profile:
|
if user.profile:
|
||||||
@@ -24,19 +24,42 @@ def reverse(apps, schema_editor):
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('bookmarks', '0006_bookmark_is_archived'),
|
("bookmarks", "0006_bookmark_is_archived"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserProfile',
|
name="UserProfile",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('theme',
|
"id",
|
||||||
models.CharField(choices=[('auto', 'Auto'), ('light', 'Light'), ('dark', 'Dark')], default='auto',
|
models.AutoField(
|
||||||
max_length=10)),
|
auto_created=True,
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile',
|
primary_key=True,
|
||||||
to=settings.AUTH_USER_MODEL)),
|
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),
|
migrations.RunPython(forwards, reverse),
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0007_userprofile'),
|
("bookmarks", "0007_userprofile"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='bookmark_date_display',
|
name="bookmark_date_display",
|
||||||
field=models.CharField(choices=[('relative', 'Relative'), ('absolute', 'Absolute'), ('hidden', 'Hidden')], default='relative', max_length=10),
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0008_userprofile_bookmark_date_display'),
|
("bookmarks", "0008_userprofile_bookmark_date_display"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='web_archive_snapshot_url',
|
name="web_archive_snapshot_url",
|
||||||
field=models.CharField(blank=True, max_length=2048),
|
field=models.CharField(blank=True, max_length=2048),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0009_bookmark_web_archive_snapshot_url'),
|
("bookmarks", "0009_bookmark_web_archive_snapshot_url"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='bookmark_link_target',
|
name="bookmark_link_target",
|
||||||
field=models.CharField(choices=[('_blank', 'New page'), ('_self', 'Same page')], default='_blank', max_length=10),
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0010_userprofile_bookmark_link_target'),
|
("bookmarks", "0010_userprofile_bookmark_link_target"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='web_archive_integration',
|
name="web_archive_integration",
|
||||||
field=models.CharField(choices=[('disabled', 'Disabled'), ('enabled', 'Enabled')], default='disabled', max_length=10),
|
field=models.CharField(
|
||||||
|
choices=[("disabled", "Disabled"), ("enabled", "Enabled")],
|
||||||
|
default="disabled",
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,18 +9,32 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('bookmarks', '0011_userprofile_web_archive_integration'),
|
("bookmarks", "0011_userprofile_web_archive_integration"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Toast',
|
name="Toast",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('key', models.CharField(max_length=50)),
|
"id",
|
||||||
('message', models.TextField()),
|
models.AutoField(
|
||||||
('acknowledged', models.BooleanField(default=False)),
|
auto_created=True,
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
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):
|
def forwards(apps, schema_editor):
|
||||||
for user in User.objects.all():
|
for user in User.objects.all():
|
||||||
toast = Toast(key='web_archive_opt_in_hint',
|
toast = Toast(
|
||||||
message='The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.',
|
key="web_archive_opt_in_hint",
|
||||||
owner=user)
|
message="The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.",
|
||||||
|
owner=user,
|
||||||
|
)
|
||||||
toast.save()
|
toast.save()
|
||||||
|
|
||||||
|
|
||||||
def reverse(apps, schema_editor):
|
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):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0012_toast'),
|
("bookmarks", "0012_toast"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
def forwards(apps, schema_editor):
|
def forwards(apps, schema_editor):
|
||||||
Bookmark = apps.get_model('bookmarks', 'Bookmark')
|
Bookmark = apps.get_model("bookmarks", "Bookmark")
|
||||||
Bookmark.objects.update(unread=False)
|
Bookmark.objects.update(unread=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -14,13 +14,13 @@ def reverse(apps, schema_editor):
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0013_web_archive_optin_toast'),
|
("bookmarks", "0013_web_archive_optin_toast"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='unread',
|
name="unread",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.RunPython(forwards, reverse),
|
migrations.RunPython(forwards, reverse),
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('bookmarks', '0014_alter_bookmark_unread'),
|
("bookmarks", "0014_alter_bookmark_unread"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='FeedToken',
|
name="FeedToken",
|
||||||
fields=[
|
fields=[
|
||||||
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
|
(
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
"key",
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feed_token', to=settings.AUTH_USER_MODEL)),
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0015_feedtoken'),
|
("bookmarks", "0015_feedtoken"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='shared',
|
name="shared",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0016_bookmark_shared'),
|
("bookmarks", "0016_bookmark_shared"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='enable_sharing',
|
name="enable_sharing",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0017_userprofile_enable_sharing'),
|
("bookmarks", "0017_userprofile_enable_sharing"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='favicon_file',
|
name="favicon_file",
|
||||||
field=models.CharField(blank=True, max_length=512),
|
field=models.CharField(blank=True, max_length=512),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0018_bookmark_favicon_file'),
|
("bookmarks", "0018_bookmark_favicon_file"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='enable_favicons',
|
name="enable_favicons",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0019_userprofile_enable_favicons'),
|
("bookmarks", "0019_userprofile_enable_favicons"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='tag_search',
|
name="tag_search",
|
||||||
field=models.CharField(choices=[('strict', 'Strict'), ('lax', 'Lax')], default='strict', max_length=10),
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0020_userprofile_tag_search'),
|
("bookmarks", "0020_userprofile_tag_search"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='display_url',
|
name="display_url",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0021_userprofile_display_url'),
|
("bookmarks", "0021_userprofile_display_url"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bookmark',
|
model_name="bookmark",
|
||||||
name='notes',
|
name="notes",
|
||||||
field=models.TextField(blank=True),
|
field=models.TextField(blank=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0022_bookmark_notes'),
|
("bookmarks", "0022_bookmark_notes"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='permanent_notes',
|
name="permanent_notes",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0023_userprofile_permanent_notes'),
|
("bookmarks", "0023_userprofile_permanent_notes"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='enable_public_sharing',
|
name="enable_public_sharing",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookmarks', '0024_userprofile_enable_public_sharing'),
|
("bookmarks", "0024_userprofile_enable_public_sharing"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userprofile',
|
model_name="userprofile",
|
||||||
name='search_preferences',
|
name="search_preferences",
|
||||||
field=models.JSONField(default=dict),
|
field=models.JSONField(default=dict),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ class Tag(models.Model):
|
|||||||
def sanitize_tag_name(tag_name: str):
|
def sanitize_tag_name(tag_name: str):
|
||||||
# strip leading/trailing spaces
|
# strip leading/trailing spaces
|
||||||
# replace inner spaces with replacement char
|
# 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:
|
if not tag_string:
|
||||||
return []
|
return []
|
||||||
names = tag_string.strip().split(delimiter)
|
names = tag_string.strip().split(delimiter)
|
||||||
@@ -42,7 +42,7 @@ def parse_tag_string(tag_string: str, delimiter: str = ','):
|
|||||||
return names
|
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)
|
return delimiter.join(tag_names)
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class Bookmark(models.Model):
|
|||||||
return [tag.name for tag in self.tags.all()]
|
return [tag.name for tag in self.tags.all()]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.resolved_title + ' (' + self.url[:30] + '...)'
|
return self.resolved_title + " (" + self.url[:30] + "...)"
|
||||||
|
|
||||||
|
|
||||||
class BookmarkForm(forms.ModelForm):
|
class BookmarkForm(forms.ModelForm):
|
||||||
@@ -90,15 +90,13 @@ class BookmarkForm(forms.ModelForm):
|
|||||||
url = forms.CharField(validators=[BookmarkURLValidator()])
|
url = forms.CharField(validators=[BookmarkURLValidator()])
|
||||||
tag_string = forms.CharField(required=False)
|
tag_string = forms.CharField(required=False)
|
||||||
# Do not require title and description in form as we fill these automatically if they are empty
|
# Do not require title and description in form as we fill these automatically if they are empty
|
||||||
title = forms.CharField(max_length=512,
|
title = forms.CharField(max_length=512, required=False)
|
||||||
required=False)
|
description = forms.CharField(required=False, widget=forms.Textarea())
|
||||||
description = forms.CharField(required=False,
|
|
||||||
widget=forms.Textarea())
|
|
||||||
# Include website title and description as hidden field as they only provide info when editing bookmarks
|
# Include website title and description as hidden field as they only provide info when editing bookmarks
|
||||||
website_title = forms.CharField(max_length=512,
|
website_title = forms.CharField(
|
||||||
required=False, widget=forms.HiddenInput())
|
max_length=512, required=False, widget=forms.HiddenInput()
|
||||||
website_description = forms.CharField(required=False,
|
)
|
||||||
widget=forms.HiddenInput())
|
website_description = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||||
unread = forms.BooleanField(required=False)
|
unread = forms.BooleanField(required=False)
|
||||||
shared = forms.BooleanField(required=False)
|
shared = forms.BooleanField(required=False)
|
||||||
# Hidden field that determines whether to close window/tab after saving the bookmark
|
# Hidden field that determines whether to close window/tab after saving the bookmark
|
||||||
@@ -107,16 +105,16 @@ class BookmarkForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Bookmark
|
model = Bookmark
|
||||||
fields = [
|
fields = [
|
||||||
'url',
|
"url",
|
||||||
'tag_string',
|
"tag_string",
|
||||||
'title',
|
"title",
|
||||||
'description',
|
"description",
|
||||||
'notes',
|
"notes",
|
||||||
'website_title',
|
"website_title",
|
||||||
'website_description',
|
"website_description",
|
||||||
'unread',
|
"unread",
|
||||||
'shared',
|
"shared",
|
||||||
'auto_close',
|
"auto_close",
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -125,45 +123,47 @@ class BookmarkForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class BookmarkSearch:
|
class BookmarkSearch:
|
||||||
SORT_ADDED_ASC = 'added_asc'
|
SORT_ADDED_ASC = "added_asc"
|
||||||
SORT_ADDED_DESC = 'added_desc'
|
SORT_ADDED_DESC = "added_desc"
|
||||||
SORT_TITLE_ASC = 'title_asc'
|
SORT_TITLE_ASC = "title_asc"
|
||||||
SORT_TITLE_DESC = 'title_desc'
|
SORT_TITLE_DESC = "title_desc"
|
||||||
|
|
||||||
FILTER_SHARED_OFF = 'off'
|
FILTER_SHARED_OFF = "off"
|
||||||
FILTER_SHARED_SHARED = 'yes'
|
FILTER_SHARED_SHARED = "yes"
|
||||||
FILTER_SHARED_UNSHARED = 'no'
|
FILTER_SHARED_UNSHARED = "no"
|
||||||
|
|
||||||
FILTER_UNREAD_OFF = 'off'
|
FILTER_UNREAD_OFF = "off"
|
||||||
FILTER_UNREAD_YES = 'yes'
|
FILTER_UNREAD_YES = "yes"
|
||||||
FILTER_UNREAD_NO = 'no'
|
FILTER_UNREAD_NO = "no"
|
||||||
|
|
||||||
params = ['q', 'user', 'sort', 'shared', 'unread']
|
params = ["q", "user", "sort", "shared", "unread"]
|
||||||
preferences = ['sort', 'shared', 'unread']
|
preferences = ["sort", "shared", "unread"]
|
||||||
defaults = {
|
defaults = {
|
||||||
'q': '',
|
"q": "",
|
||||||
'user': '',
|
"user": "",
|
||||||
'sort': SORT_ADDED_DESC,
|
"sort": SORT_ADDED_DESC,
|
||||||
'shared': FILTER_SHARED_OFF,
|
"shared": FILTER_SHARED_OFF,
|
||||||
'unread': FILTER_UNREAD_OFF,
|
"unread": FILTER_UNREAD_OFF,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
q: str = None,
|
self,
|
||||||
user: str = None,
|
q: str = None,
|
||||||
sort: str = None,
|
user: str = None,
|
||||||
shared: str = None,
|
sort: str = None,
|
||||||
unread: str = None,
|
shared: str = None,
|
||||||
preferences: dict = None):
|
unread: str = None,
|
||||||
|
preferences: dict = None,
|
||||||
|
):
|
||||||
if not preferences:
|
if not preferences:
|
||||||
preferences = {}
|
preferences = {}
|
||||||
self.defaults = {**BookmarkSearch.defaults, **preferences}
|
self.defaults = {**BookmarkSearch.defaults, **preferences}
|
||||||
|
|
||||||
self.q = q or self.defaults['q']
|
self.q = q or self.defaults["q"]
|
||||||
self.user = user or self.defaults['user']
|
self.user = user or self.defaults["user"]
|
||||||
self.sort = sort or self.defaults['sort']
|
self.sort = sort or self.defaults["sort"]
|
||||||
self.shared = shared or self.defaults['shared']
|
self.shared = shared or self.defaults["shared"]
|
||||||
self.unread = unread or self.defaults['unread']
|
self.unread = unread or self.defaults["unread"]
|
||||||
|
|
||||||
def is_modified(self, param):
|
def is_modified(self, param):
|
||||||
value = self.__dict__[param]
|
value = self.__dict__[param]
|
||||||
@@ -175,7 +175,11 @@ class BookmarkSearch:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def modified_preferences(self):
|
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
|
@property
|
||||||
def has_modifications(self):
|
def has_modifications(self):
|
||||||
@@ -191,7 +195,9 @@ class BookmarkSearch:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def preferences_dict(self):
|
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
|
@staticmethod
|
||||||
def from_request(query_dict: QueryDict, preferences: dict = None):
|
def from_request(query_dict: QueryDict, preferences: dict = None):
|
||||||
@@ -206,20 +212,20 @@ class BookmarkSearch:
|
|||||||
|
|
||||||
class BookmarkSearchForm(forms.Form):
|
class BookmarkSearchForm(forms.Form):
|
||||||
SORT_CHOICES = [
|
SORT_CHOICES = [
|
||||||
(BookmarkSearch.SORT_ADDED_ASC, 'Added ↑'),
|
(BookmarkSearch.SORT_ADDED_ASC, "Added ↑"),
|
||||||
(BookmarkSearch.SORT_ADDED_DESC, 'Added ↓'),
|
(BookmarkSearch.SORT_ADDED_DESC, "Added ↓"),
|
||||||
(BookmarkSearch.SORT_TITLE_ASC, 'Title ↑'),
|
(BookmarkSearch.SORT_TITLE_ASC, "Title ↑"),
|
||||||
(BookmarkSearch.SORT_TITLE_DESC, 'Title ↓'),
|
(BookmarkSearch.SORT_TITLE_DESC, "Title ↓"),
|
||||||
]
|
]
|
||||||
FILTER_SHARED_CHOICES = [
|
FILTER_SHARED_CHOICES = [
|
||||||
(BookmarkSearch.FILTER_SHARED_OFF, 'Off'),
|
(BookmarkSearch.FILTER_SHARED_OFF, "Off"),
|
||||||
(BookmarkSearch.FILTER_SHARED_SHARED, 'Shared'),
|
(BookmarkSearch.FILTER_SHARED_SHARED, "Shared"),
|
||||||
(BookmarkSearch.FILTER_SHARED_UNSHARED, 'Unshared'),
|
(BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"),
|
||||||
]
|
]
|
||||||
FILTER_UNREAD_CHOICES = [
|
FILTER_UNREAD_CHOICES = [
|
||||||
(BookmarkSearch.FILTER_UNREAD_OFF, 'Off'),
|
(BookmarkSearch.FILTER_UNREAD_OFF, "Off"),
|
||||||
(BookmarkSearch.FILTER_UNREAD_YES, 'Unread'),
|
(BookmarkSearch.FILTER_UNREAD_YES, "Unread"),
|
||||||
(BookmarkSearch.FILTER_UNREAD_NO, 'Read'),
|
(BookmarkSearch.FILTER_UNREAD_NO, "Read"),
|
||||||
]
|
]
|
||||||
|
|
||||||
q = forms.CharField()
|
q = forms.CharField()
|
||||||
@@ -228,7 +234,12 @@ class BookmarkSearchForm(forms.Form):
|
|||||||
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
|
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
|
||||||
unread = forms.ChoiceField(choices=FILTER_UNREAD_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__()
|
super().__init__()
|
||||||
editable_fields = editable_fields or []
|
editable_fields = editable_fields or []
|
||||||
self.editable_fields = editable_fields
|
self.editable_fields = editable_fields
|
||||||
@@ -236,8 +247,8 @@ class BookmarkSearchForm(forms.Form):
|
|||||||
# set choices for user field if users are provided
|
# set choices for user field if users are provided
|
||||||
if users:
|
if users:
|
||||||
user_choices = [(user.username, user.username) for user in users]
|
user_choices = [(user.username, user.username) for user in users]
|
||||||
user_choices.insert(0, ('', 'Everyone'))
|
user_choices.insert(0, ("", "Everyone"))
|
||||||
self.fields['user'].choices = user_choices
|
self.fields["user"].choices = user_choices
|
||||||
|
|
||||||
for param in search.params:
|
for param in search.params:
|
||||||
# set initial values for modified params
|
# set initial values for modified params
|
||||||
@@ -251,50 +262,70 @@ class BookmarkSearchForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.Model):
|
||||||
THEME_AUTO = 'auto'
|
THEME_AUTO = "auto"
|
||||||
THEME_LIGHT = 'light'
|
THEME_LIGHT = "light"
|
||||||
THEME_DARK = 'dark'
|
THEME_DARK = "dark"
|
||||||
THEME_CHOICES = [
|
THEME_CHOICES = [
|
||||||
(THEME_AUTO, 'Auto'),
|
(THEME_AUTO, "Auto"),
|
||||||
(THEME_LIGHT, 'Light'),
|
(THEME_LIGHT, "Light"),
|
||||||
(THEME_DARK, 'Dark'),
|
(THEME_DARK, "Dark"),
|
||||||
]
|
]
|
||||||
BOOKMARK_DATE_DISPLAY_RELATIVE = 'relative'
|
BOOKMARK_DATE_DISPLAY_RELATIVE = "relative"
|
||||||
BOOKMARK_DATE_DISPLAY_ABSOLUTE = 'absolute'
|
BOOKMARK_DATE_DISPLAY_ABSOLUTE = "absolute"
|
||||||
BOOKMARK_DATE_DISPLAY_HIDDEN = 'hidden'
|
BOOKMARK_DATE_DISPLAY_HIDDEN = "hidden"
|
||||||
BOOKMARK_DATE_DISPLAY_CHOICES = [
|
BOOKMARK_DATE_DISPLAY_CHOICES = [
|
||||||
(BOOKMARK_DATE_DISPLAY_RELATIVE, 'Relative'),
|
(BOOKMARK_DATE_DISPLAY_RELATIVE, "Relative"),
|
||||||
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, 'Absolute'),
|
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, "Absolute"),
|
||||||
(BOOKMARK_DATE_DISPLAY_HIDDEN, 'Hidden'),
|
(BOOKMARK_DATE_DISPLAY_HIDDEN, "Hidden"),
|
||||||
]
|
]
|
||||||
BOOKMARK_LINK_TARGET_BLANK = '_blank'
|
BOOKMARK_LINK_TARGET_BLANK = "_blank"
|
||||||
BOOKMARK_LINK_TARGET_SELF = '_self'
|
BOOKMARK_LINK_TARGET_SELF = "_self"
|
||||||
BOOKMARK_LINK_TARGET_CHOICES = [
|
BOOKMARK_LINK_TARGET_CHOICES = [
|
||||||
(BOOKMARK_LINK_TARGET_BLANK, 'New page'),
|
(BOOKMARK_LINK_TARGET_BLANK, "New page"),
|
||||||
(BOOKMARK_LINK_TARGET_SELF, 'Same page'),
|
(BOOKMARK_LINK_TARGET_SELF, "Same page"),
|
||||||
]
|
]
|
||||||
WEB_ARCHIVE_INTEGRATION_DISABLED = 'disabled'
|
WEB_ARCHIVE_INTEGRATION_DISABLED = "disabled"
|
||||||
WEB_ARCHIVE_INTEGRATION_ENABLED = 'enabled'
|
WEB_ARCHIVE_INTEGRATION_ENABLED = "enabled"
|
||||||
WEB_ARCHIVE_INTEGRATION_CHOICES = [
|
WEB_ARCHIVE_INTEGRATION_CHOICES = [
|
||||||
(WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'),
|
(WEB_ARCHIVE_INTEGRATION_DISABLED, "Disabled"),
|
||||||
(WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'),
|
(WEB_ARCHIVE_INTEGRATION_ENABLED, "Enabled"),
|
||||||
]
|
]
|
||||||
TAG_SEARCH_STRICT = 'strict'
|
TAG_SEARCH_STRICT = "strict"
|
||||||
TAG_SEARCH_LAX = 'lax'
|
TAG_SEARCH_LAX = "lax"
|
||||||
TAG_SEARCH_CHOICES = [
|
TAG_SEARCH_CHOICES = [
|
||||||
(TAG_SEARCH_STRICT, 'Strict'),
|
(TAG_SEARCH_STRICT, "Strict"),
|
||||||
(TAG_SEARCH_LAX, 'Lax'),
|
(TAG_SEARCH_LAX, "Lax"),
|
||||||
]
|
]
|
||||||
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
user = models.OneToOneField(
|
||||||
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
get_user_model(), related_name="profile", on_delete=models.CASCADE
|
||||||
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False,
|
)
|
||||||
default=BOOKMARK_DATE_DISPLAY_RELATIVE)
|
theme = models.CharField(
|
||||||
bookmark_link_target = models.CharField(max_length=10, choices=BOOKMARK_LINK_TARGET_CHOICES, blank=False,
|
max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO
|
||||||
default=BOOKMARK_LINK_TARGET_BLANK)
|
)
|
||||||
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
|
bookmark_date_display = models.CharField(
|
||||||
default=WEB_ARCHIVE_INTEGRATION_DISABLED)
|
max_length=10,
|
||||||
tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False,
|
choices=BOOKMARK_DATE_DISPLAY_CHOICES,
|
||||||
default=TAG_SEARCH_STRICT)
|
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_sharing = models.BooleanField(default=False, null=False)
|
||||||
enable_public_sharing = models.BooleanField(default=False, null=False)
|
enable_public_sharing = models.BooleanField(default=False, null=False)
|
||||||
enable_favicons = 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 UserProfileForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserProfile
|
model = UserProfile
|
||||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search',
|
fields = [
|
||||||
'enable_sharing', 'enable_public_sharing', 'enable_favicons', 'display_url', 'permanent_notes']
|
"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())
|
@receiver(post_save, sender=get_user_model())
|
||||||
@@ -332,11 +373,13 @@ class FeedToken(models.Model):
|
|||||||
"""
|
"""
|
||||||
Adapted from authtoken.models.Token
|
Adapted from authtoken.models.Token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = models.CharField(max_length=40, primary_key=True)
|
key = models.CharField(max_length=40, primary_key=True)
|
||||||
user = models.OneToOneField(get_user_model(),
|
user = models.OneToOneField(
|
||||||
related_name='feed_token',
|
get_user_model(),
|
||||||
on_delete=models.CASCADE,
|
related_name="feed_token",
|
||||||
)
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -10,18 +10,24 @@ from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
|
|||||||
from bookmarks.utils import unique
|
from bookmarks.utils import unique
|
||||||
|
|
||||||
|
|
||||||
def query_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
def query_bookmarks(
|
||||||
return _base_bookmarks_query(user, profile, search) \
|
user: User, profile: UserProfile, search: BookmarkSearch
|
||||||
.filter(is_archived=False)
|
) -> QuerySet:
|
||||||
|
return _base_bookmarks_query(user, profile, search).filter(is_archived=False)
|
||||||
|
|
||||||
|
|
||||||
def query_archived_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet:
|
def query_archived_bookmarks(
|
||||||
return _base_bookmarks_query(user, profile, search) \
|
user: User, profile: UserProfile, search: BookmarkSearch
|
||||||
.filter(is_archived=True)
|
) -> QuerySet:
|
||||||
|
return _base_bookmarks_query(user, profile, search).filter(is_archived=True)
|
||||||
|
|
||||||
|
|
||||||
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, search: BookmarkSearch,
|
def query_shared_bookmarks(
|
||||||
public_only: bool) -> QuerySet:
|
user: Optional[User],
|
||||||
|
profile: UserProfile,
|
||||||
|
search: BookmarkSearch,
|
||||||
|
public_only: bool,
|
||||||
|
) -> QuerySet:
|
||||||
conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True)
|
conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True)
|
||||||
if public_only:
|
if public_only:
|
||||||
conditions = conditions & Q(owner__profile__enable_public_sharing=True)
|
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)
|
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
|
query_set = Bookmark.objects
|
||||||
|
|
||||||
# Filter for user
|
# Filter for user
|
||||||
@@ -40,34 +48,32 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
|
|||||||
query = parse_query_string(search.q)
|
query = parse_query_string(search.q)
|
||||||
|
|
||||||
# Filter for search terms and tags
|
# Filter for search terms and tags
|
||||||
for term in query['search_terms']:
|
for term in query["search_terms"]:
|
||||||
conditions = Q(title__icontains=term) \
|
conditions = (
|
||||||
| Q(description__icontains=term) \
|
Q(title__icontains=term)
|
||||||
| Q(notes__icontains=term) \
|
| Q(description__icontains=term)
|
||||||
| Q(website_title__icontains=term) \
|
| Q(notes__icontains=term)
|
||||||
| Q(website_description__icontains=term) \
|
| Q(website_title__icontains=term)
|
||||||
| Q(url__icontains=term)
|
| Q(website_description__icontains=term)
|
||||||
|
| Q(url__icontains=term)
|
||||||
|
)
|
||||||
|
|
||||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
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)
|
query_set = query_set.filter(conditions)
|
||||||
|
|
||||||
for tag_name in query['tag_names']:
|
for tag_name in query["tag_names"]:
|
||||||
query_set = query_set.filter(
|
query_set = query_set.filter(tags__name__iexact=tag_name)
|
||||||
tags__name__iexact=tag_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# Untagged bookmarks
|
# Untagged bookmarks
|
||||||
if query['untagged']:
|
if query["untagged"]:
|
||||||
query_set = query_set.filter(
|
query_set = query_set.filter(tags=None)
|
||||||
tags=None
|
|
||||||
)
|
|
||||||
# Legacy unread bookmarks filter from query
|
# Legacy unread bookmarks filter from query
|
||||||
if query['unread']:
|
if query["unread"]:
|
||||||
query_set = query_set.filter(
|
query_set = query_set.filter(unread=True)
|
||||||
unread=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unread filter from bookmark search
|
# Unread filter from bookmark search
|
||||||
if search.unread == BookmarkSearch.FILTER_UNREAD_YES:
|
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
|
# Sort by date added
|
||||||
if search.sort == BookmarkSearch.SORT_ADDED_ASC:
|
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:
|
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
|
# 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
|
# For the title, the resolved_title logic from the Bookmark entity needs
|
||||||
# to be replicated as there is no corresponding database field
|
# to be replicated as there is no corresponding database field
|
||||||
query_set = query_set.annotate(
|
query_set = query_set.annotate(
|
||||||
effective_title=Case(
|
effective_title=Case(
|
||||||
When(Q(title__isnull=False) & ~Q(title__exact=''), then=Lower('title')),
|
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')),
|
When(
|
||||||
default=Lower('url'),
|
Q(website_title__isnull=False) & ~Q(website_title__exact=""),
|
||||||
output_field=CharField()
|
then=Lower("website_title"),
|
||||||
))
|
),
|
||||||
|
default=Lower("url"),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# For SQLite, if the ICU extension is loaded, use the custom collation
|
# For SQLite, if the ICU extension is loaded, use the custom collation
|
||||||
# loaded into the connection. This results in an improved sort order for
|
# loaded into the connection. This results in an improved sort order for
|
||||||
# unicode characters (umlauts, etc.)
|
# unicode characters (umlauts, etc.)
|
||||||
if settings.USE_SQLITE and settings.USE_SQLITE_ICU_EXTENSION:
|
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:
|
else:
|
||||||
order_field = 'effective_title'
|
order_field = "effective_title"
|
||||||
|
|
||||||
if search.sort == BookmarkSearch.SORT_TITLE_ASC:
|
if search.sort == BookmarkSearch.SORT_TITLE_ASC:
|
||||||
query_set = query_set.order_by(order_field)
|
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
|
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)
|
bookmarks_query = query_bookmarks(user, profile, search)
|
||||||
|
|
||||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
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()
|
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)
|
bookmarks_query = query_archived_bookmarks(user, profile, search)
|
||||||
|
|
||||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
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()
|
return query_set.distinct()
|
||||||
|
|
||||||
|
|
||||||
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, search: BookmarkSearch,
|
def query_shared_bookmark_tags(
|
||||||
public_only: bool) -> QuerySet:
|
user: Optional[User],
|
||||||
|
profile: UserProfile,
|
||||||
|
search: BookmarkSearch,
|
||||||
|
public_only: bool,
|
||||||
|
) -> QuerySet:
|
||||||
bookmarks_query = query_shared_bookmarks(user, profile, search, public_only)
|
bookmarks_query = query_shared_bookmarks(user, profile, search, public_only)
|
||||||
|
|
||||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
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()
|
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)
|
bookmarks_query = query_shared_bookmarks(None, profile, search, public_only)
|
||||||
|
|
||||||
query_set = User.objects.filter(bookmark__in=bookmarks_query)
|
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):
|
def parse_query_string(query_string):
|
||||||
# Sanitize query params
|
# Sanitize query params
|
||||||
if not query_string:
|
if not query_string:
|
||||||
query_string = ''
|
query_string = ""
|
||||||
|
|
||||||
# Split query into search terms and tags
|
# 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]
|
keywords = [word for word in keywords if word]
|
||||||
|
|
||||||
search_terms = [word for word in keywords if word[0] != '#' and 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 = [word[1:] for word in keywords if word[0] == "#"]
|
||||||
tag_names = unique(tag_names, str.lower)
|
tag_names = unique(tag_names, str.lower)
|
||||||
|
|
||||||
# Special search commands
|
# Special search commands
|
||||||
untagged = '!untagged' in keywords
|
untagged = "!untagged" in keywords
|
||||||
unread = '!unread' in keywords
|
unread = "!unread" in keywords
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'search_terms': search_terms,
|
"search_terms": search_terms,
|
||||||
'tag_names': tag_names,
|
"tag_names": tag_names,
|
||||||
'untagged': untagged,
|
"untagged": untagged,
|
||||||
'unread': unread,
|
"unread": unread,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ from bookmarks.services import tasks
|
|||||||
|
|
||||||
def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User):
|
def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User):
|
||||||
# If URL is already bookmarked, then update it
|
# 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:
|
if existing_bookmark is not None:
|
||||||
_merge_bookmark_data(bookmark, existing_bookmark)
|
_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):
|
def archive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
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):
|
def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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)
|
tag_names = parse_tag_string(tag_string)
|
||||||
tags = get_or_create_tags(tag_names, current_user)
|
tags = get_or_create_tags(tag_names, current_user)
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||||
bookmark.tags.add(*tags)
|
relationships = []
|
||||||
bookmark.date_modified = timezone.now()
|
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)
|
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)
|
tag_names = parse_tag_string(tag_string)
|
||||||
tags = get_or_create_tags(tag_names, current_user)
|
tags = get_or_create_tags(tag_names, current_user)
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||||
bookmark.tags.remove(*tags)
|
for tag in tags:
|
||||||
bookmark.date_modified = timezone.now()
|
# 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):
|
def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
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):
|
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
||||||
|
|||||||
@@ -13,37 +13,41 @@ def export_netscape_html(bookmarks: List[Bookmark]):
|
|||||||
[append_bookmark(doc, bookmark) for bookmark in bookmarks]
|
[append_bookmark(doc, bookmark) for bookmark in bookmarks]
|
||||||
append_list_end(doc)
|
append_list_end(doc)
|
||||||
|
|
||||||
return '\n\r'.join(doc)
|
return "\n\r".join(doc)
|
||||||
|
|
||||||
|
|
||||||
def append_header(doc: BookmarkDocument):
|
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('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">')
|
||||||
doc.append('<TITLE>Bookmarks</TITLE>')
|
doc.append("<TITLE>Bookmarks</TITLE>")
|
||||||
doc.append('<H1>Bookmarks</H1>')
|
doc.append("<H1>Bookmarks</H1>")
|
||||||
|
|
||||||
|
|
||||||
def append_list_start(doc: BookmarkDocument):
|
def append_list_start(doc: BookmarkDocument):
|
||||||
doc.append('<DL><p>')
|
doc.append("<DL><p>")
|
||||||
|
|
||||||
|
|
||||||
def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
|
def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
|
||||||
url = bookmark.url
|
url = bookmark.url
|
||||||
title = html.escape(bookmark.resolved_title or '')
|
title = html.escape(bookmark.resolved_title or "")
|
||||||
desc = html.escape(bookmark.resolved_description or '')
|
desc = html.escape(bookmark.resolved_description or "")
|
||||||
if bookmark.notes:
|
if bookmark.notes:
|
||||||
desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]'
|
desc += f"[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]"
|
||||||
tags = ','.join(bookmark.tag_names)
|
tag_names = bookmark.tag_names
|
||||||
toread = '1' if bookmark.unread else '0'
|
if bookmark.is_archived:
|
||||||
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())
|
added = int(bookmark.date_added.timestamp())
|
||||||
|
|
||||||
doc.append(
|
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:
|
if desc:
|
||||||
doc.append(f'<DD>{desc}')
|
doc.append(f"<DD>{desc}")
|
||||||
|
|
||||||
|
|
||||||
def append_list_end(doc: BookmarkDocument):
|
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
|
# register mime type for .ico files, which is not included in the default
|
||||||
# mimetypes of the Docker image
|
# mimetypes of the Docker image
|
||||||
mimetypes.add_type('image/x-icon', '.ico')
|
mimetypes.add_type("image/x-icon", ".ico")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_favicon_folder():
|
def _ensure_favicon_folder():
|
||||||
@@ -23,16 +23,16 @@ def _ensure_favicon_folder():
|
|||||||
|
|
||||||
|
|
||||||
def _url_to_filename(url: str) -> str:
|
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:
|
def _get_url_parameters(url: str) -> dict:
|
||||||
parsed_uri = urlparse(url)
|
parsed_uri = urlparse(url)
|
||||||
return {
|
return {
|
||||||
# https://example.com/foo?bar -> https://example.com
|
# 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
|
# 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
|
# Create favicon folder if not exists
|
||||||
_ensure_favicon_folder()
|
_ensure_favicon_folder()
|
||||||
# Use scheme+hostname as favicon filename to reuse icon for all pages on the same domain
|
# 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)
|
favicon_file = _check_existing_favicon(favicon_name)
|
||||||
|
|
||||||
if not favicon_file:
|
if not favicon_file:
|
||||||
# Load favicon from provider, save to file
|
# Load favicon from provider, save to file
|
||||||
favicon_url = settings.LD_FAVICON_PROVIDER.format(**url_parameters)
|
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:
|
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)
|
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)
|
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):
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
file.write(chunk)
|
file.write(chunk)
|
||||||
logger.debug(f'Saved favicon as: {favicon_path}')
|
logger.debug(f"Saved favicon as: {favicon_path}")
|
||||||
|
|
||||||
return favicon_file
|
return favicon_file
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import List
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookmarks.models import Bookmark, Tag, parse_tag_string
|
from bookmarks.models import Bookmark, Tag
|
||||||
from bookmarks.services import tasks
|
from bookmarks.services import tasks
|
||||||
from bookmarks.services.parser import parse, NetscapeBookmark
|
from bookmarks.services.parser import parse, NetscapeBookmark
|
||||||
from bookmarks.utils import parse_timestamp
|
from bookmarks.utils import parse_timestamp
|
||||||
@@ -55,18 +55,20 @@ class TagCache:
|
|||||||
self.cache[tag.name.lower()] = tag
|
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()
|
result = ImportResult()
|
||||||
import_start = timezone.now()
|
import_start = timezone.now()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
netscape_bookmarks = parse(html)
|
netscape_bookmarks = parse(html)
|
||||||
except:
|
except:
|
||||||
logging.exception('Could not read bookmarks file.')
|
logging.exception("Could not read bookmarks file.")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
parse_end = timezone.now()
|
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 and cache all tags beforehand
|
||||||
_create_missing_tags(netscape_bookmarks, user)
|
_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)
|
tasks.schedule_bookmarks_without_favicons(user)
|
||||||
|
|
||||||
end = timezone.now()
|
end = timezone.now()
|
||||||
logger.debug(f'Import duration: {end - import_start}')
|
logger.debug(f"Import duration: {end - import_start}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -93,8 +95,7 @@ def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User)
|
|||||||
tags_to_create = []
|
tags_to_create = []
|
||||||
|
|
||||||
for netscape_bookmark in netscape_bookmarks:
|
for netscape_bookmark in netscape_bookmarks:
|
||||||
tag_names = parse_tag_string(netscape_bookmark.tag_string)
|
for tag_name in netscape_bookmark.tag_names:
|
||||||
for tag_name in tag_names:
|
|
||||||
tag = tag_cache.get(tag_name)
|
tag = tag_cache.get(tag_name)
|
||||||
if not tag:
|
if not tag:
|
||||||
tag = Tag(name=tag_name, owner=user)
|
tag = Tag(name=tag_name, owner=user)
|
||||||
@@ -111,7 +112,7 @@ def _get_batches(items: List, batch_size: int):
|
|||||||
num_items = len(items)
|
num_items = len(items)
|
||||||
|
|
||||||
while offset < num_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:
|
if len(batch) > 0:
|
||||||
batches.append(batch)
|
batches.append(batch)
|
||||||
offset = offset + batch_size
|
offset = offset + batch_size
|
||||||
@@ -119,11 +120,13 @@ def _get_batches(items: List, batch_size: int):
|
|||||||
return batches
|
return batches
|
||||||
|
|
||||||
|
|
||||||
def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
def _import_batch(
|
||||||
user: User,
|
netscape_bookmarks: List[NetscapeBookmark],
|
||||||
options: ImportOptions,
|
user: User,
|
||||||
tag_cache: TagCache,
|
options: ImportOptions,
|
||||||
result: ImportResult):
|
tag_cache: TagCache,
|
||||||
|
result: ImportResult,
|
||||||
|
):
|
||||||
# Query existing bookmarks
|
# Query existing bookmarks
|
||||||
batch_urls = [bookmark.href for bookmark in netscape_bookmarks]
|
batch_urls = [bookmark.href for bookmark in netscape_bookmarks]
|
||||||
existing_bookmarks = Bookmark.objects.filter(owner=user, url__in=batch_urls)
|
existing_bookmarks = Bookmark.objects.filter(owner=user, url__in=batch_urls)
|
||||||
@@ -137,7 +140,13 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
|||||||
try:
|
try:
|
||||||
# Lookup existing bookmark by URL, or create new bookmark if there is no bookmark for that URL yet
|
# Lookup existing bookmark by URL, or create new bookmark if there is no bookmark for that URL yet
|
||||||
bookmark = next(
|
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:
|
if not bookmark:
|
||||||
bookmark = Bookmark(owner=user)
|
bookmark = Bookmark(owner=user)
|
||||||
is_update = False
|
is_update = False
|
||||||
@@ -147,7 +156,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
|||||||
_copy_bookmark_data(netscape_bookmark, bookmark, options)
|
_copy_bookmark_data(netscape_bookmark, bookmark, options)
|
||||||
# Validate bookmark fields, exclude owner to prevent n+1 database query,
|
# Validate bookmark fields, exclude owner to prevent n+1 database query,
|
||||||
# also there is no specific validation on owner
|
# also there is no specific validation on owner
|
||||||
bookmark.clean_fields(exclude=['owner'])
|
bookmark.clean_fields(exclude=["owner"])
|
||||||
# Schedule for update or insert
|
# Schedule for update or insert
|
||||||
if is_update:
|
if is_update:
|
||||||
bookmarks_to_update.append(bookmark)
|
bookmarks_to_update.append(bookmark)
|
||||||
@@ -156,20 +165,25 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
|||||||
|
|
||||||
result.success = result.success + 1
|
result.success = result.success + 1
|
||||||
except:
|
except:
|
||||||
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + '...'
|
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..."
|
||||||
logging.exception('Error importing bookmark: ' + shortened_bookmark_tag_str)
|
logging.exception("Error importing bookmark: " + shortened_bookmark_tag_str)
|
||||||
result.failed = result.failed + 1
|
result.failed = result.failed + 1
|
||||||
|
|
||||||
# Bulk update bookmarks in DB
|
# Bulk update bookmarks in DB
|
||||||
Bookmark.objects.bulk_update(bookmarks_to_update, ['url',
|
Bookmark.objects.bulk_update(
|
||||||
'date_added',
|
bookmarks_to_update,
|
||||||
'date_modified',
|
[
|
||||||
'unread',
|
"url",
|
||||||
'shared',
|
"date_added",
|
||||||
'title',
|
"date_modified",
|
||||||
'description',
|
"unread",
|
||||||
'notes',
|
"shared",
|
||||||
'owner'])
|
"title",
|
||||||
|
"description",
|
||||||
|
"notes",
|
||||||
|
"owner",
|
||||||
|
],
|
||||||
|
)
|
||||||
# Bulk insert new bookmarks into DB
|
# Bulk insert new bookmarks into DB
|
||||||
Bookmark.objects.bulk_create(bookmarks_to_create)
|
Bookmark.objects.bulk_create(bookmarks_to_create)
|
||||||
|
|
||||||
@@ -184,18 +198,24 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
|||||||
for netscape_bookmark in netscape_bookmarks:
|
for netscape_bookmark in netscape_bookmarks:
|
||||||
# Lookup bookmark by URL again
|
# Lookup bookmark by URL again
|
||||||
bookmark = next(
|
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:
|
if not bookmark:
|
||||||
# Something is wrong, we should have just created this 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(
|
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
|
continue
|
||||||
|
|
||||||
# Get tag models by string, schedule inserts for bookmark -> tag associations
|
# Get tag models by string, schedule inserts for bookmark -> tag associations
|
||||||
tag_names = parse_tag_string(netscape_bookmark.tag_string)
|
tags = tag_cache.get_all(netscape_bookmark.tag_names)
|
||||||
tags = tag_cache.get_all(tag_names)
|
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
relationships.append(BookmarkToTagRelationShip(bookmark=bookmark, tag=tag))
|
relationships.append(BookmarkToTagRelationShip(bookmark=bookmark, tag=tag))
|
||||||
|
|
||||||
@@ -203,7 +223,9 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
|
|||||||
BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True)
|
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
|
bookmark.url = netscape_bookmark.href
|
||||||
if netscape_bookmark.date_added:
|
if netscape_bookmark.date_added:
|
||||||
bookmark.date_added = parse_timestamp(netscape_bookmark.date_added)
|
bookmark.date_added = parse_timestamp(netscape_bookmark.date_added)
|
||||||
@@ -219,3 +241,5 @@ def _copy_bookmark_data(netscape_bookmark: NetscapeBookmark, bookmark: Bookmark,
|
|||||||
bookmark.notes = netscape_bookmark.notes
|
bookmark.notes = netscape_bookmark.notes
|
||||||
if options.map_private_flag and not netscape_bookmark.private:
|
if options.map_private_flag and not netscape_bookmark.private:
|
||||||
bookmark.shared = True
|
bookmark.shared = True
|
||||||
|
if netscape_bookmark.archived:
|
||||||
|
bookmark.is_archived = True
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from dataclasses import dataclass
|
|||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from bookmarks.models import parse_tag_string
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NetscapeBookmark:
|
class NetscapeBookmark:
|
||||||
@@ -10,9 +12,10 @@ class NetscapeBookmark:
|
|||||||
description: str
|
description: str
|
||||||
notes: str
|
notes: str
|
||||||
date_added: str
|
date_added: str
|
||||||
tag_string: str
|
tag_names: List[str]
|
||||||
to_read: bool
|
to_read: bool
|
||||||
private: bool
|
private: bool
|
||||||
|
archived: bool
|
||||||
|
|
||||||
|
|
||||||
class BookmarkParser(HTMLParser):
|
class BookmarkParser(HTMLParser):
|
||||||
@@ -22,29 +25,29 @@ class BookmarkParser(HTMLParser):
|
|||||||
|
|
||||||
self.current_tag = None
|
self.current_tag = None
|
||||||
self.bookmark = None
|
self.bookmark = None
|
||||||
self.href = ''
|
self.href = ""
|
||||||
self.add_date = ''
|
self.add_date = ""
|
||||||
self.tags = ''
|
self.tags = ""
|
||||||
self.title = ''
|
self.title = ""
|
||||||
self.description = ''
|
self.description = ""
|
||||||
self.notes = ''
|
self.notes = ""
|
||||||
self.toread = ''
|
self.toread = ""
|
||||||
self.private = ''
|
self.private = ""
|
||||||
|
|
||||||
def handle_starttag(self, tag: str, attrs: list):
|
def handle_starttag(self, tag: str, attrs: list):
|
||||||
name = 'handle_start_' + tag.lower()
|
name = "handle_start_" + tag.lower()
|
||||||
if name in dir(self):
|
if name in dir(self):
|
||||||
getattr(self, name)({k.lower(): v for k, v in attrs})
|
getattr(self, name)({k.lower(): v for k, v in attrs})
|
||||||
self.current_tag = tag
|
self.current_tag = tag
|
||||||
|
|
||||||
def handle_endtag(self, tag: str):
|
def handle_endtag(self, tag: str):
|
||||||
name = 'handle_end_' + tag.lower()
|
name = "handle_end_" + tag.lower()
|
||||||
if name in dir(self):
|
if name in dir(self):
|
||||||
getattr(self, name)()
|
getattr(self, name)()
|
||||||
self.current_tag = None
|
self.current_tag = None
|
||||||
|
|
||||||
def handle_data(self, data):
|
def handle_data(self, data):
|
||||||
name = f'handle_{self.current_tag}_data'
|
name = f"handle_{self.current_tag}_data"
|
||||||
if name in dir(self):
|
if name in dir(self):
|
||||||
getattr(self, name)(data)
|
getattr(self, name)(data)
|
||||||
|
|
||||||
@@ -56,16 +59,24 @@ class BookmarkParser(HTMLParser):
|
|||||||
|
|
||||||
def handle_start_a(self, attrs: Dict[str, str]):
|
def handle_start_a(self, attrs: Dict[str, str]):
|
||||||
vars(self).update(attrs)
|
vars(self).update(attrs)
|
||||||
|
tag_names = parse_tag_string(self.tags)
|
||||||
|
archived = "linkding:archived" in self.tags
|
||||||
|
try:
|
||||||
|
tag_names.remove("linkding:archived")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
self.bookmark = NetscapeBookmark(
|
self.bookmark = NetscapeBookmark(
|
||||||
href=self.href,
|
href=self.href,
|
||||||
title='',
|
title="",
|
||||||
description='',
|
description="",
|
||||||
notes='',
|
notes="",
|
||||||
date_added=self.add_date,
|
date_added=self.add_date,
|
||||||
tag_string=self.tags,
|
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
|
# Mark as private by default, also when attribute is not specified
|
||||||
private=self.private != '0',
|
private=self.private != "0",
|
||||||
|
archived=archived,
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_a_data(self, data):
|
def handle_a_data(self, data):
|
||||||
@@ -73,9 +84,9 @@ class BookmarkParser(HTMLParser):
|
|||||||
|
|
||||||
def handle_dd_data(self, data):
|
def handle_dd_data(self, data):
|
||||||
desc = data.strip()
|
desc = data.strip()
|
||||||
if '[linkding-notes]' in desc:
|
if "[linkding-notes]" in desc:
|
||||||
self.notes = desc.split('[linkding-notes]')[1].split('[/linkding-notes]')[0]
|
self.notes = desc.split("[linkding-notes]")[1].split("[/linkding-notes]")[0]
|
||||||
self.description = desc.split('[linkding-notes]')[0]
|
self.description = desc.split("[linkding-notes]")[0]
|
||||||
|
|
||||||
def add_bookmark(self):
|
def add_bookmark(self):
|
||||||
if self.bookmark:
|
if self.bookmark:
|
||||||
@@ -84,14 +95,14 @@ class BookmarkParser(HTMLParser):
|
|||||||
self.bookmark.notes = self.notes
|
self.bookmark.notes = self.notes
|
||||||
self.bookmarks.append(self.bookmark)
|
self.bookmarks.append(self.bookmark)
|
||||||
self.bookmark = None
|
self.bookmark = None
|
||||||
self.href = ''
|
self.href = ""
|
||||||
self.add_date = ''
|
self.add_date = ""
|
||||||
self.tags = ''
|
self.tags = ""
|
||||||
self.title = ''
|
self.title = ""
|
||||||
self.description = ''
|
self.description = ""
|
||||||
self.notes = ''
|
self.notes = ""
|
||||||
self.toread = ''
|
self.toread = ""
|
||||||
self.private = ''
|
self.private = ""
|
||||||
|
|
||||||
|
|
||||||
def parse(html: str) -> List[NetscapeBookmark]:
|
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):
|
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]
|
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):
|
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:
|
def is_web_archive_integration_active(user: User) -> bool:
|
||||||
background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS
|
background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS
|
||||||
web_archive_integration_enabled = \
|
web_archive_integration_enabled = (
|
||||||
user.profile.web_archive_integration == UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
|
user.profile.web_archive_integration
|
||||||
|
== UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
|
||||||
|
)
|
||||||
|
|
||||||
return background_tasks_enabled and 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):
|
def _load_newest_snapshot(bookmark: Bookmark):
|
||||||
try:
|
try:
|
||||||
logger.info(f'Load existing snapshot for bookmark. url={bookmark.url}')
|
logger.info(f"Load existing snapshot for bookmark. url={bookmark.url}")
|
||||||
cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(bookmark.url)
|
cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(
|
||||||
|
bookmark.url
|
||||||
|
)
|
||||||
existing_snapshot = cdx_api.newest()
|
existing_snapshot = cdx_api.newest()
|
||||||
|
|
||||||
if existing_snapshot:
|
if existing_snapshot:
|
||||||
bookmark.web_archive_snapshot_url = existing_snapshot.archive_url
|
bookmark.web_archive_snapshot_url = existing_snapshot.archive_url
|
||||||
bookmark.save(update_fields=['web_archive_snapshot_url'])
|
bookmark.save(update_fields=["web_archive_snapshot_url"])
|
||||||
logger.info(f'Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}')
|
logger.info(
|
||||||
|
f"Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}"
|
||||||
|
)
|
||||||
|
|
||||||
except NoCDXRecordFound:
|
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:
|
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):
|
def _create_snapshot(bookmark: Bookmark):
|
||||||
logger.info(f'Create new snapshot for bookmark. url={bookmark.url}...')
|
logger.info(f"Create new snapshot for bookmark. url={bookmark.url}...")
|
||||||
archive = waybackpy.WaybackMachineSaveAPI(bookmark.url, DEFAULT_USER_AGENT, max_tries=1)
|
archive = waybackpy.WaybackMachineSaveAPI(
|
||||||
|
bookmark.url, DEFAULT_USER_AGENT, max_tries=1
|
||||||
|
)
|
||||||
archive.save()
|
archive.save()
|
||||||
bookmark.web_archive_snapshot_url = archive.archive_url
|
bookmark.web_archive_snapshot_url = archive.archive_url
|
||||||
bookmark.save(update_fields=['web_archive_snapshot_url'])
|
bookmark.save(update_fields=["web_archive_snapshot_url"])
|
||||||
logger.info(f'Successfully created new snapshot for bookmark:. url={bookmark.url}')
|
logger.info(f"Successfully created new snapshot for bookmark:. url={bookmark.url}")
|
||||||
|
|
||||||
|
|
||||||
@background()
|
@background()
|
||||||
@@ -72,10 +82,13 @@ def _create_web_archive_snapshot_task(bookmark_id: int, force_update: bool):
|
|||||||
return
|
return
|
||||||
except TooManyRequestsError:
|
except TooManyRequestsError:
|
||||||
logger.error(
|
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:
|
except WaybackError as error:
|
||||||
logger.error(f'Failed to create snapshot, trying to load newest snapshot as fallback. url={bookmark.url}',
|
logger.error(
|
||||||
exc_info=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 the newest snapshot as fallback
|
||||||
_load_newest_snapshot(bookmark)
|
_load_newest_snapshot(bookmark)
|
||||||
@@ -102,7 +115,9 @@ def schedule_bookmarks_without_snapshots(user: User):
|
|||||||
@background()
|
@background()
|
||||||
def _schedule_bookmarks_without_snapshots_task(user_id: int):
|
def _schedule_bookmarks_without_snapshots_task(user_id: int):
|
||||||
user = get_user_model().objects.get(id=user_id)
|
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:
|
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
|
# 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:
|
except Bookmark.DoesNotExist:
|
||||||
return
|
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)
|
new_favicon_file = favicon_loader.load_favicon(bookmark.url)
|
||||||
|
|
||||||
if new_favicon_file != bookmark.favicon_file:
|
if new_favicon_file != bookmark.favicon_file:
|
||||||
bookmark.favicon_file = new_favicon_file
|
bookmark.favicon_file = new_favicon_file
|
||||||
bookmark.save(update_fields=['favicon_file'])
|
bookmark.save(update_fields=["favicon_file"])
|
||||||
logger.info(f'Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}')
|
logger.info(
|
||||||
|
f"Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def schedule_bookmarks_without_favicons(user: User):
|
def schedule_bookmarks_without_favicons(user: User):
|
||||||
@@ -146,11 +163,13 @@ def schedule_bookmarks_without_favicons(user: User):
|
|||||||
@background()
|
@background()
|
||||||
def _schedule_bookmarks_without_favicons_task(user_id: int):
|
def _schedule_bookmarks_without_favicons_task(user_id: int):
|
||||||
user = get_user_model().objects.get(id=user_id)
|
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 = []
|
tasks = []
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
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)
|
tasks.append(task)
|
||||||
|
|
||||||
Task.objects.bulk_create(tasks)
|
Task.objects.bulk_create(tasks)
|
||||||
@@ -168,7 +187,9 @@ def _schedule_refresh_favicons_task(user_id: int):
|
|||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
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)
|
tasks.append(task)
|
||||||
|
|
||||||
Task.objects.bulk_create(tasks)
|
Task.objects.bulk_create(tasks)
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI):
|
|||||||
|
|
||||||
def newest(self):
|
def newest(self):
|
||||||
unix_timestamp = int(time.time())
|
unix_timestamp = int(time.time())
|
||||||
self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(unix_timestamp)
|
self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(
|
||||||
self.sort = 'closest'
|
unix_timestamp
|
||||||
|
)
|
||||||
|
self.sort = "closest"
|
||||||
self.limit = -5
|
self.limit = -5
|
||||||
|
|
||||||
newest_snapshot = None
|
newest_snapshot = None
|
||||||
@@ -37,4 +39,4 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI):
|
|||||||
super().add_payload(payload)
|
super().add_payload(payload)
|
||||||
# Set fastLatest query param, as we are only using this API to get the latest snapshot and using fastLatest
|
# 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
|
# makes searching for latest snapshots faster
|
||||||
payload['fastLatest'] = 'true'
|
payload["fastLatest"] = "true"
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ class WebsiteMetadata:
|
|||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'url': self.url,
|
"url": self.url,
|
||||||
'title': self.title,
|
"title": self.title,
|
||||||
'description': self.description,
|
"description": self.description,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -34,17 +34,29 @@ def load_website_metadata(url: str):
|
|||||||
start = timezone.now()
|
start = timezone.now()
|
||||||
page_text = load_page(url)
|
page_text = load_page(url)
|
||||||
end = timezone.now()
|
end = timezone.now()
|
||||||
logger.debug(f'Load duration: {end - start}')
|
logger.debug(f"Load duration: {end - start}")
|
||||||
|
|
||||||
start = timezone.now()
|
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
|
title = soup.title.string.strip() if soup.title is not None else None
|
||||||
description_tag = soup.find('meta', attrs={'name': 'description'})
|
description_tag = soup.find("meta", attrs={"name": "description"})
|
||||||
description = description = description_tag['content'].strip() if description_tag and description_tag[
|
description = (
|
||||||
'content'] else None
|
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()
|
end = timezone.now()
|
||||||
logger.debug(f'Parsing duration: {end - start}')
|
logger.debug(f"Parsing duration: {end - start}")
|
||||||
finally:
|
finally:
|
||||||
return WebsiteMetadata(url=url, title=title, description=description)
|
return WebsiteMetadata(url=url, title=title, description=description)
|
||||||
|
|
||||||
@@ -68,30 +80,30 @@ def load_page(url: str):
|
|||||||
else:
|
else:
|
||||||
content = content + chunk
|
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
|
# 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:
|
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
|
content = content.split(end_of_head)[0] + end_of_head
|
||||||
break
|
break
|
||||||
# Stop reading if we exceed limit
|
# Stop reading if we exceed limit
|
||||||
if size > MAX_CONTENT_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
|
break
|
||||||
if hasattr(r, '_content_consumed'):
|
if hasattr(r, "_content_consumed"):
|
||||||
logger.debug(f'Request consumed: {r._content_consumed}')
|
logger.debug(f"Request consumed: {r._content_consumed}")
|
||||||
|
|
||||||
# Use charset_normalizer to determine encoding that best matches the response content
|
# 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
|
# 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,
|
# This is different from Response.text which does respect the encoding specified in the response first,
|
||||||
# before trying to determine one
|
# before trying to determine one
|
||||||
results = from_bytes(content or '')
|
results = from_bytes(content or "")
|
||||||
return str(results.best())
|
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():
|
def fake_request_headers():
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ def user_logged_in(sender, request, user, **kwargs):
|
|||||||
def extend_sqlite(connection=None, **kwargs):
|
def extend_sqlite(connection=None, **kwargs):
|
||||||
# Load ICU extension into Sqlite connection to support case-insensitive
|
# Load ICU extension into Sqlite connection to support case-insensitive
|
||||||
# comparisons with unicode characters
|
# 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.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:
|
with connection.cursor() as cursor:
|
||||||
# Load an ICU collation for case-insensitive ordering.
|
# Load an ICU collation for case-insensitive ordering.
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ ul.bookmark-list {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Bookmarks */
|
/* Bookmarks */
|
||||||
li[ld-bookmark-item] {
|
li[ld-bookmark-item] {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -122,6 +134,27 @@ li[ld-bookmark-item] {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
&.unread .title a {
|
||||||
|
|||||||
@@ -43,6 +43,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
<i class="form-icon"></i>
|
<i class="form-icon"></i>
|
||||||
</label>
|
</label>
|
||||||
<div class="title">
|
<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 %}
|
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
|
||||||
<img src="{% static bookmark_item.favicon_file %}" alt="">
|
<img src="{% static bookmark_item.favicon_file %}" alt="">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ bookmark_item.title }}
|
<span>{{ bookmark_item.title }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% if bookmark_list.show_url %}
|
{% if bookmark_list.show_url %}
|
||||||
|
|||||||
@@ -115,8 +115,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ cancel_url }}" class="btn">Nevermind</a>
|
<a href="{{ cancel_url }}" class="btn">Nevermind</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
/**
|
/**
|
||||||
* - Pre-fill title and description placeholders with metadata from website as soon as URL changes
|
* - 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 descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
|
||||||
const notesDetails = document.querySelector('form details.notes');
|
const notesDetails = document.querySelector('form details.notes');
|
||||||
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
|
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 unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
||||||
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
||||||
const websiteTitleInput = document.getElementById('{{ form.website_title.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');
|
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
|
||||||
|
|
||||||
if (existingBookmark && !editedBookmarkId) {
|
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';
|
bookmarkExistsHint.style['display'] = 'block';
|
||||||
notesDetails.open = !!existingBookmark.notes;
|
notesDetails.open = !!existingBookmark.notes;
|
||||||
updateInput(titleInput, existingBookmark.title);
|
updateInput(titleInput, existingBookmark.title);
|
||||||
|
|||||||
@@ -43,6 +43,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -122,5 +122,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
props: {
|
props: {
|
||||||
name: 'q',
|
name: 'q',
|
||||||
placeholder: 'Search for words or #tags',
|
placeholder: 'Search for words or #tags',
|
||||||
value: '{{ search.q|safe }}',
|
value: input.value,
|
||||||
tags: uniqueTags,
|
tags: uniqueTags,
|
||||||
mode: '{{ mode }}',
|
mode: '{{ mode }}',
|
||||||
linkTarget: '{{ request.user_profile.bookmark_link_target }}',
|
linkTarget: '{{ request.user_profile.bookmark_link_target }}',
|
||||||
|
|||||||
@@ -46,6 +46,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
Machine</a>.
|
Machine</a>.
|
||||||
This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in
|
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.
|
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.
|
rel="noopener">Internet Archive</a> if you make use of this feature.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<h2>Browser Extension</h2>
|
<h2>Browser Extension</h2>
|
||||||
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The extension is available in the official extension stores for:</p>
|
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The extension is available in the official extension stores for:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://addons.mozilla.org/de/firefox/addon/linkding-extension/" target="_blank">Firefox</a></li>
|
<li><a href="https://addons.mozilla.org/firefox/addon/linkding-extension/" target="_blank">Firefox</a></li>
|
||||||
<li><a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe" target="_blank">Chrome</a></li>
|
<li><a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe" target="_blank">Chrome</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>The extension is <a href="https://github.com/sissbruecker/linkding-extension" target="_blank">open source</a> as well, which enables you to build and manually load it into any browser that supports Chrome extensions.</p>
|
<p>The extension is <a href="https://github.com/sissbruecker/linkding-extension" target="_blank">open source</a> as well, which enables you to build and manually load it into any browser that supports Chrome extensions.</p>
|
||||||
|
|||||||
@@ -2,48 +2,67 @@ from typing import List
|
|||||||
|
|
||||||
from django import template
|
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 = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('bookmarks/form.html', name='bookmark_form', takes_context=True)
|
@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):
|
def bookmark_form(
|
||||||
|
context,
|
||||||
|
form: BookmarkForm,
|
||||||
|
cancel_url: str,
|
||||||
|
bookmark_id: int = 0,
|
||||||
|
auto_close: bool = False,
|
||||||
|
):
|
||||||
return {
|
return {
|
||||||
'request': context['request'],
|
"request": context["request"],
|
||||||
'form': form,
|
"form": form,
|
||||||
'auto_close': auto_close,
|
"auto_close": auto_close,
|
||||||
'bookmark_id': bookmark_id,
|
"bookmark_id": bookmark_id,
|
||||||
'cancel_url': cancel_url
|
"cancel_url": cancel_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('bookmarks/search.html', name='bookmark_search', takes_context=True)
|
@register.inclusion_tag(
|
||||||
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''):
|
"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]
|
tag_names = [tag.name for tag in tags]
|
||||||
tags_string = build_tag_string(tag_names, ' ')
|
tags_string = build_tag_string(tag_names, " ")
|
||||||
search_form = BookmarkSearchForm(search, editable_fields=['q'])
|
search_form = BookmarkSearchForm(search, editable_fields=["q"])
|
||||||
|
|
||||||
if mode == 'shared':
|
if mode == "shared":
|
||||||
preferences_form = BookmarkSearchForm(search, editable_fields=['sort'])
|
preferences_form = BookmarkSearchForm(search, editable_fields=["sort"])
|
||||||
else:
|
else:
|
||||||
preferences_form = BookmarkSearchForm(search, editable_fields=['sort', 'shared', 'unread'])
|
preferences_form = BookmarkSearchForm(
|
||||||
|
search, editable_fields=["sort", "shared", "unread"]
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
'request': context['request'],
|
"request": context["request"],
|
||||||
'search': search,
|
"search": search,
|
||||||
'search_form': search_form,
|
"search_form": search_form,
|
||||||
'preferences_form': preferences_form,
|
"preferences_form": preferences_form,
|
||||||
'tags_string': tags_string,
|
"tags_string": tags_string,
|
||||||
'mode': mode,
|
"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]):
|
def user_select(context, search: BookmarkSearch, users: List[User]):
|
||||||
sorted_users = sorted(users, key=lambda x: str.lower(x.username))
|
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 {
|
return {
|
||||||
'search': search,
|
"search": search,
|
||||||
'users': sorted_users,
|
"users": sorted_users,
|
||||||
'form': form,
|
"form": form,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ NUM_ADJACENT_PAGES = 2
|
|||||||
register = template.Library()
|
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):
|
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 {
|
return {"page": page, "visible_page_numbers": visible_page_numbers}
|
||||||
'page': page,
|
|
||||||
'visible_page_numbers': visible_page_numbers
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]:
|
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()
|
visible_pages = set()
|
||||||
|
|
||||||
# Add adjacent pages around current page
|
# Add adjacent pages around current page
|
||||||
visible_pages |= set(range(
|
visible_pages |= set(
|
||||||
max(1, current_page_number - NUM_ADJACENT_PAGES),
|
range(
|
||||||
min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1
|
max(1, current_page_number - NUM_ADJACENT_PAGES),
|
||||||
))
|
min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Add first page
|
# Add first page
|
||||||
visible_pages.add(1)
|
visible_pages.add(1)
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ def add_tag_to_query(context, tag_name: str):
|
|||||||
params = context.request.GET.copy()
|
params = context.request.GET.copy()
|
||||||
|
|
||||||
# Append to or create query string
|
# Append to or create query string
|
||||||
if params.__contains__('q'):
|
if params.__contains__("q"):
|
||||||
query_string = params.__getitem__('q') + ' '
|
query_string = params.__getitem__("q") + " "
|
||||||
else:
|
else:
|
||||||
query_string = ''
|
query_string = ""
|
||||||
query_string = query_string + '#' + tag_name
|
query_string = query_string + "#" + tag_name
|
||||||
params.__setitem__('q', query_string)
|
params.__setitem__("q", query_string)
|
||||||
|
|
||||||
return params.urlencode()
|
return params.urlencode()
|
||||||
|
|
||||||
@@ -41,20 +41,26 @@ def add_tag_to_query(context, tag_name: str):
|
|||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def remove_tag_from_query(context, tag_name: str):
|
def remove_tag_from_query(context, tag_name: str):
|
||||||
params = context.request.GET.copy()
|
params = context.request.GET.copy()
|
||||||
if params.__contains__('q'):
|
if params.__contains__("q"):
|
||||||
# Split query string into parts
|
# Split query string into parts
|
||||||
query_string = params.__getitem__('q')
|
query_string = params.__getitem__("q")
|
||||||
query_parts = query_string.split()
|
query_parts = query_string.split()
|
||||||
# Remove tag with hash
|
# Remove tag with hash
|
||||||
tag_name_with_hash = '#' + tag_name
|
tag_name_with_hash = "#" + tag_name
|
||||||
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)]
|
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
|
# When using lax tag search, also remove tag without hash
|
||||||
profile = context.request.user_profile
|
profile = context.request.user_profile
|
||||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
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
|
# Rebuild query string
|
||||||
query_string = ' '.join(query_parts)
|
query_string = " ".join(query_parts)
|
||||||
params.__setitem__('q', query_string)
|
params.__setitem__("q", query_string)
|
||||||
|
|
||||||
return params.urlencode()
|
return params.urlencode()
|
||||||
|
|
||||||
@@ -71,38 +77,38 @@ def replace_query_param(context, **kwargs):
|
|||||||
return query.urlencode()
|
return query.urlencode()
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='hash_tag')
|
@register.filter(name="hash_tag")
|
||||||
def hash_tag(tag_name):
|
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):
|
def first_char(text):
|
||||||
return text[0]
|
return text[0]
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='remaining_chars')
|
@register.filter(name="remaining_chars")
|
||||||
def remaining_chars(text, index):
|
def remaining_chars(text, index):
|
||||||
return text[index:]
|
return text[index:]
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='humanize_absolute_date')
|
@register.filter(name="humanize_absolute_date")
|
||||||
def humanize_absolute_date(value):
|
def humanize_absolute_date(value):
|
||||||
if value in (None, ''):
|
if value in (None, ""):
|
||||||
return ''
|
return ""
|
||||||
return utils.humanize_absolute_date(value)
|
return utils.humanize_absolute_date(value)
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='humanize_relative_date')
|
@register.filter(name="humanize_relative_date")
|
||||||
def humanize_relative_date(value):
|
def humanize_relative_date(value):
|
||||||
if value in (None, ''):
|
if value in (None, ""):
|
||||||
return ''
|
return ""
|
||||||
return utils.humanize_relative_date(value)
|
return utils.humanize_relative_date(value)
|
||||||
|
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def htmlmin(parser, token):
|
def htmlmin(parser, token):
|
||||||
nodelist = parser.parse(('endhtmlmin',))
|
nodelist = parser.parse(("endhtmlmin",))
|
||||||
parser.delete_first_token()
|
parser.delete_first_token()
|
||||||
return HtmlMinNode(nodelist)
|
return HtmlMinNode(nodelist)
|
||||||
|
|
||||||
@@ -114,7 +120,7 @@ class HtmlMinNode(template.Node):
|
|||||||
def render(self, context):
|
def render(self, context):
|
||||||
output = self.nodelist.render(context)
|
output = self.nodelist.render(context)
|
||||||
|
|
||||||
output = re.sub(r'\s+', ' ', output)
|
output = re.sub(r"\s+", " ", output)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -123,11 +129,11 @@ class HtmlMinNode(template.Node):
|
|||||||
def render_markdown(context, markdown_text):
|
def render_markdown(context, markdown_text):
|
||||||
# naive approach to reusing the renderer for a single request
|
# naive approach to reusing the renderer for a single request
|
||||||
# works for bookmark list for now
|
# works for bookmark list for now
|
||||||
if not ('markdown_renderer' in context):
|
if not ("markdown_renderer" in context):
|
||||||
renderer = markdown.Markdown(extensions=['fenced_code', 'nl2br'])
|
renderer = markdown.Markdown(extensions=["fenced_code", "nl2br"])
|
||||||
context['markdown_renderer'] = renderer
|
context["markdown_renderer"] = renderer
|
||||||
else:
|
else:
|
||||||
renderer = context['markdown_renderer']
|
renderer = context["markdown_renderer"]
|
||||||
|
|
||||||
as_html = renderer.convert(markdown_text)
|
as_html = renderer.convert(markdown_text)
|
||||||
sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs)
|
sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs)
|
||||||
|
|||||||
@@ -18,26 +18,29 @@ class BookmarkFactoryMixin:
|
|||||||
|
|
||||||
def get_or_create_test_user(self):
|
def get_or_create_test_user(self):
|
||||||
if self.user is None:
|
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
|
return self.user
|
||||||
|
|
||||||
def setup_bookmark(self,
|
def setup_bookmark(
|
||||||
is_archived: bool = False,
|
self,
|
||||||
unread: bool = False,
|
is_archived: bool = False,
|
||||||
shared: bool = False,
|
unread: bool = False,
|
||||||
tags=None,
|
shared: bool = False,
|
||||||
user: User = None,
|
tags=None,
|
||||||
url: str = '',
|
user: User = None,
|
||||||
title: str = None,
|
url: str = "",
|
||||||
description: str = '',
|
title: str = None,
|
||||||
notes: str = '',
|
description: str = "",
|
||||||
website_title: str = '',
|
notes: str = "",
|
||||||
website_description: str = '',
|
website_title: str = "",
|
||||||
web_archive_snapshot_url: str = '',
|
website_description: str = "",
|
||||||
favicon_file: str = '',
|
web_archive_snapshot_url: str = "",
|
||||||
added: datetime = None,
|
favicon_file: str = "",
|
||||||
):
|
added: datetime = None,
|
||||||
|
):
|
||||||
if title is None:
|
if title is None:
|
||||||
title = get_random_string(length=32)
|
title = get_random_string(length=32)
|
||||||
if tags is None:
|
if tags is None:
|
||||||
@@ -46,7 +49,7 @@ class BookmarkFactoryMixin:
|
|||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
if not url:
|
if not url:
|
||||||
unique_id = get_random_string(length=32)
|
unique_id = get_random_string(length=32)
|
||||||
url = 'https://example.com/' + unique_id
|
url = "https://example.com/" + unique_id
|
||||||
if added is None:
|
if added is None:
|
||||||
added = timezone.now()
|
added = timezone.now()
|
||||||
bookmark = Bookmark(
|
bookmark = Bookmark(
|
||||||
@@ -71,49 +74,53 @@ class BookmarkFactoryMixin:
|
|||||||
bookmark.save()
|
bookmark.save()
|
||||||
return bookmark
|
return bookmark
|
||||||
|
|
||||||
def setup_numbered_bookmarks(self,
|
def setup_numbered_bookmarks(
|
||||||
count: int,
|
self,
|
||||||
prefix: str = '',
|
count: int,
|
||||||
suffix: str = '',
|
prefix: str = "",
|
||||||
tag_prefix: str = '',
|
suffix: str = "",
|
||||||
archived: bool = False,
|
tag_prefix: str = "",
|
||||||
unread: bool = False,
|
archived: bool = False,
|
||||||
shared: bool = False,
|
unread: bool = False,
|
||||||
with_tags: bool = False,
|
shared: bool = False,
|
||||||
user: User = None):
|
with_tags: bool = False,
|
||||||
|
user: User = None,
|
||||||
|
):
|
||||||
user = user or self.get_or_create_test_user()
|
user = user or self.get_or_create_test_user()
|
||||||
bookmarks = []
|
bookmarks = []
|
||||||
|
|
||||||
if not prefix:
|
if not prefix:
|
||||||
if archived:
|
if archived:
|
||||||
prefix = 'Archived Bookmark'
|
prefix = "Archived Bookmark"
|
||||||
elif shared:
|
elif shared:
|
||||||
prefix = 'Shared Bookmark'
|
prefix = "Shared Bookmark"
|
||||||
else:
|
else:
|
||||||
prefix = 'Bookmark'
|
prefix = "Bookmark"
|
||||||
|
|
||||||
if not tag_prefix:
|
if not tag_prefix:
|
||||||
if archived:
|
if archived:
|
||||||
tag_prefix = 'Archived Tag'
|
tag_prefix = "Archived Tag"
|
||||||
elif shared:
|
elif shared:
|
||||||
tag_prefix = 'Shared Tag'
|
tag_prefix = "Shared Tag"
|
||||||
else:
|
else:
|
||||||
tag_prefix = 'Tag'
|
tag_prefix = "Tag"
|
||||||
|
|
||||||
for i in range(1, count + 1):
|
for i in range(1, count + 1):
|
||||||
title = f'{prefix} {i}{suffix}'
|
title = f"{prefix} {i}{suffix}"
|
||||||
url = f'https://example.com/{prefix}/{i}'
|
url = f"https://example.com/{prefix}/{i}"
|
||||||
tags = []
|
tags = []
|
||||||
if with_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)]
|
tags = [self.setup_tag(name=tag_name, user=user)]
|
||||||
bookmark = self.setup_bookmark(url=url,
|
bookmark = self.setup_bookmark(
|
||||||
title=title,
|
url=url,
|
||||||
is_archived=archived,
|
title=title,
|
||||||
unread=unread,
|
is_archived=archived,
|
||||||
shared=shared,
|
unread=unread,
|
||||||
tags=tags,
|
shared=shared,
|
||||||
user=user)
|
tags=tags,
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
bookmarks.append(bookmark)
|
bookmarks.append(bookmark)
|
||||||
|
|
||||||
return bookmarks
|
return bookmarks
|
||||||
@@ -121,7 +128,7 @@ class BookmarkFactoryMixin:
|
|||||||
def get_numbered_bookmark(self, title: str):
|
def get_numbered_bookmark(self, title: str):
|
||||||
return Bookmark.objects.get(title=title)
|
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:
|
if user is None:
|
||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
if not name:
|
if not name:
|
||||||
@@ -130,10 +137,15 @@ class BookmarkFactoryMixin:
|
|||||||
tag.save()
|
tag.save()
|
||||||
return tag
|
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:
|
if not name:
|
||||||
name = get_random_string(length=32)
|
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_sharing = enable_sharing
|
||||||
user.profile.enable_public_sharing = enable_public_sharing
|
user.profile.enable_public_sharing = enable_public_sharing
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
@@ -161,17 +173,17 @@ class LinkdingApiTestCase(APITestCase):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def post(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
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)
|
self.assertEqual(response.status_code, expected_status_code)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def put(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
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)
|
self.assertEqual(response.status_code, expected_status_code)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
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)
|
self.assertEqual(response.status_code, expected_status_code)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -182,14 +194,16 @@ class LinkdingApiTestCase(APITestCase):
|
|||||||
|
|
||||||
|
|
||||||
class BookmarkHtmlTag:
|
class BookmarkHtmlTag:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
href: str = '',
|
self,
|
||||||
title: str = '',
|
href: str = "",
|
||||||
description: str = '',
|
title: str = "",
|
||||||
add_date: str = '',
|
description: str = "",
|
||||||
tags: str = '',
|
add_date: str = "",
|
||||||
to_read: bool = False,
|
tags: str = "",
|
||||||
private: bool = True):
|
to_read: bool = False,
|
||||||
|
private: bool = True,
|
||||||
|
):
|
||||||
self.href = href
|
self.href = href
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
@@ -201,7 +215,7 @@ class BookmarkHtmlTag:
|
|||||||
|
|
||||||
class ImportTestMixin:
|
class ImportTestMixin:
|
||||||
def render_tag(self, tag: BookmarkHtmlTag):
|
def render_tag(self, tag: BookmarkHtmlTag):
|
||||||
return f'''
|
return f"""
|
||||||
<DT>
|
<DT>
|
||||||
<A {f'HREF="{tag.href}"' if tag.href else ''}
|
<A {f'HREF="{tag.href}"' if tag.href else ''}
|
||||||
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
|
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
|
||||||
@@ -211,13 +225,13 @@ class ImportTestMixin:
|
|||||||
{tag.title if tag.title else ''}
|
{tag.title if tag.title else ''}
|
||||||
</A>
|
</A>
|
||||||
{f'<DD>{tag.description}' if tag.description else ''}
|
{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:
|
if tags:
|
||||||
rendered_tags = [self.render_tag(tag) for tag in tags]
|
rendered_tags = [self.render_tag(tag) for tag in tags]
|
||||||
tags_html = '\n'.join(rendered_tags)
|
tags_html = "\n".join(rendered_tags)
|
||||||
return f'''
|
return f"""
|
||||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||||
<TITLE>Bookmarks</TITLE>
|
<TITLE>Bookmarks</TITLE>
|
||||||
@@ -225,34 +239,34 @@ class ImportTestMixin:
|
|||||||
<DL><p>
|
<DL><p>
|
||||||
{tags_html}
|
{tags_html}
|
||||||
</DL><p>
|
</DL><p>
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
_words = [
|
_words = [
|
||||||
'quasi',
|
"quasi",
|
||||||
'consequatur',
|
"consequatur",
|
||||||
'necessitatibus',
|
"necessitatibus",
|
||||||
'debitis',
|
"debitis",
|
||||||
'quod',
|
"quod",
|
||||||
'vero',
|
"vero",
|
||||||
'qui',
|
"qui",
|
||||||
'commodi',
|
"commodi",
|
||||||
'quod',
|
"quod",
|
||||||
'odio',
|
"odio",
|
||||||
'aliquam',
|
"aliquam",
|
||||||
'veniam',
|
"veniam",
|
||||||
'architecto',
|
"architecto",
|
||||||
'consequatur',
|
"consequatur",
|
||||||
'autem',
|
"autem",
|
||||||
'qui',
|
"qui",
|
||||||
'iste',
|
"iste",
|
||||||
'asperiores',
|
"asperiores",
|
||||||
'soluta',
|
"soluta",
|
||||||
'et',
|
"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:
|
if num_words is None:
|
||||||
num_words = random.randint(5, 10)
|
num_words = random.randint(5, 10)
|
||||||
selected_words = random.choices(_words, k=num_words)
|
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)
|
selected_words.append(including_word)
|
||||||
random.shuffle(selected_words)
|
random.shuffle(selected_words)
|
||||||
|
|
||||||
return ' '.join(selected_words)
|
return " ".join(selected_words)
|
||||||
|
|
||||||
|
|
||||||
def disable_logging(f):
|
def disable_logging(f):
|
||||||
@@ -275,5 +289,5 @@ def disable_logging(f):
|
|||||||
|
|
||||||
|
|
||||||
def collapse_whitespace(text: str):
|
def collapse_whitespace(text: str):
|
||||||
text = text.replace('\n', '').replace('\r', '')
|
text = text.replace("\n", "").replace("\r", "")
|
||||||
return ' '.join(text.split())
|
return " ".join(text.split())
|
||||||
|
|||||||
@@ -6,21 +6,24 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
|
|||||||
|
|
||||||
class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin):
|
class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
def assertSharedBookmarksLinkCount(self, response, count):
|
def assertSharedBookmarksLinkCount(self, response, count):
|
||||||
url = reverse('bookmarks:shared')
|
url = reverse("bookmarks:shared")
|
||||||
self.assertContains(response, f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
|
self.assertContains(
|
||||||
count=count)
|
response,
|
||||||
|
f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def test_publicly_shared_bookmarks_link(self):
|
def test_publicly_shared_bookmarks_link(self):
|
||||||
# should not render link if no public shares exist
|
# should not render link if no public shares exist
|
||||||
user = self.setup_user(enable_sharing=True)
|
user = self.setup_user(enable_sharing=True)
|
||||||
self.setup_bookmark(user=user, shared=True)
|
self.setup_bookmark(user=user, shared=True)
|
||||||
|
|
||||||
response = self.client.get(reverse('login'))
|
response = self.client.get(reverse("login"))
|
||||||
self.assertSharedBookmarksLinkCount(response, 0)
|
self.assertSharedBookmarksLinkCount(response, 0)
|
||||||
|
|
||||||
# should render link if public shares exist
|
# should render link if public shares exist
|
||||||
user.profile.enable_public_sharing = True
|
user.profile.enable_public_sharing = True
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
|
|
||||||
response = self.client.get(reverse('login'))
|
response = self.client.get(reverse("login"))
|
||||||
self.assertSharedBookmarksLinkCount(response, 1)
|
self.assertSharedBookmarksLinkCount(response, 1)
|
||||||
|
|||||||
@@ -7,23 +7,35 @@ from django.test import TestCase
|
|||||||
|
|
||||||
class AppOptionsTestCase(TestCase):
|
class AppOptionsTestCase(TestCase):
|
||||||
def setUp(self) -> None:
|
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):
|
def test_empty_csrf_trusted_origins(self):
|
||||||
module = importlib.reload(self.settings_module)
|
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):
|
def test_single_csrf_trusted_origin(self):
|
||||||
module = importlib.reload(self.settings_module)
|
module = importlib.reload(self.settings_module)
|
||||||
|
|
||||||
self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS'))
|
self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
|
||||||
self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com'])
|
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):
|
def test_multiple_csrf_trusted_origin(self):
|
||||||
module = importlib.reload(self.settings_module)
|
module = importlib.reload(self.settings_module)
|
||||||
|
|
||||||
self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS'))
|
self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
|
||||||
self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com', 'http://linkding.example.com'])
|
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
|
# Reproducing configuration from the settings logic here
|
||||||
# ideally this test would just override the respective options
|
# ideally this test would just override the respective options
|
||||||
@modify_settings(
|
@modify_settings(
|
||||||
MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'},
|
MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"},
|
||||||
AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'}
|
AUTHENTICATION_BACKENDS={
|
||||||
|
"prepend": "django.contrib.auth.backends.RemoteUserBackend"
|
||||||
|
},
|
||||||
)
|
)
|
||||||
def test_auth_proxy_authentication(self):
|
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}
|
headers = {"REMOTE_USER": user.username}
|
||||||
response = self.client.get(reverse('bookmarks:index'), **headers)
|
response = self.client.get(reverse("bookmarks:index"), **headers)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Reproducing configuration from the settings logic here
|
# Reproducing configuration from the settings logic here
|
||||||
# ideally this test would just override the respective options
|
# ideally this test would just override the respective options
|
||||||
@modify_settings(
|
@modify_settings(
|
||||||
MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'},
|
MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"},
|
||||||
AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'}
|
AUTHENTICATION_BACKENDS={
|
||||||
|
"prepend": "django.contrib.auth.backends.RemoteUserBackend"
|
||||||
|
},
|
||||||
)
|
)
|
||||||
def test_auth_proxy_with_custom_header(self):
|
def test_auth_proxy_with_custom_header(self):
|
||||||
with patch.object(CustomRemoteUserMiddleware, 'header', new_callable=PropertyMock) as mock:
|
with patch.object(
|
||||||
mock.return_value = 'Custom-User'
|
CustomRemoteUserMiddleware, "header", new_callable=PropertyMock
|
||||||
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123')
|
) as mock:
|
||||||
|
mock.return_value = "Custom-User"
|
||||||
|
user = User.objects.create_user(
|
||||||
|
"auth_proxy_user", "user@example.com", "password123"
|
||||||
|
)
|
||||||
|
|
||||||
headers = {'Custom-User': user.username}
|
headers = {"Custom-User": user.username}
|
||||||
response = self.client.get(reverse('bookmarks:index'), **headers)
|
response = self.client.get(reverse("bookmarks:index"), **headers)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_auth_proxy_is_disabled_by_default(self):
|
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}
|
headers = {"REMOTE_USER": user.username}
|
||||||
response = self.client.get(reverse('bookmarks:index'), **headers, follow=True)
|
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())
|
self.assertEqual(len(bookmarks), Bookmark.objects.count())
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
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):
|
def test_archive_should_archive_bookmark(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'archive': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"archive": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
self.assertTrue(bookmark.is_archived)
|
self.assertTrue(bookmark.is_archived)
|
||||||
|
|
||||||
def test_can_only_archive_own_bookmarks(self):
|
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)
|
bookmark = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
response = self.client.post(
|
||||||
'archive': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"archive": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
@@ -46,20 +57,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_unarchive_should_unarchive_bookmark(self):
|
def test_unarchive_should_unarchive_bookmark(self):
|
||||||
bookmark = self.setup_bookmark(is_archived=True)
|
bookmark = self.setup_bookmark(is_archived=True)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'unarchive': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"unarchive": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
self.assertFalse(bookmark.is_archived)
|
self.assertFalse(bookmark.is_archived)
|
||||||
|
|
||||||
def test_unarchive_can_only_archive_own_bookmarks(self):
|
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)
|
bookmark = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
response = self.client.post(
|
||||||
'unarchive': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"unarchive": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
@@ -68,28 +87,39 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_delete_should_delete_bookmark(self):
|
def test_delete_should_delete_bookmark(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'remove': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"remove": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(Bookmark.objects.count(), 0)
|
self.assertEqual(Bookmark.objects.count(), 0)
|
||||||
|
|
||||||
def test_delete_can_only_delete_own_bookmarks(self):
|
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)
|
bookmark = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
response = self.client.post(
|
||||||
'remove': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"remove": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists())
|
self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists())
|
||||||
|
|
||||||
def test_mark_as_read(self):
|
def test_mark_as_read(self):
|
||||||
bookmark = self.setup_bookmark(unread=True)
|
bookmark = self.setup_bookmark(unread=True)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'mark_as_read': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"mark_as_read": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
self.assertFalse(bookmark.unread)
|
self.assertFalse(bookmark.unread)
|
||||||
@@ -97,21 +127,29 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_unshare_should_unshare_bookmark(self):
|
def test_unshare_should_unshare_bookmark(self):
|
||||||
bookmark = self.setup_bookmark(shared=True)
|
bookmark = self.setup_bookmark(shared=True)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'unshare': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"unshare": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
self.assertFalse(bookmark.shared)
|
self.assertFalse(bookmark.shared)
|
||||||
|
|
||||||
def test_can_only_unshare_own_bookmarks(self):
|
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)
|
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
response = self.client.post(
|
||||||
'unshare': [bookmark.id],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"unshare": [bookmark.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
@@ -123,27 +161,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_archive'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
def test_can_only_bulk_archive_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(user=other_user)
|
||||||
bookmark2 = self.setup_bookmark(user=other_user)
|
bookmark2 = self.setup_bookmark(user=other_user)
|
||||||
bookmark3 = self.setup_bookmark(user=other_user)
|
bookmark3 = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_archive'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||||
bookmark3 = self.setup_bookmark(is_archived=True)
|
bookmark3 = self.setup_bookmark(is_archived=True)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:archived.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_unarchive'],
|
reverse("bookmarks:archived.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
def test_can_only_bulk_unarchive_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
bookmark2 = 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)
|
bookmark3 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:archived.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_unarchive'],
|
reverse("bookmarks:archived.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
||||||
|
|
||||||
def test_can_only_bulk_delete_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(user=other_user)
|
||||||
bookmark2 = self.setup_bookmark(user=other_user)
|
bookmark2 = self.setup_bookmark(user=other_user)
|
||||||
bookmark3 = self.setup_bookmark(user=other_user)
|
bookmark3 = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).first())
|
||||||
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||||
@@ -218,12 +304,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_tag'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
"bulk_action": ["bulk_tag"],
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -234,19 +327,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||||
|
|
||||||
def test_can_only_bulk_tag_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(user=other_user)
|
||||||
bookmark2 = self.setup_bookmark(user=other_user)
|
bookmark2 = self.setup_bookmark(user=other_user)
|
||||||
bookmark3 = self.setup_bookmark(user=other_user)
|
bookmark3 = self.setup_bookmark(user=other_user)
|
||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_tag'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
"bulk_action": ["bulk_tag"],
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -263,12 +365,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_untag'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
"bulk_action": ["bulk_untag"],
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -279,19 +388,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark3.tags.all(), [])
|
self.assertCountEqual(bookmark3.tags.all(), [])
|
||||||
|
|
||||||
def test_can_only_bulk_untag_own_bookmarks(self):
|
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()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
bookmark1 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
bookmark1 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||||
bookmark2 = 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)
|
bookmark3 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_untag'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
"bulk_action": ["bulk_untag"],
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -306,27 +424,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(unread=True)
|
bookmark2 = self.setup_bookmark(unread=True)
|
||||||
bookmark3 = self.setup_bookmark(unread=True)
|
bookmark3 = self.setup_bookmark(unread=True)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_read'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||||
|
|
||||||
def test_can_only_bulk_mark_as_read_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(unread=True, user=other_user)
|
||||||
bookmark2 = 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)
|
bookmark3 = self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_read'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(unread=False)
|
||||||
bookmark3 = self.setup_bookmark(unread=False)
|
bookmark3 = self.setup_bookmark(unread=False)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_unread'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||||
|
|
||||||
def test_can_only_bulk_mark_as_unread_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(unread=False, user=other_user)
|
||||||
bookmark2 = 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)
|
bookmark3 = self.setup_bookmark(unread=False, user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_unread'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(shared=False)
|
||||||
bookmark3 = self.setup_bookmark(shared=False)
|
bookmark3 = self.setup_bookmark(shared=False)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_share'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).shared)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||||
|
|
||||||
def test_can_only_bulk_share_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(shared=False, user=other_user)
|
||||||
bookmark2 = 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)
|
bookmark3 = self.setup_bookmark(shared=False, user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_share'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).shared)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(shared=True)
|
||||||
bookmark3 = self.setup_bookmark(shared=True)
|
bookmark3 = self.setup_bookmark(shared=True)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_unshare'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).shared)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||||
|
|
||||||
def test_can_only_bulk_unshare_own_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(shared=True, user=other_user)
|
||||||
bookmark2 = 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)
|
bookmark3 = self.setup_bookmark(shared=True, user=other_user)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_unshare'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"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=bookmark1.id).shared)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
|
||||||
@@ -430,11 +612,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_archive'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"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=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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):
|
def test_bulk_select_across_ignores_page(self):
|
||||||
self.setup_numbered_bookmarks(100)
|
self.setup_numbered_bookmarks(100)
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action') + '?page=2', {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:index.action") + "?page=2",
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"bulk_action": ["bulk_delete"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bulk_select_across": ["on"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(0, Bookmark.objects.count())
|
self.assertEqual(0, Bookmark.objects.count())
|
||||||
|
|
||||||
@@ -455,85 +643,108 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
# create a number of bookmarks with different states / visibility
|
# 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)
|
||||||
self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
||||||
self.setup_numbered_bookmarks(3,
|
self.setup_numbered_bookmarks(
|
||||||
shared=True,
|
3,
|
||||||
prefix="Joe's Bookmark",
|
shared=True,
|
||||||
user=self.setup_user(enable_sharing=True))
|
prefix="Joe's Bookmark",
|
||||||
|
user=self.setup_user(enable_sharing=True),
|
||||||
|
)
|
||||||
|
|
||||||
def test_index_action_bulk_select_across_only_affects_active_bookmarks(self):
|
def test_index_action_bulk_select_across_only_affects_active_bookmarks(self):
|
||||||
self.setup_bulk_edit_scope_test_data()
|
self.setup_bulk_edit_scope_test_data()
|
||||||
|
|
||||||
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 1').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 2").first())
|
||||||
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 3').first())
|
self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 3").first())
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"bulk_action": ["bulk_delete"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bulk_select_across": ["on"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(6, Bookmark.objects.count())
|
self.assertEqual(6, Bookmark.objects.count())
|
||||||
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 1').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 2").first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 3').first())
|
self.assertIsNone(Bookmark.objects.filter(title="Bookmark 3").first())
|
||||||
|
|
||||||
def test_index_action_bulk_select_across_respects_query(self):
|
def test_index_action_bulk_select_across_respects_query(self):
|
||||||
self.setup_numbered_bookmarks(3, prefix='foo')
|
self.setup_numbered_bookmarks(3, prefix="foo")
|
||||||
self.setup_numbered_bookmarks(3, prefix='bar')
|
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', {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:index.action") + "?q=foo",
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"bulk_action": ["bulk_delete"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bulk_select_across": ["on"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
|
self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count())
|
||||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
|
self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count())
|
||||||
|
|
||||||
def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self):
|
def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self):
|
||||||
self.setup_bulk_edit_scope_test_data()
|
self.setup_bulk_edit_scope_test_data()
|
||||||
|
|
||||||
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 1').first())
|
self.assertIsNotNone(
|
||||||
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 2').first())
|
Bookmark.objects.filter(title="Archived Bookmark 1").first()
|
||||||
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 3').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'), {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:archived.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"bulk_action": ["bulk_delete"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bulk_select_across": ["on"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(6, Bookmark.objects.count())
|
self.assertEqual(6, Bookmark.objects.count())
|
||||||
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 1').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 2").first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 3').first())
|
self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 3").first())
|
||||||
|
|
||||||
def test_archived_action_bulk_select_across_respects_query(self):
|
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="foo", archived=True)
|
||||||
self.setup_numbered_bookmarks(3, prefix='bar', 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', {
|
self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:archived.action") + "?q=foo",
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"bulk_action": ["bulk_delete"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bulk_select_across": ["on"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
|
self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count())
|
||||||
self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
|
self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count())
|
||||||
|
|
||||||
def test_shared_action_bulk_select_across_not_supported(self):
|
def test_shared_action_bulk_select_across_not_supported(self):
|
||||||
self.setup_bulk_edit_scope_test_data()
|
self.setup_bulk_edit_scope_test_data()
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:shared.action'), {
|
response = self.client.post(
|
||||||
'bulk_action': ['bulk_delete'],
|
reverse("bookmarks:shared.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bulk_select_across': ['on'],
|
"bulk_action": ["bulk_delete"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bulk_select_across": ["on"],
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_handles_empty_bookmark_id(self):
|
def test_handles_empty_bookmark_id(self):
|
||||||
@@ -541,17 +752,23 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
response = self.client.post(
|
||||||
'bulk_action': ['bulk_archive'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
})
|
"bulk_action": ["bulk_archive"],
|
||||||
|
"bulk_execute": [""],
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:index.action'), {
|
response = self.client.post(
|
||||||
'bulk_action': ['bulk_archive'],
|
reverse("bookmarks:index.action"),
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [],
|
"bulk_action": ["bulk_archive"],
|
||||||
})
|
"bulk_execute": [""],
|
||||||
|
"bookmark_id": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
||||||
@@ -561,9 +778,16 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
self.client.post(reverse('bookmarks:index.action'), {
|
self.client.post(
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
reverse("bookmarks:index.action"),
|
||||||
})
|
{
|
||||||
|
"bookmark_id": [
|
||||||
|
str(bookmark1.id),
|
||||||
|
str(bookmark2.id),
|
||||||
|
str(bookmark3.id),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
||||||
|
|
||||||
@@ -572,14 +796,25 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
url = reverse('bookmarks:index.action') + '?return_url=' + reverse('bookmarks:settings.index')
|
url = (
|
||||||
response = self.client.post(url, {
|
reverse("bookmarks:index.action")
|
||||||
'bulk_action': ['bulk_archive'],
|
+ "?return_url="
|
||||||
'bulk_execute': [''],
|
+ reverse("bookmarks:settings.index")
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
)
|
||||||
})
|
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):
|
def test_should_not_redirect_to_external_url(self):
|
||||||
bookmark1 = self.setup_bookmark()
|
bookmark1 = self.setup_bookmark()
|
||||||
@@ -587,19 +822,27 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark3 = self.setup_bookmark()
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
def post_with(return_url, follow=None):
|
def post_with(return_url, follow=None):
|
||||||
url = reverse('bookmarks:index.action') + f'?return_url={return_url}'
|
url = reverse("bookmarks:index.action") + f"?return_url={return_url}"
|
||||||
return self.client.post(url, {
|
return self.client.post(
|
||||||
'bulk_action': ['bulk_archive'],
|
url,
|
||||||
'bulk_execute': [''],
|
{
|
||||||
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
"bulk_action": ["bulk_archive"],
|
||||||
}, follow=follow)
|
"bulk_execute": [""],
|
||||||
|
"bookmark_id": [
|
||||||
|
str(bookmark1.id),
|
||||||
|
str(bookmark2.id),
|
||||||
|
str(bookmark3.id),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
follow=follow,
|
||||||
|
)
|
||||||
|
|
||||||
response = post_with('https://example.com')
|
response = post_with("https://example.com")
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||||
response = post_with('//example.com')
|
response = post_with("//example.com")
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||||
response = post_with('://example.com')
|
response = post_with("://example.com")
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
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)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
|
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):
|
class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||||
@@ -15,33 +19,41 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
self.client.force_login(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())
|
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)
|
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))
|
self.assertEqual(len(bookmark_items), len(bookmarks))
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
bookmark_item = bookmark_list.select_one(
|
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)
|
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())
|
soup = self.make_soup(response.content.decode())
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
bookmark_item = soup.select_one(
|
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)
|
self.assertIsNone(bookmark_item)
|
||||||
|
|
||||||
def assertVisibleTags(self, response, tags: List[Tag]):
|
def assertVisibleTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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)
|
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))
|
self.assertEqual(len(tag_items), len(tags))
|
||||||
|
|
||||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
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]):
|
def assertInvisibleTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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]
|
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]):
|
def assertSelectedTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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)
|
self.assertIsNotNone(selected_tags)
|
||||||
|
|
||||||
tag_list = selected_tags.select('a')
|
tag_list = selected_tags.select("a")
|
||||||
self.assertEqual(len(tag_list), len(tags))
|
self.assertEqual(len(tag_list), len(tags))
|
||||||
|
|
||||||
for tag in 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):
|
def assertEditLink(self, response, url):
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<a href="{url}">Edit</a>
|
<a href="{url}">Edit</a>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def assertBulkActionForm(self, response, url: str):
|
def assertBulkActionForm(self, response, url: str):
|
||||||
html = collapse_whitespace(response.content.decode())
|
html = collapse_whitespace(response.content.decode())
|
||||||
needle = collapse_whitespace(f'''
|
needle = collapse_whitespace(
|
||||||
|
f"""
|
||||||
<form class="bookmark-actions"
|
<form class="bookmark-actions"
|
||||||
action="{url}"
|
action="{url}"
|
||||||
method="post" autocomplete="off">
|
method="post" autocomplete="off">
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
self.assertIn(needle, html)
|
self.assertIn(needle, html)
|
||||||
|
|
||||||
def test_should_list_archived_and_user_owned_bookmarks(self):
|
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)
|
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
|
||||||
invisible_bookmarks = [
|
invisible_bookmarks = [
|
||||||
self.setup_bookmark(is_archived=False),
|
self.setup_bookmark(is_archived=False),
|
||||||
self.setup_bookmark(is_archived=True, user=other_user),
|
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.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||||
|
|
||||||
def test_should_list_bookmarks_matching_query(self):
|
def test_should_list_bookmarks_matching_query(self):
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo', archived=True)
|
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar', archived=True)
|
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())
|
html = collapse_whitespace(response.content.decode())
|
||||||
|
|
||||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||||
|
|
||||||
def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
|
def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
|
||||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
other_user = User.objects.create_user(
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
|
"otheruser", "otheruser@example.com", "password123"
|
||||||
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,
|
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||||
tag_prefix='otheruser')
|
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)
|
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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_tags)
|
self.assertInvisibleTags(response, invisible_tags)
|
||||||
|
|
||||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
def test_should_list_tags_for_bookmarks_matching_query(self):
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='foo',
|
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||||
tag_prefix='foo')
|
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')
|
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)
|
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
|
||||||
invisible_tags = self.get_tags_from_bookmarks(invisible_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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_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):
|
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
user_profile.search_preferences = {
|
user_profile.search_preferences = {
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
unread_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=True, with_tags=True, prefix='unread',
|
unread_bookmarks = self.setup_numbered_bookmarks(
|
||||||
tag_prefix='unread')
|
3,
|
||||||
read_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=False, with_tags=True, prefix='read',
|
archived=True,
|
||||||
tag_prefix='read')
|
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)
|
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
|
||||||
read_tags = self.get_tags_from_bookmarks(read_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.assertVisibleBookmarks(response, unread_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, read_bookmarks)
|
self.assertInvisibleBookmarks(response, read_bookmarks)
|
||||||
self.assertVisibleTags(response, unread_tags)
|
self.assertVisibleTags(response, unread_tags)
|
||||||
@@ -167,11 +216,15 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||||||
]
|
]
|
||||||
self.setup_bookmark(is_archived=True, tags=tags)
|
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]])
|
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 = [
|
tags = [
|
||||||
self.setup_tag(),
|
self.setup_tag(),
|
||||||
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)
|
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]])
|
self.assertSelectedTags(response, [tags[1]])
|
||||||
|
|
||||||
@@ -198,16 +254,19 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||||||
]
|
]
|
||||||
self.setup_bookmark(tags=tags, is_archived=True)
|
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]])
|
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||||
|
|
||||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
|
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):
|
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||||
user = self.get_or_create_test_user()
|
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)
|
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):
|
def test_edit_link_return_url_respects_search_options(self):
|
||||||
bookmark = self.setup_bookmark(title='foo', is_archived=True)
|
bookmark = self.setup_bookmark(title="foo", is_archived=True)
|
||||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||||
base_url = reverse('bookmarks:archived')
|
base_url = reverse("bookmarks:archived")
|
||||||
|
|
||||||
# without query params
|
# without query params
|
||||||
return_url = urllib.parse.quote(base_url)
|
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)
|
response = self.client.get(base_url)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query
|
# with query
|
||||||
url_params = '?q=foo'
|
url_params = "?q=foo"
|
||||||
return_url = urllib.parse.quote(base_url + url_params)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query and sort and page
|
# 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)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
def test_bulk_edit_respects_search_options(self):
|
def test_bulk_edit_respects_search_options(self):
|
||||||
action_url = reverse('bookmarks:archived.action')
|
action_url = reverse("bookmarks:archived.action")
|
||||||
base_url = reverse('bookmarks:archived')
|
base_url = reverse("bookmarks:archived")
|
||||||
|
|
||||||
# without params
|
# without params
|
||||||
return_url = urllib.parse.quote_plus(base_url)
|
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)
|
response = self.client.get(base_url)
|
||||||
self.assertBulkActionForm(response, url)
|
self.assertBulkActionForm(response, url)
|
||||||
|
|
||||||
# with query
|
# with query
|
||||||
url_params = '?q=foo'
|
url_params = "?q=foo"
|
||||||
return_url = urllib.parse.quote_plus(base_url + url_params)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertBulkActionForm(response, url)
|
self.assertBulkActionForm(response, url)
|
||||||
|
|
||||||
# with query and sort
|
# 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)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertBulkActionForm(response, url)
|
self.assertBulkActionForm(response, url)
|
||||||
|
|
||||||
def test_allowed_bulk_actions(self):
|
def test_allowed_bulk_actions(self):
|
||||||
url = reverse('bookmarks:archived')
|
url = reverse("bookmarks:archived")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<select name="bulk_action" class="form-select select-sm">
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
<option value="bulk_unarchive">Unarchive</option>
|
<option value="bulk_unarchive">Unarchive</option>
|
||||||
<option value="bulk_delete">Delete</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_read">Mark as read</option>
|
||||||
<option value="bulk_unread">Mark as unread</option>
|
<option value="bulk_unread">Mark as unread</option>
|
||||||
</select>
|
</select>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
user_profile.enable_sharing = True
|
user_profile.enable_sharing = True
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
url = reverse('bookmarks:archived')
|
url = reverse("bookmarks:archived")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<select name="bulk_action" class="form-select select-sm">
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
<option value="bulk_unarchive">Unarchive</option>
|
<option value="bulk_unarchive">Unarchive</option>
|
||||||
<option value="bulk_delete">Delete</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_share">Share</option>
|
||||||
<option value="bulk_unshare">Unshare</option>
|
<option value="bulk_unshare">Unshare</option>
|
||||||
</select>
|
</select>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_apply_search_preferences(self):
|
def test_apply_search_preferences(self):
|
||||||
# no params
|
# 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.status_code, 302)
|
||||||
self.assertEqual(response.url, reverse('bookmarks:archived'))
|
self.assertEqual(response.url, reverse("bookmarks:archived"))
|
||||||
|
|
||||||
# some params
|
# some params
|
||||||
response = self.client.post(reverse('bookmarks:archived'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:archived"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
})
|
"q": "foo",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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
|
# params with default value are removed
|
||||||
response = self.client.post(reverse('bookmarks:archived'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:archived"),
|
||||||
'user': '',
|
{
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
"q": "foo",
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
"user": "",
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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
|
# page is removed
|
||||||
response = self.client.post(reverse('bookmarks:archived'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:archived"),
|
||||||
'page': '2',
|
{
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"q": "foo",
|
||||||
})
|
"page": "2",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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):
|
def test_save_search_preferences(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
|
|
||||||
# no params
|
# no params
|
||||||
self.client.post(reverse('bookmarks:archived'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:archived"),
|
||||||
})
|
{
|
||||||
|
"save": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# with param
|
# with param
|
||||||
self.client.post(reverse('bookmarks:archived'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:archived"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
})
|
"save": "",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# add a param
|
# add a param
|
||||||
self.client.post(reverse('bookmarks:archived'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:archived"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"save": "",
|
||||||
})
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# remove a param
|
# remove a param
|
||||||
self.client.post(reverse('bookmarks:archived'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:archived"),
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
{
|
||||||
})
|
"save": "",
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# ignores non-preferences
|
# ignores non-preferences
|
||||||
self.client.post(reverse('bookmarks:archived'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:archived"),
|
||||||
'q': 'foo',
|
{
|
||||||
'user': 'john',
|
"save": "",
|
||||||
'page': '3',
|
"q": "foo",
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"user": "john",
|
||||||
})
|
"page": "3",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_url_encode_bookmark_actions_url(self):
|
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)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
soup = self.make_soup(html)
|
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'],
|
self.assertEqual(
|
||||||
'/bookmarks/archived/action?q=%23foo&return_url=%2Fbookmarks%2Farchived%3Fq%3D%2523foo')
|
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
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
|
||||||
|
|
||||||
class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryMixin):
|
class BookmarkArchivedViewPerformanceTestCase(
|
||||||
|
TransactionTestCase, BookmarkFactoryMixin
|
||||||
|
):
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
@@ -26,8 +28,10 @@ class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFacto
|
|||||||
# capture number of queries
|
# capture number of queries
|
||||||
context = CaptureQueriesContext(self.get_connection())
|
context = CaptureQueriesContext(self.get_connection())
|
||||||
with context:
|
with context:
|
||||||
response = self.client.get(reverse('bookmarks:archived'))
|
response = self.client.get(reverse("bookmarks:archived"))
|
||||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks)
|
self.assertContains(
|
||||||
|
response, "<li ld-bookmark-item>", num_initial_bookmarks
|
||||||
|
)
|
||||||
|
|
||||||
number_of_queries = context.final_queries
|
number_of_queries = context.final_queries
|
||||||
|
|
||||||
@@ -38,5 +42,9 @@ class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFacto
|
|||||||
|
|
||||||
# assert num queries doesn't increase
|
# assert num queries doesn't increase
|
||||||
with self.assertNumQueries(number_of_queries):
|
with self.assertNumQueries(number_of_queries):
|
||||||
response = self.client.get(reverse('bookmarks:archived'))
|
response = self.client.get(reverse("bookmarks:archived"))
|
||||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks + num_additional_bookmarks)
|
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:
|
if overrides is None:
|
||||||
overrides = {}
|
overrides = {}
|
||||||
form_data = {
|
form_data = {
|
||||||
'url': 'http://example.com/edited',
|
"url": "http://example.com/edited",
|
||||||
'tag_string': 'editedtag1 editedtag2',
|
"tag_string": "editedtag1 editedtag2",
|
||||||
'title': 'edited title',
|
"title": "edited title",
|
||||||
'description': 'edited description',
|
"description": "edited description",
|
||||||
'notes': 'edited notes',
|
"notes": "edited notes",
|
||||||
'unread': False,
|
"unread": False,
|
||||||
'shared': False,
|
"shared": False,
|
||||||
}
|
}
|
||||||
return {**form_data, **overrides}
|
return {**form_data, **overrides}
|
||||||
|
|
||||||
def test_should_edit_bookmark(self):
|
def test_should_edit_bookmark(self):
|
||||||
bookmark = self.setup_bookmark()
|
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()
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(bookmark.owner, self.user)
|
self.assertEqual(bookmark.owner, self.user)
|
||||||
self.assertEqual(bookmark.url, form_data['url'])
|
self.assertEqual(bookmark.url, form_data["url"])
|
||||||
self.assertEqual(bookmark.title, form_data['title'])
|
self.assertEqual(bookmark.title, form_data["title"])
|
||||||
self.assertEqual(bookmark.description, form_data['description'])
|
self.assertEqual(bookmark.description, form_data["description"])
|
||||||
self.assertEqual(bookmark.notes, form_data['notes'])
|
self.assertEqual(bookmark.notes, form_data["notes"])
|
||||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
self.assertEqual(bookmark.unread, form_data["unread"])
|
||||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
self.assertEqual(bookmark.shared, form_data["shared"])
|
||||||
self.assertEqual(bookmark.tags.count(), 2)
|
self.assertEqual(bookmark.tags.count(), 2)
|
||||||
tags = bookmark.tags.order_by('name').all()
|
tags = bookmark.tags.order_by("name").all()
|
||||||
self.assertEqual(tags[0].name, 'editedtag1')
|
self.assertEqual(tags[0].name, "editedtag1")
|
||||||
self.assertEqual(tags[1].name, 'editedtag2')
|
self.assertEqual(tags[1].name, "editedtag2")
|
||||||
|
|
||||||
def test_should_edit_unread_state(self):
|
def test_should_edit_unread_state(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
form_data = self.create_form_data({'id': bookmark.id, 'unread': True})
|
form_data = self.create_form_data({"id": bookmark.id, "unread": True})
|
||||||
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()
|
bookmark.refresh_from_db()
|
||||||
self.assertTrue(bookmark.unread)
|
self.assertTrue(bookmark.unread)
|
||||||
|
|
||||||
form_data = self.create_form_data({'id': bookmark.id, 'unread': False})
|
form_data = self.create_form_data({"id": bookmark.id, "unread": False})
|
||||||
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()
|
bookmark.refresh_from_db()
|
||||||
self.assertFalse(bookmark.unread)
|
self.assertFalse(bookmark.unread)
|
||||||
|
|
||||||
def test_should_edit_shared_state(self):
|
def test_should_edit_shared_state(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
form_data = self.create_form_data({'id': bookmark.id, 'shared': True})
|
form_data = self.create_form_data({"id": bookmark.id, "shared": True})
|
||||||
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()
|
bookmark.refresh_from_db()
|
||||||
self.assertTrue(bookmark.shared)
|
self.assertTrue(bookmark.shared)
|
||||||
|
|
||||||
form_data = self.create_form_data({'id': bookmark.id, 'shared': False})
|
form_data = self.create_form_data({"id": bookmark.id, "shared": False})
|
||||||
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()
|
bookmark.refresh_from_db()
|
||||||
self.assertFalse(bookmark.shared)
|
self.assertFalse(bookmark.shared)
|
||||||
|
|
||||||
def test_should_prefill_bookmark_form_fields(self):
|
def test_should_prefill_bookmark_form_fields(self):
|
||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
bookmark = self.setup_bookmark(tags=[tag1, tag2], title='edited title', description='edited description',
|
bookmark = self.setup_bookmark(
|
||||||
notes='edited notes', website_title='website title',
|
tags=[tag1, tag2],
|
||||||
website_description='website description')
|
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()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<input type="text" name="url" value="{bookmark.url}" placeholder=" "
|
<input type="text" name="url" value="{bookmark.url}" placeholder=" "
|
||||||
autofocus class="form-input" required id="id_url">
|
autofocus class="form-input" required id="id_url">
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
tag_string = build_tag_string(bookmark.tag_names, ' ')
|
tag_string = build_tag_string(bookmark.tag_names, " ")
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<input ld-tag-autocomplete type="text" name="tag_string" value="{tag_string}"
|
<input ld-tag-autocomplete type="text" name="tag_string" value="{tag_string}"
|
||||||
autocomplete="off" autocapitalize="off" class="form-input" id="id_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"
|
<input type="text" name="title" value="{bookmark.title}" maxlength="512" autocomplete="off"
|
||||||
class="form-input" id="id_title">
|
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">
|
<textarea name="description" cols="40" rows="2" class="form-input" id="id_description">
|
||||||
{bookmark.description}
|
{bookmark.description}
|
||||||
</textarea>
|
</textarea>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">
|
<textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">
|
||||||
{bookmark.notes}
|
{bookmark.notes}
|
||||||
</textarea>
|
</textarea>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<input type="hidden" name="website_title" id="id_website_title"
|
<input type="hidden" name="website_title" id="id_website_title"
|
||||||
value="{bookmark.website_title}">
|
value="{bookmark.website_title}">
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<input type="hidden" name="website_description" id="id_website_description"
|
<input type="hidden" name="website_description" id="id_website_description"
|
||||||
value="{bookmark.website_description}">
|
value="{bookmark.website_description}">
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_redirect_to_return_url(self):
|
def test_should_redirect_to_return_url(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
form_data = self.create_form_data()
|
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)
|
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):
|
def test_should_redirect_to_bookmark_index_by_default(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
form_data = self.create_form_data()
|
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):
|
def test_should_not_redirect_to_external_url(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
def post_with(return_url, follow=None):
|
def post_with(return_url, follow=None):
|
||||||
form_data = self.create_form_data()
|
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)
|
return self.client.post(url, form_data, follow=follow)
|
||||||
|
|
||||||
response = post_with('https://example.com')
|
response = post_with("https://example.com")
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||||
response = post_with('//example.com')
|
response = post_with("//example.com")
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
self.assertRedirects(response, reverse("bookmarks:index"))
|
||||||
response = post_with('://example.com')
|
response = post_with("://example.com")
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
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)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def test_can_only_edit_own_bookmarks(self):
|
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)
|
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()
|
bookmark.refresh_from_db()
|
||||||
self.assertNotEqual(bookmark.url, form_data['url'])
|
self.assertNotEqual(bookmark.url, form_data["url"])
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def test_should_respect_share_profile_setting(self):
|
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.enable_sharing = False
|
||||||
self.user.profile.save()
|
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()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML('''
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
<label for="id_shared" class="form-checkbox">
|
<label for="id_shared" class="form-checkbox">
|
||||||
<input type="checkbox" name="shared" id="id_shared">
|
<input type="checkbox" name="shared" id="id_shared">
|
||||||
<i class="form-icon"></i>
|
<i class="form-icon"></i>
|
||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</label>
|
</label>
|
||||||
''', html, count=0)
|
""",
|
||||||
|
html,
|
||||||
|
count=0,
|
||||||
|
)
|
||||||
|
|
||||||
self.user.profile.enable_sharing = True
|
self.user.profile.enable_sharing = True
|
||||||
self.user.profile.save()
|
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()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML('''
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
<label for="id_shared" class="form-checkbox">
|
<label for="id_shared" class="form-checkbox">
|
||||||
<input type="checkbox" name="shared" id="id_shared">
|
<input type="checkbox" name="shared" id="id_shared">
|
||||||
<i class="form-icon"></i>
|
<i class="form-icon"></i>
|
||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</label>
|
</label>
|
||||||
''', html, count=1)
|
""",
|
||||||
|
html,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_hide_notes_if_there_are_no_notes(self):
|
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||||
bookmark = self.setup_bookmark()
|
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)
|
self.assertContains(response, '<details class="notes">', count=1)
|
||||||
|
|
||||||
def test_should_show_notes_if_there_are_notes(self):
|
def test_should_show_notes_if_there_are_notes(self):
|
||||||
bookmark = self.setup_bookmark(notes='test notes')
|
bookmark = self.setup_bookmark(notes="test notes")
|
||||||
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" open>', count=1)
|
self.assertContains(response, '<details class="notes" open>', count=1)
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
|
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):
|
class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||||
@@ -15,33 +19,41 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
self.client.force_login(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())
|
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)
|
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))
|
self.assertEqual(len(bookmark_items), len(bookmarks))
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
bookmark_item = bookmark_list.select_one(
|
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)
|
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())
|
soup = self.make_soup(response.content.decode())
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
bookmark_item = soup.select_one(
|
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)
|
self.assertIsNone(bookmark_item)
|
||||||
|
|
||||||
def assertVisibleTags(self, response, tags: List[Tag]):
|
def assertVisibleTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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)
|
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))
|
self.assertEqual(len(tag_items), len(tags))
|
||||||
|
|
||||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
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]):
|
def assertInvisibleTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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]
|
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]):
|
def assertSelectedTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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)
|
self.assertIsNotNone(selected_tags)
|
||||||
|
|
||||||
tag_list = selected_tags.select('a')
|
tag_list = selected_tags.select("a")
|
||||||
self.assertEqual(len(tag_list), len(tags))
|
self.assertEqual(len(tag_list), len(tags))
|
||||||
|
|
||||||
for tag in 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):
|
def assertEditLink(self, response, url):
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<a href="{url}">Edit</a>
|
<a href="{url}">Edit</a>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def assertBulkActionForm(self, response, url: str):
|
def assertBulkActionForm(self, response, url: str):
|
||||||
html = collapse_whitespace(response.content.decode())
|
html = collapse_whitespace(response.content.decode())
|
||||||
needle = collapse_whitespace(f'''
|
needle = collapse_whitespace(
|
||||||
|
f"""
|
||||||
<form class="bookmark-actions"
|
<form class="bookmark-actions"
|
||||||
action="{url}"
|
action="{url}"
|
||||||
method="post" autocomplete="off">
|
method="post" autocomplete="off">
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
self.assertIn(needle, html)
|
self.assertIn(needle, html)
|
||||||
|
|
||||||
def test_should_list_unarchived_and_user_owned_bookmarks(self):
|
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)
|
visible_bookmarks = self.setup_numbered_bookmarks(3)
|
||||||
invisible_bookmarks = [
|
invisible_bookmarks = [
|
||||||
self.setup_bookmark(is_archived=True),
|
self.setup_bookmark(is_archived=True),
|
||||||
self.setup_bookmark(user=other_user),
|
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.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||||
|
|
||||||
def test_should_list_bookmarks_matching_query(self):
|
def test_should_list_bookmarks_matching_query(self):
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo')
|
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix="foo")
|
||||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar')
|
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.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||||
|
|
||||||
def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self):
|
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)
|
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')
|
archived_bookmarks = self.setup_numbered_bookmarks(
|
||||||
other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, user=other_user, tag_prefix='otheruser')
|
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)
|
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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_tags)
|
self.assertInvisibleTags(response, invisible_tags)
|
||||||
|
|
||||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
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')
|
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='bar', tag_prefix='bar')
|
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)
|
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
|
||||||
invisible_tags = self.get_tags_from_bookmarks(invisible_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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_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):
|
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
user_profile.search_preferences = {
|
user_profile.search_preferences = {
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
unread_bookmarks = self.setup_numbered_bookmarks(3, unread=True, with_tags=True, prefix='unread',
|
unread_bookmarks = self.setup_numbered_bookmarks(
|
||||||
tag_prefix='unread')
|
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')
|
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)
|
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
|
||||||
read_tags = self.get_tags_from_bookmarks(read_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.assertVisibleBookmarks(response, unread_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, read_bookmarks)
|
self.assertInvisibleBookmarks(response, read_bookmarks)
|
||||||
self.assertVisibleTags(response, unread_tags)
|
self.assertVisibleTags(response, unread_tags)
|
||||||
@@ -163,11 +199,16 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
]
|
]
|
||||||
self.setup_bookmark(tags=tags)
|
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]])
|
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 = [
|
tags = [
|
||||||
self.setup_tag(),
|
self.setup_tag(),
|
||||||
self.setup_tag(),
|
self.setup_tag(),
|
||||||
@@ -177,7 +218,9 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
]
|
]
|
||||||
self.setup_bookmark(title=tags[0].name, tags=tags)
|
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]])
|
self.assertSelectedTags(response, [tags[1]])
|
||||||
|
|
||||||
@@ -194,16 +237,18 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
]
|
]
|
||||||
self.setup_bookmark(tags=tags)
|
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]])
|
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||||
|
|
||||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3)
|
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):
|
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
@@ -212,71 +257,72 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
|
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3)
|
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):
|
def test_edit_link_return_url_respects_search_options(self):
|
||||||
bookmark = self.setup_bookmark(title='foo')
|
bookmark = self.setup_bookmark(title="foo")
|
||||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||||
base_url = reverse('bookmarks:index')
|
base_url = reverse("bookmarks:index")
|
||||||
|
|
||||||
# without query params
|
# without query params
|
||||||
return_url = urllib.parse.quote(base_url)
|
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)
|
response = self.client.get(base_url)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query
|
# with query
|
||||||
url_params = '?q=foo'
|
url_params = "?q=foo"
|
||||||
return_url = urllib.parse.quote(base_url + url_params)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query and sort and page
|
# 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)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
def test_bulk_edit_respects_search_options(self):
|
def test_bulk_edit_respects_search_options(self):
|
||||||
action_url = reverse('bookmarks:index.action')
|
action_url = reverse("bookmarks:index.action")
|
||||||
base_url = reverse('bookmarks:index')
|
base_url = reverse("bookmarks:index")
|
||||||
|
|
||||||
# without params
|
# without params
|
||||||
return_url = urllib.parse.quote_plus(base_url)
|
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)
|
response = self.client.get(base_url)
|
||||||
self.assertBulkActionForm(response, url)
|
self.assertBulkActionForm(response, url)
|
||||||
|
|
||||||
# with query
|
# with query
|
||||||
url_params = '?q=foo'
|
url_params = "?q=foo"
|
||||||
return_url = urllib.parse.quote_plus(base_url + url_params)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertBulkActionForm(response, url)
|
self.assertBulkActionForm(response, url)
|
||||||
|
|
||||||
# with query and sort
|
# 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)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertBulkActionForm(response, url)
|
self.assertBulkActionForm(response, url)
|
||||||
|
|
||||||
def test_allowed_bulk_actions(self):
|
def test_allowed_bulk_actions(self):
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<select name="bulk_action" class="form-select select-sm">
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
<option value="bulk_archive">Archive</option>
|
<option value="bulk_archive">Archive</option>
|
||||||
<option value="bulk_delete">Delete</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_read">Mark as read</option>
|
||||||
<option value="bulk_unread">Mark as unread</option>
|
<option value="bulk_unread">Mark as unread</option>
|
||||||
</select>
|
</select>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
user_profile.enable_sharing = True
|
user_profile.enable_sharing = True
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
url = reverse('bookmarks:index')
|
url = reverse("bookmarks:index")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<select name="bulk_action" class="form-select select-sm">
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
<option value="bulk_archive">Archive</option>
|
<option value="bulk_archive">Archive</option>
|
||||||
<option value="bulk_delete">Delete</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_share">Share</option>
|
||||||
<option value="bulk_unshare">Unshare</option>
|
<option value="bulk_unshare">Unshare</option>
|
||||||
</select>
|
</select>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_apply_search_preferences(self):
|
def test_apply_search_preferences(self):
|
||||||
# no params
|
# 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.status_code, 302)
|
||||||
self.assertEqual(response.url, reverse('bookmarks:index'))
|
self.assertEqual(response.url, reverse("bookmarks:index"))
|
||||||
|
|
||||||
# some params
|
# some params
|
||||||
response = self.client.post(reverse('bookmarks:index'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:index"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
})
|
"q": "foo",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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
|
# params with default value are removed
|
||||||
response = self.client.post(reverse('bookmarks:index'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:index"),
|
||||||
'user': '',
|
{
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
"q": "foo",
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
"user": "",
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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
|
# page is removed
|
||||||
response = self.client.post(reverse('bookmarks:index'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:index"),
|
||||||
'page': '2',
|
{
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"q": "foo",
|
||||||
})
|
"page": "2",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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):
|
def test_save_search_preferences(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
|
|
||||||
# no params
|
# no params
|
||||||
self.client.post(reverse('bookmarks:index'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:index"),
|
||||||
})
|
{
|
||||||
|
"save": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# with param
|
# with param
|
||||||
self.client.post(reverse('bookmarks:index'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:index"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
})
|
"save": "",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# add a param
|
# add a param
|
||||||
self.client.post(reverse('bookmarks:index'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:index"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"save": "",
|
||||||
})
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# remove a param
|
# remove a param
|
||||||
self.client.post(reverse('bookmarks:index'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:index"),
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
{
|
||||||
})
|
"save": "",
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# ignores non-preferences
|
# ignores non-preferences
|
||||||
self.client.post(reverse('bookmarks:index'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:index"),
|
||||||
'q': 'foo',
|
{
|
||||||
'user': 'john',
|
"save": "",
|
||||||
'page': '3',
|
"q": "foo",
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"user": "john",
|
||||||
})
|
"page": "3",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_url_encode_bookmark_actions_url(self):
|
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)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
soup = self.make_soup(html)
|
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'],
|
self.assertEqual(
|
||||||
'/bookmarks/action?q=%23foo&return_url=%2Fbookmarks%3Fq%3D%2523foo')
|
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
|
# capture number of queries
|
||||||
context = CaptureQueriesContext(self.get_connection())
|
context = CaptureQueriesContext(self.get_connection())
|
||||||
with context:
|
with context:
|
||||||
response = self.client.get(reverse('bookmarks:index'))
|
response = self.client.get(reverse("bookmarks:index"))
|
||||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks)
|
self.assertContains(
|
||||||
|
response, "<li ld-bookmark-item>", num_initial_bookmarks
|
||||||
|
)
|
||||||
|
|
||||||
number_of_queries = context.final_queries
|
number_of_queries = context.final_queries
|
||||||
|
|
||||||
@@ -38,5 +40,9 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
|
|||||||
|
|
||||||
# assert num queries doesn't increase
|
# assert num queries doesn't increase
|
||||||
with self.assertNumQueries(number_of_queries):
|
with self.assertNumQueries(number_of_queries):
|
||||||
response = self.client.get(reverse('bookmarks:index'))
|
response = self.client.get(reverse("bookmarks:index"))
|
||||||
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks + num_additional_bookmarks)
|
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:
|
if overrides is None:
|
||||||
overrides = {}
|
overrides = {}
|
||||||
form_data = {
|
form_data = {
|
||||||
'url': 'http://example.com',
|
"url": "http://example.com",
|
||||||
'tag_string': 'tag1 tag2',
|
"tag_string": "tag1 tag2",
|
||||||
'title': 'test title',
|
"title": "test title",
|
||||||
'description': 'test description',
|
"description": "test description",
|
||||||
'notes': 'test notes',
|
"notes": "test notes",
|
||||||
'unread': False,
|
"unread": False,
|
||||||
'shared': False,
|
"shared": False,
|
||||||
'auto_close': '',
|
"auto_close": "",
|
||||||
}
|
}
|
||||||
return {**form_data, **overrides}
|
return {**form_data, **overrides}
|
||||||
|
|
||||||
def test_should_create_new_bookmark(self):
|
def test_should_create_new_bookmark(self):
|
||||||
form_data = self.create_form_data()
|
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)
|
self.assertEqual(Bookmark.objects.count(), 1)
|
||||||
|
|
||||||
bookmark = Bookmark.objects.first()
|
bookmark = Bookmark.objects.first()
|
||||||
self.assertEqual(bookmark.owner, self.user)
|
self.assertEqual(bookmark.owner, self.user)
|
||||||
self.assertEqual(bookmark.url, form_data['url'])
|
self.assertEqual(bookmark.url, form_data["url"])
|
||||||
self.assertEqual(bookmark.title, form_data['title'])
|
self.assertEqual(bookmark.title, form_data["title"])
|
||||||
self.assertEqual(bookmark.description, form_data['description'])
|
self.assertEqual(bookmark.description, form_data["description"])
|
||||||
self.assertEqual(bookmark.notes, form_data['notes'])
|
self.assertEqual(bookmark.notes, form_data["notes"])
|
||||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
self.assertEqual(bookmark.unread, form_data["unread"])
|
||||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
self.assertEqual(bookmark.shared, form_data["shared"])
|
||||||
self.assertEqual(bookmark.tags.count(), 2)
|
self.assertEqual(bookmark.tags.count(), 2)
|
||||||
tags = bookmark.tags.order_by('name').all()
|
tags = bookmark.tags.order_by("name").all()
|
||||||
self.assertEqual(tags[0].name, 'tag1')
|
self.assertEqual(tags[0].name, "tag1")
|
||||||
self.assertEqual(tags[1].name, 'tag2')
|
self.assertEqual(tags[1].name, "tag2")
|
||||||
|
|
||||||
def test_should_create_new_unread_bookmark(self):
|
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)
|
self.assertEqual(Bookmark.objects.count(), 1)
|
||||||
|
|
||||||
@@ -57,9 +57,9 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertTrue(bookmark.unread)
|
self.assertTrue(bookmark.unread)
|
||||||
|
|
||||||
def test_should_create_new_shared_bookmark(self):
|
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)
|
self.assertEqual(Bookmark.objects.count(), 1)
|
||||||
|
|
||||||
@@ -67,125 +67,146 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertTrue(bookmark.shared)
|
self.assertTrue(bookmark.shared)
|
||||||
|
|
||||||
def test_should_prefill_url_from_url_parameter(self):
|
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()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
'<input type="text" name="url" value="http://example.com" '
|
'<input type="text" name="url" value="http://example.com" '
|
||||||
'placeholder=" " autofocus class="form-input" required '
|
'placeholder=" " autofocus class="form-input" required '
|
||||||
'id="id_url">',
|
'id="id_url">',
|
||||||
html)
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_prefill_title_from_url_parameter(self):
|
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()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
'<input type="text" name="title" value="Example Title" '
|
'<input type="text" name="title" value="Example Title" '
|
||||||
'class="form-input" maxlength="512" autocomplete="off" '
|
'class="form-input" maxlength="512" autocomplete="off" '
|
||||||
'id="id_title">',
|
'id="id_title">',
|
||||||
html)
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_prefill_description_from_url_parameter(self):
|
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()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
'<textarea name="description" class="form-input" cols="40" '
|
'<textarea name="description" class="form-input" cols="40" '
|
||||||
'rows="2" id="id_description">Example Site Description</textarea>',
|
'rows="2" id="id_description">Example Site Description</textarea>',
|
||||||
html)
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_enable_auto_close_when_specified_in_url_parameter(self):
|
def test_should_enable_auto_close_when_specified_in_url_parameter(self):
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("bookmarks:new") + "?auto_close")
|
||||||
reverse('bookmarks:new') + '?auto_close')
|
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
'<input type="hidden" name="auto_close" value="true" '
|
'<input type="hidden" name="auto_close" value="true" '
|
||||||
'id="id_auto_close">',
|
'id="id_auto_close">',
|
||||||
html)
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(
|
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(self):
|
||||||
self):
|
response = self.client.get(reverse("bookmarks:new"))
|
||||||
response = self.client.get(reverse('bookmarks:new'))
|
|
||||||
html = response.content.decode()
|
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):
|
def test_should_redirect_to_index_view(self):
|
||||||
form_data = self.create_form_data()
|
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):
|
def test_should_not_redirect_to_external_url(self):
|
||||||
form_data = self.create_form_data()
|
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):
|
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):
|
def test_should_respect_share_profile_setting(self):
|
||||||
self.user.profile.enable_sharing = False
|
self.user.profile.enable_sharing = False
|
||||||
self.user.profile.save()
|
self.user.profile.save()
|
||||||
response = self.client.get(reverse('bookmarks:new'))
|
response = self.client.get(reverse("bookmarks:new"))
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML('''
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
<label for="id_shared" class="form-checkbox">
|
<label for="id_shared" class="form-checkbox">
|
||||||
<input type="checkbox" name="shared" id="id_shared">
|
<input type="checkbox" name="shared" id="id_shared">
|
||||||
<i class="form-icon"></i>
|
<i class="form-icon"></i>
|
||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</label>
|
</label>
|
||||||
''', html, count=0)
|
""",
|
||||||
|
html,
|
||||||
|
count=0,
|
||||||
|
)
|
||||||
|
|
||||||
self.user.profile.enable_sharing = True
|
self.user.profile.enable_sharing = True
|
||||||
self.user.profile.save()
|
self.user.profile.save()
|
||||||
response = self.client.get(reverse('bookmarks:new'))
|
response = self.client.get(reverse("bookmarks:new"))
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML('''
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
<label for="id_shared" class="form-checkbox">
|
<label for="id_shared" class="form-checkbox">
|
||||||
<input type="checkbox" name="shared" id="id_shared">
|
<input type="checkbox" name="shared" id="id_shared">
|
||||||
<i class="form-icon"></i>
|
<i class="form-icon"></i>
|
||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</label>
|
</label>
|
||||||
''', html, count=1)
|
""",
|
||||||
|
html,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_show_respective_share_hint(self):
|
def test_should_show_respective_share_hint(self):
|
||||||
self.user.profile.enable_sharing = True
|
self.user.profile.enable_sharing = True
|
||||||
self.user.profile.save()
|
self.user.profile.save()
|
||||||
|
|
||||||
response = self.client.get(reverse('bookmarks:new'))
|
response = self.client.get(reverse("bookmarks:new"))
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
self.assertInHTML('''
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
<div class="form-input-hint">
|
<div class="form-input-hint">
|
||||||
Share this bookmark with other registered users.
|
Share this bookmark with other registered users.
|
||||||
</div>
|
</div>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
self.user.profile.enable_public_sharing = True
|
self.user.profile.enable_public_sharing = True
|
||||||
self.user.profile.save()
|
self.user.profile.save()
|
||||||
|
|
||||||
response = self.client.get(reverse('bookmarks:new'))
|
response = self.client.get(reverse("bookmarks:new"))
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
self.assertInHTML('''
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
<div class="form-input-hint">
|
<div class="form-input-hint">
|
||||||
Share this bookmark with other registered users and anonymous users.
|
Share this bookmark with other registered users and anonymous users.
|
||||||
</div>
|
</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):
|
self.assertContains(response, '<details class="notes">', count=1)
|
||||||
bookmark = self.setup_bookmark()
|
|
||||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
|
||||||
|
|
||||||
self.assertContains(response, '<details class="notes">', count=1)
|
|
||||||
|
|||||||
@@ -9,40 +9,45 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
|
|||||||
# no params
|
# no params
|
||||||
search = BookmarkSearch()
|
search = BookmarkSearch()
|
||||||
form = BookmarkSearchForm(search)
|
form = BookmarkSearchForm(search)
|
||||||
self.assertEqual(form['q'].initial, '')
|
self.assertEqual(form["q"].initial, "")
|
||||||
self.assertEqual(form['user'].initial, '')
|
self.assertEqual(form["user"].initial, "")
|
||||||
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC)
|
self.assertEqual(form["sort"].initial, BookmarkSearch.SORT_ADDED_DESC)
|
||||||
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_OFF)
|
self.assertEqual(form["shared"].initial, BookmarkSearch.FILTER_SHARED_OFF)
|
||||||
self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_OFF)
|
self.assertEqual(form["unread"].initial, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||||
|
|
||||||
# with params
|
# with params
|
||||||
search = BookmarkSearch(q='search query',
|
search = BookmarkSearch(
|
||||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
q="search query",
|
||||||
user='user123',
|
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
user="user123",
|
||||||
unread=BookmarkSearch.FILTER_UNREAD_YES)
|
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||||
|
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
)
|
||||||
form = BookmarkSearchForm(search)
|
form = BookmarkSearchForm(search)
|
||||||
self.assertEqual(form['q'].initial, 'search query')
|
self.assertEqual(form["q"].initial, "search query")
|
||||||
self.assertEqual(form['user'].initial, 'user123')
|
self.assertEqual(form["user"].initial, "user123")
|
||||||
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC)
|
self.assertEqual(form["sort"].initial, BookmarkSearch.SORT_ADDED_ASC)
|
||||||
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_SHARED)
|
self.assertEqual(form["shared"].initial, BookmarkSearch.FILTER_SHARED_SHARED)
|
||||||
self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_YES)
|
self.assertEqual(form["unread"].initial, BookmarkSearch.FILTER_UNREAD_YES)
|
||||||
|
|
||||||
def test_user_options(self):
|
def test_user_options(self):
|
||||||
users = [
|
users = [
|
||||||
self.setup_user('user1'),
|
self.setup_user("user1"),
|
||||||
self.setup_user('user2'),
|
self.setup_user("user2"),
|
||||||
self.setup_user('user3'),
|
self.setup_user("user3"),
|
||||||
]
|
]
|
||||||
search = BookmarkSearch()
|
search = BookmarkSearch()
|
||||||
form = BookmarkSearchForm(search, users=users)
|
form = BookmarkSearchForm(search, users=users)
|
||||||
|
|
||||||
self.assertCountEqual(form['user'].field.choices, [
|
self.assertCountEqual(
|
||||||
('', 'Everyone'),
|
form["user"].field.choices,
|
||||||
('user1', 'user1'),
|
[
|
||||||
('user2', 'user2'),
|
("", "Everyone"),
|
||||||
('user3', 'user3'),
|
("user1", "user1"),
|
||||||
])
|
("user2", "user2"),
|
||||||
|
("user3", "user3"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_hidden_fields(self):
|
def test_hidden_fields(self):
|
||||||
# no modified params
|
# no modified params
|
||||||
@@ -51,24 +56,27 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(len(form.hidden_fields()), 0)
|
self.assertEqual(len(form.hidden_fields()), 0)
|
||||||
|
|
||||||
# some modified params
|
# some modified params
|
||||||
search = BookmarkSearch(q='search query',
|
search = BookmarkSearch(q="search query", sort=BookmarkSearch.SORT_ADDED_ASC)
|
||||||
sort=BookmarkSearch.SORT_ADDED_ASC)
|
|
||||||
form = BookmarkSearchForm(search)
|
form = BookmarkSearchForm(search)
|
||||||
self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort']])
|
self.assertCountEqual(form.hidden_fields(), [form["q"], form["sort"]])
|
||||||
|
|
||||||
# all modified params
|
# all modified params
|
||||||
search = BookmarkSearch(q='search query',
|
search = BookmarkSearch(
|
||||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
q="search query",
|
||||||
user='user123',
|
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
user="user123",
|
||||||
unread=BookmarkSearch.FILTER_UNREAD_YES)
|
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||||
|
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
)
|
||||||
form = BookmarkSearchForm(search)
|
form = BookmarkSearchForm(search)
|
||||||
self.assertCountEqual(form.hidden_fields(),
|
self.assertCountEqual(
|
||||||
[form['q'], form['sort'], form['user'], form['shared'], form['unread']])
|
form.hidden_fields(),
|
||||||
|
[form["q"], form["sort"], form["user"], form["shared"], form["unread"]],
|
||||||
|
)
|
||||||
|
|
||||||
# some modified params are editable fields
|
# some modified params are editable fields
|
||||||
search = BookmarkSearch(q='search query',
|
search = BookmarkSearch(
|
||||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
q="search query", sort=BookmarkSearch.SORT_ADDED_ASC, user="user123"
|
||||||
user='user123')
|
)
|
||||||
form = BookmarkSearchForm(search, editable_fields=['q', 'user'])
|
form = BookmarkSearchForm(search, editable_fields=["q", "user"])
|
||||||
self.assertCountEqual(form.hidden_fields(), [form['sort']])
|
self.assertCountEqual(form.hidden_fields(), [form["sort"]])
|
||||||
|
|||||||
@@ -10,57 +10,59 @@ class BookmarkSearchModelTest(TestCase):
|
|||||||
query_dict = QueryDict()
|
query_dict = QueryDict()
|
||||||
|
|
||||||
search = BookmarkSearch.from_request(query_dict)
|
search = BookmarkSearch.from_request(query_dict)
|
||||||
self.assertEqual(search.q, '')
|
self.assertEqual(search.q, "")
|
||||||
self.assertEqual(search.user, '')
|
self.assertEqual(search.user, "")
|
||||||
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
|
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
|
||||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
||||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||||
|
|
||||||
# some params
|
# 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)
|
bookmark_search = BookmarkSearch.from_request(query_dict)
|
||||||
self.assertEqual(bookmark_search.q, 'search query')
|
self.assertEqual(bookmark_search.q, "search query")
|
||||||
self.assertEqual(bookmark_search.user, 'user123')
|
self.assertEqual(bookmark_search.user, "user123")
|
||||||
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
|
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
|
||||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
||||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||||
|
|
||||||
# all params
|
# 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)
|
search = BookmarkSearch.from_request(query_dict)
|
||||||
self.assertEqual(search.q, 'search query')
|
self.assertEqual(search.q, "search query")
|
||||||
self.assertEqual(search.user, 'user123')
|
self.assertEqual(search.user, "user123")
|
||||||
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
|
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
|
||||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
|
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
|
||||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
|
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
|
||||||
|
|
||||||
# respects preferences
|
# respects preferences
|
||||||
preferences = {
|
preferences = {
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
query_dict = QueryDict('q=search query')
|
query_dict = QueryDict("q=search query")
|
||||||
|
|
||||||
search = BookmarkSearch.from_request(query_dict, preferences)
|
search = BookmarkSearch.from_request(query_dict, preferences)
|
||||||
self.assertEqual(search.q, 'search query')
|
self.assertEqual(search.q, "search query")
|
||||||
self.assertEqual(search.user, '')
|
self.assertEqual(search.user, "")
|
||||||
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
|
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
|
||||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
|
||||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
|
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
|
||||||
|
|
||||||
# query overrides preferences
|
# query overrides preferences
|
||||||
preferences = {
|
preferences = {
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_SHARED,
|
"shared": BookmarkSearch.FILTER_SHARED_SHARED,
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"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)
|
search = BookmarkSearch.from_request(query_dict, preferences)
|
||||||
self.assertEqual(search.q, '')
|
self.assertEqual(search.q, "")
|
||||||
self.assertEqual(search.user, '')
|
self.assertEqual(search.user, "")
|
||||||
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC)
|
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC)
|
||||||
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED)
|
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED)
|
||||||
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
|
||||||
@@ -72,28 +74,36 @@ class BookmarkSearchModelTest(TestCase):
|
|||||||
self.assertEqual(len(modified_params), 0)
|
self.assertEqual(len(modified_params), 0)
|
||||||
|
|
||||||
# params are default values
|
# 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
|
modified_params = bookmark_search.modified_params
|
||||||
self.assertEqual(len(modified_params), 0)
|
self.assertEqual(len(modified_params), 0)
|
||||||
|
|
||||||
# some modified params
|
# 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
|
modified_params = bookmark_search.modified_params
|
||||||
self.assertCountEqual(modified_params, ['q', 'sort'])
|
self.assertCountEqual(modified_params, ["q", "sort"])
|
||||||
|
|
||||||
# all modified params
|
# all modified params
|
||||||
bookmark_search = BookmarkSearch(q='search query',
|
bookmark_search = BookmarkSearch(
|
||||||
sort=BookmarkSearch.SORT_ADDED_ASC,
|
q="search query",
|
||||||
user='user123',
|
sort=BookmarkSearch.SORT_ADDED_ASC,
|
||||||
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
user="user123",
|
||||||
unread=BookmarkSearch.FILTER_UNREAD_YES)
|
shared=BookmarkSearch.FILTER_SHARED_SHARED,
|
||||||
|
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
)
|
||||||
modified_params = bookmark_search.modified_params
|
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 are not modified params
|
||||||
preferences = {
|
preferences = {
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
bookmark_search = BookmarkSearch(preferences=preferences)
|
bookmark_search = BookmarkSearch(preferences=preferences)
|
||||||
modified_params = bookmark_search.modified_params
|
modified_params = bookmark_search.modified_params
|
||||||
@@ -101,27 +111,31 @@ class BookmarkSearchModelTest(TestCase):
|
|||||||
|
|
||||||
# param is not modified if it matches the preference
|
# param is not modified if it matches the preference
|
||||||
preferences = {
|
preferences = {
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_ASC,
|
bookmark_search = BookmarkSearch(
|
||||||
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
sort=BookmarkSearch.SORT_TITLE_ASC,
|
||||||
preferences=preferences)
|
unread=BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
preferences=preferences,
|
||||||
|
)
|
||||||
modified_params = bookmark_search.modified_params
|
modified_params = bookmark_search.modified_params
|
||||||
self.assertEqual(len(modified_params), 0)
|
self.assertEqual(len(modified_params), 0)
|
||||||
|
|
||||||
# overriding preferences is a modified param
|
# overriding preferences is a modified param
|
||||||
preferences = {
|
preferences = {
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_SHARED,
|
"shared": BookmarkSearch.FILTER_SHARED_SHARED,
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC,
|
bookmark_search = BookmarkSearch(
|
||||||
shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
|
sort=BookmarkSearch.SORT_TITLE_DESC,
|
||||||
unread=BookmarkSearch.FILTER_UNREAD_OFF,
|
shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
|
||||||
preferences=preferences)
|
unread=BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
preferences=preferences,
|
||||||
|
)
|
||||||
modified_params = bookmark_search.modified_params
|
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):
|
def test_has_modifications(self):
|
||||||
# no params
|
# no params
|
||||||
@@ -129,34 +143,49 @@ class BookmarkSearchModelTest(TestCase):
|
|||||||
self.assertFalse(bookmark_search.has_modifications)
|
self.assertFalse(bookmark_search.has_modifications)
|
||||||
|
|
||||||
# params are default values
|
# 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)
|
self.assertFalse(bookmark_search.has_modifications)
|
||||||
|
|
||||||
# modified params
|
# 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)
|
self.assertTrue(bookmark_search.has_modifications)
|
||||||
|
|
||||||
def test_preferences_dict(self):
|
def test_preferences_dict(self):
|
||||||
# no params
|
# no params
|
||||||
bookmark_search = BookmarkSearch()
|
bookmark_search = BookmarkSearch()
|
||||||
self.assertEqual(bookmark_search.preferences_dict, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
bookmark_search.preferences_dict,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# with params
|
# with params
|
||||||
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES)
|
bookmark_search = BookmarkSearch(
|
||||||
self.assertEqual(bookmark_search.preferences_dict, {
|
sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES
|
||||||
'sort': BookmarkSearch.SORT_TITLE_DESC,
|
)
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
self.assertEqual(
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
bookmark_search.preferences_dict,
|
||||||
})
|
{
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_DESC,
|
||||||
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# only returns preferences
|
# only returns preferences
|
||||||
bookmark_search = BookmarkSearch(q='search query', user='user123')
|
bookmark_search = BookmarkSearch(q="search query", user="user123")
|
||||||
self.assertEqual(bookmark_search.preferences_dict, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
bookmark_search.preferences_dict,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"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):
|
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()
|
rf = RequestFactory()
|
||||||
request = rf.get(url)
|
request = rf.get(url)
|
||||||
request.user = self.get_or_create_test_user()
|
request.user = self.get_or_create_test_user()
|
||||||
request.user_profile = self.get_or_create_test_user().profile
|
request.user_profile = self.get_or_create_test_user().profile
|
||||||
search = BookmarkSearch.from_request(request.GET)
|
search = BookmarkSearch.from_request(request.GET)
|
||||||
context = RequestContext(request, {
|
context = RequestContext(
|
||||||
'request': request,
|
request,
|
||||||
'search': search,
|
{
|
||||||
'tags': tags,
|
"request": request,
|
||||||
'mode': mode,
|
"search": search,
|
||||||
})
|
"tags": tags,
|
||||||
|
"mode": mode,
|
||||||
|
},
|
||||||
|
)
|
||||||
template_to_render = Template(
|
template_to_render = Template(
|
||||||
'{% load bookmarks %}'
|
"{% load bookmarks %}" "{% bookmark_search search tags mode %}"
|
||||||
'{% bookmark_search search tags mode %}'
|
|
||||||
)
|
)
|
||||||
return template_to_render.render(context)
|
return template_to_render.render(context)
|
||||||
|
|
||||||
@@ -31,7 +35,7 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
self.assertIsNotNone(input)
|
self.assertIsNotNone(input)
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
self.assertEqual(input['value'], value)
|
self.assertEqual(input["value"], value)
|
||||||
|
|
||||||
def assertNoHiddenInput(self, form: BeautifulSoup, name: str):
|
def assertNoHiddenInput(self, form: BeautifulSoup, name: str):
|
||||||
input = form.select_one(f'input[name="{name}"][type="hidden"]')
|
input = form.select_one(f'input[name="{name}"][type="hidden"]')
|
||||||
@@ -42,19 +46,19 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
self.assertIsNotNone(input)
|
self.assertIsNotNone(input)
|
||||||
|
|
||||||
if value is not None:
|
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):
|
def assertSelect(self, form: BeautifulSoup, name: str, value: str = None):
|
||||||
select = form.select_one(f'select[name="{name}"]')
|
select = form.select_one(f'select[name="{name}"]')
|
||||||
self.assertIsNotNone(select)
|
self.assertIsNotNone(select)
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
options = select.select('option')
|
options = select.select("option")
|
||||||
for option in options:
|
for option in options:
|
||||||
if option['value'] == value:
|
if option["value"] == value:
|
||||||
self.assertTrue(option.has_attr('selected'))
|
self.assertTrue(option.has_attr("selected"))
|
||||||
else:
|
else:
|
||||||
self.assertFalse(option.has_attr('selected'))
|
self.assertFalse(option.has_attr("selected"))
|
||||||
|
|
||||||
def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str = None):
|
def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str = None):
|
||||||
radios = form.select(f'input[name="{name}"][type="radio"]')
|
radios = form.select(f'input[name="{name}"][type="radio"]')
|
||||||
@@ -62,165 +66,182 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
for radio in radios:
|
for radio in radios:
|
||||||
if radio['value'] == value:
|
if radio["value"] == value:
|
||||||
self.assertTrue(radio.has_attr('checked'))
|
self.assertTrue(radio.has_attr("checked"))
|
||||||
else:
|
else:
|
||||||
self.assertFalse(radio.has_attr('checked'))
|
self.assertFalse(radio.has_attr("checked"))
|
||||||
|
|
||||||
def assertNoRadioGroup(self, form: BeautifulSoup, name: str):
|
def assertNoRadioGroup(self, form: BeautifulSoup, name: str):
|
||||||
radios = form.select(f'input[name="{name}"][type="radio"]')
|
radios = form.select(f'input[name="{name}"][type="radio"]')
|
||||||
self.assertTrue(len(radios) == 0)
|
self.assertTrue(len(radios) == 0)
|
||||||
|
|
||||||
def assertUnmodifiedLabel(self, html: str, text: str, id: str = ''):
|
def assertUnmodifiedLabel(self, html: str, text: str, id: str = ""):
|
||||||
id_attr = f'for="{id}"' if id else ''
|
id_attr = f'for="{id}"' if id else ""
|
||||||
tag = 'label' if id else 'div'
|
tag = "label" if id else "div"
|
||||||
needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>'
|
needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>'
|
||||||
|
|
||||||
self.assertInHTML(needle, html)
|
self.assertInHTML(needle, html)
|
||||||
|
|
||||||
def assertModifiedLabel(self, html: str, text: str, id: str = ''):
|
def assertModifiedLabel(self, html: str, text: str, id: str = ""):
|
||||||
id_attr = f'for="{id}"' if id else ''
|
id_attr = f'for="{id}"' if id else ""
|
||||||
tag = 'label' if id else 'div'
|
tag = "label" if id else "div"
|
||||||
needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>'
|
needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>'
|
||||||
|
|
||||||
self.assertInHTML(needle, html)
|
self.assertInHTML(needle, html)
|
||||||
|
|
||||||
def test_search_form_inputs(self):
|
def test_search_form_inputs(self):
|
||||||
# Without params
|
# Without params
|
||||||
url = '/test'
|
url = "/test"
|
||||||
rendered_template = self.render_template(url)
|
rendered_template = self.render_template(url)
|
||||||
soup = self.make_soup(rendered_template)
|
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.assertSearchInput(search_form, "q")
|
||||||
self.assertNoHiddenInput(search_form, 'user')
|
self.assertNoHiddenInput(search_form, "user")
|
||||||
self.assertNoHiddenInput(search_form, 'sort')
|
self.assertNoHiddenInput(search_form, "sort")
|
||||||
self.assertNoHiddenInput(search_form, 'shared')
|
self.assertNoHiddenInput(search_form, "shared")
|
||||||
self.assertNoHiddenInput(search_form, 'unread')
|
self.assertNoHiddenInput(search_form, "unread")
|
||||||
|
|
||||||
# With params
|
# 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)
|
rendered_template = self.render_template(url)
|
||||||
soup = self.make_soup(rendered_template)
|
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.assertSearchInput(search_form, "q", "foo")
|
||||||
self.assertHiddenInput(search_form, 'user', 'john')
|
self.assertHiddenInput(search_form, "user", "john")
|
||||||
self.assertHiddenInput(search_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
|
self.assertHiddenInput(search_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
|
||||||
self.assertHiddenInput(search_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED)
|
self.assertHiddenInput(
|
||||||
self.assertHiddenInput(search_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES)
|
search_form, "shared", BookmarkSearch.FILTER_SHARED_SHARED
|
||||||
|
)
|
||||||
|
self.assertHiddenInput(search_form, "unread", BookmarkSearch.FILTER_UNREAD_YES)
|
||||||
|
|
||||||
def test_preferences_form_inputs(self):
|
def test_preferences_form_inputs(self):
|
||||||
# Without params
|
# Without params
|
||||||
url = '/test'
|
url = "/test"
|
||||||
rendered_template = self.render_template(url)
|
rendered_template = self.render_template(url)
|
||||||
soup = self.make_soup(rendered_template)
|
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, "q")
|
||||||
self.assertNoHiddenInput(preferences_form, 'user')
|
self.assertNoHiddenInput(preferences_form, "user")
|
||||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
self.assertNoHiddenInput(preferences_form, "sort")
|
||||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
self.assertNoHiddenInput(preferences_form, "shared")
|
||||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
self.assertNoHiddenInput(preferences_form, "unread")
|
||||||
|
|
||||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC)
|
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_ADDED_DESC)
|
||||||
self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_OFF)
|
self.assertRadioGroup(
|
||||||
self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_OFF)
|
preferences_form, "shared", BookmarkSearch.FILTER_SHARED_OFF
|
||||||
|
)
|
||||||
|
self.assertRadioGroup(
|
||||||
|
preferences_form, "unread", BookmarkSearch.FILTER_UNREAD_OFF
|
||||||
|
)
|
||||||
|
|
||||||
# With params
|
# 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)
|
rendered_template = self.render_template(url)
|
||||||
soup = self.make_soup(rendered_template)
|
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, "q", "foo")
|
||||||
self.assertHiddenInput(preferences_form, 'user', 'john')
|
self.assertHiddenInput(preferences_form, "user", "john")
|
||||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
self.assertNoHiddenInput(preferences_form, "sort")
|
||||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
self.assertNoHiddenInput(preferences_form, "shared")
|
||||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
self.assertNoHiddenInput(preferences_form, "unread")
|
||||||
|
|
||||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
|
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
|
||||||
self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED)
|
self.assertRadioGroup(
|
||||||
self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES)
|
preferences_form, "shared", BookmarkSearch.FILTER_SHARED_SHARED
|
||||||
|
)
|
||||||
|
self.assertRadioGroup(
|
||||||
|
preferences_form, "unread", BookmarkSearch.FILTER_UNREAD_YES
|
||||||
|
)
|
||||||
|
|
||||||
def test_preferences_form_inputs_shared_mode(self):
|
def test_preferences_form_inputs_shared_mode(self):
|
||||||
# Without params
|
# Without params
|
||||||
url = '/test'
|
url = "/test"
|
||||||
rendered_template = self.render_template(url, mode='shared')
|
rendered_template = self.render_template(url, mode="shared")
|
||||||
soup = self.make_soup(rendered_template)
|
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, "q")
|
||||||
self.assertNoHiddenInput(preferences_form, 'user')
|
self.assertNoHiddenInput(preferences_form, "user")
|
||||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
self.assertNoHiddenInput(preferences_form, "sort")
|
||||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
self.assertNoHiddenInput(preferences_form, "shared")
|
||||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
self.assertNoHiddenInput(preferences_form, "unread")
|
||||||
|
|
||||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC)
|
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_ADDED_DESC)
|
||||||
self.assertNoRadioGroup(preferences_form, 'shared')
|
self.assertNoRadioGroup(preferences_form, "shared")
|
||||||
self.assertNoRadioGroup(preferences_form, 'unread')
|
self.assertNoRadioGroup(preferences_form, "unread")
|
||||||
|
|
||||||
# With params
|
# With params
|
||||||
url = '/test?q=foo&user=john&sort=title_asc'
|
url = "/test?q=foo&user=john&sort=title_asc"
|
||||||
rendered_template = self.render_template(url, mode='shared')
|
rendered_template = self.render_template(url, mode="shared")
|
||||||
soup = self.make_soup(rendered_template)
|
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, "q", "foo")
|
||||||
self.assertHiddenInput(preferences_form, 'user', 'john')
|
self.assertHiddenInput(preferences_form, "user", "john")
|
||||||
self.assertNoHiddenInput(preferences_form, 'sort')
|
self.assertNoHiddenInput(preferences_form, "sort")
|
||||||
self.assertNoHiddenInput(preferences_form, 'shared')
|
self.assertNoHiddenInput(preferences_form, "shared")
|
||||||
self.assertNoHiddenInput(preferences_form, 'unread')
|
self.assertNoHiddenInput(preferences_form, "unread")
|
||||||
|
|
||||||
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
|
self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
|
||||||
self.assertNoRadioGroup(preferences_form, 'shared')
|
self.assertNoRadioGroup(preferences_form, "shared")
|
||||||
self.assertNoRadioGroup(preferences_form, 'unread')
|
self.assertNoRadioGroup(preferences_form, "unread")
|
||||||
|
|
||||||
def test_modified_indicator(self):
|
def test_modified_indicator(self):
|
||||||
# Without modifications
|
# Without modifications
|
||||||
url = '/test'
|
url = "/test"
|
||||||
rendered_template = self.render_template(url)
|
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
|
# With modifications
|
||||||
url = '/test?sort=title_asc'
|
url = "/test?sort=title_asc"
|
||||||
rendered_template = self.render_template(url)
|
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
|
# Ignores non-preferences modifications
|
||||||
url = '/test?q=foo&user=john'
|
url = "/test?q=foo&user=john"
|
||||||
rendered_template = self.render_template(url)
|
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):
|
def test_modified_labels(self):
|
||||||
# Without modifications
|
# Without modifications
|
||||||
url = '/test'
|
url = "/test"
|
||||||
rendered_template = self.render_template(url)
|
rendered_template = self.render_template(url)
|
||||||
|
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
|
self.assertUnmodifiedLabel(rendered_template, "Shared filter")
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
|
self.assertUnmodifiedLabel(rendered_template, "Unread filter")
|
||||||
|
|
||||||
# Modified sort
|
# Modified sort
|
||||||
url = '/test?sort=title_asc'
|
url = "/test?sort=title_asc"
|
||||||
rendered_template = self.render_template(url)
|
rendered_template = self.render_template(url)
|
||||||
self.assertModifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
self.assertModifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
|
self.assertUnmodifiedLabel(rendered_template, "Shared filter")
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
|
self.assertUnmodifiedLabel(rendered_template, "Unread filter")
|
||||||
|
|
||||||
# Modified shared
|
# Modified shared
|
||||||
url = '/test?shared=yes'
|
url = "/test?shared=yes"
|
||||||
rendered_template = self.render_template(url)
|
rendered_template = self.render_template(url)
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||||
self.assertModifiedLabel(rendered_template, 'Shared filter')
|
self.assertModifiedLabel(rendered_template, "Shared filter")
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
|
self.assertUnmodifiedLabel(rendered_template, "Unread filter")
|
||||||
|
|
||||||
# Modified unread
|
# Modified unread
|
||||||
url = '/test?unread=yes'
|
url = "/test?unread=yes"
|
||||||
rendered_template = self.render_template(url)
|
rendered_template = self.render_template(url)
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
|
self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
|
||||||
self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
|
self.assertUnmodifiedLabel(rendered_template, "Shared filter")
|
||||||
self.assertModifiedLabel(rendered_template, 'Unread filter')
|
self.assertModifiedLabel(rendered_template, "Unread filter")
|
||||||
|
|||||||
@@ -15,39 +15,50 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
user = self.get_or_create_test_user()
|
user = self.get_or_create_test_user()
|
||||||
self.client.force_login(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(
|
self.assertInHTML(
|
||||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
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())
|
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)
|
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))
|
self.assertEqual(len(bookmark_items), len(bookmarks))
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
bookmark_item = bookmark_list.select_one(
|
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)
|
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())
|
soup = self.make_soup(response.content.decode())
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
bookmark_item = soup.select_one(
|
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)
|
self.assertIsNone(bookmark_item)
|
||||||
|
|
||||||
def assertVisibleTags(self, response, tags: List[Tag]):
|
def assertVisibleTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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)
|
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))
|
self.assertEqual(len(tag_items), len(tags))
|
||||||
|
|
||||||
tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
|
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]):
|
def assertInvisibleTags(self, response, tags: List[Tag]):
|
||||||
soup = self.make_soup(response.content.decode())
|
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]
|
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]):
|
def assertVisibleUserOptions(self, response, users: List[User]):
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
user_options = [
|
user_options = ['<option value="" selected="">Everyone</option>']
|
||||||
'<option value="" selected="">Everyone</option>'
|
|
||||||
]
|
|
||||||
for user in users:
|
for user in users:
|
||||||
user_options.append(f'<option value="{user.username}">{user.username}</option>')
|
user_options.append(
|
||||||
user_select_html = f'''
|
f'<option value="{user.username}">{user.username}</option>'
|
||||||
|
)
|
||||||
|
user_select_html = f"""
|
||||||
<select name="user" class="form-select" required="" id="id_user">
|
<select name="user" class="form-select" required="" id="id_user">
|
||||||
{''.join(user_options)}
|
{''.join(user_options)}
|
||||||
</select>
|
</select>
|
||||||
'''
|
"""
|
||||||
|
|
||||||
self.assertInHTML(user_select_html, html)
|
self.assertInHTML(user_select_html, html)
|
||||||
|
|
||||||
def assertEditLink(self, response, url):
|
def assertEditLink(self, response, url):
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<a href="{url}">Edit</a>
|
<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()
|
self.authenticate()
|
||||||
user1 = self.setup_user(enable_sharing=True)
|
user1 = self.setup_user(enable_sharing=True)
|
||||||
user2 = 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),
|
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.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||||
@@ -124,7 +140,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
self.setup_bookmark(shared=True, user=user3),
|
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)
|
response = self.client.get(url)
|
||||||
|
|
||||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
@@ -134,10 +150,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
self.authenticate()
|
self.authenticate()
|
||||||
user = self.setup_user(enable_sharing=True)
|
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)
|
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.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_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)
|
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
|
||||||
user2 = self.setup_user(enable_sharing=True)
|
user2 = self.setup_user(enable_sharing=True)
|
||||||
|
|
||||||
visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user1, prefix='user1')
|
visible_bookmarks = self.setup_numbered_bookmarks(
|
||||||
invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user2, prefix='user2')
|
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.assertVisibleBookmarks(response, visible_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, invisible_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()
|
self.authenticate()
|
||||||
user1 = self.setup_user(enable_sharing=True)
|
user1 = self.setup_user(enable_sharing=True)
|
||||||
user2 = 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=False, user=user3, tags=[invisible_tags[2]])
|
||||||
self.setup_bookmark(shared=True, user=user4, tags=[invisible_tags[3]])
|
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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_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=user2, tags=[invisible_tags[0]])
|
||||||
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[1]])
|
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)
|
response = self.client.get(url)
|
||||||
|
|
||||||
self.assertVisibleTags(response, visible_tags)
|
self.assertVisibleTags(response, visible_tags)
|
||||||
@@ -225,15 +249,21 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
self.setup_tag(user=user3),
|
self.setup_tag(user=user3),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.setup_bookmark(shared=True, user=user1, title='searchvalue', tags=[visible_tags[0]])
|
self.setup_bookmark(
|
||||||
self.setup_bookmark(shared=True, user=user2, title='searchvalue', tags=[visible_tags[1]])
|
shared=True, user=user1, title="searchvalue", tags=[visible_tags[0]]
|
||||||
self.setup_bookmark(shared=True, user=user3, title='searchvalue', tags=[visible_tags[2]])
|
)
|
||||||
|
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=user1, tags=[invisible_tags[0]])
|
||||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
|
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
|
||||||
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[2]])
|
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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_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[0]])
|
||||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
|
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.assertVisibleTags(response, visible_tags)
|
||||||
self.assertInvisibleTags(response, invisible_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):
|
def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
expected_visible_users = [
|
expected_visible_users = [
|
||||||
self.setup_user(name='user_a', enable_sharing=True),
|
self.setup_user(name="user_a", enable_sharing=True),
|
||||||
self.setup_user(name='user_b', 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[0])
|
||||||
self.setup_bookmark(shared=True, user=expected_visible_users[1])
|
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=False, user=self.setup_user(enable_sharing=True))
|
||||||
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=False))
|
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)
|
self.assertVisibleUserOptions(response, expected_visible_users)
|
||||||
|
|
||||||
def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
|
def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
|
||||||
# users with public sharing enabled
|
# users with public sharing enabled
|
||||||
expected_visible_users = [
|
expected_visible_users = [
|
||||||
self.setup_user(name='user_a', enable_sharing=True, enable_public_sharing=True),
|
self.setup_user(
|
||||||
self.setup_user(name='user_b', enable_sharing=True, enable_public_sharing=True),
|
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[0])
|
||||||
self.setup_bookmark(shared=True, user=expected_visible_users[1])
|
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))
|
||||||
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)
|
self.assertVisibleUserOptions(response, expected_visible_users)
|
||||||
|
|
||||||
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
|
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 = self.get_or_create_test_user().profile
|
||||||
user_profile.search_preferences = {
|
user_profile.search_preferences = {
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
}
|
}
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
unread_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=True, with_tags=True, prefix='unread',
|
unread_bookmarks = self.setup_numbered_bookmarks(
|
||||||
tag_prefix='unread', user=other_user)
|
3,
|
||||||
read_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=False, with_tags=True, prefix='read',
|
shared=True,
|
||||||
tag_prefix='read', user=other_user)
|
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)
|
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
|
||||||
read_tags = self.get_tags_from_bookmarks(read_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.assertVisibleBookmarks(response, unread_bookmarks)
|
||||||
self.assertInvisibleBookmarks(response, read_bookmarks)
|
self.assertInvisibleBookmarks(response, read_bookmarks)
|
||||||
self.assertVisibleTags(response, unread_tags)
|
self.assertVisibleTags(response, unread_tags)
|
||||||
@@ -325,12 +373,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
visible_bookmarks = [
|
visible_bookmarks = [
|
||||||
self.setup_bookmark(shared=True),
|
self.setup_bookmark(shared=True),
|
||||||
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):
|
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
@@ -342,12 +390,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
visible_bookmarks = [
|
visible_bookmarks = [
|
||||||
self.setup_bookmark(shared=True),
|
self.setup_bookmark(shared=True),
|
||||||
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):
|
def test_edit_link_return_url_respects_search_options(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
@@ -355,148 +403,227 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
user.profile.enable_sharing = True
|
user.profile.enable_sharing = True
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
|
|
||||||
bookmark = self.setup_bookmark(title='foo', shared=True, user=user)
|
bookmark = self.setup_bookmark(title="foo", shared=True, user=user)
|
||||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||||
base_url = reverse('bookmarks:shared')
|
base_url = reverse("bookmarks:shared")
|
||||||
|
|
||||||
# without query params
|
# without query params
|
||||||
return_url = urllib.parse.quote(base_url)
|
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)
|
response = self.client.get(base_url)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query
|
# with query
|
||||||
url_params = '?q=foo'
|
url_params = "?q=foo"
|
||||||
return_url = urllib.parse.quote(base_url + url_params)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query and user
|
# 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)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
# with query and sort and page
|
# 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)
|
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)
|
response = self.client.get(base_url + url_params)
|
||||||
self.assertEditLink(response, url)
|
self.assertEditLink(response, url)
|
||||||
|
|
||||||
def test_apply_search_preferences(self):
|
def test_apply_search_preferences(self):
|
||||||
# no params
|
# 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.status_code, 302)
|
||||||
self.assertEqual(response.url, reverse('bookmarks:shared'))
|
self.assertEqual(response.url, reverse("bookmarks:shared"))
|
||||||
|
|
||||||
# some params
|
# some params
|
||||||
response = self.client.post(reverse('bookmarks:shared'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:shared"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
})
|
"q": "foo",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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
|
# params with default value are removed
|
||||||
response = self.client.post(reverse('bookmarks:shared'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:shared"),
|
||||||
'user': '',
|
{
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
"q": "foo",
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
"user": "",
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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
|
# page is removed
|
||||||
response = self.client.post(reverse('bookmarks:shared'), {
|
response = self.client.post(
|
||||||
'q': 'foo',
|
reverse("bookmarks:shared"),
|
||||||
'page': '2',
|
{
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"q": "foo",
|
||||||
})
|
"page": "2",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
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):
|
def test_save_search_preferences(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
|
|
||||||
# no params
|
# no params
|
||||||
self.client.post(reverse('bookmarks:shared'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:shared"),
|
||||||
})
|
{
|
||||||
|
"save": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# with param
|
# with param
|
||||||
self.client.post(reverse('bookmarks:shared'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:shared"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
})
|
"save": "",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# add a param
|
# add a param
|
||||||
self.client.post(reverse('bookmarks:shared'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:shared"),
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"save": "",
|
||||||
})
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# remove a param
|
# remove a param
|
||||||
self.client.post(reverse('bookmarks:shared'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:shared"),
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
{
|
||||||
})
|
"save": "",
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_ADDED_DESC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_YES,
|
"sort": BookmarkSearch.SORT_ADDED_DESC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_YES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# ignores non-preferences
|
# ignores non-preferences
|
||||||
self.client.post(reverse('bookmarks:shared'), {
|
self.client.post(
|
||||||
'save': '',
|
reverse("bookmarks:shared"),
|
||||||
'q': 'foo',
|
{
|
||||||
'user': 'john',
|
"save": "",
|
||||||
'page': '3',
|
"q": "foo",
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
"user": "john",
|
||||||
})
|
"page": "3",
|
||||||
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
|
},
|
||||||
|
)
|
||||||
user_profile.refresh_from_db()
|
user_profile.refresh_from_db()
|
||||||
self.assertEqual(user_profile.search_preferences, {
|
self.assertEqual(
|
||||||
'sort': BookmarkSearch.SORT_TITLE_ASC,
|
user_profile.search_preferences,
|
||||||
'shared': BookmarkSearch.FILTER_SHARED_OFF,
|
{
|
||||||
'unread': BookmarkSearch.FILTER_UNREAD_OFF,
|
"sort": BookmarkSearch.SORT_TITLE_ASC,
|
||||||
})
|
"shared": BookmarkSearch.FILTER_SHARED_OFF,
|
||||||
|
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_url_encode_bookmark_actions_url(self):
|
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)
|
response = self.client.get(url)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
soup = self.make_soup(html)
|
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'],
|
self.assertEqual(
|
||||||
'/bookmarks/shared/action?q=%23foo&return_url=%2Fbookmarks%2Fshared%3Fq%3D%2523foo')
|
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
|
# capture number of queries
|
||||||
context = CaptureQueriesContext(self.get_connection())
|
context = CaptureQueriesContext(self.get_connection())
|
||||||
with context:
|
with context:
|
||||||
response = self.client.get(reverse('bookmarks:shared'))
|
response = self.client.get(reverse("bookmarks:shared"))
|
||||||
self.assertContains(response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks)
|
self.assertContains(
|
||||||
|
response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks
|
||||||
|
)
|
||||||
|
|
||||||
number_of_queries = context.final_queries
|
number_of_queries = context.final_queries
|
||||||
|
|
||||||
@@ -40,5 +42,9 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
|
|||||||
|
|
||||||
# assert num queries doesn't increase
|
# assert num queries doesn't increase
|
||||||
with self.assertNumQueries(number_of_queries):
|
with self.assertNumQueries(number_of_queries):
|
||||||
response = self.client.get(reverse('bookmarks:shared'))
|
response = self.client.get(reverse("bookmarks:shared"))
|
||||||
self.assertContains(response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks + num_additional_bookmarks)
|
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()
|
User = get_user_model()
|
||||||
|
|
||||||
ENABLED_URL_VALIDATION_TEST_CASES = [
|
ENABLED_URL_VALIDATION_TEST_CASES = [
|
||||||
('thisisnotavalidurl', False),
|
("thisisnotavalidurl", False),
|
||||||
('http://domain', False),
|
("http://domain", False),
|
||||||
('unknownscheme://domain.com', False),
|
("unknownscheme://domain.com", False),
|
||||||
('http://domain.com', True),
|
("http://domain.com", True),
|
||||||
('http://www.domain.com', True),
|
("http://www.domain.com", True),
|
||||||
('https://domain.com', True),
|
("https://domain.com", True),
|
||||||
('https://www.domain.com', True),
|
("https://www.domain.com", True),
|
||||||
]
|
]
|
||||||
|
|
||||||
DISABLED_URL_VALIDATION_TEST_CASES = [
|
DISABLED_URL_VALIDATION_TEST_CASES = [
|
||||||
('thisisnotavalidurl', True),
|
("thisisnotavalidurl", True),
|
||||||
('http://domain', True),
|
("http://domain", True),
|
||||||
('unknownscheme://domain.com', True),
|
("unknownscheme://domain.com", True),
|
||||||
('http://domain.com', True),
|
("http://domain.com", True),
|
||||||
('http://www.domain.com', True),
|
("http://www.domain.com", True),
|
||||||
('https://domain.com', True),
|
("https://domain.com", True),
|
||||||
('https://www.domain.com', True),
|
("https://www.domain.com", True),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BookmarkValidationTestCase(TestCase):
|
class BookmarkValidationTestCase(TestCase):
|
||||||
|
|
||||||
def setUp(self) -> None:
|
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):
|
def test_bookmark_model_should_not_allow_missing_url(self):
|
||||||
bookmark = Bookmark(
|
bookmark = Bookmark(
|
||||||
date_added=datetime.datetime.now(),
|
date_added=datetime.datetime.now(),
|
||||||
date_modified=datetime.datetime.now(),
|
date_modified=datetime.datetime.now(),
|
||||||
owner=self.user
|
owner=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -46,10 +48,10 @@ class BookmarkValidationTestCase(TestCase):
|
|||||||
|
|
||||||
def test_bookmark_model_should_not_allow_empty_url(self):
|
def test_bookmark_model_should_not_allow_empty_url(self):
|
||||||
bookmark = Bookmark(
|
bookmark = Bookmark(
|
||||||
url='',
|
url="",
|
||||||
date_added=datetime.datetime.now(),
|
date_added=datetime.datetime.now(),
|
||||||
date_modified=datetime.datetime.now(),
|
date_modified=datetime.datetime.now(),
|
||||||
owner=self.user
|
owner=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -64,15 +66,15 @@ class BookmarkValidationTestCase(TestCase):
|
|||||||
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
|
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
|
||||||
|
|
||||||
def test_bookmark_form_should_validate_required_fields(self):
|
def test_bookmark_form_should_validate_required_fields(self):
|
||||||
form = BookmarkForm(data={'url': ''})
|
form = BookmarkForm(data={"url": ""})
|
||||||
|
|
||||||
self.assertEqual(len(form.errors), 1)
|
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.assertEqual(len(form.errors), 1)
|
||||||
self.assertIn('required', str(form.errors))
|
self.assertIn("required", str(form.errors))
|
||||||
|
|
||||||
@override_settings(LD_DISABLE_URL_VALIDATION=False)
|
@override_settings(LD_DISABLE_URL_VALIDATION=False)
|
||||||
def test_bookmark_form_should_validate_url_if_not_disabled_in_settings(self):
|
def test_bookmark_form_should_validate_url_if_not_disabled_in_settings(self):
|
||||||
@@ -89,23 +91,25 @@ class BookmarkValidationTestCase(TestCase):
|
|||||||
url=url,
|
url=url,
|
||||||
date_added=datetime.datetime.now(),
|
date_added=datetime.datetime.now(),
|
||||||
date_modified=datetime.datetime.now(),
|
date_modified=datetime.datetime.now(),
|
||||||
owner=self.user
|
owner=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bookmark.full_clean()
|
bookmark.full_clean()
|
||||||
self.assertTrue(expectation, 'Did not expect validation error')
|
self.assertTrue(expectation, "Did not expect validation error")
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
self.assertFalse(expectation, 'Expected validation error')
|
self.assertFalse(expectation, "Expected validation error")
|
||||||
self.assertTrue('url' in e.message_dict, 'Expected URL validation to fail')
|
self.assertTrue(
|
||||||
|
"url" in e.message_dict, "Expected URL validation to fail"
|
||||||
|
)
|
||||||
|
|
||||||
def _run_bookmark_form_url_validity_checks(self, cases):
|
def _run_bookmark_form_url_validity_checks(self, cases):
|
||||||
for case in cases:
|
for case in cases:
|
||||||
url, expectation = case
|
url, expectation = case
|
||||||
form = BookmarkForm(data={'url': url})
|
form = BookmarkForm(data={"url": url})
|
||||||
|
|
||||||
if expectation:
|
if expectation:
|
||||||
self.assertEqual(len(form.errors), 0)
|
self.assertEqual(len(form.errors), 0)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(len(form.errors), 1)
|
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):
|
class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
|
self.api_token = Token.objects.get_or_create(
|
||||||
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
|
user=self.get_or_create_test_user()
|
||||||
|
)[0]
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key)
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
return connections[DEFAULT_DB_ALIAS]
|
return connections[DEFAULT_DB_ALIAS]
|
||||||
@@ -26,7 +28,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
|||||||
# capture number of queries
|
# capture number of queries
|
||||||
context = CaptureQueriesContext(self.get_connection())
|
context = CaptureQueriesContext(self.get_connection())
|
||||||
with context:
|
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
|
number_of_queries = context.final_queries
|
||||||
|
|
||||||
@@ -41,7 +46,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
|||||||
# capture number of queries
|
# capture number of queries
|
||||||
context = CaptureQueriesContext(self.get_connection())
|
context = CaptureQueriesContext(self.get_connection())
|
||||||
with context:
|
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
|
number_of_queries = context.final_queries
|
||||||
|
|
||||||
@@ -57,7 +65,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
|||||||
# capture number of queries
|
# capture number of queries
|
||||||
context = CaptureQueriesContext(self.get_connection())
|
context = CaptureQueriesContext(self.get_connection())
|
||||||
with context:
|
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
|
number_of_queries = context.final_queries
|
||||||
|
|
||||||
|
|||||||
@@ -9,47 +9,68 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
|
|||||||
|
|
||||||
class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||||
def authenticate(self) -> None:
|
def authenticate(self) -> None:
|
||||||
self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
|
self.api_token = Token.objects.get_or_create(
|
||||||
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
|
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):
|
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.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):
|
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.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):
|
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.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):
|
def test_create_bookmark_requires_authentication(self):
|
||||||
data = {
|
data = {
|
||||||
'url': 'https://example.com/',
|
"url": "https://example.com/",
|
||||||
'title': 'Test title',
|
"title": "Test title",
|
||||||
'description': 'Test description',
|
"description": "Test description",
|
||||||
'notes': 'Test notes',
|
"notes": "Test notes",
|
||||||
'is_archived': False,
|
"is_archived": False,
|
||||||
'unread': False,
|
"unread": False,
|
||||||
'shared': False,
|
"shared": False,
|
||||||
'tag_names': ['tag1', 'tag2']
|
"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.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):
|
def test_get_bookmark_requires_authentication(self):
|
||||||
bookmark = self.setup_bookmark()
|
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)
|
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):
|
def test_update_bookmark_requires_authentication(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
data = {'url': 'https://example.com/'}
|
data = {"url": "https://example.com/"}
|
||||||
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
|
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||||
|
|
||||||
self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
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):
|
def test_patch_bookmark_requires_authentication(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
data = {'url': 'https://example.com'}
|
data = {"url": "https://example.com"}
|
||||||
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
|
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||||
|
|
||||||
self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
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):
|
def test_delete_bookmark_requires_authentication(self):
|
||||||
bookmark = self.setup_bookmark()
|
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)
|
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
@@ -87,7 +108,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
|||||||
|
|
||||||
def test_archive_requires_authentication(self):
|
def test_archive_requires_authentication(self):
|
||||||
bookmark = self.setup_bookmark()
|
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)
|
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
@@ -96,7 +117,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
|||||||
|
|
||||||
def test_unarchive_requires_authentication(self):
|
def test_unarchive_requires_authentication(self):
|
||||||
bookmark = self.setup_bookmark(is_archived=True)
|
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)
|
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)
|
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def test_check_requires_authentication(self):
|
def test_check_requires_authentication(self):
|
||||||
url = reverse('bookmarks:bookmark-check')
|
url = reverse("bookmarks:bookmark-check")
|
||||||
check_url = urllib.parse.quote_plus('https://example.com')
|
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.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):
|
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)
|
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|||||||
@@ -16,34 +16,48 @@ from bookmarks.views.partials import contexts
|
|||||||
|
|
||||||
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||||
|
|
||||||
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
|
def assertBookmarksLink(
|
||||||
favicon_img = f'<img src="/static/{bookmark.favicon_file}" alt="">' if bookmark.favicon_file else ''
|
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(
|
self.assertInHTML(
|
||||||
f'''
|
f"""
|
||||||
<a href="{bookmark.url}"
|
<a href="{bookmark.url}"
|
||||||
target="{link_target}"
|
target="{link_target}"
|
||||||
rel="noopener">
|
rel="noopener">
|
||||||
{favicon_img}
|
{favicon_img}
|
||||||
{bookmark.resolved_title}
|
<span>{bookmark.resolved_title}</span>
|
||||||
</a>
|
</a>
|
||||||
''',
|
""",
|
||||||
html
|
html,
|
||||||
)
|
)
|
||||||
|
|
||||||
def assertDateLabel(self, html: str, label_content: str):
|
def assertDateLabel(self, html: str, label_content: str):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<span>{label_content}</span>
|
<span>{label_content}</span>
|
||||||
<span class="separator">|</span>
|
<span class="separator">|</span>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'):
|
def assertWebArchiveLink(
|
||||||
self.assertInHTML(f'''
|
self, html: str, label_content: str, url: str, link_target: str = "_blank"
|
||||||
|
):
|
||||||
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<a href="{url}"
|
<a href="{url}"
|
||||||
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
|
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
|
||||||
{label_content} ∞
|
{label_content} ∞
|
||||||
</a>
|
</a>
|
||||||
<span class="separator">|</span>
|
<span class="separator">|</span>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||||
self.assertBookmarkActionsCount(html, bookmark, count=1)
|
self.assertBookmarkActionsCount(html, bookmark, count=1)
|
||||||
@@ -53,20 +67,32 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
|
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
|
||||||
# Edit link
|
# Edit link
|
||||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<a href="{edit_url}?return_url=/bookmarks">Edit</a>
|
<a href="{edit_url}?return_url=/bookmarks">Edit</a>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
# Archive link
|
# Archive link
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<button type="submit" name="archive" value="{bookmark.id}"
|
<button type="submit" name="archive" value="{bookmark.id}"
|
||||||
class="btn btn-link btn-sm">Archive</button>
|
class="btn btn-link btn-sm">Archive</button>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
# Delete link
|
# Delete link
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<button ld-confirm-button type="submit" name="remove" value="{bookmark.id}"
|
<button ld-confirm-button type="submit" name="remove" value="{bookmark.id}"
|
||||||
class="btn btn-link btn-sm">Remove</button>
|
class="btn btn-link btn-sm">Remove</button>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def assertShareInfo(self, html: str, bookmark: Bookmark):
|
def assertShareInfo(self, html: str, bookmark: Bookmark):
|
||||||
self.assertShareInfoCount(html, bookmark, 1)
|
self.assertShareInfoCount(html, bookmark, 1)
|
||||||
@@ -75,11 +101,15 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertShareInfoCount(html, bookmark, 0)
|
self.assertShareInfoCount(html, bookmark, 0)
|
||||||
|
|
||||||
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<span>Shared by
|
<span>Shared by
|
||||||
<a href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
<a href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||||
</span>
|
</span>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def assertFaviconVisible(self, html: str, bookmark: Bookmark):
|
def assertFaviconVisible(self, html: str, bookmark: Bookmark):
|
||||||
self.assertFaviconCount(html, bookmark, 1)
|
self.assertFaviconCount(html, bookmark, 1)
|
||||||
@@ -88,47 +118,68 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertFaviconCount(html, bookmark, 0)
|
self.assertFaviconCount(html, bookmark, 0)
|
||||||
|
|
||||||
def assertFaviconCount(self, html: str, bookmark: Bookmark, count=1):
|
def assertFaviconCount(self, html: str, bookmark: Bookmark, count=1):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<img src="/static/{bookmark.favicon_file}" alt="">
|
<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):
|
def assertBookmarkURLCount(
|
||||||
self.assertInHTML(f'''
|
self, html: str, bookmark: Bookmark, link_target: str = "_blank", count=0
|
||||||
|
):
|
||||||
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<div class="url-path truncate">
|
<div class="url-path truncate">
|
||||||
<a href="{bookmark.url}" target="{link_target}" rel="noopener"
|
<a href="{bookmark.url}" target="{link_target}" rel="noopener"
|
||||||
class="url-display text-sm">
|
class="url-display text-sm">
|
||||||
{bookmark.url}
|
{bookmark.url}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
''', html, count)
|
""",
|
||||||
|
html,
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
|
||||||
def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark):
|
def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark):
|
||||||
self.assertBookmarkURLCount(html, bookmark, count=1)
|
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)
|
self.assertBookmarkURLCount(html, bookmark, count=0)
|
||||||
|
|
||||||
def assertNotes(self, html: str, notes_html: str, count=1):
|
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 bg-gray text-gray-dark">
|
||||||
<div class="notes-content">
|
<div class="notes-content">
|
||||||
{notes_html}
|
{notes_html}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def assertNotesToggle(self, html: str, count=1):
|
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">
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||||
<use xlink:href="#ld-icon-note"></use>
|
<use xlink:href="#ld-icon-note"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Notes
|
Notes
|
||||||
</button>
|
</button>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1):
|
def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<button type="submit" name="unshare" value="{bookmark.id}"
|
<button type="submit" name="unshare" value="{bookmark.id}"
|
||||||
class="btn btn-link btn-sm btn-icon"
|
class="btn btn-link btn-sm btn-icon"
|
||||||
ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?">
|
ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?">
|
||||||
@@ -137,10 +188,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
</svg>
|
</svg>
|
||||||
Shared
|
Shared
|
||||||
</button>
|
</button>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def assertMarkAsReadButton(self, html: str, bookmark: Bookmark, count=1):
|
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}"
|
<button type="submit" name="mark_as_read" value="{bookmark.id}"
|
||||||
class="btn btn-link btn-sm btn-icon"
|
class="btn btn-link btn-sm btn-icon"
|
||||||
ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?">
|
ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?">
|
||||||
@@ -149,12 +204,19 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
</svg>
|
</svg>
|
||||||
Unread
|
Unread
|
||||||
</button>
|
</button>
|
||||||
''', html, count=count)
|
""",
|
||||||
|
html,
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
|
|
||||||
def render_template(self,
|
def render_template(
|
||||||
url='/bookmarks',
|
self,
|
||||||
context_type: Type[contexts.BookmarkListContext] = contexts.ActiveBookmarkListContext,
|
url="/bookmarks",
|
||||||
user: User | AnonymousUser = None) -> str:
|
context_type: Type[
|
||||||
|
contexts.BookmarkListContext
|
||||||
|
] = contexts.ActiveBookmarkListContext,
|
||||||
|
user: User | AnonymousUser = None,
|
||||||
|
) -> str:
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
request = rf.get(url)
|
request = rf.get(url)
|
||||||
request.user = user or self.get_or_create_test_user()
|
request.user = user or self.get_or_create_test_user()
|
||||||
@@ -162,14 +224,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
middleware(request)
|
middleware(request)
|
||||||
|
|
||||||
bookmark_list_context = context_type(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(
|
template = Template("{% include 'bookmarks/bookmark_list.html' %}")
|
||||||
"{% include 'bookmarks/bookmark_list.html' %}"
|
|
||||||
)
|
|
||||||
return template.render(context)
|
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 = self.setup_bookmark()
|
||||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||||
bookmark.web_archive_snapshot_url = web_archive_url
|
bookmark.web_archive_snapshot_url = web_archive_url
|
||||||
@@ -180,38 +242,46 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
return bookmark
|
return bookmark
|
||||||
|
|
||||||
def test_should_respect_absolute_date_setting(self):
|
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()
|
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)
|
self.assertDateLabel(html, formatted_date)
|
||||||
|
|
||||||
def test_should_render_web_archive_link_with_absolute_date_setting(self):
|
def test_should_render_web_archive_link_with_absolute_date_setting(self):
|
||||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE,
|
bookmark = self.setup_date_format_test(
|
||||||
'https://web.archive.org/web/20210811214511/https://wanikani.com/')
|
UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE,
|
||||||
|
"https://web.archive.org/web/20210811214511/https://wanikani.com/",
|
||||||
|
)
|
||||||
html = self.render_template()
|
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):
|
def test_should_respect_relative_date_setting(self):
|
||||||
self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE)
|
self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||||
html = self.render_template()
|
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):
|
def test_should_render_web_archive_link_with_relative_date_setting(self):
|
||||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
bookmark = self.setup_date_format_test(
|
||||||
'https://web.archive.org/web/20210811214511/https://wanikani.com/')
|
UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||||
|
"https://web.archive.org/web/20210811214511/https://wanikani.com/",
|
||||||
|
)
|
||||||
html = self.render_template()
|
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):
|
def test_bookmark_link_target_should_be_blank_by_default(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
html = self.render_template()
|
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):
|
def test_bookmark_link_target_should_respect_user_profile(self):
|
||||||
profile = self.get_or_create_test_user().profile
|
profile = self.get_or_create_test_user().profile
|
||||||
@@ -221,17 +291,19 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
html = self.render_template()
|
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):
|
def test_web_archive_link_target_should_be_blank_by_default(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
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()
|
bookmark.save()
|
||||||
|
|
||||||
html = self.render_template()
|
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):
|
def test_web_archive_link_target_should_respect_user_profile(self):
|
||||||
profile = self.get_or_create_test_user().profile
|
profile = self.get_or_create_test_user().profile
|
||||||
@@ -240,12 +312,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
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()
|
bookmark.save()
|
||||||
|
|
||||||
html = self.render_template()
|
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):
|
def test_should_reflect_unread_state_as_css_class(self):
|
||||||
self.setup_bookmark(unread=True)
|
self.setup_bookmark(unread=True)
|
||||||
@@ -281,7 +355,9 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertNoShareInfo(html, bookmark)
|
self.assertNoShareInfo(html, bookmark)
|
||||||
|
|
||||||
def test_show_share_info_for_non_owned_bookmarks(self):
|
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.enable_sharing = True
|
||||||
other_user.profile.save()
|
other_user.profile.save()
|
||||||
|
|
||||||
@@ -292,25 +368,32 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertShareInfo(html, bookmark)
|
self.assertShareInfo(html, bookmark)
|
||||||
|
|
||||||
def test_share_info_user_link_keeps_query_params(self):
|
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.enable_sharing = True
|
||||||
other_user.profile.save()
|
other_user.profile.save()
|
||||||
|
|
||||||
bookmark = self.setup_bookmark(user=other_user, shared=True, title='foo')
|
bookmark = self.setup_bookmark(user=other_user, shared=True, title="foo")
|
||||||
html = self.render_template(url='/bookmarks?q=foo', context_type=contexts.SharedBookmarkListContext)
|
html = self.render_template(
|
||||||
|
url="/bookmarks?q=foo", context_type=contexts.SharedBookmarkListContext
|
||||||
|
)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(
|
||||||
|
f"""
|
||||||
<span>Shared by
|
<span>Shared by
|
||||||
<a href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
<a href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||||
</span>
|
</span>
|
||||||
''', html)
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_favicon_should_be_visible_when_favicons_enabled(self):
|
def test_favicon_should_be_visible_when_favicons_enabled(self):
|
||||||
profile = self.get_or_create_test_user().profile
|
profile = self.get_or_create_test_user().profile
|
||||||
profile.enable_favicons = True
|
profile.enable_favicons = True
|
||||||
profile.save()
|
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()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertFaviconVisible(html, bookmark)
|
self.assertFaviconVisible(html, bookmark)
|
||||||
@@ -320,7 +403,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
profile.enable_favicons = True
|
profile.enable_favicons = True
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
bookmark = self.setup_bookmark(favicon_file='')
|
bookmark = self.setup_bookmark(favicon_file="")
|
||||||
html = self.render_template()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertFaviconHidden(html, bookmark)
|
self.assertFaviconHidden(html, bookmark)
|
||||||
@@ -330,7 +413,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
profile.enable_favicons = False
|
profile.enable_favicons = False
|
||||||
profile.save()
|
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()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertFaviconHidden(html, bookmark)
|
self.assertFaviconHidden(html, bookmark)
|
||||||
@@ -428,21 +511,23 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark()
|
self.setup_bookmark()
|
||||||
html = self.render_template()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertNotes(html, '', 0)
|
self.assertNotes(html, "", 0)
|
||||||
self.assertNotesToggle(html, 0)
|
self.assertNotesToggle(html, 0)
|
||||||
|
|
||||||
def test_with_notes(self):
|
def test_with_notes(self):
|
||||||
self.setup_bookmark(notes='Test note')
|
self.setup_bookmark(notes="Test note")
|
||||||
html = self.render_template()
|
html = self.render_template()
|
||||||
|
|
||||||
note_html = '<p>Test note</p>'
|
note_html = "<p>Test note</p>"
|
||||||
self.assertNotes(html, note_html, 1)
|
self.assertNotes(html, note_html, 1)
|
||||||
|
|
||||||
def test_note_renders_markdown(self):
|
def test_note_renders_markdown(self):
|
||||||
self.setup_bookmark(notes='**Example:** `print("Hello world!")`')
|
self.setup_bookmark(notes='**Example:** `print("Hello world!")`')
|
||||||
html = self.render_template()
|
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)
|
self.assertNotes(html, note_html, 1)
|
||||||
|
|
||||||
def test_note_cleans_html(self):
|
def test_note_cleans_html(self):
|
||||||
@@ -453,7 +538,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertNotes(html, note_html, 1)
|
self.assertNotes(html, note_html, 1)
|
||||||
|
|
||||||
def test_notes_are_hidden_initially_by_default(self):
|
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())
|
html = collapse_whitespace(self.render_template())
|
||||||
|
|
||||||
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
||||||
@@ -463,7 +548,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
profile.permanent_notes = False
|
profile.permanent_notes = False
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
self.setup_bookmark(notes='Test note')
|
self.setup_bookmark(notes="Test note")
|
||||||
html = collapse_whitespace(self.render_template())
|
html = collapse_whitespace(self.render_template())
|
||||||
|
|
||||||
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
||||||
@@ -473,13 +558,15 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
profile.permanent_notes = True
|
profile.permanent_notes = True
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
self.setup_bookmark(notes='Test note')
|
self.setup_bookmark(notes="Test note")
|
||||||
html = collapse_whitespace(self.render_template())
|
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):
|
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()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertNotesToggle(html, 1)
|
self.assertNotesToggle(html, 1)
|
||||||
@@ -489,7 +576,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
profile.permanent_notes = False
|
profile.permanent_notes = False
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
self.setup_bookmark(notes='Test note')
|
self.setup_bookmark(notes="Test note")
|
||||||
html = self.render_template()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertNotesToggle(html, 1)
|
self.assertNotesToggle(html, 1)
|
||||||
@@ -499,7 +586,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
profile.permanent_notes = True
|
profile.permanent_notes = True
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
self.setup_bookmark(notes='Test note')
|
self.setup_bookmark(notes="Test note")
|
||||||
html = self.render_template()
|
html = self.render_template()
|
||||||
|
|
||||||
self.assertNotesToggle(html, 0)
|
self.assertNotesToggle(html, 0)
|
||||||
@@ -512,25 +599,35 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
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.notes = '**Example:** `print("Hello world!")`'
|
||||||
bookmark.favicon_file = 'https_example_com.png'
|
bookmark.favicon_file = "https_example_com.png"
|
||||||
bookmark.shared = True
|
bookmark.shared = True
|
||||||
bookmark.unread = True
|
bookmark.unread = True
|
||||||
bookmark.save()
|
bookmark.save()
|
||||||
|
|
||||||
html = self.render_template(context_type=contexts.SharedBookmarkListContext, user=AnonymousUser())
|
html = self.render_template(
|
||||||
self.assertBookmarksLink(html, bookmark, link_target='_blank')
|
context_type=contexts.SharedBookmarkListContext, user=AnonymousUser()
|
||||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
|
)
|
||||||
|
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.assertNoBookmarkActions(html, bookmark)
|
||||||
self.assertShareInfo(html, bookmark)
|
self.assertShareInfo(html, bookmark)
|
||||||
self.assertMarkAsReadButton(html, bookmark, count=0)
|
self.assertMarkAsReadButton(html, bookmark, count=0)
|
||||||
self.assertUnshareButton(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.assertNotes(html, note_html, 1)
|
||||||
self.assertFaviconVisible(html, bookmark)
|
self.assertFaviconVisible(html, bookmark)
|
||||||
|
|
||||||
def test_empty_state(self):
|
def test_empty_state(self):
|
||||||
html = self.render_template()
|
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):
|
class BookmarkTestCase(TestCase):
|
||||||
|
|
||||||
def test_bookmark_resolved_title(self):
|
def test_bookmark_resolved_title(self):
|
||||||
bookmark = Bookmark(title='Custom title', website_title='Website title', url='https://example.com')
|
bookmark = Bookmark(
|
||||||
self.assertEqual(bookmark.resolved_title, 'Custom title')
|
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')
|
bookmark = Bookmark(
|
||||||
self.assertEqual(bookmark.resolved_title, 'Website title')
|
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')
|
bookmark = Bookmark(title="", website_title="", url="https://example.com")
|
||||||
self.assertEqual(bookmark.resolved_title, '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.models import Bookmark, Tag
|
||||||
from bookmarks.services import tasks
|
from bookmarks.services import tasks
|
||||||
from bookmarks.services import website_loader
|
from bookmarks.services import website_loader
|
||||||
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \
|
from bookmarks.services.bookmarks import (
|
||||||
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks, mark_bookmarks_as_read, \
|
create_bookmark,
|
||||||
mark_bookmarks_as_unread, share_bookmarks, unshare_bookmarks
|
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.services.website_loader import WebsiteMetadata
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
|
||||||
@@ -22,36 +34,48 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.get_or_create_test_user()
|
self.get_or_create_test_user()
|
||||||
|
|
||||||
def test_create_should_update_website_metadata(self):
|
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(
|
expected_metadata = WebsiteMetadata(
|
||||||
'https://example.com',
|
"https://example.com", "Website title", "Website description"
|
||||||
'Website title',
|
|
||||||
'Website description'
|
|
||||||
)
|
)
|
||||||
mock_load_website_metadata.return_value = expected_metadata
|
mock_load_website_metadata.return_value = expected_metadata
|
||||||
|
|
||||||
bookmark_data = Bookmark(url='https://example.com',
|
bookmark_data = Bookmark(
|
||||||
title='Updated Title',
|
url="https://example.com",
|
||||||
description='Updated description',
|
title="Updated Title",
|
||||||
unread=True,
|
description="Updated description",
|
||||||
shared=True,
|
unread=True,
|
||||||
is_archived=True)
|
shared=True,
|
||||||
created_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user())
|
is_archived=True,
|
||||||
|
)
|
||||||
|
created_bookmark = create_bookmark(
|
||||||
|
bookmark_data, "", self.get_or_create_test_user()
|
||||||
|
)
|
||||||
|
|
||||||
created_bookmark.refresh_from_db()
|
created_bookmark.refresh_from_db()
|
||||||
self.assertEqual(expected_metadata.title, created_bookmark.website_title)
|
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):
|
def test_create_should_update_existing_bookmark_with_same_url(self):
|
||||||
original_bookmark = self.setup_bookmark(url='https://example.com', unread=False, shared=False)
|
original_bookmark = self.setup_bookmark(
|
||||||
bookmark_data = Bookmark(url='https://example.com',
|
url="https://example.com", unread=False, shared=False
|
||||||
title='Updated Title',
|
)
|
||||||
description='Updated description',
|
bookmark_data = Bookmark(
|
||||||
notes='Updated notes',
|
url="https://example.com",
|
||||||
unread=True,
|
title="Updated Title",
|
||||||
shared=True,
|
description="Updated description",
|
||||||
is_archived=True)
|
notes="Updated notes",
|
||||||
updated_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user())
|
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(Bookmark.objects.count(), 1)
|
||||||
self.assertEqual(updated_bookmark.id, original_bookmark.id)
|
self.assertEqual(updated_bookmark.id, original_bookmark.id)
|
||||||
@@ -64,75 +88,91 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertFalse(updated_bookmark.is_archived)
|
self.assertFalse(updated_bookmark.is_archived)
|
||||||
|
|
||||||
def test_create_should_create_web_archive_snapshot(self):
|
def test_create_should_create_web_archive_snapshot(self):
|
||||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
with patch.object(
|
||||||
bookmark_data = Bookmark(url='https://example.com')
|
tasks, "create_web_archive_snapshot"
|
||||||
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user)
|
) 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):
|
def test_create_should_load_favicon(self):
|
||||||
with patch.object(tasks, 'load_favicon') as mock_load_favicon:
|
with patch.object(tasks, "load_favicon") as mock_load_favicon:
|
||||||
bookmark_data = Bookmark(url='https://example.com')
|
bookmark_data = Bookmark(url="https://example.com")
|
||||||
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user)
|
bookmark = create_bookmark(bookmark_data, "tag1,tag2", self.user)
|
||||||
|
|
||||||
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
||||||
|
|
||||||
def test_update_should_create_web_archive_snapshot_if_url_did_change(self):
|
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 = self.setup_bookmark()
|
||||||
bookmark.url = 'https://example.com/updated'
|
bookmark.url = "https://example.com/updated"
|
||||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
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):
|
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 = self.setup_bookmark()
|
||||||
bookmark.title = 'updated title'
|
bookmark.title = "updated title"
|
||||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||||
|
|
||||||
mock_create_web_archive_snapshot.assert_not_called()
|
mock_create_web_archive_snapshot.assert_not_called()
|
||||||
|
|
||||||
def test_update_should_update_website_metadata_if_url_did_change(self):
|
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(
|
expected_metadata = WebsiteMetadata(
|
||||||
'https://example.com/updated',
|
"https://example.com/updated",
|
||||||
'Updated website title',
|
"Updated website title",
|
||||||
'Updated website description'
|
"Updated website description",
|
||||||
)
|
)
|
||||||
mock_load_website_metadata.return_value = expected_metadata
|
mock_load_website_metadata.return_value = expected_metadata
|
||||||
|
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
bookmark.url = 'https://example.com/updated'
|
bookmark.url = "https://example.com/updated"
|
||||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||||
|
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
mock_load_website_metadata.assert_called_once()
|
mock_load_website_metadata.assert_called_once()
|
||||||
self.assertEqual(expected_metadata.title, bookmark.website_title)
|
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):
|
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 = self.setup_bookmark()
|
||||||
bookmark.title = 'updated title'
|
bookmark.title = "updated title"
|
||||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||||
|
|
||||||
mock_load_website_metadata.assert_not_called()
|
mock_load_website_metadata.assert_not_called()
|
||||||
|
|
||||||
def test_update_should_update_favicon(self):
|
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 = self.setup_bookmark()
|
||||||
bookmark.title = 'updated title'
|
bookmark.title = "updated title"
|
||||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
update_bookmark(bookmark, "tag1,tag2", self.user)
|
||||||
|
|
||||||
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
||||||
|
|
||||||
def test_archive_bookmark(self):
|
def test_archive_bookmark(self):
|
||||||
bookmark = Bookmark(
|
bookmark = Bookmark(
|
||||||
url='https://example.com',
|
url="https://example.com",
|
||||||
date_added=timezone.now(),
|
date_added=timezone.now(),
|
||||||
date_modified=timezone.now(),
|
date_modified=timezone.now(),
|
||||||
owner=self.user
|
owner=self.user,
|
||||||
)
|
)
|
||||||
bookmark.save()
|
bookmark.save()
|
||||||
|
|
||||||
@@ -146,7 +186,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
def test_unarchive_bookmark(self):
|
def test_unarchive_bookmark(self):
|
||||||
bookmark = Bookmark(
|
bookmark = Bookmark(
|
||||||
url='https://example.com',
|
url="https://example.com",
|
||||||
date_added=timezone.now(),
|
date_added=timezone.now(),
|
||||||
date_modified=timezone.now(),
|
date_modified=timezone.now(),
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
@@ -165,7 +205,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = 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=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
def test_archive_bookmarks_should_only_archive_user_owned_bookmarks(self):
|
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()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
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=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = 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=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||||
bookmark3 = 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=bookmark1.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||||
bookmark3 = 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.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
def test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(is_archived=True)
|
||||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||||
inaccessible_bookmark = self.setup_bookmark(is_archived=True, user=other_user)
|
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=bookmark1.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||||
bookmark3 = 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=bookmark1.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = 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=bookmark1.id).first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.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())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
||||||
|
|
||||||
def test_delete_bookmarks_should_only_delete_user_owned_bookmarks(self):
|
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()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
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=bookmark1.id).first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.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):
|
def test_delete_bookmarks_should_accept_mix_of_int_and_string_ids(self):
|
||||||
bookmark1 = self.setup_bookmark()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = 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=bookmark1.id).first())
|
||||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||||
@@ -302,8 +375,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
|
|
||||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}',
|
tag_bookmarks(
|
||||||
self.get_or_create_test_user())
|
[bookmark1.id, bookmark2.id, bookmark3.id],
|
||||||
|
f"{tag1.name},{tag2.name}",
|
||||||
|
self.get_or_create_test_user(),
|
||||||
|
)
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -318,7 +394,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = 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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -326,8 +406,8 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
self.assertEqual(2, Tag.objects.count())
|
self.assertEqual(2, Tag.objects.count())
|
||||||
|
|
||||||
tag1 = Tag.objects.filter(name='tag1').first()
|
tag1 = Tag.objects.filter(name="tag1").first()
|
||||||
tag2 = Tag.objects.filter(name='tag2').first()
|
tag2 = Tag.objects.filter(name="tag2").first()
|
||||||
|
|
||||||
self.assertIsNotNone(tag1)
|
self.assertIsNotNone(tag1)
|
||||||
self.assertIsNotNone(tag2)
|
self.assertIsNotNone(tag2)
|
||||||
@@ -336,6 +416,31 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||||
self.assertCountEqual(bookmark3.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):
|
def test_tag_bookmarks_should_only_tag_specified_bookmarks(self):
|
||||||
bookmark1 = self.setup_bookmark()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
@@ -343,7 +448,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = 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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -354,15 +463,20 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||||
|
|
||||||
def test_tag_bookmarks_should_only_tag_user_owned_bookmarks(self):
|
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()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
|
|
||||||
tag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}',
|
tag_bookmarks(
|
||||||
self.get_or_create_test_user())
|
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||||
|
f"{tag1.name},{tag2.name}",
|
||||||
|
self.get_or_create_test_user(),
|
||||||
|
)
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -379,8 +493,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
|
|
||||||
tag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}',
|
tag_bookmarks(
|
||||||
self.get_or_create_test_user())
|
[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(bookmark1.tags.all(), [tag1, tag2])
|
||||||
self.assertCountEqual(bookmark2.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])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = 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}',
|
untag_bookmarks(
|
||||||
self.get_or_create_test_user())
|
[bookmark1.id, bookmark2.id, bookmark3.id],
|
||||||
|
f"{tag1.name},{tag2.name}",
|
||||||
|
self.get_or_create_test_user(),
|
||||||
|
)
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -411,7 +531,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = 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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -422,15 +546,20 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark3.tags.all(), [])
|
self.assertCountEqual(bookmark3.tags.all(), [])
|
||||||
|
|
||||||
def test_untag_bookmarks_should_only_tag_user_owned_bookmarks(self):
|
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()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
bookmark1 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark1 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
inaccessible_bookmark = self.setup_bookmark(user=other_user, 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}',
|
untag_bookmarks(
|
||||||
self.get_or_create_test_user())
|
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||||
|
f"{tag1.name},{tag2.name}",
|
||||||
|
self.get_or_create_test_user(),
|
||||||
|
)
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -447,8 +576,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = 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}',
|
untag_bookmarks(
|
||||||
self.get_or_create_test_user())
|
[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(bookmark1.tags.all(), [])
|
||||||
self.assertCountEqual(bookmark2.tags.all(), [])
|
self.assertCountEqual(bookmark2.tags.all(), [])
|
||||||
@@ -459,7 +591,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(unread=True)
|
bookmark2 = self.setup_bookmark(unread=True)
|
||||||
bookmark3 = 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=bookmark1.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(unread=True)
|
||||||
bookmark3 = 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.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||||
|
|
||||||
def test_mark_bookmarks_as_read_should_only_update_user_owned_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(unread=True)
|
||||||
bookmark2 = self.setup_bookmark(unread=True)
|
bookmark2 = self.setup_bookmark(unread=True)
|
||||||
inaccessible_bookmark = self.setup_bookmark(unread=True, user=other_user)
|
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=bookmark1.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(unread=True)
|
||||||
bookmark3 = 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=bookmark1.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(unread=False)
|
||||||
bookmark3 = 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=bookmark1.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(unread=False)
|
||||||
bookmark3 = 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.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
|
||||||
|
|
||||||
def test_mark_bookmarks_as_unread_should_only_update_user_owned_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(unread=False)
|
||||||
bookmark2 = self.setup_bookmark(unread=False)
|
bookmark2 = self.setup_bookmark(unread=False)
|
||||||
inaccessible_bookmark = self.setup_bookmark(unread=False, user=other_user)
|
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=bookmark1.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(unread=False)
|
||||||
bookmark3 = 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=bookmark1.id).unread)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(shared=False)
|
||||||
bookmark3 = 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=bookmark1.id).shared)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||||
|
|
||||||
def test_share_bookmarks_should_only_update_user_owned_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(shared=False)
|
||||||
bookmark2 = self.setup_bookmark(shared=False)
|
bookmark2 = self.setup_bookmark(shared=False)
|
||||||
inaccessible_bookmark = self.setup_bookmark(shared=False, user=other_user)
|
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=bookmark1.id).shared)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(shared=False)
|
||||||
bookmark3 = 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=bookmark1.id).shared)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(shared=True)
|
||||||
bookmark3 = 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=bookmark1.id).shared)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
|
||||||
|
|
||||||
def test_unshare_bookmarks_should_only_update_user_owned_bookmarks(self):
|
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)
|
bookmark1 = self.setup_bookmark(shared=True)
|
||||||
bookmark2 = self.setup_bookmark(shared=True)
|
bookmark2 = self.setup_bookmark(shared=True)
|
||||||
inaccessible_bookmark = self.setup_bookmark(shared=True, user=other_user)
|
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=bookmark1.id).shared)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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)
|
bookmark2 = self.setup_bookmark(shared=True)
|
||||||
bookmark3 = 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=bookmark1.id).shared)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.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