mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-15 06:29:21 +02:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a4df586a8a | ||
![]() |
d9b7996e06 | ||
![]() |
92f62d3ded | ||
![]() |
9c48085829 | ||
![]() |
77e1525402 | ||
![]() |
9df80e01de | ||
![]() |
ec34cc523f | ||
![]() |
eb0b092d17 | ||
![]() |
39e8f03345 | ||
![]() |
d43b97e0c0 |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## v1.25.0 (18/03/2024)
|
||||
|
||||
### What's Changed
|
||||
* Improve PWA capabilities by @hugo-vrijswijk in https://github.com/sissbruecker/linkding/pull/630
|
||||
* build improvements by @hugo-vrijswijk in https://github.com/sissbruecker/linkding/pull/649
|
||||
* Add support for oidc by @Nighmared in https://github.com/sissbruecker/linkding/pull/389
|
||||
* Add option for custom CSS by @sissbruecker in https://github.com/sissbruecker/linkding/pull/652
|
||||
* Update backup location to safe directory by @bphenriques in https://github.com/sissbruecker/linkding/pull/653
|
||||
* Include web archive link in /api/bookmarks/ by @sissbruecker in https://github.com/sissbruecker/linkding/pull/655
|
||||
* Add RSS feeds for shared bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/656
|
||||
* Bump django from 5.0.2 to 5.0.3 by @dependabot in https://github.com/sissbruecker/linkding/pull/658
|
||||
|
||||
### New Contributors
|
||||
* @hugo-vrijswijk made their first contribution in https://github.com/sissbruecker/linkding/pull/630
|
||||
* @Nighmared made their first contribution in https://github.com/sissbruecker/linkding/pull/389
|
||||
* @bphenriques made their first contribution in https://github.com/sissbruecker/linkding/pull/653
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.24.2...v1.25.0
|
||||
|
||||
---
|
||||
|
||||
## v1.24.2 (16/03/2024)
|
||||
|
||||
### What's Changed
|
||||
* Fix logout button by @sissbruecker in https://github.com/sissbruecker/linkding/pull/648
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.24.1...v1.24.2
|
||||
|
||||
---
|
||||
|
||||
## v1.24.1 (16/03/2024)
|
||||
|
||||
### What's Changed
|
||||
|
133
bookmarks/e2e/e2e_test_bookmark_details_modal.py
Normal file
133
bookmarks/e2e/e2e_test_bookmark_details_modal.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright, expect
|
||||
|
||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
from bookmarks.models import Bookmark
|
||||
|
||||
|
||||
class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
||||
def test_show_details(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
title = details_modal.locator("h2")
|
||||
expect(title).to_have_text(bookmark.title)
|
||||
|
||||
def test_close_details(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
# close with close button
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
details_modal.locator("button.close").click()
|
||||
expect(details_modal).to_be_hidden()
|
||||
|
||||
# close with backdrop
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
overlay = details_modal.locator(".modal-overlay")
|
||||
overlay.click(position={"x": 0, "y": 0})
|
||||
expect(details_modal).to_be_hidden()
|
||||
|
||||
def test_toggle_archived(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
# archive
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
details_modal.get_by_text("Archived", exact=False).click()
|
||||
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
|
||||
|
||||
# unarchive
|
||||
url = reverse("bookmarks:archived")
|
||||
self.page.goto(self.live_server_url + url)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
details_modal.get_by_text("Archived", exact=False).click()
|
||||
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
|
||||
|
||||
def test_toggle_unread(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
# mark as unread
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
|
||||
details_modal.get_by_text("Unread").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Unread")).to_be_visible()
|
||||
|
||||
# mark as read
|
||||
details_modal.get_by_text("Unread").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Unread")).not_to_be_visible()
|
||||
|
||||
def test_toggle_shared(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_sharing = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
# share bookmark
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
|
||||
details_modal.get_by_text("Shared").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Shared")).to_be_visible()
|
||||
|
||||
# unshare bookmark
|
||||
details_modal.get_by_text("Shared").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Shared")).not_to_be_visible()
|
||||
|
||||
def test_edit_return_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse("bookmarks:index") + f"?q={bookmark.title}"
|
||||
self.open(url, p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
|
||||
# Navigate to edit page
|
||||
with self.page.expect_navigation():
|
||||
details_modal.get_by_text("Edit").click()
|
||||
|
||||
# Cancel edit, verify return url
|
||||
with self.page.expect_navigation(url=self.live_server_url + url):
|
||||
self.page.get_by_text("Nevermind").click()
|
||||
|
||||
def test_delete(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse("bookmarks:index") + f"?q={bookmark.title}"
|
||||
self.open(url, p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
|
||||
# Delete bookmark, verify return url
|
||||
with self.page.expect_navigation(url=self.live_server_url + url):
|
||||
details_modal.get_by_text("Delete...").click()
|
||||
details_modal.get_by_text("Confirm").click()
|
||||
|
||||
# verify bookmark is deleted
|
||||
self.locate_bookmark(bookmark.title)
|
||||
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 0)
|
37
bookmarks/e2e/e2e_test_bookmark_details_view.py
Normal file
37
bookmarks/e2e/e2e_test_bookmark_details_view.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
|
||||
|
||||
class BookmarkDetailsViewE2ETestCase(LinkdingE2ETestCase):
|
||||
def test_edit_return_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:details", args=[bookmark.id]), p)
|
||||
|
||||
# Navigate to edit page
|
||||
with self.page.expect_navigation():
|
||||
self.page.get_by_text("Edit").click()
|
||||
|
||||
# Cancel edit, verify return url
|
||||
with self.page.expect_navigation(
|
||||
url=self.live_server_url
|
||||
+ reverse("bookmarks:details", args=[bookmark.id])
|
||||
):
|
||||
self.page.get_by_text("Nevermind").click()
|
||||
|
||||
def test_delete_return_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:details", args=[bookmark.id]), p)
|
||||
|
||||
# Trigger delete, verify return url
|
||||
# Should probably return to last bookmark list page, but for now just returns to index
|
||||
with self.page.expect_navigation(
|
||||
url=self.live_server_url + reverse("bookmarks:index")
|
||||
):
|
||||
self.page.get_by_text("Delete...").click()
|
||||
self.page.get_by_text("Confirm").click()
|
@@ -39,6 +39,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
bookmark_list = self.locate_bookmark_list()
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
@@ -46,6 +47,8 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
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()
|
||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
@@ -74,6 +77,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:archived"), p)
|
||||
|
||||
bookmark_list = self.locate_bookmark_list()
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
@@ -81,6 +85,8 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
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()
|
||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
self.assertEqual(
|
||||
50,
|
||||
@@ -109,6 +115,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:index") + "?q=foo", p)
|
||||
|
||||
bookmark_list = self.locate_bookmark_list()
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
@@ -116,6 +123,8 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
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()
|
||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
self.assertEqual(
|
||||
50,
|
||||
@@ -144,6 +153,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
with sync_playwright() as p:
|
||||
self.open(reverse("bookmarks:archived") + "?q=foo", p)
|
||||
|
||||
bookmark_list = self.locate_bookmark_list()
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
@@ -151,6 +161,8 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
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()
|
||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
self.assertEqual(
|
||||
50,
|
||||
@@ -269,14 +281,13 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
url = reverse("bookmarks:index")
|
||||
page = self.open(url, p)
|
||||
|
||||
bookmark_list = self.locate_bookmark_list()
|
||||
|
||||
# Select all bookmarks, enable select across
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
self.locate_bulk_edit_select_across().click()
|
||||
|
||||
# Get reference for bookmark list
|
||||
bookmark_list = page.locator("ul[ld-bookmark-list]")
|
||||
|
||||
# Execute bulk action
|
||||
self.select_bulk_action("Mark as unread")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
@@ -302,6 +313,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
url = reverse("bookmarks:index")
|
||||
self.open(url, p)
|
||||
|
||||
bookmark_list = self.locate_bookmark_list()
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
|
||||
@@ -312,6 +324,8 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
||||
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()
|
||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
expect(self.locate_bulk_edit_select_all()).not_to_be_checked()
|
||||
self.locate_bulk_edit_select_all().click()
|
||||
|
@@ -2,6 +2,7 @@ from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright, expect
|
||||
|
||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
from bookmarks.models import UserProfile
|
||||
|
||||
|
||||
class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
|
||||
@@ -39,3 +40,49 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
|
||||
expect(enable_sharing).not_to_be_checked()
|
||||
expect(enable_public_sharing).not_to_be_checked()
|
||||
expect(enable_public_sharing).to_be_disabled()
|
||||
|
||||
def test_should_not_show_bookmark_description_max_lines_when_display_inline(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.bookmark_description_display = (
|
||||
UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_INLINE
|
||||
)
|
||||
profile.save()
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse("bookmarks:settings.general"))
|
||||
|
||||
max_lines = page.get_by_label("Bookmark description max lines")
|
||||
expect(max_lines).to_be_hidden()
|
||||
|
||||
def test_should_show_bookmark_description_max_lines_when_display_separate(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.bookmark_description_display = (
|
||||
UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_SEPARATE
|
||||
)
|
||||
profile.save()
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse("bookmarks:settings.general"))
|
||||
|
||||
max_lines = page.get_by_label("Bookmark description max lines")
|
||||
expect(max_lines).to_be_visible()
|
||||
|
||||
def test_should_update_bookmark_description_max_lines_when_changing_display(self):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse("bookmarks:settings.general"))
|
||||
|
||||
max_lines = page.get_by_label("Bookmark description max lines")
|
||||
expect(max_lines).to_be_hidden()
|
||||
|
||||
display = page.get_by_label("Bookmark description", exact=True)
|
||||
display.select_option("separate")
|
||||
expect(max_lines).to_be_visible()
|
||||
|
||||
display.select_option("inline")
|
||||
expect(max_lines).to_be_hidden()
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from django.contrib.staticfiles.testing import LiveServerTestCase
|
||||
from playwright.sync_api import BrowserContext, Playwright, Page
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
@@ -38,10 +39,25 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
||||
def assertReloads(self, count: int):
|
||||
self.assertEqual(self.num_loads, count)
|
||||
|
||||
def locate_bookmark_list(self):
|
||||
return self.page.locator("ul[ld-bookmark-list]")
|
||||
|
||||
def locate_bookmark(self, title: str):
|
||||
bookmark_tags = self.page.locator("li[ld-bookmark-item]")
|
||||
return bookmark_tags.filter(has_text=title)
|
||||
|
||||
def locate_details_modal(self):
|
||||
return self.page.locator(".modal.bookmark-details")
|
||||
|
||||
def open_details_modal(self, bookmark):
|
||||
details_button = self.locate_bookmark(bookmark.title).get_by_text("View")
|
||||
details_button.click()
|
||||
|
||||
details_modal = self.locate_details_modal()
|
||||
expect(details_modal).to_be_visible()
|
||||
|
||||
return details_modal
|
||||
|
||||
def locate_bulk_edit_bar(self):
|
||||
return self.page.locator(".bulk-edit-bar")
|
||||
|
||||
|
38
bookmarks/frontend/behaviors/bookmark-details.js
Normal file
38
bookmarks/frontend/behaviors/bookmark-details.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { registerBehavior } from "./index";
|
||||
|
||||
class BookmarkDetails {
|
||||
constructor(element) {
|
||||
this.form = element.querySelector(".status form");
|
||||
if (!this.form) {
|
||||
// Form may not exist if user does not own the bookmark
|
||||
return;
|
||||
}
|
||||
this.form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
this.submitForm();
|
||||
});
|
||||
|
||||
const inputs = this.form.querySelectorAll("input");
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
this.submitForm();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
const url = this.form.action;
|
||||
const formData = new FormData(this.form);
|
||||
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
redirect: "manual", // ignore redirect
|
||||
});
|
||||
|
||||
// Refresh bookmark page if it exists
|
||||
document.dispatchEvent(new CustomEvent("bookmark-page-refresh"));
|
||||
}
|
||||
}
|
||||
|
||||
registerBehavior("ld-bookmark-details", BookmarkDetails);
|
@@ -8,6 +8,10 @@ class BookmarkPage {
|
||||
|
||||
this.bookmarkList = element.querySelector(".bookmark-list-container");
|
||||
this.tagCloud = element.querySelector(".tag-cloud-container");
|
||||
|
||||
document.addEventListener("bookmark-page-refresh", () => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
async onFormSubmit(event) {
|
||||
|
@@ -38,10 +38,14 @@ class ConfirmButtonBehavior {
|
||||
container.append(question);
|
||||
}
|
||||
|
||||
const buttonClasses = Array.from(this.button.classList.values())
|
||||
.filter((cls) => cls.startsWith("btn"))
|
||||
.join(" ");
|
||||
|
||||
const cancelButton = document.createElement(this.button.nodeName);
|
||||
cancelButton.type = "button";
|
||||
cancelButton.innerText = question ? "No" : "Cancel";
|
||||
cancelButton.className = "btn btn-link btn-sm mr-1";
|
||||
cancelButton.className = `${buttonClasses} mr-1`;
|
||||
cancelButton.addEventListener("click", this.reset.bind(this));
|
||||
|
||||
const confirmButton = document.createElement(this.button.nodeName);
|
||||
@@ -49,7 +53,7 @@ class ConfirmButtonBehavior {
|
||||
confirmButton.name = this.button.dataset.name;
|
||||
confirmButton.value = this.button.dataset.value;
|
||||
confirmButton.innerText = question ? "Yes" : "Confirm";
|
||||
confirmButton.className = "btn btn-link btn-sm";
|
||||
confirmButton.className = buttonClasses;
|
||||
confirmButton.addEventListener("click", this.reset.bind(this));
|
||||
|
||||
container.append(cancelButton, confirmButton);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { registerBehavior } from "./index";
|
||||
import { applyBehaviors, registerBehavior } from "./index";
|
||||
|
||||
class ModalBehavior {
|
||||
constructor(element) {
|
||||
@@ -7,14 +7,50 @@ class ModalBehavior {
|
||||
this.toggle = toggle;
|
||||
}
|
||||
|
||||
onToggleClick() {
|
||||
async onToggleClick(event) {
|
||||
// Ignore Ctrl + click
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Create modal either by teleporting existing content or fetching from URL
|
||||
const modal = this.toggle.hasAttribute("modal-content")
|
||||
? this.createFromContent()
|
||||
: await this.createFromUrl();
|
||||
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register close handlers
|
||||
const modalOverlay = modal.querySelector(".modal-overlay");
|
||||
const closeButton = modal.querySelector("button.close");
|
||||
modalOverlay.addEventListener("click", this.onClose.bind(this));
|
||||
closeButton.addEventListener("click", this.onClose.bind(this));
|
||||
|
||||
document.body.append(modal);
|
||||
applyBehaviors(document.body);
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
async createFromUrl() {
|
||||
const url = this.toggle.getAttribute("modal-url");
|
||||
const modalHtml = await fetch(url).then((response) => response.text());
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(modalHtml, "text/html");
|
||||
return doc.querySelector(".modal");
|
||||
}
|
||||
|
||||
createFromContent() {
|
||||
const contentSelector = this.toggle.getAttribute("modal-content");
|
||||
const content = document.querySelector(contentSelector);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal
|
||||
// Todo: make title configurable, only used for tag cloud for now
|
||||
const modal = document.createElement("div");
|
||||
modal.classList.add("modal", "active");
|
||||
modal.innerHTML = `
|
||||
@@ -22,7 +58,7 @@ class ModalBehavior {
|
||||
<div class="modal-container">
|
||||
<div class="modal-header d-flex justify-between align-center">
|
||||
<div class="modal-title h5">Tags</div>
|
||||
<button class="btn btn-link close">
|
||||
<button class="close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 6l-12 12"></path>
|
||||
@@ -36,29 +72,28 @@ class ModalBehavior {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Teleport content element
|
||||
const contentOwner = content.parentElement;
|
||||
const contentContainer = modal.querySelector(".content");
|
||||
contentContainer.append(content);
|
||||
this.content = content;
|
||||
this.contentOwner = contentOwner;
|
||||
|
||||
// Register close handlers
|
||||
const modalOverlay = modal.querySelector(".modal-overlay");
|
||||
const closeButton = modal.querySelector(".btn.close");
|
||||
modalOverlay.addEventListener("click", this.onClose.bind(this));
|
||||
closeButton.addEventListener("click", this.onClose.bind(this));
|
||||
|
||||
document.body.append(modal);
|
||||
this.modal = modal;
|
||||
return modal;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
// Teleport content back
|
||||
this.contentOwner.append(this.content);
|
||||
if (this.content && this.contentOwner) {
|
||||
this.contentOwner.append(this.content);
|
||||
}
|
||||
|
||||
// Remove modal
|
||||
this.modal.remove();
|
||||
this.modal.classList.add("closing");
|
||||
this.modal.addEventListener("animationend", (event) => {
|
||||
if (event.animationName === "fade-out") {
|
||||
this.modal.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -151,18 +151,20 @@
|
||||
}
|
||||
|
||||
.form-autocomplete.small .form-autocomplete-input {
|
||||
height: 1.4rem;
|
||||
min-height: 1.4rem;
|
||||
height: var(--control-size-sm);
|
||||
min-height: var(--control-size-sm);
|
||||
padding: 0.05rem 0.3rem;
|
||||
}
|
||||
|
||||
.form-autocomplete.small .form-autocomplete-input input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.7rem;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.form-autocomplete.small .menu .menu-item {
|
||||
font-size: 0.7rem;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import './behaviors/bookmark-page';
|
||||
import './behaviors/bulk-edit';
|
||||
import './behaviors/confirm-button';
|
||||
import './behaviors/dropdown';
|
||||
import './behaviors/modal';
|
||||
import './behaviors/global-shortcuts';
|
||||
import './behaviors/tag-autocomplete';
|
||||
export { default as TagAutoComplete } from './components/TagAutocomplete.svelte';
|
||||
export { default as SearchAutoComplete } from './components/SearchAutoComplete.svelte';
|
||||
export { ApiClient } from './api';
|
||||
import "./behaviors/bookmark-details";
|
||||
import "./behaviors/bookmark-page";
|
||||
import "./behaviors/bulk-edit";
|
||||
import "./behaviors/confirm-button";
|
||||
import "./behaviors/dropdown";
|
||||
import "./behaviors/modal";
|
||||
import "./behaviors/global-shortcuts";
|
||||
import "./behaviors/tag-autocomplete";
|
||||
export { default as TagAutoComplete } from "./components/TagAutocomplete.svelte";
|
||||
export { default as SearchAutoComplete } from "./components/SearchAutoComplete.svelte";
|
||||
export { ApiClient } from "./api";
|
||||
|
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-23 21:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0026_userprofile_custom_css"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="bookmark_description_display",
|
||||
field=models.CharField(
|
||||
choices=[("inline", "Inline"), ("separate", "Separate")],
|
||||
default="inline",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="bookmark_description_max_lines",
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-29 20:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0027_userprofile_bookmark_description_display_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="display_archive_bookmark_action",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="display_edit_bookmark_action",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="display_remove_bookmark_action",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="display_view_bookmark_action",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
34
bookmarks/migrations/0029_bookmark_list_actions_toast.py
Normal file
34
bookmarks/migrations/0029_bookmark_list_actions_toast.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-29 21:25
|
||||
|
||||
from django.db import migrations
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from bookmarks.models import Toast
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
|
||||
for user in User.objects.all():
|
||||
toast = Toast(
|
||||
key="bookmark_list_actions_hint",
|
||||
message="This version adds a new link to each bookmark to view details in a dialog. If you feel there is too much clutter you can now hide individual links in the settings.",
|
||||
owner=user,
|
||||
)
|
||||
toast.save()
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
Toast.objects.filter(key="bookmark_list_actions_hint").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0028_userprofile_display_archive_bookmark_action_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards, reverse),
|
||||
]
|
@@ -278,6 +278,12 @@ class UserProfile(models.Model):
|
||||
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, "Absolute"),
|
||||
(BOOKMARK_DATE_DISPLAY_HIDDEN, "Hidden"),
|
||||
]
|
||||
BOOKMARK_DESCRIPTION_DISPLAY_INLINE = "inline"
|
||||
BOOKMARK_DESCRIPTION_DISPLAY_SEPARATE = "separate"
|
||||
BOOKMARK_DESCRIPTION_DISPLAY_CHOICES = [
|
||||
(BOOKMARK_DESCRIPTION_DISPLAY_INLINE, "Inline"),
|
||||
(BOOKMARK_DESCRIPTION_DISPLAY_SEPARATE, "Separate"),
|
||||
]
|
||||
BOOKMARK_LINK_TARGET_BLANK = "_blank"
|
||||
BOOKMARK_LINK_TARGET_SELF = "_self"
|
||||
BOOKMARK_LINK_TARGET_CHOICES = [
|
||||
@@ -308,6 +314,16 @@ class UserProfile(models.Model):
|
||||
blank=False,
|
||||
default=BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
)
|
||||
bookmark_description_display = models.CharField(
|
||||
max_length=10,
|
||||
choices=BOOKMARK_DESCRIPTION_DISPLAY_CHOICES,
|
||||
blank=False,
|
||||
default=BOOKMARK_DESCRIPTION_DISPLAY_INLINE,
|
||||
)
|
||||
bookmark_description_max_lines = models.IntegerField(
|
||||
null=False,
|
||||
default=1,
|
||||
)
|
||||
bookmark_link_target = models.CharField(
|
||||
max_length=10,
|
||||
choices=BOOKMARK_LINK_TARGET_CHOICES,
|
||||
@@ -330,6 +346,10 @@ class UserProfile(models.Model):
|
||||
enable_public_sharing = models.BooleanField(default=False, null=False)
|
||||
enable_favicons = models.BooleanField(default=False, null=False)
|
||||
display_url = models.BooleanField(default=False, null=False)
|
||||
display_view_bookmark_action = models.BooleanField(default=True, null=False)
|
||||
display_edit_bookmark_action = models.BooleanField(default=True, null=False)
|
||||
display_archive_bookmark_action = models.BooleanField(default=True, null=False)
|
||||
display_remove_bookmark_action = models.BooleanField(default=True, null=False)
|
||||
permanent_notes = models.BooleanField(default=False, null=False)
|
||||
custom_css = models.TextField(blank=True, null=False)
|
||||
search_preferences = models.JSONField(default=dict, null=False)
|
||||
@@ -341,6 +361,8 @@ class UserProfileForm(forms.ModelForm):
|
||||
fields = [
|
||||
"theme",
|
||||
"bookmark_date_display",
|
||||
"bookmark_description_display",
|
||||
"bookmark_description_max_lines",
|
||||
"bookmark_link_target",
|
||||
"web_archive_integration",
|
||||
"tag_search",
|
||||
@@ -348,6 +370,10 @@ class UserProfileForm(forms.ModelForm):
|
||||
"enable_public_sharing",
|
||||
"enable_favicons",
|
||||
"display_url",
|
||||
"display_view_bookmark_action",
|
||||
"display_edit_bookmark_action",
|
||||
"display_archive_bookmark_action",
|
||||
"display_remove_bookmark_action",
|
||||
"permanent_notes",
|
||||
"custom_css",
|
||||
]
|
||||
|
@@ -9,7 +9,7 @@ body {
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: $unit-10;
|
||||
margin-bottom: $unit-9;
|
||||
|
||||
.logo {
|
||||
width: 28px;
|
||||
@@ -50,14 +50,14 @@ section.content-area {
|
||||
border-bottom: solid 1px $border-color;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: $unit-6;
|
||||
padding-bottom: $unit-2;
|
||||
margin-bottom: $unit-4;
|
||||
column-gap: $unit-5;
|
||||
padding-bottom: $unit-1;
|
||||
margin-bottom: $unit-3;
|
||||
|
||||
h2 {
|
||||
flex: 0 0 auto;
|
||||
line-height: 1.8rem;
|
||||
margin-bottom: 0;
|
||||
line-height: $unit-9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
@@ -95,10 +95,6 @@ span.confirmation {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.text-gray-dark {
|
||||
color: $gray-color-dark;
|
||||
}
|
||||
@@ -124,10 +120,6 @@ span.confirmation {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn.btn-wide {
|
||||
padding-left: $unit-6;
|
||||
padding-right: $unit-6;
|
||||
|
79
bookmarks/styles/bookmark-details.scss
Normal file
79
bookmarks/styles/bookmark-details.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
/* Common styles */
|
||||
.bookmark-details {
|
||||
h2 {
|
||||
flex: 1 1 0;
|
||||
align-items: flex-start;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.weblinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2;
|
||||
}
|
||||
|
||||
a.weblink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-2;
|
||||
}
|
||||
|
||||
a.weblink img, a.weblink svg {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $body-font-color;
|
||||
}
|
||||
|
||||
a.weblink span {
|
||||
flex: 1 1 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tags a {
|
||||
color: $alternative-color;
|
||||
}
|
||||
|
||||
.status form {
|
||||
display: flex;
|
||||
gap: $unit-2;
|
||||
}
|
||||
|
||||
.status form .form-group, .status form .form-switch {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bookmark details view specific */
|
||||
.bookmark-details.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-6;
|
||||
}
|
||||
|
||||
/* Bookmark details modal specific */
|
||||
.bookmark-details.modal {
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $unit-2;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
@@ -1,11 +1,10 @@
|
||||
.bookmarks-page.grid {
|
||||
grid-gap: $unit-10;
|
||||
grid-gap: $unit-9;
|
||||
}
|
||||
|
||||
/* Bookmark area header controls */
|
||||
.bookmarks-page .content-area-header {
|
||||
--searchbox-max-width: 350px;
|
||||
--searchbox-height: 1.8rem;
|
||||
|
||||
@media (max-width: $size-sm) {
|
||||
--searchbox-max-width: initial;
|
||||
@@ -20,18 +19,18 @@
|
||||
|
||||
// Regular input
|
||||
input[type='search'] {
|
||||
height: var(--searchbox-height);
|
||||
height: $control-size;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
// Enhanced auto-complete input
|
||||
// This needs a bit more wrangling to make the CSS component align with the attached button
|
||||
.form-autocomplete {
|
||||
height: var(--searchbox-height);
|
||||
height: $control-size;
|
||||
|
||||
.form-autocomplete-input {
|
||||
width: 100%;
|
||||
height: var(--searchbox-height);
|
||||
height: $control-size;
|
||||
|
||||
input[type='search'] {
|
||||
width: 100%;
|
||||
@@ -72,6 +71,7 @@
|
||||
.menu {
|
||||
padding: $unit-4;
|
||||
min-width: 250px;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.menu .actions {
|
||||
@@ -82,9 +82,11 @@
|
||||
|
||||
.radio-group {
|
||||
margin-bottom: $unit-1;
|
||||
|
||||
.form-label {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.form-radio.form-inline {
|
||||
margin: 0 $unit-2 0 0;
|
||||
padding: 0;
|
||||
@@ -92,6 +94,7 @@
|
||||
align-items: center;
|
||||
column-gap: $unit-1;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
top: 0;
|
||||
position: relative;
|
||||
@@ -105,6 +108,9 @@ ul.bookmark-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
/* Increase line-height for better separation within / between items */
|
||||
line-height: 1.1rem;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
@@ -122,59 +128,81 @@ ul.bookmark-list {
|
||||
/* Bookmarks */
|
||||
li[ld-bookmark-item] {
|
||||
position: relative;
|
||||
margin-top: $unit-2;
|
||||
|
||||
[ld-bulk-edit-checkbox].form-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title img {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title img + a {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.title a {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&[data-tooltip]:hover::after, &[data-tooltip]:focus::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
height: fit-content;
|
||||
background-color: #292f62;
|
||||
color: #fff;
|
||||
padding: $unit-1;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid #424a8c;
|
||||
font-size: $font-size-sm;
|
||||
font-style: normal;
|
||||
white-space: normal;
|
||||
animation: 0.3s ease 0s appear;
|
||||
}
|
||||
.title a[data-tooltip]:hover::after, .title a[data-tooltip]:focus::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: max-content;
|
||||
max-width: 90%;
|
||||
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;
|
||||
pointer-events: none;
|
||||
animation: 0.3s ease 0s appear;
|
||||
}
|
||||
|
||||
&.unread .title a {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.title img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: $unit-h;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.url-display {
|
||||
.url-path, .url-display {
|
||||
font-size: $font-size-sm;
|
||||
color: $secondary-link-color;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: $gray-color-dark;
|
||||
}
|
||||
|
||||
.description.separate {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: var(--ld-bookmark-description-max-lines, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tags {
|
||||
a, a:visited:hover {
|
||||
color: $alternative-color;
|
||||
}
|
||||
@@ -195,6 +223,8 @@ li[ld-bookmark-item] {
|
||||
}
|
||||
|
||||
.actions {
|
||||
font-size: $font-size-sm;
|
||||
|
||||
a, button.btn-link {
|
||||
color: $gray-color;
|
||||
padding: 0;
|
||||
@@ -211,10 +241,6 @@ li[ld-bookmark-item] {
|
||||
color: $gray-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +249,8 @@ li[ld-bookmark-item] {
|
||||
}
|
||||
|
||||
.tag-cloud {
|
||||
/* Increase line-height for better separation within / between items */
|
||||
line-height: 1.1rem;
|
||||
|
||||
.selected-tags {
|
||||
margin-bottom: $unit-4;
|
||||
@@ -258,55 +286,13 @@ ul.bookmark-list {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.show-notes .notes,
|
||||
li.show-notes .notes {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bookmark notes markdown styles */
|
||||
ul.bookmark-list .notes-content {
|
||||
& {
|
||||
.notes .markdown {
|
||||
padding: $unit-2 $unit-3;
|
||||
}
|
||||
|
||||
p, ul, ol, pre, blockquote {
|
||||
margin: 0 0 $unit-2 0;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-left: $unit-4;
|
||||
}
|
||||
|
||||
ul li, ol li {
|
||||
margin-top: $unit-1;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: $unit-1 $unit-2;
|
||||
background-color: $code-bg-color;
|
||||
border-radius: $unit-1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> pre:first-child:last-child {
|
||||
padding: 0;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
&.show-notes .notes,
|
||||
li.show-notes .notes {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +306,7 @@ $bulk-edit-transition-duration: 400ms;
|
||||
.bulk-edit-bar {
|
||||
margin-top: -1px;
|
||||
margin-left: -$bulk-edit-bar-offset;
|
||||
margin-bottom: $unit-4;
|
||||
margin-bottom: $unit-3;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height $bulk-edit-transition-duration;
|
||||
@@ -342,7 +328,6 @@ $bulk-edit-transition-duration: 400ms;
|
||||
width: $bulk-edit-toggle-width;
|
||||
margin: 0 0 0 $bulk-edit-toggle-offset;
|
||||
padding: 0;
|
||||
min-height: 1rem;
|
||||
}
|
||||
|
||||
/* Bookmark checkboxes */
|
||||
@@ -350,8 +335,10 @@ $bulk-edit-transition-duration: 400ms;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: $bulk-edit-toggle-width;
|
||||
min-height: $bulk-edit-toggle-width;
|
||||
left: -$bulk-edit-toggle-width - $bulk-edit-toggle-offset;
|
||||
top: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
@@ -359,7 +346,7 @@ $bulk-edit-transition-duration: 400ms;
|
||||
transition: all $bulk-edit-transition-duration;
|
||||
|
||||
.form-icon {
|
||||
top: $unit-1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +358,7 @@ $bulk-edit-transition-duration: 400ms;
|
||||
/* Actions */
|
||||
.bulk-edit-actions {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
padding: $unit-1 0;
|
||||
border-top: solid 1px $border-color;
|
||||
gap: $unit-2;
|
||||
|
40
bookmarks/styles/markdown.scss
Normal file
40
bookmarks/styles/markdown.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
.markdown {
|
||||
p, ul, ol, pre, blockquote {
|
||||
margin: 0 0 $unit-2 0;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-left: $unit-4;
|
||||
}
|
||||
|
||||
ul li, ol li {
|
||||
margin-top: $unit-1;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: $unit-1 $unit-2;
|
||||
background-color: $code-bg-color;
|
||||
border-radius: $unit-1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> pre:first-child:last-child {
|
||||
padding: 0;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
@@ -37,6 +37,14 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.columns-2 {
|
||||
--grid-columns: 2;
|
||||
}
|
||||
|
||||
.gap-0 {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.col-1 {
|
||||
grid-column: unquote("span min(1, var(--grid-columns))");
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
.settings-page {
|
||||
section.content-area {
|
||||
margin-bottom: $unit-12;
|
||||
margin-bottom: $unit-10;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $unit-4;
|
||||
margin-bottom: $unit-3;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,32 @@
|
||||
|
||||
// Variables and mixins
|
||||
@import "../../node_modules/spectre.css/src/variables";
|
||||
|
||||
// Customize variables to reduce font and control sizes
|
||||
|
||||
// Can use CSS variables for font sizes, as they are not used in SCSS calculations
|
||||
$font-size: var(--font-size);
|
||||
$font-size-sm: var(--font-size-sm);
|
||||
$font-size-lg: var(--font-size-lg);
|
||||
|
||||
// Can't use CSS variables for these, used in SCSS calculations
|
||||
$line-height: 1rem;
|
||||
$control-size: $unit-8;
|
||||
$control-size-sm: $unit-6;
|
||||
$control-size-lg: $unit-9;
|
||||
|
||||
// Declare defaults for CSS variables, expose SCSS variables as CSS variables
|
||||
html {
|
||||
--font-size: 0.7rem;
|
||||
--font-size-sm: 0.65rem;
|
||||
--font-size-lg: 0.8rem;
|
||||
|
||||
--control-size: #{$control-size};
|
||||
--control-size-sm: #{$control-size-sm};
|
||||
--control-size-lg: #{$control-size-lg};
|
||||
}
|
||||
|
||||
// Mixins
|
||||
@import "../../node_modules/spectre.css/src/mixins";
|
||||
|
||||
/*! Spectre.css v#{$version} | MIT License | github.com/picturepan2/spectre */
|
||||
@@ -64,19 +90,6 @@ a:visited:hover {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
// Fix radio button sub-pixel size
|
||||
.form-radio .form-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.form-radio input:checked + .form-icon::before {
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
// Make code work with light and dark theme
|
||||
code {
|
||||
color: $gray-color-dark;
|
||||
@@ -127,6 +140,53 @@ ul.menu li:first-child {
|
||||
}
|
||||
}
|
||||
|
||||
// Customize modal animation
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal.active .modal-container, .modal.active .modal-overlay {
|
||||
animation: fade-in .15s ease 1;
|
||||
}
|
||||
|
||||
.modal.active.closing .modal-container, .modal.active.closing .modal-overlay {
|
||||
animation: fade-out .15s ease 1;
|
||||
}
|
||||
|
||||
// Customize menu animation
|
||||
.dropdown .menu {
|
||||
animation: fade-in .15s ease 1;
|
||||
}
|
||||
|
||||
// Modal close button
|
||||
.modal .modal-header button.close {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
opacity: .85;
|
||||
color: $gray-color-dark;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Increase input font size on small viewports to prevent zooming on focus the input
|
||||
// on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max
|
||||
// viewport size
|
||||
|
@@ -7,9 +7,11 @@
|
||||
// Import style modules
|
||||
@import "base";
|
||||
@import "responsive";
|
||||
@import "bookmark-details";
|
||||
@import "bookmark-page";
|
||||
@import "bookmark-form";
|
||||
@import "settings";
|
||||
@import "markdown";
|
||||
|
||||
/* Dark theme overrides */
|
||||
|
||||
@@ -40,8 +42,17 @@ a:focus, .btn:focus {
|
||||
}
|
||||
|
||||
.form-checkbox input:checked + .form-icon, .form-radio input:checked + .form-icon, .form-switch input:checked + .form-icon {
|
||||
background: $dt-primary-button-color;
|
||||
border-color: $dt-primary-button-color;
|
||||
background: $dt-primary-input-color;
|
||||
border-color: $dt-primary-input-color;
|
||||
}
|
||||
|
||||
.form-switch .form-icon::before, .form-switch input:active + .form-icon::before {
|
||||
background: $light-color;
|
||||
}
|
||||
|
||||
.form-switch input:checked + .form-icon {
|
||||
background: $dt-primary-input-color;
|
||||
border-color: $dt-primary-input-color;
|
||||
}
|
||||
|
||||
.form-radio input:checked + .form-icon::before {
|
||||
|
@@ -7,6 +7,8 @@
|
||||
// Import style modules
|
||||
@import "base";
|
||||
@import "responsive";
|
||||
@import "bookmark-details";
|
||||
@import "bookmark-page";
|
||||
@import "bookmark-form";
|
||||
@import "settings";
|
||||
@import "markdown";
|
||||
|
@@ -1,5 +1,3 @@
|
||||
$html-font-size: 18px !default;
|
||||
|
||||
$body-bg: #161822 !default;
|
||||
$bg-color: lighten($body-bg, 5%) !default;
|
||||
$bg-color-light: lighten($body-bg, 5%) !default;
|
||||
@@ -30,4 +28,5 @@ $code-bg-color: rgba(255, 255, 255, 0.1);
|
||||
$code-shadow-color: rgba(255, 255, 255, 0.2);
|
||||
|
||||
/* Dark theme specific */
|
||||
$dt-primary-input-color: #5C68E7 !default;
|
||||
$dt-primary-button-color: #5761cb !default;
|
||||
|
@@ -1,5 +1,3 @@
|
||||
$html-font-size: 18px !default;
|
||||
|
||||
$alternative-color: #05a6a3;
|
||||
$alternative-color-dark: darken($alternative-color, 5%);
|
||||
|
||||
|
@@ -6,50 +6,66 @@
|
||||
{% include 'bookmarks/empty_bookmarks.html' %}
|
||||
{% else %}
|
||||
<ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
|
||||
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};"
|
||||
data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
|
||||
{% for bookmark_item in bookmark_list.items %}
|
||||
<li ld-bookmark-item{% if bookmark_item.css_classes %} class="{{ bookmark_item.css_classes }}"{% endif %}>
|
||||
<label ld-bulk-edit-checkbox class="form-checkbox">
|
||||
<input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
<div class="title">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener" >
|
||||
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
|
||||
<img src="{% static bookmark_item.favicon_file %}" alt="">
|
||||
{% endif %}
|
||||
<span>{{ bookmark_item.title }}</span>
|
||||
<label ld-bulk-edit-checkbox class="form-checkbox">
|
||||
<input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
|
||||
<img src="{% static bookmark_item.favicon_file %}" alt="">
|
||||
{% endif %}
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
|
||||
<span>{{ bookmark_item.title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if bookmark_list.show_url %}
|
||||
<div class="url-path truncate">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
|
||||
class="url-display text-sm">
|
||||
class="url-display">
|
||||
{{ bookmark_item.url }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="description truncate">
|
||||
{% if bookmark_list.description_display == 'inline' %}
|
||||
<div class="description inline truncate">
|
||||
{% if bookmark_item.tag_names %}
|
||||
<span class="tags">
|
||||
{% for tag_name in bookmark_item.tag_names %}
|
||||
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if bookmark_item.tag_names and bookmark_item.description %} | {% endif %}
|
||||
{% if bookmark_item.description %}
|
||||
<span>{{ bookmark_item.description }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if bookmark_item.description %}
|
||||
<div class="description separate">
|
||||
{{ bookmark_item.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bookmark_item.tag_names %}
|
||||
<span>
|
||||
<div class="tags">
|
||||
{% for tag_name in bookmark_item.tag_names %}
|
||||
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bookmark_item.tag_names and bookmark_item.description %} | {% endif %}
|
||||
{% if bookmark_item.description %}
|
||||
<span>{{ bookmark_item.description }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bookmark_item.notes %}
|
||||
<div class="notes bg-gray text-gray-dark">
|
||||
<div class="notes-content">
|
||||
<div class="markdown">
|
||||
{% markdown bookmark_item.notes %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="actions text-gray text-sm">
|
||||
<div class="actions text-gray">
|
||||
{% if bookmark_item.display_date %}
|
||||
{% if bookmark_item.web_archive_snapshot_url %}
|
||||
<a href="{{ bookmark_item.web_archive_snapshot_url }}"
|
||||
@@ -61,23 +77,35 @@
|
||||
{% else %}
|
||||
<span>{{ bookmark_item.display_date }}</span>
|
||||
{% endif %}
|
||||
<span class="separator">|</span>
|
||||
<span>|</span>
|
||||
{% endif %}
|
||||
{# View link is visible for both owned and shared bookmarks #}
|
||||
{% if bookmark_list.show_view_action %}
|
||||
<a ld-modal
|
||||
modal-url="{% url 'bookmarks:details_modal' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}"
|
||||
href="{% url 'bookmarks:details' bookmark_item.id %}">View</a>
|
||||
{% endif %}
|
||||
{% if bookmark_item.is_editable %}
|
||||
{# Bookmark owner actions #}
|
||||
<a href="{% url 'bookmarks:edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
|
||||
{% if bookmark_item.is_archived %}
|
||||
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="archive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Archive
|
||||
{% if bookmark_list.show_edit_action %}
|
||||
<a href="{% url 'bookmarks:edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
|
||||
{% endif %}
|
||||
{% if bookmark_list.show_archive_action %}
|
||||
{% if bookmark_item.is_archived %}
|
||||
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="archive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Archive
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if bookmark_list.show_remove_action %}
|
||||
<button ld-confirm-button type="submit" name="remove" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Remove
|
||||
</button>
|
||||
{% endif %}
|
||||
<button ld-confirm-button type="submit" name="remove" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Remove
|
||||
</button>
|
||||
{% else %}
|
||||
{# Shared bookmark actions #}
|
||||
<span>Shared by
|
||||
@@ -86,7 +114,7 @@
|
||||
{% endif %}
|
||||
{% if bookmark_item.has_extra_actions %}
|
||||
<div class="extra-actions">
|
||||
<span class="separator hide-sm">|</span>
|
||||
<span class="hide-sm">|</span>
|
||||
{% if bookmark_item.show_mark_as_read %}
|
||||
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
|
13
bookmarks/templates/bookmarks/details.html
Normal file
13
bookmarks/templates/bookmarks/details.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div ld-bookmark-details class="bookmark-details page">
|
||||
{% if request.user == bookmark.owner %}
|
||||
{% include 'bookmarks/details/actions.html' %}
|
||||
{% endif %}
|
||||
{% include 'bookmarks/details/title.html' %}
|
||||
<div>
|
||||
{% include 'bookmarks/details/content.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
13
bookmarks/templates/bookmarks/details/actions.html
Normal file
13
bookmarks/templates/bookmarks/details/actions.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="actions">
|
||||
<div class="left-actions">
|
||||
<a class="btn" href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ edit_return_url|urlencode }}">Edit</a>
|
||||
</div>
|
||||
<div class="right-actions">
|
||||
<form action="{% url 'bookmarks:index.action' %}?return_url={{ delete_return_url|urlencode }}" method="post">
|
||||
{% csrf_token %}
|
||||
<button ld-confirm-button type="submit" name="remove" value="{{ bookmark.id }}" class="btn btn-link text-error">
|
||||
Delete...
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
85
bookmarks/templates/bookmarks/details/content.html
Normal file
85
bookmarks/templates/bookmarks/details/content.html
Normal file
@@ -0,0 +1,85 @@
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
|
||||
<div class="weblinks">
|
||||
<a class="weblink" href="{{ bookmark.url }}" rel="noopener"
|
||||
target="{{ request.user_profile.bookmark_link_target }}">
|
||||
{% if bookmark.favicon_file and request.user_profile.enable_favicons %}
|
||||
<img class="favicon" src="{% static bookmark.favicon_file %}" alt="">
|
||||
{% endif %}
|
||||
<span>{{ bookmark.url }}</span>
|
||||
</a>
|
||||
{% if bookmark.web_archive_snapshot_url %}
|
||||
<a class="weblink" href="{{ bookmark.web_archive_snapshot_url }}"
|
||||
target="{{ request.user_profile.bookmark_link_target }}">
|
||||
{% if bookmark.favicon_file and request.user_profile.enable_favicons %}
|
||||
<svg class="favicon" viewBox="0 0 76 86" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m76 82v4h-76l.00080851-4zm-3-6v5h-70v-5zm-62.6696277-54 .8344146.4217275.4176066 6.7436084.4176065 10.9576581v10.5383496l-.4176065 13.1364492-.0694681 8.8498268-1.1825531.3523804h-4.17367003l-1.25202116-.3523804-.48627608-8.8498268-.41840503-13.0662957v-10.5375432l.41840503-11.028618.38167482-6.7798947.87034634-.3854412zm60.0004653 0 .8353798.4217275.4168913 6.7436084.4168913 10.9576581v10.5383496l-.4168913 13.1364492-.0686832 8.8498268-1.1835879.3523804h-4.1737047l-1.2522712-.3523804-.4879704-8.8498268-.4168913-13.0662957v-10.5375432l.4168913-11.028618.3833483-6.7798947.8697215-.3854412zm-42.000632 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2529447-.3523804-.4863246-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8688361-.3854412zm23 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2521462-.3523804-.4871231-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8696347-.3854412zm21.6697944-9v7h-70v-7zm-35.7200748-13 36.7200748 8.4088317-1.4720205 2.5911683h-70.32799254l-2.19998696-2.10140371z"
|
||||
fill="currentColor" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
<span>View on Internet Archive</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<dl class="grid columns-2 columns-sm-1 gap-0">
|
||||
{% if request.user == bookmark.owner %}
|
||||
<div class="status col-2">
|
||||
<dt>Status</dt>
|
||||
<dd class="d-flex" style="gap: .8rem">
|
||||
<form action="{% url 'bookmarks:details' bookmark.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="is_archived" {% if bookmark.is_archived %}checked{% endif %}>
|
||||
<i class="form-icon"></i> Archived
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="unread" {% if bookmark.unread %}checked{% endif %}>
|
||||
<i class="form-icon"></i> Unread
|
||||
</label>
|
||||
</div>
|
||||
{% if request.user_profile.enable_sharing %}
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="shared" {% if bookmark.shared %}checked{% endif %}>
|
||||
<i class="form-icon"></i> Shared
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bookmark.tag_names %}
|
||||
<div class="tags col-1">
|
||||
<dt>Tags</dt>
|
||||
<dd>
|
||||
{% for tag_name in bookmark.tag_names %}
|
||||
<a href="{% url 'bookmarks:index' %}?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="date-added col-1">
|
||||
<dt>Date added</dt>
|
||||
<dd>
|
||||
<span>{{ bookmark.date_added }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
{% if bookmark.resolved_description %}
|
||||
<div class="description col-2">
|
||||
<dt>Description</dt>
|
||||
<dd>{{ bookmark.resolved_description }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bookmark.notes %}
|
||||
<div class="notes col-2">
|
||||
<dt>Notes</dt>
|
||||
<dd class="markdown">{% markdown bookmark.notes %}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
3
bookmarks/templates/bookmarks/details/title.html
Normal file
3
bookmarks/templates/bookmarks/details/title.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h2>
|
||||
{{ bookmark.resolved_title }}
|
||||
</h2>
|
27
bookmarks/templates/bookmarks/details_modal.html
Normal file
27
bookmarks/templates/bookmarks/details_modal.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div ld-bookmark-details class="modal active bookmark-details">
|
||||
<div class="modal-overlay" aria-label="Close"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
{% include 'bookmarks/details/title.html' %}
|
||||
<button class="close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 6l-12 12"></path>
|
||||
<path d="M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="content">
|
||||
{% include 'bookmarks/details/content.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.user == bookmark.owner %}
|
||||
<div class="modal-footer">
|
||||
{% include 'bookmarks/details/actions.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
@@ -35,7 +35,7 @@
|
||||
|
||||
{# Tag cloud #}
|
||||
<section class="content-area col-1 hide-md">
|
||||
<div class="content-area-header mb-4">
|
||||
<div class="content-area-header">
|
||||
<h2>Tags</h2>
|
||||
</div>
|
||||
<div class="tag-cloud-container">
|
||||
|
@@ -105,9 +105,9 @@
|
||||
<form action="{% url 'bookmarks:toasts.acknowledge' %}?return_url={{ request.path | urlencode }}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for toast in toast_messages %}
|
||||
<div class="toast">
|
||||
<div class="toast d-flex">
|
||||
{{ toast.message }}
|
||||
<button type="submit" name="toast" value="{{ toast.id }}" class="btn btn-clear float-right"></button>
|
||||
<button type="submit" name="toast" value="{{ toast.id }}" class="btn btn-clear"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</form>
|
||||
|
@@ -6,8 +6,8 @@
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0" style="padding-right: 0.2rem">
|
||||
Bookmarks
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
||||
style="height:1rem;width:1rem;vertical-align: text-bottom;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
style="height:1rem;width:1rem;vertical-align: middle;">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"/>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<path d="M18 9v11"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="menu text-sm" tabindex="0">
|
||||
<div class="menu" tabindex="0">
|
||||
<form id="search_preferences" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% if 'sort' in preferences_form.editable_fields %}
|
||||
|
@@ -29,6 +29,23 @@
|
||||
be hidden.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_description_display.id_for_label }}" class="form-label">Bookmark
|
||||
description</label>
|
||||
{{ form.bookmark_description_display|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
Whether to show bookmark descriptions and tags in the same line, or as separate blocks.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group {% if request.user_profile.bookmark_description_display == 'inline' %}d-hide{% endif %}">
|
||||
<label for="{{ form.bookmark_description_max_lines.id_for_label }}" class="form-label">Bookmark description
|
||||
max lines</label>
|
||||
{{ form.bookmark_description_max_lines|add_class:"form-input width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
Limits the number of lines that are displayed for the bookmark description.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.display_url.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_url }}
|
||||
@@ -48,6 +65,28 @@
|
||||
Alternatively the keyboard shortcut <code>e</code> can be used to temporarily show all notes.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Bookmark actions</label>
|
||||
<label for="{{ form.display_view_bookmark_action.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_view_bookmark_action }}
|
||||
<i class="form-icon"></i> View
|
||||
</label>
|
||||
<label for="{{ form.display_edit_bookmark_action.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_edit_bookmark_action }}
|
||||
<i class="form-icon"></i> Edit
|
||||
</label>
|
||||
<label for="{{ form.display_archive_bookmark_action.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_archive_bookmark_action }}
|
||||
<i class="form-icon"></i> Archive
|
||||
</label>
|
||||
<label for="{{ form.display_remove_bookmark_action.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_remove_bookmark_action }}
|
||||
<i class="form-icon"></i> Remove
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
Which actions to display for each bookmark.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
|
||||
{{ form.bookmark_link_target|add_class:"form-select width-25 width-sm-100" }}
|
||||
@@ -124,18 +163,18 @@
|
||||
href="{% url 'bookmarks:shared' %}">shared bookmarks page</a>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<details {% if form.custom_css.value %}open{% endif %}>
|
||||
<summary>Custom CSS</summary>
|
||||
<label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
|
||||
<div class="mt-2">
|
||||
{{ form.custom_css|add_class:"form-input custom-css"|attr:"rows:6" }}
|
||||
<div class="form-group">
|
||||
<details {% if form.custom_css.value %}open{% endif %}>
|
||||
<summary>Custom CSS</summary>
|
||||
<label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
|
||||
<div class="mt-2">
|
||||
{{ form.custom_css|add_class:"form-input custom-css"|attr:"rows:6" }}
|
||||
</div>
|
||||
</details>
|
||||
<div class="form-input-hint">
|
||||
Allows to add custom CSS to the page.
|
||||
</div>
|
||||
</details>
|
||||
<div class="form-input-hint">
|
||||
Allows to add custom CSS to the page.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2">
|
||||
{% if update_profile_success_message %}
|
||||
@@ -195,10 +234,6 @@
|
||||
<section class="content-area">
|
||||
<h2>Export</h2>
|
||||
<p>Export all bookmarks in Netscape HTML format.</p>
|
||||
<p>
|
||||
Note that exporting bookmark notes is currently not supported due to limitations of the format.
|
||||
For proper backups please use a database backup as described in the documentation.
|
||||
</p>
|
||||
<a class="btn btn-primary" href="{% url 'bookmarks:settings.export' %}">Download (.html)</a>
|
||||
{% if export_error %}
|
||||
<div class="has-error">
|
||||
@@ -237,10 +272,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Automatically disable public bookmark sharing if bookmark sharing is disabled
|
||||
const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}");
|
||||
const enablePublicSharing = document.getElementById("{{ form.enable_public_sharing.id_for_label }}");
|
||||
const bookmarkDescriptionDisplay = document.getElementById("{{ form.bookmark_description_display.id_for_label }}");
|
||||
const bookmarkDescriptionMaxLines = document.getElementById("{{ form.bookmark_description_max_lines.id_for_label }}");
|
||||
|
||||
// Automatically disable public bookmark sharing if bookmark sharing is disabled
|
||||
function updatePublicSharing() {
|
||||
if (enableSharing.checked) {
|
||||
enablePublicSharing.disabled = false;
|
||||
@@ -252,6 +289,18 @@
|
||||
|
||||
updatePublicSharing();
|
||||
enableSharing.addEventListener("change", updatePublicSharing);
|
||||
|
||||
// Automatically hide the bookmark description max lines input if the description display is set to inline
|
||||
function updateBookmarkDescriptionMaxLines() {
|
||||
if (bookmarkDescriptionDisplay.value === "inline") {
|
||||
bookmarkDescriptionMaxLines.closest(".form-group").classList.add("d-hide");
|
||||
} else {
|
||||
bookmarkDescriptionMaxLines.closest(".form-group").classList.remove("d-hide");
|
||||
}
|
||||
}
|
||||
|
||||
updateBookmarkDescriptionMaxLines();
|
||||
bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
562
bookmarks/tests/test_bookmark_details_modal.py
Normal file
562
bookmarks/tests/test_bookmark_details_modal.py
Normal file
@@ -0,0 +1,562 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import formats
|
||||
|
||||
from bookmarks.models import UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
|
||||
class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
def setUp(self):
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def get_base_url(self, bookmark):
|
||||
return reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
|
||||
def get_details(self, bookmark, return_url=""):
|
||||
url = self.get_base_url(bookmark)
|
||||
if return_url:
|
||||
url += f"?return_url={return_url}"
|
||||
response = self.client.get(url)
|
||||
soup = self.make_soup(response.content)
|
||||
return soup
|
||||
|
||||
def find_section(self, soup, section_name):
|
||||
dt = soup.find("dt", string=section_name)
|
||||
dd = dt.find_next_sibling("dd") if dt else None
|
||||
return dd
|
||||
|
||||
def get_section(self, soup, section_name):
|
||||
dd = self.find_section(soup, section_name)
|
||||
self.assertIsNotNone(dd)
|
||||
return dd
|
||||
|
||||
def find_weblink(self, soup, url):
|
||||
return soup.find("a", {"class": "weblink", "href": url})
|
||||
|
||||
def test_access(self):
|
||||
# own bookmark
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# other user's bookmark
|
||||
other_user = self.setup_user()
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# non-existent bookmark
|
||||
response = self.client.get(reverse("bookmarks:details_modal", args=[9999]))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# guest user
|
||||
self.client.logout()
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_access_with_sharing(self):
|
||||
# shared bookmark, sharing disabled
|
||||
other_user = self.setup_user()
|
||||
bookmark = self.setup_bookmark(shared=True, user=other_user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# shared bookmark, sharing enabled
|
||||
profile = other_user.profile
|
||||
profile.enable_sharing = True
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# shared bookmark, guest user, no public sharing
|
||||
self.client.logout()
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# shared bookmark, guest user, public sharing
|
||||
profile.enable_public_sharing = True
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_displays_title(self):
|
||||
# with title
|
||||
bookmark = self.setup_bookmark(title="Test title")
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
title = soup.find("h2")
|
||||
self.assertIsNotNone(title)
|
||||
self.assertEqual(title.text.strip(), bookmark.title)
|
||||
|
||||
# with website title
|
||||
bookmark = self.setup_bookmark(title="", website_title="Website title")
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
title = soup.find("h2")
|
||||
self.assertIsNotNone(title)
|
||||
self.assertEqual(title.text.strip(), bookmark.website_title)
|
||||
|
||||
# with URL only
|
||||
bookmark = self.setup_bookmark(title="", website_title="")
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
title = soup.find("h2")
|
||||
self.assertIsNotNone(title)
|
||||
self.assertEqual(title.text.strip(), bookmark.url)
|
||||
|
||||
def test_website_link(self):
|
||||
# basics
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.url)
|
||||
self.assertIsNotNone(link)
|
||||
self.assertEqual(link["href"], bookmark.url)
|
||||
self.assertEqual(link.text.strip(), bookmark.url)
|
||||
|
||||
# favicons disabled
|
||||
bookmark = self.setup_bookmark(favicon_file="example.png")
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.url)
|
||||
image = link.select_one("img")
|
||||
self.assertIsNone(image)
|
||||
|
||||
# favicons enabled, no favicon
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file="")
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.url)
|
||||
image = link.select_one("img")
|
||||
self.assertIsNone(image)
|
||||
|
||||
# favicons enabled, favicon present
|
||||
bookmark = self.setup_bookmark(favicon_file="example.png")
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.url)
|
||||
image = link.select_one("img")
|
||||
self.assertIsNotNone(image)
|
||||
self.assertEqual(image["src"], "/static/example.png")
|
||||
|
||||
def test_internet_archive_link(self):
|
||||
# without snapshot url
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
self.assertIsNone(link)
|
||||
|
||||
# with snapshot url
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com/")
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
self.assertIsNotNone(link)
|
||||
self.assertEqual(link["href"], bookmark.web_archive_snapshot_url)
|
||||
self.assertEqual(link.text.strip(), "View on Internet Archive")
|
||||
|
||||
# favicons disabled
|
||||
bookmark = self.setup_bookmark(
|
||||
web_archive_snapshot_url="https://example.com/", favicon_file="example.png"
|
||||
)
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
image = link.select_one("svg")
|
||||
self.assertIsNone(image)
|
||||
|
||||
# favicons enabled, no favicon
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(
|
||||
web_archive_snapshot_url="https://example.com/", favicon_file=""
|
||||
)
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
image = link.select_one("svg")
|
||||
self.assertIsNone(image)
|
||||
|
||||
# favicons enabled, favicon present
|
||||
bookmark = self.setup_bookmark(
|
||||
web_archive_snapshot_url="https://example.com/", favicon_file="example.png"
|
||||
)
|
||||
soup = self.get_details(bookmark)
|
||||
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
image = link.select_one("svg")
|
||||
self.assertIsNotNone(image)
|
||||
|
||||
def test_weblinks_respect_target_setting(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com/")
|
||||
|
||||
# target blank
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_BLANK
|
||||
profile.save()
|
||||
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
website_link = self.find_weblink(soup, bookmark.url)
|
||||
self.assertIsNotNone(website_link)
|
||||
self.assertEqual(website_link["target"], UserProfile.BOOKMARK_LINK_TARGET_BLANK)
|
||||
|
||||
web_archive_link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
self.assertIsNotNone(web_archive_link)
|
||||
self.assertEqual(
|
||||
web_archive_link["target"], UserProfile.BOOKMARK_LINK_TARGET_BLANK
|
||||
)
|
||||
|
||||
# target self
|
||||
profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
|
||||
profile.save()
|
||||
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
website_link = self.find_weblink(soup, bookmark.url)
|
||||
self.assertIsNotNone(website_link)
|
||||
self.assertEqual(website_link["target"], UserProfile.BOOKMARK_LINK_TARGET_SELF)
|
||||
|
||||
web_archive_link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||
self.assertIsNotNone(web_archive_link)
|
||||
self.assertEqual(
|
||||
web_archive_link["target"], UserProfile.BOOKMARK_LINK_TARGET_SELF
|
||||
)
|
||||
|
||||
def test_status(self):
|
||||
# renders form
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Status")
|
||||
|
||||
form = section.find("form")
|
||||
self.assertIsNotNone(form)
|
||||
self.assertEqual(
|
||||
form["action"], reverse("bookmarks:details", args=[bookmark.id])
|
||||
)
|
||||
self.assertEqual(form["method"], "post")
|
||||
|
||||
# sharing disabled
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Status")
|
||||
|
||||
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
|
||||
self.assertIsNotNone(archived)
|
||||
unread = section.find("input", {"type": "checkbox", "name": "unread"})
|
||||
self.assertIsNotNone(unread)
|
||||
shared = section.find("input", {"type": "checkbox", "name": "shared"})
|
||||
self.assertIsNone(shared)
|
||||
|
||||
# sharing enabled
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_sharing = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Status")
|
||||
|
||||
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
|
||||
self.assertIsNotNone(archived)
|
||||
unread = section.find("input", {"type": "checkbox", "name": "unread"})
|
||||
self.assertIsNotNone(unread)
|
||||
shared = section.find("input", {"type": "checkbox", "name": "shared"})
|
||||
self.assertIsNotNone(shared)
|
||||
|
||||
# unchecked
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Status")
|
||||
|
||||
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
|
||||
self.assertFalse(archived.has_attr("checked"))
|
||||
unread = section.find("input", {"type": "checkbox", "name": "unread"})
|
||||
self.assertFalse(unread.has_attr("checked"))
|
||||
shared = section.find("input", {"type": "checkbox", "name": "shared"})
|
||||
self.assertFalse(shared.has_attr("checked"))
|
||||
|
||||
# checked
|
||||
bookmark = self.setup_bookmark(is_archived=True, unread=True, shared=True)
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Status")
|
||||
|
||||
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
|
||||
self.assertTrue(archived.has_attr("checked"))
|
||||
unread = section.find("input", {"type": "checkbox", "name": "unread"})
|
||||
self.assertTrue(unread.has_attr("checked"))
|
||||
shared = section.find("input", {"type": "checkbox", "name": "shared"})
|
||||
self.assertTrue(shared.has_attr("checked"))
|
||||
|
||||
def test_status_visibility(self):
|
||||
# own bookmark
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.find_section(soup, "Status")
|
||||
form_action = reverse("bookmarks:details", args=[bookmark.id])
|
||||
form = soup.find("form", {"action": form_action})
|
||||
self.assertIsNotNone(section)
|
||||
self.assertIsNotNone(form)
|
||||
|
||||
# other user's bookmark
|
||||
other_user = self.setup_user(enable_sharing=True)
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.find_section(soup, "Status")
|
||||
form_action = reverse("bookmarks:details", args=[bookmark.id])
|
||||
form = soup.find("form", {"action": form_action})
|
||||
self.assertIsNone(section)
|
||||
self.assertIsNone(form)
|
||||
|
||||
# guest user
|
||||
self.client.logout()
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.find_section(soup, "Status")
|
||||
form_action = reverse("bookmarks:details", args=[bookmark.id])
|
||||
form = soup.find("form", {"action": form_action})
|
||||
self.assertIsNone(section)
|
||||
self.assertIsNone(form)
|
||||
|
||||
def test_status_update(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
# update status
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "on", "unread": "on", "shared": "on"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
self.assertTrue(bookmark.is_archived)
|
||||
self.assertTrue(bookmark.unread)
|
||||
self.assertTrue(bookmark.shared)
|
||||
|
||||
# update individual status
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "", "unread": "on", "shared": ""},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
self.assertFalse(bookmark.is_archived)
|
||||
self.assertTrue(bookmark.unread)
|
||||
self.assertFalse(bookmark.shared)
|
||||
|
||||
def test_status_update_access(self):
|
||||
# no sharing
|
||||
other_user = self.setup_user()
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "on", "unread": "on", "shared": "on"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# shared, sharing disabled
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "on", "unread": "on", "shared": "on"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# shared, sharing enabled
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
profile = other_user.profile
|
||||
profile.enable_sharing = True
|
||||
profile.save()
|
||||
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "on", "unread": "on", "shared": "on"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# shared, public sharing enabled
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
profile = other_user.profile
|
||||
profile.enable_public_sharing = True
|
||||
profile.save()
|
||||
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "on", "unread": "on", "shared": "on"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# guest user
|
||||
self.client.logout()
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
|
||||
response = self.client.post(
|
||||
self.get_base_url(bookmark),
|
||||
{"is_archived": "on", "unread": "on", "shared": "on"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_date_added(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Date added")
|
||||
|
||||
expected_date = formats.date_format(bookmark.date_added, "DATETIME_FORMAT")
|
||||
date = section.find("span", string=expected_date)
|
||||
self.assertIsNotNone(date)
|
||||
|
||||
def test_tags(self):
|
||||
# without tags
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
section = self.find_section(soup, "Tags")
|
||||
self.assertIsNone(section)
|
||||
|
||||
# with tags
|
||||
bookmark = self.setup_bookmark(tags=[self.setup_tag(), self.setup_tag()])
|
||||
|
||||
soup = self.get_details(bookmark)
|
||||
section = self.get_section(soup, "Tags")
|
||||
|
||||
for tag in bookmark.tags.all():
|
||||
tag_link = section.find("a", string=f"#{tag.name}")
|
||||
self.assertIsNotNone(tag_link)
|
||||
expected_url = reverse("bookmarks:index") + f"?q=%23{tag.name}"
|
||||
self.assertEqual(tag_link["href"], expected_url)
|
||||
|
||||
def test_description(self):
|
||||
# without description
|
||||
bookmark = self.setup_bookmark(description="", website_description="")
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
section = self.find_section(soup, "Description")
|
||||
self.assertIsNone(section)
|
||||
|
||||
# with description
|
||||
bookmark = self.setup_bookmark(description="Test description")
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
section = self.get_section(soup, "Description")
|
||||
self.assertEqual(section.text.strip(), bookmark.description)
|
||||
|
||||
# with website description
|
||||
bookmark = self.setup_bookmark(
|
||||
description="", website_description="Website description"
|
||||
)
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
section = self.get_section(soup, "Description")
|
||||
self.assertEqual(section.text.strip(), bookmark.website_description)
|
||||
|
||||
def test_notes(self):
|
||||
# without notes
|
||||
bookmark = self.setup_bookmark()
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
section = self.find_section(soup, "Notes")
|
||||
self.assertIsNone(section)
|
||||
|
||||
# with notes
|
||||
bookmark = self.setup_bookmark(notes="Test notes")
|
||||
soup = self.get_details(bookmark)
|
||||
|
||||
section = self.get_section(soup, "Notes")
|
||||
self.assertEqual(section.decode_contents(), "<p>Test notes</p>")
|
||||
|
||||
def test_edit_link(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
# with default return URL
|
||||
soup = self.get_details(bookmark)
|
||||
edit_link = soup.find("a", string="Edit")
|
||||
self.assertIsNotNone(edit_link)
|
||||
details_url = reverse("bookmarks:details", args=[bookmark.id])
|
||||
expected_url = (
|
||||
reverse("bookmarks:edit", args=[bookmark.id]) + "?return_url=" + details_url
|
||||
)
|
||||
self.assertEqual(edit_link["href"], expected_url)
|
||||
|
||||
# with custom return URL
|
||||
soup = self.get_details(bookmark, return_url="/custom")
|
||||
edit_link = soup.find("a", string="Edit")
|
||||
self.assertIsNotNone(edit_link)
|
||||
expected_url = (
|
||||
reverse("bookmarks:edit", args=[bookmark.id]) + "?return_url=/custom"
|
||||
)
|
||||
self.assertEqual(edit_link["href"], expected_url)
|
||||
|
||||
def test_delete_button(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
# basics
|
||||
soup = self.get_details(bookmark)
|
||||
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
|
||||
self.assertIsNotNone(delete_button)
|
||||
self.assertEqual(delete_button.text.strip(), "Delete...")
|
||||
self.assertEqual(delete_button["value"], str(bookmark.id))
|
||||
|
||||
form = delete_button.find_parent("form")
|
||||
self.assertIsNotNone(form)
|
||||
expected_url = reverse("bookmarks:index.action") + f"?return_url=/bookmarks"
|
||||
self.assertEqual(form["action"], expected_url)
|
||||
|
||||
# with custom return URL
|
||||
soup = self.get_details(bookmark, return_url="/custom")
|
||||
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
|
||||
form = delete_button.find_parent("form")
|
||||
expected_url = reverse("bookmarks:index.action") + f"?return_url=/custom"
|
||||
self.assertEqual(form["action"], expected_url)
|
||||
|
||||
def test_actions_visibility(self):
|
||||
# with sharing
|
||||
other_user = self.setup_user(enable_sharing=True)
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
|
||||
soup = self.get_details(bookmark)
|
||||
edit_link = soup.find("a", string="Edit")
|
||||
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
|
||||
self.assertIsNone(edit_link)
|
||||
self.assertIsNone(delete_button)
|
||||
|
||||
# with public sharing
|
||||
profile = other_user.profile
|
||||
profile.enable_public_sharing = True
|
||||
profile.save()
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
|
||||
soup = self.get_details(bookmark)
|
||||
edit_link = soup.find("a", string="Edit")
|
||||
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
|
||||
self.assertIsNone(edit_link)
|
||||
self.assertIsNone(delete_button)
|
||||
|
||||
# guest user
|
||||
self.client.logout()
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
|
||||
soup = self.get_details(bookmark)
|
||||
edit_link = soup.find("a", string="Edit")
|
||||
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
|
||||
self.assertIsNone(edit_link)
|
||||
self.assertIsNone(delete_button)
|
8
bookmarks/tests/test_bookmark_details_view.py
Normal file
8
bookmarks/tests/test_bookmark_details_view.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.test_bookmark_details_modal import BookmarkDetailsModalTestCase
|
||||
|
||||
|
||||
class BookmarkDetailsViewTestCase(BookmarkDetailsModalTestCase):
|
||||
def get_base_url(self, bookmark):
|
||||
return reverse("bookmarks:details", args=[bookmark.id])
|
@@ -10,11 +10,11 @@ from django.utils import timezone, formats
|
||||
|
||||
from bookmarks.middlewares import UserProfileMiddleware
|
||||
from bookmarks.models import Bookmark, UserProfile, User
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, collapse_whitespace
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||
from bookmarks.views.partials import contexts
|
||||
|
||||
|
||||
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def assertBookmarksLink(
|
||||
self, html: str, bookmark: Bookmark, link_target: str = "_blank"
|
||||
@@ -26,10 +26,10 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
)
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
{favicon_img}
|
||||
<a href="{bookmark.url}"
|
||||
target="{link_target}"
|
||||
rel="noopener">
|
||||
{favicon_img}
|
||||
<span>{bookmark.resolved_title}</span>
|
||||
</a>
|
||||
""",
|
||||
@@ -40,7 +40,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<span>{label_content}</span>
|
||||
<span class="separator">|</span>
|
||||
<span>|</span>
|
||||
""",
|
||||
html,
|
||||
)
|
||||
@@ -54,19 +54,39 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
|
||||
{label_content} ∞
|
||||
</a>
|
||||
<span class="separator">|</span>
|
||||
<span>|</span>
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=1)
|
||||
def assertViewLink(
|
||||
self, html: str, bookmark: Bookmark, return_url=reverse("bookmarks:index")
|
||||
):
|
||||
self.assertViewLinkCount(html, bookmark, return_url=return_url)
|
||||
|
||||
def assertNoBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=0)
|
||||
def assertNoViewLink(
|
||||
self, html: str, bookmark: Bookmark, return_url=reverse("bookmarks:index")
|
||||
):
|
||||
self.assertViewLinkCount(html, bookmark, count=0, return_url=return_url)
|
||||
|
||||
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
# Edit link
|
||||
def assertViewLinkCount(
|
||||
self,
|
||||
html: str,
|
||||
bookmark: Bookmark,
|
||||
count=1,
|
||||
return_url=reverse("bookmarks:index"),
|
||||
):
|
||||
details_url = reverse("bookmarks:details", args=[bookmark.id])
|
||||
details_modal_url = reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a ld-modal modal-url="{details_modal_url}?return_url={return_url}" href="{details_url}">View</a>
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertEditLinkCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
@@ -75,7 +95,8 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
# Archive link
|
||||
|
||||
def assertArchiveLinkCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button type="submit" name="archive" value="{bookmark.id}"
|
||||
@@ -84,7 +105,8 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
html,
|
||||
count=count,
|
||||
)
|
||||
# Delete link
|
||||
|
||||
def assertDeleteLinkCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<button ld-confirm-button type="submit" name="remove" value="{bookmark.id}"
|
||||
@@ -94,6 +116,17 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
count=count,
|
||||
)
|
||||
|
||||
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=1)
|
||||
|
||||
def assertNoBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=0)
|
||||
|
||||
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertEditLinkCount(html, bookmark, count=count)
|
||||
self.assertArchiveLinkCount(html, bookmark, count=count)
|
||||
self.assertDeleteLinkCount(html, bookmark, count=count)
|
||||
|
||||
def assertShareInfo(self, html: str, bookmark: Bookmark):
|
||||
self.assertShareInfoCount(html, bookmark, 1)
|
||||
|
||||
@@ -101,6 +134,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertShareInfoCount(html, bookmark, 0)
|
||||
|
||||
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
# Shared by link
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<span>Shared by
|
||||
@@ -133,7 +167,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
f"""
|
||||
<div class="url-path truncate">
|
||||
<a href="{bookmark.url}" target="{link_target}" rel="noopener"
|
||||
class="url-display text-sm">
|
||||
class="url-display">
|
||||
{bookmark.url}
|
||||
</a>
|
||||
</div>
|
||||
@@ -154,7 +188,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<div class="notes bg-gray text-gray-dark">
|
||||
<div class="notes-content">
|
||||
<div class="markdown">
|
||||
{notes_html}
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,6 +275,172 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
user.profile.save()
|
||||
return bookmark
|
||||
|
||||
def inline_bookmark_description_test(self, bookmark):
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
|
||||
has_description = bool(bookmark.description)
|
||||
has_tags = len(bookmark.tags.all()) > 0
|
||||
|
||||
# inline description block exists
|
||||
description = soup.select_one(".description.inline.truncate")
|
||||
self.assertIsNotNone(description)
|
||||
|
||||
# separate description block does not exist
|
||||
separate_description = soup.select_one(".description.separate")
|
||||
self.assertIsNone(separate_description)
|
||||
|
||||
# one direct child element per description or tags
|
||||
children = description.find_all(recursive=False)
|
||||
expected_child_count = (
|
||||
0 + (1 if has_description else 0) + (1 if has_tags else 0)
|
||||
)
|
||||
self.assertEqual(len(children), expected_child_count)
|
||||
|
||||
# has separator between description and tags
|
||||
if has_description and has_tags:
|
||||
self.assertTrue("|" in description.text)
|
||||
|
||||
# contains description text
|
||||
if has_description:
|
||||
description_text = description.find("span", text=bookmark.description)
|
||||
self.assertIsNotNone(description_text)
|
||||
|
||||
if not has_tags:
|
||||
# no tags element
|
||||
tags = soup.select_one(".tags")
|
||||
self.assertIsNone(tags)
|
||||
else:
|
||||
# tags element exists
|
||||
tags = soup.select_one(".tags")
|
||||
self.assertIsNotNone(tags)
|
||||
|
||||
# one link for each tag
|
||||
tag_links = tags.find_all("a")
|
||||
self.assertEqual(len(tag_links), len(bookmark.tags.all()))
|
||||
|
||||
for tag in bookmark.tags.all():
|
||||
tag_link = tags.find("a", text=f"#{tag.name}")
|
||||
self.assertIsNotNone(tag_link)
|
||||
self.assertEqual(tag_link["href"], f"?q=%23{tag.name}")
|
||||
|
||||
def test_inline_bookmark_description(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.bookmark_description_display = (
|
||||
UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_INLINE
|
||||
)
|
||||
profile.save()
|
||||
|
||||
# no description, no tags
|
||||
bookmark = self.setup_bookmark(description="")
|
||||
self.inline_bookmark_description_test(bookmark)
|
||||
|
||||
# with description, no tags
|
||||
bookmark = self.setup_bookmark(description="Test description")
|
||||
self.inline_bookmark_description_test(bookmark)
|
||||
|
||||
# no description, with tags
|
||||
Bookmark.objects.all().delete()
|
||||
bookmark = self.setup_bookmark(
|
||||
description="", tags=[self.setup_tag(), self.setup_tag(), self.setup_tag()]
|
||||
)
|
||||
self.inline_bookmark_description_test(bookmark)
|
||||
|
||||
# with description, with tags
|
||||
Bookmark.objects.all().delete()
|
||||
bookmark = self.setup_bookmark(
|
||||
description="Test description",
|
||||
tags=[self.setup_tag(), self.setup_tag(), self.setup_tag()],
|
||||
)
|
||||
self.inline_bookmark_description_test(bookmark)
|
||||
|
||||
def separate_bookmark_description_test(self, bookmark):
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
|
||||
has_description = bool(bookmark.description)
|
||||
has_tags = len(bookmark.tags.all()) > 0
|
||||
|
||||
# inline description block does not exist
|
||||
inline_description = soup.select_one(".description.inline")
|
||||
self.assertIsNone(inline_description)
|
||||
|
||||
if not has_description:
|
||||
# no description element
|
||||
description = soup.select_one(".description")
|
||||
self.assertIsNone(description)
|
||||
else:
|
||||
# contains description text
|
||||
description = soup.select_one(".description.separate")
|
||||
self.assertIsNotNone(description)
|
||||
self.assertEqual(description.text.strip(), bookmark.description)
|
||||
|
||||
if not has_tags:
|
||||
# no tags element
|
||||
tags = soup.select_one(".tags")
|
||||
self.assertIsNone(tags)
|
||||
else:
|
||||
# tags element exists
|
||||
tags = soup.select_one(".tags")
|
||||
self.assertIsNotNone(tags)
|
||||
|
||||
# one link for each tag
|
||||
tag_links = tags.find_all("a")
|
||||
self.assertEqual(len(tag_links), len(bookmark.tags.all()))
|
||||
|
||||
for tag in bookmark.tags.all():
|
||||
tag_link = tags.find("a", text=f"#{tag.name}")
|
||||
self.assertIsNotNone(tag_link)
|
||||
self.assertEqual(tag_link["href"], f"?q=%23{tag.name}")
|
||||
|
||||
def test_separate_bookmark_description(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.bookmark_description_display = (
|
||||
UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_SEPARATE
|
||||
)
|
||||
profile.save()
|
||||
|
||||
# no description, no tags
|
||||
bookmark = self.setup_bookmark(description="")
|
||||
self.separate_bookmark_description_test(bookmark)
|
||||
|
||||
# with description, no tags
|
||||
bookmark = self.setup_bookmark(description="Test description")
|
||||
self.separate_bookmark_description_test(bookmark)
|
||||
|
||||
# no description, with tags
|
||||
Bookmark.objects.all().delete()
|
||||
bookmark = self.setup_bookmark(
|
||||
description="", tags=[self.setup_tag(), self.setup_tag(), self.setup_tag()]
|
||||
)
|
||||
self.separate_bookmark_description_test(bookmark)
|
||||
|
||||
# with description, with tags
|
||||
Bookmark.objects.all().delete()
|
||||
bookmark = self.setup_bookmark(
|
||||
description="Test description",
|
||||
tags=[self.setup_tag(), self.setup_tag(), self.setup_tag()],
|
||||
)
|
||||
self.separate_bookmark_description_test(bookmark)
|
||||
|
||||
def test_bookmark_description_max_lines(self):
|
||||
self.setup_bookmark()
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list")
|
||||
style = bookmark_list["style"]
|
||||
self.assertIn("--ld-bookmark-description-max-lines:1;", style)
|
||||
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.bookmark_description_max_lines = 3
|
||||
profile.save()
|
||||
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list")
|
||||
style = bookmark_list["style"]
|
||||
self.assertIn("--ld-bookmark-description-max-lines:3;", style)
|
||||
|
||||
def test_should_respect_absolute_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(
|
||||
UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE
|
||||
@@ -351,9 +551,58 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_template()
|
||||
|
||||
self.assertViewLink(html, bookmark)
|
||||
self.assertBookmarkActions(html, bookmark)
|
||||
self.assertNoShareInfo(html, bookmark)
|
||||
|
||||
def test_hide_view_link(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.display_view_bookmark_action = False
|
||||
profile.save()
|
||||
|
||||
html = self.render_template()
|
||||
self.assertViewLinkCount(html, bookmark, count=0)
|
||||
self.assertEditLinkCount(html, bookmark, count=1)
|
||||
self.assertArchiveLinkCount(html, bookmark, count=1)
|
||||
self.assertDeleteLinkCount(html, bookmark, count=1)
|
||||
|
||||
def test_hide_edit_link(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.display_edit_bookmark_action = False
|
||||
profile.save()
|
||||
|
||||
html = self.render_template()
|
||||
self.assertViewLinkCount(html, bookmark, count=1)
|
||||
self.assertEditLinkCount(html, bookmark, count=0)
|
||||
self.assertArchiveLinkCount(html, bookmark, count=1)
|
||||
self.assertDeleteLinkCount(html, bookmark, count=1)
|
||||
|
||||
def test_hide_archive_link(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.display_archive_bookmark_action = False
|
||||
profile.save()
|
||||
|
||||
html = self.render_template()
|
||||
self.assertViewLinkCount(html, bookmark, count=1)
|
||||
self.assertEditLinkCount(html, bookmark, count=1)
|
||||
self.assertArchiveLinkCount(html, bookmark, count=0)
|
||||
self.assertDeleteLinkCount(html, bookmark, count=1)
|
||||
|
||||
def test_hide_remove_link(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.display_remove_bookmark_action = False
|
||||
profile.save()
|
||||
|
||||
html = self.render_template()
|
||||
self.assertViewLinkCount(html, bookmark, count=1)
|
||||
self.assertEditLinkCount(html, bookmark, count=1)
|
||||
self.assertArchiveLinkCount(html, bookmark, count=1)
|
||||
self.assertDeleteLinkCount(html, bookmark, count=0)
|
||||
|
||||
def test_show_share_info_for_non_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user(
|
||||
"otheruser", "otheruser@example.com", "password123"
|
||||
@@ -364,6 +613,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
html = self.render_template(context_type=contexts.SharedBookmarkListContext)
|
||||
|
||||
self.assertViewLink(html, bookmark, return_url=reverse("bookmarks:shared"))
|
||||
self.assertNoBookmarkActions(html, bookmark)
|
||||
self.assertShareInfo(html, bookmark)
|
||||
|
||||
@@ -539,9 +789,11 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_notes_are_hidden_initially_by_default(self):
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = collapse_whitespace(self.render_template())
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list.show-notes")
|
||||
|
||||
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
||||
self.assertIsNone(bookmark_list)
|
||||
|
||||
def test_notes_are_hidden_initially_with_permanent_notes_disabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
@@ -549,9 +801,11 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.save()
|
||||
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = collapse_whitespace(self.render_template())
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list.show-notes")
|
||||
|
||||
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
|
||||
self.assertIsNone(bookmark_list)
|
||||
|
||||
def test_notes_are_visible_initially_with_permanent_notes_enabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
@@ -559,11 +813,11 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
profile.save()
|
||||
|
||||
self.setup_bookmark(notes="Test note")
|
||||
html = collapse_whitespace(self.render_template())
|
||||
html = self.render_template()
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list.show-notes")
|
||||
|
||||
self.assertIn(
|
||||
'<ul class="bookmark-list show-notes" data-bookmarks-total="1">', html
|
||||
)
|
||||
self.assertIsNotNone(bookmark_list)
|
||||
|
||||
def test_toggle_notes_is_visible_by_default(self):
|
||||
self.setup_bookmark(notes="Test note")
|
||||
@@ -615,6 +869,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
self.assertWebArchiveLink(
|
||||
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_blank"
|
||||
)
|
||||
self.assertViewLink(html, bookmark, return_url=reverse("bookmarks:shared"))
|
||||
self.assertNoBookmarkActions(html, bookmark)
|
||||
self.assertShareInfo(html, bookmark)
|
||||
self.assertMarkAsReadButton(html, bookmark, count=0)
|
||||
|
@@ -23,37 +23,37 @@ class MetadataViewTestCase(TestCase):
|
||||
"src": "/static/logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/static/logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/static/logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/static/maskable-logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/static/maskable-logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/static/maskable-logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
],
|
||||
"shortcuts": [
|
||||
@@ -76,14 +76,14 @@ class MetadataViewTestCase(TestCase):
|
||||
{
|
||||
"name": "Shared",
|
||||
"url": "/bookmarks/shared",
|
||||
}
|
||||
},
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/static/linkding-screenshot.png",
|
||||
"type": "image/png",
|
||||
"sizes": "2158x1160",
|
||||
"form_factor": "wide"
|
||||
"form_factor": "wide",
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
@@ -94,8 +94,8 @@ class MetadataViewTestCase(TestCase):
|
||||
"url": "url",
|
||||
"text": "url",
|
||||
"title": "title",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
self.assertDictEqual(response_body, expected_body)
|
||||
|
||||
@@ -120,37 +120,37 @@ class MetadataViewTestCase(TestCase):
|
||||
"src": "/linkding/static/logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/linkding/static/logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/linkding/static/logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/linkding/static/maskable-logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/linkding/static/maskable-logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/linkding/static/maskable-logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
],
|
||||
"shortcuts": [
|
||||
@@ -173,14 +173,14 @@ class MetadataViewTestCase(TestCase):
|
||||
{
|
||||
"name": "Shared",
|
||||
"url": "/linkding/bookmarks/shared",
|
||||
}
|
||||
},
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/linkding/static/linkding-screenshot.png",
|
||||
"type": "image/png",
|
||||
"sizes": "2158x1160",
|
||||
"form_factor": "wide"
|
||||
"form_factor": "wide",
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
@@ -191,7 +191,7 @@ class MetadataViewTestCase(TestCase):
|
||||
"url": "url",
|
||||
"text": "url",
|
||||
"title": "title",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
self.assertDictEqual(response_body, expected_body)
|
||||
|
@@ -24,6 +24,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
form_data = {
|
||||
"theme": UserProfile.THEME_AUTO,
|
||||
"bookmark_date_display": UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
"bookmark_description_display": UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_INLINE,
|
||||
"bookmark_description_max_lines": 1,
|
||||
"bookmark_link_target": UserProfile.BOOKMARK_LINK_TARGET_BLANK,
|
||||
"web_archive_integration": UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
|
||||
"enable_sharing": False,
|
||||
@@ -31,6 +33,10 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
"enable_favicons": False,
|
||||
"tag_search": UserProfile.TAG_SEARCH_STRICT,
|
||||
"display_url": False,
|
||||
"display_view_bookmark_action": True,
|
||||
"display_edit_bookmark_action": True,
|
||||
"display_archive_bookmark_action": True,
|
||||
"display_remove_bookmark_action": True,
|
||||
"permanent_notes": False,
|
||||
"custom_css": "",
|
||||
}
|
||||
@@ -56,6 +62,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
"update_profile": "",
|
||||
"theme": UserProfile.THEME_DARK,
|
||||
"bookmark_date_display": UserProfile.BOOKMARK_DATE_DISPLAY_HIDDEN,
|
||||
"bookmark_description_display": UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_SEPARATE,
|
||||
"bookmark_description_max_lines": 3,
|
||||
"bookmark_link_target": UserProfile.BOOKMARK_LINK_TARGET_SELF,
|
||||
"web_archive_integration": UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
|
||||
"enable_sharing": True,
|
||||
@@ -63,6 +71,10 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
"enable_favicons": True,
|
||||
"tag_search": UserProfile.TAG_SEARCH_LAX,
|
||||
"display_url": True,
|
||||
"display_view_bookmark_action": False,
|
||||
"display_edit_bookmark_action": False,
|
||||
"display_archive_bookmark_action": False,
|
||||
"display_remove_bookmark_action": False,
|
||||
"permanent_notes": True,
|
||||
"custom_css": "body { background-color: #000; }",
|
||||
}
|
||||
@@ -76,6 +88,14 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(
|
||||
self.user.profile.bookmark_date_display, form_data["bookmark_date_display"]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.bookmark_description_display,
|
||||
form_data["bookmark_description_display"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.bookmark_description_max_lines,
|
||||
form_data["bookmark_description_max_lines"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.bookmark_link_target, form_data["bookmark_link_target"]
|
||||
)
|
||||
@@ -92,6 +112,22 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
)
|
||||
self.assertEqual(self.user.profile.tag_search, form_data["tag_search"])
|
||||
self.assertEqual(self.user.profile.display_url, form_data["display_url"])
|
||||
self.assertEqual(
|
||||
self.user.profile.display_view_bookmark_action,
|
||||
form_data["display_view_bookmark_action"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.display_edit_bookmark_action,
|
||||
form_data["display_edit_bookmark_action"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.display_archive_bookmark_action,
|
||||
form_data["display_archive_bookmark_action"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.display_remove_bookmark_action,
|
||||
form_data["display_remove_bookmark_action"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.profile.permanent_notes, form_data["permanent_notes"]
|
||||
)
|
||||
|
@@ -40,7 +40,7 @@ class ToastsViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
# Should render toasts container
|
||||
self.assertContains(response, '<div class="toasts">')
|
||||
# Should render two toasts
|
||||
self.assertContains(response, '<div class="toast">', count=2)
|
||||
self.assertContains(response, '<div class="toast d-flex">', count=2)
|
||||
|
||||
def test_should_not_render_acknowledged_toasts(self):
|
||||
self.create_toast(acknowledged=True)
|
||||
@@ -81,9 +81,9 @@ class ToastsViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_toast_content(self):
|
||||
toast = self.create_toast()
|
||||
expected_toast = f"""
|
||||
<div class="toast">
|
||||
<div class="toast d-flex">
|
||||
{toast.message}
|
||||
<button type="submit" name="toast" value="{toast.id}" class="btn btn-clear float-right"></button>
|
||||
<button type="submit" name="toast" value="{toast.id}" class="btn btn-clear"></button>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
@@ -34,6 +34,16 @@ urlpatterns = [
|
||||
path("bookmarks/new", views.bookmarks.new, name="new"),
|
||||
path("bookmarks/close", views.bookmarks.close, name="close"),
|
||||
path("bookmarks/<int:bookmark_id>/edit", views.bookmarks.edit, name="edit"),
|
||||
path(
|
||||
"bookmarks/<int:bookmark_id>/details",
|
||||
views.bookmarks.details,
|
||||
name="details",
|
||||
),
|
||||
path(
|
||||
"bookmarks/<int:bookmark_id>/details_modal",
|
||||
views.bookmarks.details_modal,
|
||||
name="details_modal",
|
||||
),
|
||||
# Partials
|
||||
path(
|
||||
"bookmarks/partials/bookmark-list/active",
|
||||
|
@@ -104,6 +104,59 @@ def search_action(request):
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
def _details(request, bookmark_id: int, template: str):
|
||||
try:
|
||||
bookmark = Bookmark.objects.get(pk=bookmark_id)
|
||||
except Bookmark.DoesNotExist:
|
||||
raise Http404("Bookmark does not exist")
|
||||
|
||||
is_owner = bookmark.owner == request.user
|
||||
is_shared = (
|
||||
request.user.is_authenticated
|
||||
and bookmark.shared
|
||||
and bookmark.owner.profile.enable_sharing
|
||||
)
|
||||
is_public_shared = bookmark.shared and bookmark.owner.profile.enable_public_sharing
|
||||
if not is_owner and not is_shared and not is_public_shared:
|
||||
raise Http404("Bookmark does not exist")
|
||||
|
||||
edit_return_url = get_safe_return_url(
|
||||
request.GET.get("return_url"), reverse("bookmarks:details", args=[bookmark_id])
|
||||
)
|
||||
delete_return_url = get_safe_return_url(
|
||||
request.GET.get("return_url"), reverse("bookmarks:index")
|
||||
)
|
||||
|
||||
# handles status actions form
|
||||
if request.method == "POST":
|
||||
if not is_owner:
|
||||
raise Http404("Bookmark does not exist")
|
||||
bookmark.is_archived = request.POST.get("is_archived") == "on"
|
||||
bookmark.unread = request.POST.get("unread") == "on"
|
||||
bookmark.shared = request.POST.get("shared") == "on"
|
||||
bookmark.save()
|
||||
|
||||
return HttpResponseRedirect(edit_return_url)
|
||||
|
||||
return render(
|
||||
request,
|
||||
template,
|
||||
{
|
||||
"bookmark": bookmark,
|
||||
"edit_return_url": edit_return_url,
|
||||
"delete_return_url": delete_return_url,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def details(request, bookmark_id: int):
|
||||
return _details(request, bookmark_id, "bookmarks/details.html")
|
||||
|
||||
|
||||
def details_modal(request, bookmark_id: int):
|
||||
return _details(request, bookmark_id, "bookmarks/details_modal.html")
|
||||
|
||||
|
||||
def convert_tag_string(tag_string: str):
|
||||
# Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
|
||||
# strings
|
||||
|
@@ -11,43 +11,45 @@ def manifest(request):
|
||||
"display": "standalone",
|
||||
"scope": "/" + settings.LD_CONTEXT_PATH,
|
||||
"theme_color": "#5856e0",
|
||||
"background_color": "#161822" if request.user_profile.theme == "dark" else "#ffffff",
|
||||
"background_color": (
|
||||
"#161822" if request.user_profile.theme == "dark" else "#ffffff"
|
||||
),
|
||||
"icons": [
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any"
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/maskable-logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/maskable-logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/maskable-logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "maskable"
|
||||
"purpose": "maskable",
|
||||
},
|
||||
],
|
||||
"shortcuts": [
|
||||
@@ -70,14 +72,16 @@ def manifest(request):
|
||||
{
|
||||
"name": "Shared",
|
||||
"url": "/" + settings.LD_CONTEXT_PATH + "bookmarks/shared",
|
||||
}
|
||||
},
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/" + settings.LD_CONTEXT_PATH + "static/linkding-screenshot.png",
|
||||
"src": "/"
|
||||
+ settings.LD_CONTEXT_PATH
|
||||
+ "static/linkding-screenshot.png",
|
||||
"type": "image/png",
|
||||
"sizes": "2158x1160",
|
||||
"form_factor": "wide"
|
||||
"form_factor": "wide",
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
@@ -88,8 +92,8 @@ def manifest(request):
|
||||
"url": "url",
|
||||
"text": "url",
|
||||
"title": "title",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return JsonResponse(response, status=200)
|
||||
|
@@ -96,7 +96,13 @@ class BookmarkListContext:
|
||||
)
|
||||
self.link_target = user_profile.bookmark_link_target
|
||||
self.date_display = user_profile.bookmark_date_display
|
||||
self.description_display = user_profile.bookmark_description_display
|
||||
self.description_max_lines = user_profile.bookmark_description_max_lines
|
||||
self.show_url = user_profile.display_url
|
||||
self.show_view_action = user_profile.display_view_bookmark_action
|
||||
self.show_edit_action = user_profile.display_edit_bookmark_action
|
||||
self.show_archive_action = user_profile.display_archive_bookmark_action
|
||||
self.show_remove_action = user_profile.display_remove_bookmark_action
|
||||
self.show_favicons = user_profile.enable_favicons
|
||||
self.show_notes = user_profile.permanent_notes
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.25.0",
|
||||
"version": "1.26.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
#
|
||||
asgiref==3.7.2
|
||||
# via django
|
||||
black==24.1.1
|
||||
black==24.3.0
|
||||
# via -r requirements.dev.in
|
||||
click==8.1.7
|
||||
# via black
|
||||
|
@@ -1 +1 @@
|
||||
1.25.0
|
||||
1.26.0
|
||||
|
Reference in New Issue
Block a user