mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 22:19:32 +02:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cf0cc32090 | ||
![]() |
1f2cf21585 | ||
![]() |
0dd05b9269 | ||
![]() |
5cd6d773db | ||
![]() |
d4c348cc5a | ||
![]() |
791a5c73ca | ||
![]() |
ebed0c050d | ||
![]() |
f4dd2b53b5 | ||
![]() |
b53fe09c39 | ||
![]() |
ff88e726cc | ||
![]() |
52400feacf | ||
![]() |
c93709b549 | ||
![]() |
ba904ed191 | ||
![]() |
d1f81fee0e | ||
![]() |
7b405c054d | ||
![]() |
23ad52f75d | ||
![]() |
c3a2305a5f | ||
![]() |
d4006026db | ||
![]() |
70bdf88791 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -192,7 +192,7 @@ typings/
|
||||
# Database file
|
||||
/data
|
||||
# ublock + chromium
|
||||
/uBlock0.chromium
|
||||
/uBOLite.chromium.mv3
|
||||
/chromium-profile
|
||||
# direnv
|
||||
/.direnv
|
||||
|
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## (23/09/2024)
|
||||
|
||||
### What's Changed
|
||||
* Add configuration options for pagination by @sissbruecker in https://github.com/sissbruecker/linkding/pull/835
|
||||
* Show placeholder if there is no preview image by @sissbruecker in https://github.com/sissbruecker/linkding/pull/842
|
||||
* Allow bookmarks to have empty title and description by @sissbruecker in https://github.com/sissbruecker/linkding/pull/843
|
||||
* Add clear buttons in bookmark form by @sissbruecker in https://github.com/sissbruecker/linkding/pull/846
|
||||
* Add basic fail2ban support by @sissbruecker in https://github.com/sissbruecker/linkding/pull/847
|
||||
* Add documentation website by @sissbruecker in https://github.com/sissbruecker/linkding/pull/833
|
||||
* Add go-linkding to community projects by @piero-vic in https://github.com/sissbruecker/linkding/pull/836
|
||||
* Fix a broken link to options documentation by @zbrox in https://github.com/sissbruecker/linkding/pull/844
|
||||
* Use HTTPS repository link for devcontainer by @voltagex in https://github.com/sissbruecker/linkding/pull/837
|
||||
* Bump requests version to 3.23.3 by @voltagex in https://github.com/sissbruecker/linkding/pull/839
|
||||
* Bump path-to-regexp and astro in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/840
|
||||
* Bump dependencies by @sissbruecker in https://github.com/sissbruecker/linkding/pull/841
|
||||
|
||||
### New Contributors
|
||||
* @piero-vic made their first contribution in https://github.com/sissbruecker/linkding/pull/836
|
||||
* @voltagex made their first contribution in https://github.com/sissbruecker/linkding/pull/839
|
||||
* @zbrox made their first contribution in https://github.com/sissbruecker/linkding/pull/844
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.34.0...v1.35.0
|
||||
|
||||
---
|
||||
|
||||
## v1.34.0 (16/09/2024)
|
||||
|
||||
### What's Changed
|
||||
|
@@ -13,6 +13,29 @@
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(8.26174,0,0,8.26174,-5762.21,-2037.46)">
|
||||
<text x="770.835px" y="299.13px" style="font-family:'HelveticaNeue', 'Helvetica Neue';font-size:50px;fill:rgb(94,94,219);">l<tspan x="782.685px 794.535px 823.085px 849.785px 880.185px 892.035px 920.585px " y="299.13px 299.13px 299.13px 299.13px 299.13px 299.13px 299.13px ">inkding</tspan></text>
|
||||
<g transform="matrix(50,0,0,50,770.835,299.13)">
|
||||
<rect x="0.064" y="-0.716" width="0.088" height="0.716" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,782.693,299.13)">
|
||||
<path d="M0.066,-0.615L0.066,-0.716L0.154,-0.716L0.154,-0.615L0.066,-0.615ZM0.066,-0L0.066,-0.519L0.154,-0.519L0.154,-0L0.066,-0Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,794.552,299.13)">
|
||||
<path d="M0.066,-0L0.066,-0.519L0.145,-0.519L0.145,-0.445C0.183,-0.502 0.238,-0.53 0.31,-0.53C0.341,-0.53 0.37,-0.525 0.396,-0.513C0.422,-0.502 0.442,-0.487 0.455,-0.469C0.468,-0.451 0.477,-0.429 0.482,-0.404C0.486,-0.388 0.487,-0.36 0.487,-0.319L0.487,-0L0.399,-0L0.399,-0.315C0.399,-0.351 0.396,-0.378 0.389,-0.396C0.382,-0.413 0.37,-0.428 0.353,-0.438C0.335,-0.449 0.315,-0.454 0.292,-0.454C0.254,-0.454 0.222,-0.442 0.194,-0.418C0.167,-0.395 0.154,-0.35 0.154,-0.283L0.154,-0L0.066,-0Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,823.109,299.13)">
|
||||
<path d="M0.066,-0L0.066,-0.716L0.154,-0.716L0.154,-0.308L0.362,-0.519L0.476,-0.519L0.278,-0.326L0.496,-0L0.388,-0L0.216,-0.265L0.154,-0.206L0.154,-0L0.066,-0Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,848.859,299.13)">
|
||||
<path d="M0.402,-0L0.402,-0.065C0.369,-0.014 0.321,0.012 0.257,0.012C0.216,0.012 0.178,0 0.143,-0.022C0.109,-0.045 0.082,-0.077 0.063,-0.118C0.044,-0.159 0.034,-0.206 0.034,-0.259C0.034,-0.311 0.043,-0.357 0.06,-0.399C0.077,-0.442 0.103,-0.474 0.138,-0.497C0.172,-0.519 0.211,-0.53 0.253,-0.53C0.285,-0.53 0.313,-0.524 0.337,-0.51C0.361,-0.497 0.381,-0.48 0.396,-0.459L0.396,-0.716L0.484,-0.716L0.484,-0L0.402,-0ZM0.125,-0.259C0.125,-0.192 0.139,-0.143 0.167,-0.11C0.194,-0.077 0.228,-0.061 0.266,-0.061C0.304,-0.061 0.337,-0.076 0.363,-0.107C0.39,-0.139 0.404,-0.187 0.404,-0.251C0.404,-0.322 0.39,-0.375 0.363,-0.408C0.335,-0.441 0.302,-0.458 0.262,-0.458C0.223,-0.458 0.19,-0.442 0.164,-0.41C0.138,-0.378 0.125,-0.327 0.125,-0.259Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,877.417,299.13)">
|
||||
<path d="M0.066,-0.615L0.066,-0.716L0.154,-0.716L0.154,-0.615L0.066,-0.615ZM0.066,-0L0.066,-0.519L0.154,-0.519L0.154,-0L0.066,-0Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,889.275,299.13)">
|
||||
<path d="M0.066,-0L0.066,-0.519L0.145,-0.519L0.145,-0.445C0.183,-0.502 0.238,-0.53 0.31,-0.53C0.341,-0.53 0.37,-0.525 0.396,-0.513C0.422,-0.502 0.442,-0.487 0.455,-0.469C0.468,-0.451 0.477,-0.429 0.482,-0.404C0.486,-0.388 0.487,-0.36 0.487,-0.319L0.487,-0L0.399,-0L0.399,-0.315C0.399,-0.351 0.396,-0.378 0.389,-0.396C0.382,-0.413 0.37,-0.428 0.353,-0.438C0.335,-0.449 0.315,-0.454 0.292,-0.454C0.254,-0.454 0.222,-0.442 0.194,-0.418C0.167,-0.395 0.154,-0.35 0.154,-0.283L0.154,-0L0.066,-0Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(50,0,0,50,917.833,299.13)">
|
||||
<path d="M0.05,0.043L0.135,0.056C0.139,0.082 0.149,0.101 0.165,0.113C0.187,0.13 0.217,0.138 0.254,0.138C0.295,0.138 0.326,0.13 0.349,0.113C0.371,0.097 0.386,0.074 0.394,0.045C0.398,0.027 0.4,-0.011 0.4,-0.068C0.361,-0.023 0.314,-0 0.256,-0C0.185,-0 0.13,-0.026 0.091,-0.077C0.052,-0.129 0.032,-0.19 0.032,-0.262C0.032,-0.312 0.041,-0.357 0.059,-0.399C0.077,-0.441 0.103,-0.473 0.137,-0.496C0.171,-0.519 0.211,-0.53 0.257,-0.53C0.318,-0.53 0.368,-0.506 0.408,-0.456L0.408,-0.519L0.489,-0.519L0.489,-0.07C0.489,0.01 0.481,0.068 0.464,0.101C0.448,0.135 0.422,0.162 0.386,0.181C0.351,0.201 0.307,0.21 0.255,0.21C0.193,0.21 0.143,0.196 0.105,0.168C0.067,0.141 0.049,0.099 0.05,0.043ZM0.123,-0.269C0.123,-0.201 0.136,-0.151 0.163,-0.12C0.19,-0.088 0.224,-0.073 0.265,-0.073C0.305,-0.073 0.339,-0.088 0.366,-0.119C0.394,-0.15 0.407,-0.199 0.407,-0.266C0.407,-0.329 0.393,-0.377 0.365,-0.409C0.337,-0.441 0.303,-0.458 0.263,-0.458C0.224,-0.458 0.191,-0.442 0.164,-0.41C0.136,-0.378 0.123,-0.331 0.123,-0.269Z" style="fill:rgb(94,94,219);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -49,6 +49,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
"web_archive_snapshot_url",
|
||||
"favicon_url",
|
||||
"preview_image_url",
|
||||
"tag_names",
|
||||
"date_added",
|
||||
"date_modified",
|
||||
"website_title",
|
||||
@@ -56,15 +57,9 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
list_serializer_class = BookmarkListSerializer
|
||||
|
||||
# Override optional char fields to provide default value
|
||||
title = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
description = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
notes = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
is_archived = serializers.BooleanField(required=False, default=False)
|
||||
unread = serializers.BooleanField(required=False, default=False)
|
||||
shared = serializers.BooleanField(required=False, default=False)
|
||||
# Override readonly tag_names property to allow passing a list of tag names to create/update
|
||||
tag_names = TagListField(required=False, default=[])
|
||||
# Custom tag_names field to allow passing a list of tag names to create/update
|
||||
tag_names = TagListField(required=False)
|
||||
# Custom fields to return URLs for favicon and preview image
|
||||
favicon_url = serializers.SerializerMethodField()
|
||||
preview_image_url = serializers.SerializerMethodField()
|
||||
# Add dummy website title and description fields for backwards compatibility but keep them empty
|
||||
@@ -94,15 +89,9 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
return None
|
||||
|
||||
def create(self, validated_data):
|
||||
bookmark = Bookmark()
|
||||
bookmark.url = validated_data["url"]
|
||||
bookmark.title = validated_data["title"]
|
||||
bookmark.description = validated_data["description"]
|
||||
bookmark.notes = validated_data["notes"]
|
||||
bookmark.is_archived = validated_data["is_archived"]
|
||||
bookmark.unread = validated_data["unread"]
|
||||
bookmark.shared = validated_data["shared"]
|
||||
tag_string = build_tag_string(validated_data["tag_names"])
|
||||
tag_names = validated_data.pop("tag_names", [])
|
||||
tag_string = build_tag_string(tag_names)
|
||||
bookmark = Bookmark(**validated_data)
|
||||
|
||||
saved_bookmark = create_bookmark(bookmark, tag_string, self.context["user"])
|
||||
# Unless scraping is explicitly disabled, enhance bookmark with website
|
||||
@@ -113,18 +102,33 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
return saved_bookmark
|
||||
|
||||
def update(self, instance: Bookmark, validated_data):
|
||||
# Update fields if they were provided in the payload
|
||||
for key in ["url", "title", "description", "notes", "unread", "shared"]:
|
||||
if key in validated_data:
|
||||
setattr(instance, key, validated_data[key])
|
||||
tag_names = validated_data.pop("tag_names", instance.tag_names)
|
||||
tag_string = build_tag_string(tag_names)
|
||||
|
||||
# Use tag string from payload, or use bookmark's current tags as fallback
|
||||
tag_string = build_tag_string(instance.tag_names)
|
||||
if "tag_names" in validated_data:
|
||||
tag_string = build_tag_string(validated_data["tag_names"])
|
||||
for field_name, field in self.fields.items():
|
||||
if not field.read_only and field_name in validated_data:
|
||||
setattr(instance, field_name, validated_data[field_name])
|
||||
|
||||
return update_bookmark(instance, tag_string, self.context["user"])
|
||||
|
||||
def validate(self, attrs):
|
||||
# When creating a bookmark, the service logic prevents duplicate URLs by
|
||||
# updating the existing bookmark instead. When editing a bookmark,
|
||||
# there is no assumption that it would update a different bookmark if
|
||||
# the URL is a duplicate, so raise a validation error in that case.
|
||||
if self.instance and "url" in attrs:
|
||||
is_duplicate = (
|
||||
Bookmark.objects.filter(owner=self.instance.owner, url=attrs["url"])
|
||||
.exclude(pk=self.instance.pk)
|
||||
.exists()
|
||||
)
|
||||
if is_duplicate:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "A bookmark with this URL already exists."}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
18
bookmarks/migrations/0042_userprofile_custom_css_hash.py
Normal file
18
bookmarks/migrations/0042_userprofile_custom_css_hash.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-09-28 08:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0041_merge_metadata"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="custom_css_hash",
|
||||
field=models.CharField(blank=True, max_length=32),
|
||||
),
|
||||
]
|
@@ -1,4 +1,5 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from typing import List
|
||||
@@ -168,6 +169,24 @@ class BookmarkForm(forms.ModelForm):
|
||||
self.instance and self.instance.notes
|
||||
)
|
||||
|
||||
def clean_url(self):
|
||||
# When creating a bookmark, the service logic prevents duplicate URLs by
|
||||
# updating the existing bookmark instead, which is also communicated in
|
||||
# the form's UI. When editing a bookmark, there is no assumption that
|
||||
# it would update a different bookmark if the URL is a duplicate, so
|
||||
# raise a validation error in that case.
|
||||
url = self.cleaned_data["url"]
|
||||
if self.instance.pk:
|
||||
is_duplicate = (
|
||||
Bookmark.objects.filter(owner=self.instance.owner, url=url)
|
||||
.exclude(pk=self.instance.pk)
|
||||
.exists()
|
||||
)
|
||||
if is_duplicate:
|
||||
raise forms.ValidationError("A bookmark with this URL already exists.")
|
||||
|
||||
return url
|
||||
|
||||
|
||||
class BookmarkSearch:
|
||||
SORT_ADDED_ASC = "added_asc"
|
||||
@@ -412,6 +431,7 @@ class UserProfile(models.Model):
|
||||
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)
|
||||
custom_css_hash = models.CharField(blank=True, null=False, max_length=32)
|
||||
auto_tagging_rules = models.TextField(blank=True, null=False)
|
||||
search_preferences = models.JSONField(default=dict, null=False)
|
||||
enable_automatic_html_snapshots = models.BooleanField(default=True, null=False)
|
||||
@@ -421,6 +441,15 @@ class UserProfile(models.Model):
|
||||
)
|
||||
sticky_pagination = models.BooleanField(default=False, null=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.custom_css:
|
||||
self.custom_css_hash = hashlib.md5(
|
||||
self.custom_css.encode("utf-8")
|
||||
).hexdigest()
|
||||
else:
|
||||
self.custom_css_hash = ""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@@ -7,6 +7,9 @@ def get_tags(script: str, url: str):
|
||||
parsed_url = urlparse(url.lower())
|
||||
result = set()
|
||||
|
||||
if not parsed_url.hostname:
|
||||
return result
|
||||
|
||||
for line in script.lower().split("\n"):
|
||||
if "#" in line:
|
||||
i = line.index("#")
|
||||
|
@@ -65,7 +65,6 @@ def update_bookmark(bookmark: Bookmark, tag_string, current_user: User):
|
||||
if has_url_changed:
|
||||
# Update web archive snapshot, if URL changed
|
||||
tasks.create_web_archive_snapshot(current_user, bookmark, True)
|
||||
bookmark.save()
|
||||
|
||||
return bookmark
|
||||
|
||||
|
@@ -40,9 +40,10 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
|
||||
toread = "1" if bookmark.unread else "0"
|
||||
private = "0" if bookmark.shared else "1"
|
||||
added = int(bookmark.date_added.timestamp())
|
||||
modified = int(bookmark.date_modified.timestamp())
|
||||
|
||||
doc.append(
|
||||
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
|
||||
f'<DT><A HREF="{url}" ADD_DATE="{added}" LAST_MODIFIED="{modified}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
|
||||
)
|
||||
|
||||
if desc:
|
||||
|
@@ -231,7 +231,10 @@ def _copy_bookmark_data(
|
||||
bookmark.date_added = parse_timestamp(netscape_bookmark.date_added)
|
||||
else:
|
||||
bookmark.date_added = timezone.now()
|
||||
bookmark.date_modified = bookmark.date_added
|
||||
if netscape_bookmark.date_modified:
|
||||
bookmark.date_modified = parse_timestamp(netscape_bookmark.date_modified)
|
||||
else:
|
||||
bookmark.date_modified = bookmark.date_added
|
||||
bookmark.unread = netscape_bookmark.to_read
|
||||
if netscape_bookmark.title:
|
||||
bookmark.title = netscape_bookmark.title
|
||||
|
@@ -12,6 +12,7 @@ class NetscapeBookmark:
|
||||
description: str
|
||||
notes: str
|
||||
date_added: str
|
||||
date_modified: str
|
||||
tag_names: List[str]
|
||||
to_read: bool
|
||||
private: bool
|
||||
@@ -27,6 +28,7 @@ class BookmarkParser(HTMLParser):
|
||||
self.bookmark = None
|
||||
self.href = ""
|
||||
self.add_date = ""
|
||||
self.last_modified = ""
|
||||
self.tags = ""
|
||||
self.title = ""
|
||||
self.description = ""
|
||||
@@ -72,6 +74,7 @@ class BookmarkParser(HTMLParser):
|
||||
description="",
|
||||
notes="",
|
||||
date_added=self.add_date,
|
||||
date_modified=self.last_modified,
|
||||
tag_names=tag_names,
|
||||
to_read=self.toread == "1",
|
||||
# Mark as private by default, also when attribute is not specified
|
||||
@@ -97,6 +100,7 @@ class BookmarkParser(HTMLParser):
|
||||
self.bookmark = None
|
||||
self.href = ""
|
||||
self.add_date = ""
|
||||
self.last_modified = ""
|
||||
self.tags = ""
|
||||
self.title = ""
|
||||
self.description = ""
|
||||
|
@@ -28,7 +28,7 @@
|
||||
}
|
||||
|
||||
& .preview-image {
|
||||
margin: var(--unit-4 0);
|
||||
margin: var(--unit-4) 0;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
|
@@ -141,3 +141,10 @@
|
||||
--bookmark-actions-weight: 400;
|
||||
--bulk-actions-bg-color: var(--contrast-5);
|
||||
}
|
||||
|
||||
/* Try to force dark color scheme for all native elements (e.g. upload button
|
||||
in file inputs, native select dropdown). For the select dropdown some browsers
|
||||
ignore this and use whatever users have configured in their system settings. */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
@@ -197,6 +197,16 @@ textarea.form-input {
|
||||
no-repeat right 0.35rem center / 0.4rem 0.5rem;
|
||||
padding-right: calc(var(--control-icon-size) + var(--control-padding-x));
|
||||
}
|
||||
|
||||
/* Options */
|
||||
& option {
|
||||
/* On Windows with Chrome / Edge, options seems to use the same
|
||||
background color as the select. However for the dark theme the
|
||||
background is a semi-transparent white, resulting in an opaque white
|
||||
background for the dropdown. Use the modal background color to force
|
||||
a dark background instead. */
|
||||
background: var(--modal-container-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Form element: Checkbox and Radio */
|
||||
|
@@ -33,7 +33,7 @@
|
||||
cursor: default;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">
|
||||
{% endif %}
|
||||
{% if request.user_profile.custom_css %}
|
||||
<style>{{ request.user_profile.custom_css }}</style>
|
||||
<link href="{% url 'bookmarks:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}" rel="stylesheet" type="text/css"/>
|
||||
{% endif %}
|
||||
<meta name="turbo-cache-control" content="no-preview">
|
||||
{% if not request.global_settings.enable_link_prefetch %}
|
||||
|
@@ -45,6 +45,7 @@ class BookmarkFactoryMixin:
|
||||
favicon_file: str = "",
|
||||
preview_image_file: str = "",
|
||||
added: datetime = None,
|
||||
modified: datetime = None,
|
||||
):
|
||||
if title is None:
|
||||
title = get_random_string(length=32)
|
||||
@@ -57,13 +58,15 @@ class BookmarkFactoryMixin:
|
||||
url = "https://example.com/" + unique_id
|
||||
if added is None:
|
||||
added = timezone.now()
|
||||
if modified is None:
|
||||
modified = timezone.now()
|
||||
bookmark = Bookmark(
|
||||
url=url,
|
||||
title=title,
|
||||
description=description,
|
||||
notes=notes,
|
||||
date_added=added,
|
||||
date_modified=timezone.now(),
|
||||
date_modified=modified,
|
||||
owner=user,
|
||||
is_archived=is_archived,
|
||||
unread=unread,
|
||||
@@ -320,6 +323,7 @@ class BookmarkHtmlTag:
|
||||
title: str = "",
|
||||
description: str = "",
|
||||
add_date: str = "",
|
||||
last_modified: str = "",
|
||||
tags: str = "",
|
||||
to_read: bool = False,
|
||||
private: bool = True,
|
||||
@@ -328,6 +332,7 @@ class BookmarkHtmlTag:
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.add_date = add_date
|
||||
self.last_modified = last_modified
|
||||
self.tags = tags
|
||||
self.to_read = to_read
|
||||
self.private = private
|
||||
@@ -339,6 +344,7 @@ class ImportTestMixin:
|
||||
<DT>
|
||||
<A {f'HREF="{tag.href}"' if tag.href else ''}
|
||||
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
|
||||
{f'LAST_MODIFIED="{tag.last_modified}"' if tag.last_modified else ''}
|
||||
{f'TAGS="{tag.tags}"' if tag.tags else ''}
|
||||
TOREAD="{1 if tag.to_read else 0}"
|
||||
PRIVATE="{1 if tag.private else 0}">
|
||||
|
@@ -14,6 +14,20 @@ class AutoTaggingTestCase(TestCase):
|
||||
|
||||
self.assertEqual(tags, {"example"})
|
||||
|
||||
def test_auto_tag_by_domain_handles_invalid_urls(self):
|
||||
script = """
|
||||
example.com example
|
||||
test.com test
|
||||
"""
|
||||
|
||||
url = "https://"
|
||||
tags = auto_tagging.get_tags(script, url)
|
||||
self.assertEqual(tags, set([]))
|
||||
|
||||
url = "example.com"
|
||||
tags = auto_tagging.get_tags(script, url)
|
||||
self.assertEqual(tags, set([]))
|
||||
|
||||
def test_auto_tag_by_domain_works_with_port(self):
|
||||
script = """
|
||||
example.com example
|
||||
|
@@ -26,6 +26,11 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
||||
def test_should_render_successfully(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_should_edit_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data({"id": bookmark.id})
|
||||
@@ -46,6 +51,14 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(tags[0].name, "editedtag1")
|
||||
self.assertEqual(tags[1].name, "editedtag2")
|
||||
|
||||
def test_should_return_422_with_invalid_form(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data({"id": bookmark.id, "url": ""})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:edit", args=[bookmark.id]), form_data
|
||||
)
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_should_edit_unread_state(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
@@ -128,6 +141,40 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
html,
|
||||
)
|
||||
|
||||
def test_should_prevent_duplicate_urls(self):
|
||||
edited_bookmark = self.setup_bookmark(url="http://example.com/edited")
|
||||
existing_bookmark = self.setup_bookmark(url="http://example.com/existing")
|
||||
other_user_bookmark = self.setup_bookmark(
|
||||
url="http://example.com/other-user", user=User.objects.create_user("other")
|
||||
)
|
||||
|
||||
# if the URL isn't modified it's not a duplicate
|
||||
form_data = self.create_form_data({"url": edited_bookmark.url})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:edit", args=[edited_bookmark.id]), form_data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# if the URL is already bookmarked by another user, it's not a duplicate
|
||||
form_data = self.create_form_data({"url": other_user_bookmark.url})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:edit", args=[edited_bookmark.id]), form_data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# if the URL is already bookmarked by the same user, it's a duplicate
|
||||
form_data = self.create_form_data({"url": existing_bookmark.url})
|
||||
response = self.client.post(
|
||||
reverse("bookmarks:edit", args=[edited_bookmark.id]), form_data
|
||||
)
|
||||
self.assertEqual(response.status_code, 422)
|
||||
self.assertInHTML(
|
||||
"<li>A bookmark with this URL already exists.</li>",
|
||||
response.content.decode(),
|
||||
)
|
||||
edited_bookmark.refresh_from_db()
|
||||
self.assertNotEqual(edited_bookmark.url, existing_bookmark.url)
|
||||
|
||||
def test_should_redirect_to_return_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data()
|
||||
|
@@ -46,6 +46,11 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(tags[0].name, "tag1")
|
||||
self.assertEqual(tags[1].name, "tag2")
|
||||
|
||||
def test_should_return_422_with_invalid_form(self):
|
||||
form_data = self.create_form_data({"url": ""})
|
||||
response = self.client.post(reverse("bookmarks:new"), form_data)
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_should_create_new_unread_bookmark(self):
|
||||
form_data = self.create_form_data({"unread": True})
|
||||
|
||||
|
@@ -480,7 +480,21 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
self.authenticate()
|
||||
|
||||
data = {"url": "https://example.com/"}
|
||||
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
|
||||
self.post(
|
||||
reverse("bookmarks:bookmark-list") + "?disable_scraping",
|
||||
data,
|
||||
status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
bookmark = Bookmark.objects.get(url=data["url"])
|
||||
self.assertEqual(data["url"], bookmark.url)
|
||||
self.assertEqual("", bookmark.title)
|
||||
self.assertEqual("", bookmark.description)
|
||||
self.assertEqual("", bookmark.notes)
|
||||
self.assertFalse(bookmark.is_archived)
|
||||
self.assertFalse(bookmark.unread)
|
||||
self.assertFalse(bookmark.shared)
|
||||
self.assertBookmarkListEqual([], bookmark.tag_names)
|
||||
|
||||
def test_create_archived_bookmark(self):
|
||||
self.authenticate()
|
||||
@@ -586,6 +600,28 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
|
||||
self.assertEqual(updated_bookmark.url, data["url"])
|
||||
|
||||
def test_update_bookmark_ignores_readonly_fields(self):
|
||||
self.authenticate()
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
data = {
|
||||
"url": "https://example.com/updated",
|
||||
"web_archive_snapshot_url": "test",
|
||||
"website_title": "test",
|
||||
"website_description": "test",
|
||||
}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
|
||||
self.assertEqual(data["url"], updated_bookmark.url)
|
||||
self.assertNotEqual(
|
||||
data["web_archive_snapshot_url"], updated_bookmark.web_archive_snapshot_url
|
||||
)
|
||||
self.assertNotEqual(data["website_title"], updated_bookmark.website_title)
|
||||
self.assertNotEqual(
|
||||
data["website_description"], updated_bookmark.website_description
|
||||
)
|
||||
|
||||
def test_update_bookmark_fails_without_required_fields(self):
|
||||
self.authenticate()
|
||||
bookmark = self.setup_bookmark()
|
||||
@@ -594,19 +630,24 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_update_bookmark_with_minimal_payload_clears_all_fields(self):
|
||||
def test_update_bookmark_with_minimal_payload_does_not_modify_bookmark(self):
|
||||
self.authenticate()
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark = self.setup_bookmark(
|
||||
is_archived=True, unread=True, shared=True, tags=[self.setup_tag()]
|
||||
)
|
||||
|
||||
data = {"url": "https://example.com/"}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
|
||||
self.assertEqual(updated_bookmark.url, data["url"])
|
||||
self.assertEqual(updated_bookmark.title, "")
|
||||
self.assertEqual(updated_bookmark.description, "")
|
||||
self.assertEqual(updated_bookmark.notes, "")
|
||||
self.assertEqual(updated_bookmark.tag_names, [])
|
||||
self.assertEqual(updated_bookmark.title, bookmark.title)
|
||||
self.assertEqual(updated_bookmark.description, bookmark.description)
|
||||
self.assertEqual(updated_bookmark.notes, bookmark.notes)
|
||||
self.assertEqual(updated_bookmark.is_archived, bookmark.is_archived)
|
||||
self.assertEqual(updated_bookmark.unread, bookmark.unread)
|
||||
self.assertEqual(updated_bookmark.shared, bookmark.shared)
|
||||
self.assertListEqual(updated_bookmark.tag_names, bookmark.tag_names)
|
||||
|
||||
def test_update_bookmark_unread_flag(self):
|
||||
self.authenticate()
|
||||
@@ -644,6 +685,29 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
|
||||
self.assertCountEqual(updated_bookmark.tags.all(), [tag1, tag2])
|
||||
|
||||
def test_update_bookmark_should_prevent_duplicate_urls(self):
|
||||
self.authenticate()
|
||||
edited_bookmark = self.setup_bookmark(url="https://example.com/edited")
|
||||
existing_bookmark = self.setup_bookmark(url="https://example.com/existing")
|
||||
other_user_bookmark = self.setup_bookmark(
|
||||
url="https://example.com/other", user=self.setup_user()
|
||||
)
|
||||
|
||||
# if the URL isn't modified it's not a duplicate
|
||||
data = {"url": edited_bookmark.url}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[edited_bookmark.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
|
||||
# if the URL is already bookmarked by another user, it's not a duplicate
|
||||
data = {"url": other_user_bookmark.url}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[edited_bookmark.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
|
||||
# if the URL is already bookmarked by the same user, it's a duplicate
|
||||
data = {"url": existing_bookmark.url}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[edited_bookmark.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_patch_bookmark(self):
|
||||
self.authenticate()
|
||||
bookmark = self.setup_bookmark()
|
||||
@@ -703,16 +767,42 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
tag_names = [tag.name for tag in bookmark.tags.all()]
|
||||
self.assertListEqual(tag_names, ["updated-tag-1", "updated-tag-2"])
|
||||
|
||||
def test_patch_with_empty_payload_does_not_modify_bookmark(self):
|
||||
def test_patch_ignores_readonly_fields(self):
|
||||
self.authenticate()
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
data = {
|
||||
"web_archive_snapshot_url": "test",
|
||||
"website_title": "test",
|
||||
"website_description": "test",
|
||||
}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
|
||||
self.assertNotEqual(
|
||||
data["web_archive_snapshot_url"], updated_bookmark.web_archive_snapshot_url
|
||||
)
|
||||
self.assertNotEqual(data["website_title"], updated_bookmark.website_title)
|
||||
self.assertNotEqual(
|
||||
data["website_description"], updated_bookmark.website_description
|
||||
)
|
||||
|
||||
def test_patch_with_empty_payload_does_not_modify_bookmark(self):
|
||||
self.authenticate()
|
||||
bookmark = self.setup_bookmark(
|
||||
is_archived=True, unread=True, shared=True, tags=[self.setup_tag()]
|
||||
)
|
||||
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
|
||||
self.assertEqual(updated_bookmark.url, bookmark.url)
|
||||
self.assertEqual(updated_bookmark.title, bookmark.title)
|
||||
self.assertEqual(updated_bookmark.description, bookmark.description)
|
||||
self.assertEqual(updated_bookmark.notes, bookmark.notes)
|
||||
self.assertEqual(updated_bookmark.is_archived, bookmark.is_archived)
|
||||
self.assertEqual(updated_bookmark.unread, bookmark.unread)
|
||||
self.assertEqual(updated_bookmark.shared, bookmark.shared)
|
||||
self.assertListEqual(updated_bookmark.tag_names, bookmark.tag_names)
|
||||
|
||||
def test_patch_bookmark_adds_tags_from_auto_tagging(self):
|
||||
@@ -919,6 +1009,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
{url: "https://example.com/"},
|
||||
expected_status_code=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
self.patch(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse(
|
||||
"bookmarks:bookmark-detail", args=[inaccessible_shared_bookmark.id]
|
||||
@@ -928,6 +1019,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
{url: "https://example.com/"},
|
||||
expected_status_code=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
self.patch(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse("bookmarks:bookmark-detail", args=[inaccessible_bookmark.id])
|
||||
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
@@ -87,6 +87,16 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
self.authenticate()
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
|
||||
def test_update_bookmark_only_updates_own_bookmarks(self):
|
||||
self.authenticate()
|
||||
|
||||
other_user = self.setup_user()
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
data = {"url": "https://example.com/"}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
||||
self.put(url, data, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_patch_bookmark_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
data = {"url": "https://example.com"}
|
||||
@@ -97,6 +107,16 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
|
||||
self.authenticate()
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
|
||||
def test_patch_bookmark_only_updates_own_bookmarks(self):
|
||||
self.authenticate()
|
||||
|
||||
other_user = self.setup_user()
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
data = {"url": "https://example.com"}
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
||||
self.patch(url, data, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_delete_bookmark_requires_authentication(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
|
||||
|
@@ -1,21 +0,0 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class CustomCssTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def setUp(self):
|
||||
self.client.force_login(self.get_or_create_test_user())
|
||||
|
||||
def test_does_not_render_custom_style_tag_by_default(self):
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
self.assertNotContains(response, "<style>")
|
||||
|
||||
def test_renders_custom_style_tag_if_user_has_custom_css(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.custom_css = "body { background-color: red; }"
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
self.assertContains(response, "<style>body { background-color: red; }</style>")
|
28
bookmarks/tests/test_custom_css_view.py
Normal file
28
bookmarks/tests/test_custom_css_view.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class CustomCssViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def test_with_empty_css(self):
|
||||
response = self.client.get(reverse("bookmarks:custom_css"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "text/css")
|
||||
self.assertEqual(response.headers["Cache-Control"], "public, max-age=2592000")
|
||||
self.assertEqual(response.content.decode(), "")
|
||||
|
||||
def test_with_custom_css(self):
|
||||
css = "body { background-color: red; }"
|
||||
self.user.profile.custom_css = css
|
||||
self.user.profile.save()
|
||||
|
||||
response = self.client.get(reverse("bookmarks:custom_css"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "text/css")
|
||||
self.assertEqual(response.headers["Cache-Control"], "public, max-age=2592000")
|
||||
self.assertEqual(response.content.decode(), css)
|
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from bookmarks.services import exporter
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
@@ -7,20 +8,19 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
class ExporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_export_bookmarks(self):
|
||||
added = timezone.now()
|
||||
timestamp = int(added.timestamp())
|
||||
|
||||
bookmarks = [
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/1",
|
||||
title="Title 1",
|
||||
added=added,
|
||||
added=datetime.fromtimestamp(1, timezone.utc),
|
||||
modified=datetime.fromtimestamp(11, timezone.utc),
|
||||
description="Example description",
|
||||
),
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/2",
|
||||
title="Title 2",
|
||||
added=added,
|
||||
added=datetime.fromtimestamp(2, timezone.utc),
|
||||
modified=datetime.fromtimestamp(22, timezone.utc),
|
||||
tags=[
|
||||
self.setup_tag(name="tag1"),
|
||||
self.setup_tag(name="tag2"),
|
||||
@@ -28,15 +28,24 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
],
|
||||
),
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/3", title="Title 3", added=added, unread=True
|
||||
url="https://example.com/3",
|
||||
title="Title 3",
|
||||
added=datetime.fromtimestamp(3, timezone.utc),
|
||||
modified=datetime.fromtimestamp(33, timezone.utc),
|
||||
unread=True,
|
||||
),
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/4", title="Title 4", added=added, shared=True
|
||||
url="https://example.com/4",
|
||||
title="Title 4",
|
||||
added=datetime.fromtimestamp(4, timezone.utc),
|
||||
modified=datetime.fromtimestamp(44, timezone.utc),
|
||||
shared=True,
|
||||
),
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/5",
|
||||
title="Title 5",
|
||||
added=added,
|
||||
added=datetime.fromtimestamp(5, timezone.utc),
|
||||
modified=datetime.fromtimestamp(55, timezone.utc),
|
||||
shared=True,
|
||||
description="Example description",
|
||||
notes="Example notes",
|
||||
@@ -44,20 +53,23 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/6",
|
||||
title="Title 6",
|
||||
added=added,
|
||||
added=datetime.fromtimestamp(6, timezone.utc),
|
||||
modified=datetime.fromtimestamp(66, timezone.utc),
|
||||
shared=True,
|
||||
notes="Example notes",
|
||||
),
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/7",
|
||||
title="Title 7",
|
||||
added=added,
|
||||
added=datetime.fromtimestamp(7, timezone.utc),
|
||||
modified=datetime.fromtimestamp(77, timezone.utc),
|
||||
is_archived=True,
|
||||
),
|
||||
self.setup_bookmark(
|
||||
url="https://example.com/8",
|
||||
title="Title 8",
|
||||
added=added,
|
||||
added=datetime.fromtimestamp(8, timezone.utc),
|
||||
modified=datetime.fromtimestamp(88, timezone.utc),
|
||||
tags=[self.setup_tag(name="tag4"), self.setup_tag(name="tag5")],
|
||||
is_archived=True,
|
||||
),
|
||||
@@ -65,17 +77,17 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
html = exporter.export_netscape_html(bookmarks)
|
||||
|
||||
lines = [
|
||||
f'<DT><A HREF="https://example.com/1" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>',
|
||||
'<DT><A HREF="https://example.com/1" ADD_DATE="1" LAST_MODIFIED="11" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>',
|
||||
"<DD>Example description",
|
||||
f'<DT><A HREF="https://example.com/2" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>',
|
||||
f'<DT><A HREF="https://example.com/3" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>',
|
||||
f'<DT><A HREF="https://example.com/4" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>',
|
||||
f'<DT><A HREF="https://example.com/5" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>',
|
||||
'<DT><A HREF="https://example.com/2" ADD_DATE="2" LAST_MODIFIED="22" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>',
|
||||
'<DT><A HREF="https://example.com/3" ADD_DATE="3" LAST_MODIFIED="33" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>',
|
||||
'<DT><A HREF="https://example.com/4" ADD_DATE="4" LAST_MODIFIED="44" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>',
|
||||
'<DT><A HREF="https://example.com/5" ADD_DATE="5" LAST_MODIFIED="55" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>',
|
||||
"<DD>Example description[linkding-notes]Example notes[/linkding-notes]",
|
||||
f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
|
||||
'<DT><A HREF="https://example.com/6" ADD_DATE="6" LAST_MODIFIED="66" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
|
||||
"<DD>[linkding-notes]Example notes[/linkding-notes]",
|
||||
f'<DT><A HREF="https://example.com/7" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
|
||||
f'<DT><A HREF="https://example.com/8" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
|
||||
'<DT><A HREF="https://example.com/7" ADD_DATE="7" LAST_MODIFIED="77" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
|
||||
'<DT><A HREF="https://example.com/8" ADD_DATE="8" LAST_MODIFIED="88" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
|
||||
]
|
||||
self.assertIn("\n\r".join(lines), html)
|
||||
|
||||
|
@@ -26,6 +26,9 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
self.assertEqual(bookmark.title, html_tag.title)
|
||||
self.assertEqual(bookmark.description, html_tag.description)
|
||||
self.assertEqual(bookmark.date_added, parse_timestamp(html_tag.add_date))
|
||||
self.assertEqual(
|
||||
bookmark.date_modified, parse_timestamp(html_tag.last_modified)
|
||||
)
|
||||
self.assertEqual(bookmark.unread, html_tag.to_read)
|
||||
self.assertEqual(bookmark.shared, not html_tag.private)
|
||||
|
||||
@@ -45,6 +48,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Example title",
|
||||
description="Example description",
|
||||
add_date="1",
|
||||
last_modified="11",
|
||||
tags="example-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -52,6 +56,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Foo title",
|
||||
description="",
|
||||
add_date="2",
|
||||
last_modified="22",
|
||||
tags="",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -59,6 +64,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Bar title",
|
||||
description="Bar description",
|
||||
add_date="3",
|
||||
last_modified="33",
|
||||
tags="bar-tag, other-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -66,6 +72,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Baz title",
|
||||
description="Baz description",
|
||||
add_date="4",
|
||||
last_modified="44",
|
||||
to_read=True,
|
||||
),
|
||||
]
|
||||
@@ -90,6 +97,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Example title",
|
||||
description="Example description",
|
||||
add_date="1",
|
||||
last_modified="11",
|
||||
tags="example-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -97,6 +105,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Foo title",
|
||||
description="",
|
||||
add_date="2",
|
||||
last_modified="22",
|
||||
tags="",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -104,20 +113,23 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Bar title",
|
||||
description="Bar description",
|
||||
add_date="3",
|
||||
last_modified="33",
|
||||
tags="bar-tag, other-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
href="https://example.com/unread",
|
||||
title="Unread title",
|
||||
description="Unread description",
|
||||
add_date="3",
|
||||
add_date="4",
|
||||
last_modified="44",
|
||||
to_read=True,
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
href="https://example.com/private",
|
||||
title="Private title",
|
||||
description="Private description",
|
||||
add_date="4",
|
||||
add_date="5",
|
||||
last_modified="55",
|
||||
private=True,
|
||||
),
|
||||
]
|
||||
@@ -136,6 +148,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Updated Example title",
|
||||
description="Updated Example description",
|
||||
add_date="111",
|
||||
last_modified="1111",
|
||||
tags="updated-example-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -143,6 +156,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Updated Foo title",
|
||||
description="Updated Foo description",
|
||||
add_date="222",
|
||||
last_modified="2222",
|
||||
tags="new-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -150,6 +164,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Updated Bar title",
|
||||
description="Updated Bar description",
|
||||
add_date="333",
|
||||
last_modified="3333",
|
||||
tags="updated-bar-tag, updated-other-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -157,6 +172,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Unread title",
|
||||
description="Unread description",
|
||||
add_date="3",
|
||||
last_modified="3",
|
||||
to_read=False,
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -164,9 +180,15 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
title="Private title",
|
||||
description="Private description",
|
||||
add_date="4",
|
||||
last_modified="4",
|
||||
private=False,
|
||||
),
|
||||
BookmarkHtmlTag(href="https://baz.com", add_date="444", tags="baz-tag"),
|
||||
BookmarkHtmlTag(
|
||||
href="https://baz.com",
|
||||
add_date="444",
|
||||
last_modified="4444",
|
||||
tags="baz-tag",
|
||||
),
|
||||
]
|
||||
|
||||
# Import updated data
|
||||
@@ -291,6 +313,19 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
Bookmark.objects.all()[0].date_added, timezone.datetime(2021, 1, 1)
|
||||
)
|
||||
|
||||
def test_use_add_date_when_no_last_modified(self):
|
||||
test_html = self.render_html(
|
||||
tags_html=f"""
|
||||
<DT><A HREF="https://example.com" ADD_DATE="1">Example.com</A>
|
||||
<DD>Example.com
|
||||
"""
|
||||
)
|
||||
|
||||
import_netscape_html(test_html, self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
self.assertEqual(Bookmark.objects.all()[0].date_modified, parse_timestamp("1"))
|
||||
|
||||
def test_keep_title_if_imported_bookmark_has_empty_title(self):
|
||||
test_html = self.render_html(
|
||||
tags=[BookmarkHtmlTag(href="https://example.com", title="Example.com")]
|
||||
|
@@ -2,10 +2,10 @@ from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import GlobalSettings
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
|
||||
class LayoutTestCase(TestCase, BookmarkFactoryMixin):
|
||||
class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
@@ -63,3 +63,38 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin):
|
||||
html,
|
||||
count=0,
|
||||
)
|
||||
|
||||
def test_does_not_link_custom_css_when_empty(self):
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
html = response.content.decode()
|
||||
soup = self.make_soup(html)
|
||||
|
||||
link = soup.select_one("link[rel='stylesheet'][href*='custom_css']")
|
||||
self.assertIsNone(link)
|
||||
|
||||
def test_does_link_custom_css_when_not_empty(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.custom_css = "body { background-color: red; }"
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
html = response.content.decode()
|
||||
soup = self.make_soup(html)
|
||||
|
||||
link = soup.select_one("link[rel='stylesheet'][href*='custom_css']")
|
||||
self.assertIsNotNone(link)
|
||||
|
||||
def test_custom_css_link_href(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.custom_css = "body { background-color: red; }"
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(reverse("bookmarks:index"))
|
||||
html = response.content.decode()
|
||||
soup = self.make_soup(html)
|
||||
|
||||
link = soup.select_one("link[rel='stylesheet'][href*='custom_css']")
|
||||
expected_url = (
|
||||
reverse("bookmarks:custom_css") + f"?hash={profile.custom_css_hash}"
|
||||
)
|
||||
self.assertEqual(link["href"], expected_url)
|
||||
|
@@ -18,6 +18,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
|
||||
self.assertEqual(bookmark.href, html_tag.href)
|
||||
self.assertEqual(bookmark.title, html_tag.title)
|
||||
self.assertEqual(bookmark.date_added, html_tag.add_date)
|
||||
self.assertEqual(bookmark.date_modified, html_tag.last_modified)
|
||||
self.assertEqual(bookmark.description, html_tag.description)
|
||||
self.assertEqual(bookmark.tag_names, parse_tag_string(html_tag.tags))
|
||||
self.assertEqual(bookmark.to_read, html_tag.to_read)
|
||||
@@ -30,6 +31,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
|
||||
title="Example title",
|
||||
description="Example description",
|
||||
add_date="1",
|
||||
last_modified="11",
|
||||
tags="example-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -37,6 +39,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
|
||||
title="Foo title",
|
||||
description="",
|
||||
add_date="2",
|
||||
last_modified="22",
|
||||
tags="",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
@@ -44,13 +47,14 @@ class ParserTestCase(TestCase, ImportTestMixin):
|
||||
title="Bar title",
|
||||
description="Bar description",
|
||||
add_date="3",
|
||||
last_modified="33",
|
||||
tags="bar-tag, other-tag",
|
||||
),
|
||||
BookmarkHtmlTag(
|
||||
href="https://example.com/baz",
|
||||
title="Baz title",
|
||||
description="Baz description",
|
||||
add_date="3",
|
||||
add_date="4",
|
||||
to_read=True,
|
||||
),
|
||||
]
|
||||
@@ -72,9 +76,17 @@ class ParserTestCase(TestCase, ImportTestMixin):
|
||||
title="Example title",
|
||||
description="Example description",
|
||||
add_date="1",
|
||||
last_modified="1",
|
||||
tags="example-tag",
|
||||
),
|
||||
BookmarkHtmlTag(href="", title="", description="", add_date="", tags=""),
|
||||
BookmarkHtmlTag(
|
||||
href="",
|
||||
title="",
|
||||
description="",
|
||||
add_date="",
|
||||
last_modified="",
|
||||
tags="",
|
||||
),
|
||||
]
|
||||
html = self.render_html(html_tags)
|
||||
bookmarks = parse(html)
|
||||
|
@@ -43,6 +43,7 @@ class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
response = self.client.post(reverse("change_password"), form_data)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
self.assertIn("old_password", response.context_data["form"].errors)
|
||||
|
||||
def test_should_return_error_for_mismatching_new_password(self):
|
||||
@@ -54,4 +55,5 @@ class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
response = self.client.post(reverse("change_password"), form_data)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
self.assertIn("new_password2", response.context_data["form"].errors)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import random
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
@@ -22,6 +23,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
if not overrides:
|
||||
overrides = {}
|
||||
form_data = {
|
||||
"update_profile": "",
|
||||
"theme": UserProfile.THEME_AUTO,
|
||||
"bookmark_date_display": UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
"bookmark_description_display": UserProfile.BOOKMARK_DESCRIPTION_DISPLAY_INLINE,
|
||||
@@ -195,6 +197,12 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertSuccessMessage(html, "Profile updated")
|
||||
|
||||
def test_update_profile_with_invalid_form_returns_422(self):
|
||||
form_data = self.create_profile_form_data({"items_per_page": "-1"})
|
||||
response = self.client.post(reverse("bookmarks:settings.update"), form_data)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_update_profile_should_not_be_called_without_respective_form_action(self):
|
||||
form_data = {
|
||||
"theme": UserProfile.THEME_DARK,
|
||||
@@ -210,6 +218,31 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(self.user.profile.theme, UserProfile.THEME_AUTO)
|
||||
self.assertSuccessMessage(html, "Profile updated", count=0)
|
||||
|
||||
def test_update_profile_updates_custom_css_hash(self):
|
||||
form_data = self.create_profile_form_data(
|
||||
{
|
||||
"custom_css": "body { background-color: #000; }",
|
||||
}
|
||||
)
|
||||
self.client.post(reverse("bookmarks:settings.update"), form_data, follow=True)
|
||||
self.user.profile.refresh_from_db()
|
||||
|
||||
expected_hash = hashlib.md5(form_data["custom_css"].encode("utf-8")).hexdigest()
|
||||
self.assertEqual(expected_hash, self.user.profile.custom_css_hash)
|
||||
|
||||
form_data["custom_css"] = "body { background-color: #fff; }"
|
||||
self.client.post(reverse("bookmarks:settings.update"), form_data, follow=True)
|
||||
self.user.profile.refresh_from_db()
|
||||
|
||||
expected_hash = hashlib.md5(form_data["custom_css"].encode("utf-8")).hexdigest()
|
||||
self.assertEqual(expected_hash, self.user.profile.custom_css_hash)
|
||||
|
||||
form_data["custom_css"] = ""
|
||||
self.client.post(reverse("bookmarks:settings.update"), form_data, follow=True)
|
||||
self.user.profile.refresh_from_db()
|
||||
|
||||
self.assertEqual("", self.user.profile.custom_css_hash)
|
||||
|
||||
def test_enable_favicons_should_schedule_icon_update(self):
|
||||
with patch.object(
|
||||
tasks, "schedule_bookmarks_without_favicons"
|
||||
@@ -217,7 +250,6 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
# Enabling favicons schedules update
|
||||
form_data = self.create_profile_form_data(
|
||||
{
|
||||
"update_profile": "",
|
||||
"enable_favicons": True,
|
||||
}
|
||||
)
|
||||
@@ -331,7 +363,6 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
# Enabling favicons schedules update
|
||||
form_data = self.create_profile_form_data(
|
||||
{
|
||||
"update_profile": "",
|
||||
"enable_preview_images": True,
|
||||
}
|
||||
)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import secrets
|
||||
import gzip
|
||||
import os
|
||||
import subprocess
|
||||
@@ -9,9 +10,10 @@ from bookmarks.services import singlefile
|
||||
|
||||
|
||||
class SingleFileServiceTestCase(TestCase):
|
||||
html_content = "<html><body><h1>Hello, World!</h1></body></html>"
|
||||
html_filepath = "temp.html.gz"
|
||||
temp_html_filepath = "temp.html.gz.tmp"
|
||||
def setUp(self):
|
||||
self.html_content = "<html><body><h1>Hello, World!</h1></body></html>"
|
||||
self.html_filepath = secrets.token_hex(8) + ".html.gz"
|
||||
self.temp_html_filepath = self.html_filepath + ".tmp"
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(self.html_filepath):
|
||||
@@ -64,7 +66,7 @@ class SingleFileServiceTestCase(TestCase):
|
||||
'--browser-arg="--headless=new"',
|
||||
'--browser-arg="--user-data-dir=./chromium-profile"',
|
||||
'--browser-arg="--no-sandbox"',
|
||||
'--browser-arg="--load-extension=uBlock0.chromium"',
|
||||
'--browser-arg="--load-extension=uBOLite.chromium.mv3"',
|
||||
"http://example.com",
|
||||
self.html_filepath + ".tmp",
|
||||
]
|
||||
@@ -86,7 +88,7 @@ class SingleFileServiceTestCase(TestCase):
|
||||
'--browser-arg="--headless=new"',
|
||||
'--browser-arg="--user-data-dir=./chromium-profile"',
|
||||
'--browser-arg="--no-sandbox"',
|
||||
'--browser-arg="--load-extension=uBlock0.chromium"',
|
||||
'--browser-arg="--load-extension=uBOLite.chromium.mv3"',
|
||||
"--some-option",
|
||||
"some value",
|
||||
"--another-option",
|
||||
|
@@ -65,4 +65,6 @@ urlpatterns = [
|
||||
path("health", views.health, name="health"),
|
||||
# Manifest
|
||||
path("manifest.json", views.manifest, name="manifest"),
|
||||
# Custom CSS
|
||||
path("custom_css", views.custom_css, name="custom_css"),
|
||||
]
|
||||
|
@@ -4,4 +4,5 @@ from .settings import *
|
||||
from .toasts import *
|
||||
from .health import health
|
||||
from .manifest import manifest
|
||||
from .custom_css import custom_css
|
||||
from .root import root
|
||||
|
@@ -150,7 +150,6 @@ def convert_tag_string(tag_string: str):
|
||||
|
||||
@login_required
|
||||
def new(request):
|
||||
status = 200
|
||||
initial_url = request.GET.get("url")
|
||||
initial_title = request.GET.get("title")
|
||||
initial_description = request.GET.get("description")
|
||||
@@ -169,8 +168,6 @@ def new(request):
|
||||
return HttpResponseRedirect(reverse("bookmarks:close"))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse("bookmarks:index"))
|
||||
else:
|
||||
status = 422
|
||||
else:
|
||||
form = BookmarkForm()
|
||||
if initial_url:
|
||||
@@ -186,6 +183,7 @@ def new(request):
|
||||
if initial_mark_unread:
|
||||
form.initial["unread"] = "true"
|
||||
|
||||
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
||||
context = {
|
||||
"form": form,
|
||||
"auto_close": initial_auto_close,
|
||||
@@ -216,9 +214,10 @@ def edit(request, bookmark_id: int):
|
||||
|
||||
form.initial["tag_string"] = build_tag_string(bookmark.tag_names, " ")
|
||||
|
||||
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
||||
context = {"form": form, "bookmark_id": bookmark_id, "return_url": return_url}
|
||||
|
||||
return render(request, "bookmarks/edit.html", context)
|
||||
return render(request, "bookmarks/edit.html", context, status=status)
|
||||
|
||||
|
||||
def remove(request, bookmark_id: int):
|
||||
|
10
bookmarks/views/custom_css.py
Normal file
10
bookmarks/views/custom_css.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.http import HttpResponse
|
||||
|
||||
custom_css_cache_max_age = 2592000 # 30 days
|
||||
|
||||
|
||||
def custom_css(request):
|
||||
css = request.user_profile.custom_css
|
||||
response = HttpResponse(css, content_type="text/css")
|
||||
response["Cache-Control"] = f"public, max-age={custom_css_cache_max_age}"
|
||||
return response
|
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine AS node-build
|
||||
FROM node:20-alpine AS node-build
|
||||
WORKDIR /etc/linkding
|
||||
# install build dependencies
|
||||
COPY rollup.config.mjs postcss.config.js package.json package-lock.json ./
|
||||
@@ -10,7 +10,7 @@ COPY bookmarks/styles ./bookmarks/styles
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM python:3.12.6-alpine3.20 AS python-base
|
||||
FROM python:3.12.6-alpine3.20 AS build-deps
|
||||
# Add required packages
|
||||
# alpine-sdk linux-headers pkgconfig: build Python packages from source
|
||||
# libpq-dev: build Postgres client from source
|
||||
@@ -18,24 +18,8 @@ FROM python:3.12.6-alpine3.20 AS python-base
|
||||
# libffi-dev openssl-dev rust cargo: build Python cryptography from source
|
||||
RUN apk update && apk add alpine-sdk linux-headers libpq-dev pkgconfig icu-dev sqlite-dev libffi-dev openssl-dev rust cargo
|
||||
WORKDIR /etc/linkding
|
||||
|
||||
|
||||
FROM python-base AS python-build
|
||||
# install build dependencies
|
||||
# install python dependencies
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -U pip && pip install -r requirements.txt
|
||||
# copy files needed for Django build
|
||||
COPY . .
|
||||
COPY --from=node-build /etc/linkding .
|
||||
# remove style sources
|
||||
RUN rm -rf bookmarks/styles
|
||||
# run Django part of the build
|
||||
RUN mkdir data && \
|
||||
python manage.py collectstatic
|
||||
|
||||
|
||||
FROM python-base AS prod-deps
|
||||
COPY requirements.txt ./requirements.txt
|
||||
# Need to build psycopg2 from source for ARM platforms
|
||||
RUN sed -i 's/psycopg2-binary/psycopg2/g' requirements.txt
|
||||
RUN mkdir /opt/venv && \
|
||||
@@ -44,7 +28,7 @@ RUN mkdir /opt/venv && \
|
||||
/opt/venv/bin/pip install -r requirements.txt
|
||||
|
||||
|
||||
FROM python-base AS compile-icu
|
||||
FROM build-deps AS compile-icu
|
||||
# Defines SQLite version
|
||||
# Since this is only needed for downloading the header files this probably
|
||||
# doesn't need to be up-to-date, assuming the SQLite APIs used by the ICU
|
||||
@@ -74,19 +58,23 @@ RUN set -x ; \
|
||||
addgroup -g 82 -S www-data ; \
|
||||
adduser -u 82 -D -S -G www-data www-data && exit 0 ; exit 1
|
||||
WORKDIR /etc/linkding
|
||||
# copy prod dependencies
|
||||
COPY --from=prod-deps /opt/venv /opt/venv
|
||||
# copy output from build stage
|
||||
COPY --from=python-build /etc/linkding/static static/
|
||||
# copy python dependencies
|
||||
COPY --from=build-deps /opt/venv /opt/venv
|
||||
# copy output from node build
|
||||
COPY --from=node-build /etc/linkding/bookmarks/static bookmarks/static/
|
||||
# copy compiled icu extension
|
||||
COPY --from=compile-icu /etc/linkding/libicu.so libicu.so
|
||||
# copy application code
|
||||
COPY . .
|
||||
# Activate virtual env
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PATH=/opt/venv/bin:$PATH
|
||||
# Generate static files, remove source styles that are not needed
|
||||
RUN mkdir data && \
|
||||
python manage.py collectstatic
|
||||
|
||||
# Expose uwsgi server at port 9090
|
||||
EXPOSE 9090
|
||||
# Activate virtual env
|
||||
ENV VIRTUAL_ENV /opt/venv
|
||||
ENV PATH /opt/venv/bin:$PATH
|
||||
# Allow running containers as an an arbitrary user in the root group, to support deployment scenarios like OpenShift, Podman
|
||||
RUN chmod g+w . && \
|
||||
chmod +x ./bootstrap.sh
|
||||
@@ -100,18 +88,20 @@ CMD ["./bootstrap.sh"]
|
||||
FROM node:18-alpine AS ublock-build
|
||||
WORKDIR /etc/linkding
|
||||
# Install necessary tools
|
||||
RUN apk add --no-cache curl jq unzip
|
||||
# Fetch the latest release tag
|
||||
# Download the library
|
||||
# Unzip the library
|
||||
RUN TAG=$(curl -sL https://api.github.com/repos/gorhill/uBlock/releases/latest | jq -r '.tag_name') && \
|
||||
DOWNLOAD_URL=https://github.com/gorhill/uBlock/releases/download/$TAG/uBlock0_$TAG.chromium.zip && \
|
||||
curl -L -o uBlock0.zip $DOWNLOAD_URL && \
|
||||
unzip uBlock0.zip
|
||||
# Patch assets.json to enable easylist-cookies by default
|
||||
RUN curl -L -o ./uBlock0.chromium/assets/thirdparties/easylist/easylist-cookies.txt https://ublockorigin.github.io/uAssets/thirdparties/easylist-cookies.txt
|
||||
RUN jq '."assets.json" |= del(.cdnURLs) | ."assets.json".contentURL = ["assets/assets.json"] | ."fanboy-cookiemonster" |= del(.off) | ."fanboy-cookiemonster".contentURL += ["assets/thirdparties/easylist/easylist-cookies.txt"]' ./uBlock0.chromium/assets/assets.json > temp.json && \
|
||||
mv temp.json ./uBlock0.chromium/assets/assets.json
|
||||
# Download and unzip the latest uBlock Origin Lite release
|
||||
# Patch manifest to enable annoyances by default
|
||||
# Patch ruleset-manager.js to use rulesets enabled in manifest by default
|
||||
RUN apk add --no-cache curl jq unzip && \
|
||||
TAG=$(curl -sL https://api.github.com/repos/uBlockOrigin/uBOL-home/releases/latest | jq -r '.tag_name') && \
|
||||
DOWNLOAD_URL=https://github.com/uBlockOrigin/uBOL-home/releases/download/$TAG/$TAG.chromium.mv3.zip && \
|
||||
echo "Downloading $DOWNLOAD_URL" && \
|
||||
curl -L -o uBOLite.zip $DOWNLOAD_URL && \
|
||||
unzip uBOLite.zip -d uBOLite.chromium.mv3 && \
|
||||
rm uBOLite.zip && \
|
||||
jq '.declarative_net_request.rule_resources |= map(if .id == "annoyances-overlays" or .id == "annoyances-cookies" or .id == "annoyances-social" or .id == "annoyances-widgets" or .id == "annoyances-others" then .enabled = true else . end)' \
|
||||
uBOLite.chromium.mv3/manifest.json > temp.json && \
|
||||
mv temp.json uBOLite.chromium.mv3/manifest.json && \
|
||||
sed -i 's/const out = \[ '\''default'\'' \];/const out = await dnr.getEnabledRulesets();/' uBOLite.chromium.mv3/js/ruleset-manager.js
|
||||
|
||||
|
||||
FROM linkding AS linkding-plus
|
||||
@@ -119,9 +109,11 @@ FROM linkding AS linkding-plus
|
||||
RUN apk update && apk add nodejs npm chromium
|
||||
# install single-file from fork for now, which contains several hotfixes
|
||||
RUN npm install -g https://github.com/sissbruecker/single-file-cli/tarball/4c54b3bc704cfb3e96cec2d24854caca3df0b3b6
|
||||
# copy uBlock0
|
||||
COPY --from=ublock-build /etc/linkding/uBlock0.chromium uBlock0.chromium/
|
||||
# create chromium profile folder for user running background tasks
|
||||
RUN mkdir -p chromium-profile && chown -R www-data:www-data chromium-profile
|
||||
# copy uBlock
|
||||
COPY --from=ublock-build /etc/linkding/uBOLite.chromium.mv3 uBOLite.chromium.mv3/
|
||||
# create chromium profile folder for user running background tasks and set permissions
|
||||
RUN mkdir -p chromium-profile && \
|
||||
chown -R www-data:www-data chromium-profile && \
|
||||
chown -R www-data:www-data uBOLite.chromium.mv3
|
||||
# enable snapshot support
|
||||
ENV LD_ENABLE_SNAPSHOTS=True
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine AS node-build
|
||||
FROM node:20-alpine AS node-build
|
||||
WORKDIR /etc/linkding
|
||||
# install build dependencies
|
||||
COPY rollup.config.mjs postcss.config.js package.json package-lock.json ./
|
||||
@@ -10,7 +10,7 @@ COPY bookmarks/styles ./bookmarks/styles
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM python:3.12.6-slim-bookworm AS python-base
|
||||
FROM python:3.12.6-slim-bookworm AS build-deps
|
||||
# Add required packages
|
||||
# build-essential pkg-config: build Python packages from source
|
||||
# libpq-dev: build Postgres client from source
|
||||
@@ -20,24 +20,8 @@ RUN apt-get update && apt-get -y install build-essential pkg-config libpq-dev li
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
WORKDIR /etc/linkding
|
||||
|
||||
|
||||
FROM python-base AS python-build
|
||||
# install build dependencies
|
||||
# install python dependencies
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -U pip && pip install -r requirements.txt
|
||||
# copy files needed for Django build
|
||||
COPY . .
|
||||
COPY --from=node-build /etc/linkding .
|
||||
# remove style sources
|
||||
RUN rm -rf bookmarks/styles
|
||||
# run Django part of the build
|
||||
RUN mkdir data && \
|
||||
python manage.py collectstatic
|
||||
|
||||
|
||||
FROM python-base AS prod-deps
|
||||
COPY requirements.txt ./requirements.txt
|
||||
# Need to build psycopg2 from source for ARM platforms
|
||||
RUN sed -i 's/psycopg2-binary/psycopg2/g' requirements.txt
|
||||
RUN mkdir /opt/venv && \
|
||||
@@ -46,7 +30,7 @@ RUN mkdir /opt/venv && \
|
||||
/opt/venv/bin/pip install -r requirements.txt
|
||||
|
||||
|
||||
FROM python-base AS compile-icu
|
||||
FROM build-deps AS compile-icu
|
||||
# Defines SQLite version
|
||||
# Since this is only needed for downloading the header files this probably
|
||||
# doesn't need to be up-to-date, assuming the SQLite APIs used by the ICU
|
||||
@@ -67,27 +51,31 @@ RUN wget https://www.sqlite.org/${SQLITE_RELEASE_YEAR}/sqlite-amalgamation-${SQL
|
||||
gcc -fPIC -shared icu.c `pkg-config --libs --cflags icu-uc icu-io` -o libicu.so
|
||||
|
||||
|
||||
FROM python:3.12.6-slim-bookworm as linkding
|
||||
FROM python:3.12.6-slim-bookworm AS linkding
|
||||
LABEL org.opencontainers.image.source="https://github.com/sissbruecker/linkding"
|
||||
# install runtime dependencies
|
||||
RUN apt-get update && apt-get -y install mime-support libpq-dev libicu-dev libssl3 curl
|
||||
WORKDIR /etc/linkding
|
||||
# copy prod dependencies
|
||||
COPY --from=prod-deps /opt/venv /opt/venv
|
||||
# copy output from build stage
|
||||
COPY --from=python-build /etc/linkding/static static/
|
||||
# copy python dependencies
|
||||
COPY --from=build-deps /opt/venv /opt/venv
|
||||
# copy output from node build
|
||||
COPY --from=node-build /etc/linkding/bookmarks/static bookmarks/static/
|
||||
# copy compiled icu extension
|
||||
COPY --from=compile-icu /etc/linkding/libicu.so libicu.so
|
||||
# copy application code
|
||||
COPY . .
|
||||
# Activate virtual env
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PATH=/opt/venv/bin:$PATH
|
||||
# Generate static files
|
||||
RUN mkdir data && \
|
||||
python manage.py collectstatic
|
||||
|
||||
# Expose uwsgi server at port 9090
|
||||
EXPOSE 9090
|
||||
# Activate virtual env
|
||||
ENV VIRTUAL_ENV /opt/venv
|
||||
ENV PATH /opt/venv/bin:$PATH
|
||||
# Allow running containers as an an arbitrary user in the root group, to support deployment scenarios like OpenShift, Podman
|
||||
RUN ["chmod", "g+w", "."]
|
||||
# Run bootstrap logic
|
||||
RUN ["chmod", "+x", "./bootstrap.sh"]
|
||||
RUN chmod g+w . && \
|
||||
chmod +x ./bootstrap.sh
|
||||
|
||||
HEALTHCHECK --interval=30s --retries=3 --timeout=1s \
|
||||
CMD curl -f http://localhost:${LD_SERVER_PORT:-9090}/${LD_CONTEXT_PATH}health || exit 1
|
||||
@@ -98,18 +86,20 @@ CMD ["./bootstrap.sh"]
|
||||
FROM node:18-alpine AS ublock-build
|
||||
WORKDIR /etc/linkding
|
||||
# Install necessary tools
|
||||
RUN apk add --no-cache curl jq unzip
|
||||
# Fetch the latest release tag
|
||||
# Download the library
|
||||
# Unzip the library
|
||||
RUN TAG=$(curl -sL https://api.github.com/repos/gorhill/uBlock/releases/latest | jq -r '.tag_name') && \
|
||||
DOWNLOAD_URL=https://github.com/gorhill/uBlock/releases/download/$TAG/uBlock0_$TAG.chromium.zip && \
|
||||
curl -L -o uBlock0.zip $DOWNLOAD_URL && \
|
||||
unzip uBlock0.zip
|
||||
# Patch assets.json to enable easylist-cookies by default
|
||||
RUN curl -L -o ./uBlock0.chromium/assets/thirdparties/easylist/easylist-cookies.txt https://ublockorigin.github.io/uAssets/thirdparties/easylist-cookies.txt
|
||||
RUN jq '."assets.json" |= del(.cdnURLs) | ."assets.json".contentURL = ["assets/assets.json"] | ."fanboy-cookiemonster" |= del(.off) | ."fanboy-cookiemonster".contentURL += ["assets/thirdparties/easylist/easylist-cookies.txt"]' ./uBlock0.chromium/assets/assets.json > temp.json && \
|
||||
mv temp.json ./uBlock0.chromium/assets/assets.json
|
||||
# Download and unzip the latest uBlock Origin Lite release
|
||||
# Patch manifest to enable annoyances by default
|
||||
# Patch ruleset-manager.js to use rulesets enabled in manifest by default
|
||||
RUN apk add --no-cache curl jq unzip && \
|
||||
TAG=$(curl -sL https://api.github.com/repos/uBlockOrigin/uBOL-home/releases/latest | jq -r '.tag_name') && \
|
||||
DOWNLOAD_URL=https://github.com/uBlockOrigin/uBOL-home/releases/download/$TAG/$TAG.chromium.mv3.zip && \
|
||||
echo "Downloading $DOWNLOAD_URL" && \
|
||||
curl -L -o uBOLite.zip $DOWNLOAD_URL && \
|
||||
unzip uBOLite.zip -d uBOLite.chromium.mv3 && \
|
||||
rm uBOLite.zip && \
|
||||
jq '.declarative_net_request.rule_resources |= map(if .id == "annoyances-overlays" or .id == "annoyances-cookies" or .id == "annoyances-social" or .id == "annoyances-widgets" or .id == "annoyances-others" then .enabled = true else . end)' \
|
||||
uBOLite.chromium.mv3/manifest.json > temp.json && \
|
||||
mv temp.json uBOLite.chromium.mv3/manifest.json && \
|
||||
sed -i 's/const out = \[ '\''default'\'' \];/const out = await dnr.getEnabledRulesets();/' uBOLite.chromium.mv3/js/ruleset-manager.js
|
||||
|
||||
|
||||
FROM linkding AS linkding-plus
|
||||
@@ -123,9 +113,11 @@ RUN apt-get install -y gnupg2 apt-transport-https ca-certificates && \
|
||||
apt-get update && apt-get install -y nodejs
|
||||
# install single-file from fork for now, which contains several hotfixes
|
||||
RUN npm install -g https://github.com/sissbruecker/single-file-cli/tarball/4c54b3bc704cfb3e96cec2d24854caca3df0b3b6
|
||||
# create chromium profile folder for user running background tasks
|
||||
RUN mkdir -p chromium-profile && chown -R www-data:www-data chromium-profile
|
||||
# copy uBlock0
|
||||
COPY --from=ublock-build /etc/linkding/uBlock0.chromium uBlock0.chromium/
|
||||
# copy uBlock
|
||||
COPY --from=ublock-build /etc/linkding/uBOLite.chromium.mv3 uBOLite.chromium.mv3/
|
||||
# create chromium profile folder for user running background tasks and set permissions
|
||||
RUN mkdir -p chromium-profile && \
|
||||
chown -R www-data:www-data chromium-profile && \
|
||||
chown -R www-data:www-data uBOLite.chromium.mv3
|
||||
# enable snapshot support
|
||||
ENV LD_ENABLE_SNAPSHOTS=True
|
||||
|
@@ -45,6 +45,9 @@ export default defineConfig({
|
||||
customCss: [
|
||||
'./src/styles/custom.css',
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/sissbruecker/linkding/edit/master/docs/',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
134
docs/package-lock.json
generated
134
docs/package-lock.json
generated
@@ -1517,9 +1517,9 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
|
||||
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1529,9 +1529,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
|
||||
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1541,9 +1541,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
|
||||
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1553,9 +1553,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
|
||||
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1565,9 +1565,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
|
||||
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1577,9 +1577,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
|
||||
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1589,9 +1589,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1601,9 +1601,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
|
||||
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1613,9 +1613,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1625,9 +1625,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1637,9 +1637,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1649,9 +1649,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1661,9 +1661,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
|
||||
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1673,9 +1673,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1685,9 +1685,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1697,9 +1697,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -6257,9 +6257,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
|
||||
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
@@ -6271,22 +6271,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.21.3",
|
||||
"@rollup/rollup-android-arm64": "4.21.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.21.3",
|
||||
"@rollup/rollup-darwin-x64": "4.21.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.21.3",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.21.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.21.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.21.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.21.3",
|
||||
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||
"@rollup/rollup-android-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
@@ -127,11 +127,9 @@ POST /api/bookmarks/
|
||||
Creates a new bookmark. Tags are simply assigned using their names. Including
|
||||
`is_archived: true` saves a bookmark directly to the archive.
|
||||
|
||||
If the title and description are not provided or empty, the application automatically tries to scrape them from the bookmarked website. This behavior can be disabled by adding the `disable_scraping` query parameter to the API request. If you have an application where you want to keep using scraped metadata, but also allow users to leave the title or description empty, you should:
|
||||
If the provided URL is already bookmarked, this silently updates the existing bookmark instead of creating a new one. If you are implementing a user interface, consider notifying users about this behavior. You can use the `/check` endpoint to check if a URL is already bookmarked and at the same time get the existing bookmark data. This behavior may change in the future to return an error instead.
|
||||
|
||||
- Fetch the scraped title and description using the `/check` endpoint.
|
||||
- Prefill the title and description fields in your app with the fetched values and allow users to clear those values.
|
||||
- Add the `disable_scraping` query parameter to prevent the API from adding them back again.
|
||||
If the title and description are not provided or empty, the application automatically tries to scrape them from the bookmarked website. This behavior can be disabled by adding the `disable_scraping` query parameter to the API request.
|
||||
|
||||
Example payload:
|
||||
|
||||
@@ -155,36 +153,17 @@ Example payload:
|
||||
|
||||
```
|
||||
PUT /api/bookmarks/<id>/
|
||||
```
|
||||
|
||||
Updates a bookmark.
|
||||
This is a full update, which requires at least a URL, and fields that are not specified are cleared or reset to their defaults.
|
||||
Tags are simply assigned using their names.
|
||||
|
||||
Example payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"title": "Example title",
|
||||
"description": "Example description",
|
||||
"tag_names": [
|
||||
"tag1",
|
||||
"tag2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Patch**
|
||||
|
||||
```
|
||||
PATCH /api/bookmarks/<id>/
|
||||
```
|
||||
|
||||
Updates a bookmark partially.
|
||||
Allows to modify individual fields of a bookmark.
|
||||
Updates a bookmark.
|
||||
When using `POST`, at least all required fields must be provided (currently only `url`).
|
||||
When using `PATCH`, only the fields that should be updated need to be provided.
|
||||
Regardless which method is used, any field that is not provided is not modified.
|
||||
Tags are simply assigned using their names.
|
||||
|
||||
If the provided URL is already bookmarked this returns an error.
|
||||
|
||||
Example payload:
|
||||
|
||||
```json
|
||||
|
@@ -46,6 +46,7 @@
|
||||
[data-has-hero] header {
|
||||
background: transparent;
|
||||
border-bottom: solid 1px transparent;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
|
156
package-lock.json
generated
156
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.34.0",
|
||||
"version": "1.35.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "linkding",
|
||||
"version": "1.34.0",
|
||||
"version": "1.35.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hotwired/turbo": "^8.0.6",
|
||||
@@ -238,9 +238,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz",
|
||||
"integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -251,9 +251,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz",
|
||||
"integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -264,9 +264,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
|
||||
"integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -277,9 +277,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz",
|
||||
"integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -290,9 +290,22 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz",
|
||||
"integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -303,9 +316,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
|
||||
"integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -316,9 +329,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz",
|
||||
"integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -328,10 +341,23 @@
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz",
|
||||
"integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -341,10 +367,23 @@
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
|
||||
"integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -355,9 +394,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz",
|
||||
"integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -368,9 +407,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz",
|
||||
"integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -381,9 +420,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz",
|
||||
"integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -394,9 +433,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz",
|
||||
"integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2041,9 +2080,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
|
||||
"integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
@@ -2056,19 +2095,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.13.0",
|
||||
"@rollup/rollup-android-arm64": "4.13.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.13.0",
|
||||
"@rollup/rollup-darwin-x64": "4.13.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.13.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.13.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.13.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.13.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.13.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.13.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.13.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.13.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||
"@rollup/rollup-android-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.35.0",
|
||||
"version": "1.36.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@@ -1,13 +1,18 @@
|
||||
rm -rf ublock0.chromium
|
||||
rm -rf uBOLite.chromium.mv3
|
||||
|
||||
TAG=$(curl -sL https://api.github.com/repos/gorhill/uBlock/releases/latest | jq -r '.tag_name')
|
||||
DOWNLOAD_URL=https://github.com/gorhill/uBlock/releases/download/$TAG/uBlock0_$TAG.chromium.zip
|
||||
curl -L -o uBlock0.zip $DOWNLOAD_URL
|
||||
unzip uBlock0.zip
|
||||
rm uBlock0.zip
|
||||
# Download uBlock Origin Lite
|
||||
TAG=$(curl -sL https://api.github.com/repos/uBlockOrigin/uBOL-home/releases/latest | jq -r '.tag_name')
|
||||
DOWNLOAD_URL=https://github.com/uBlockOrigin/uBOL-home/releases/download/$TAG/$TAG.chromium.mv3.zip
|
||||
echo "Downloading $DOWNLOAD_URL"
|
||||
curl -L -o uBOLite.zip $DOWNLOAD_URL
|
||||
unzip uBOLite.zip -d uBOLite.chromium.mv3
|
||||
rm uBOLite.zip
|
||||
|
||||
curl -L -o ./uBlock0.chromium/assets/thirdparties/easylist/easylist-cookies.txt https://ublockorigin.github.io/uAssets/thirdparties/easylist-cookies.txt
|
||||
jq '."assets.json" |= del(.cdnURLs) | ."assets.json".contentURL = ["assets/assets.json"] | ."fanboy-cookiemonster" |= del(.off) | ."fanboy-cookiemonster".contentURL += ["assets/thirdparties/easylist/easylist-cookies.txt"]' ./uBlock0.chromium/assets/assets.json > temp.json
|
||||
mv temp.json ./uBlock0.chromium/assets/assets.json
|
||||
# Patch uBlock Origin Lite to respect rulesets enabled in manifest.json
|
||||
sed -i '' "s/const out = \[ 'default' \];/const out = await dnr.getEnabledRulesets();/" uBOLite.chromium.mv3/js/ruleset-manager.js
|
||||
|
||||
# Enable annoyances rulesets in manifest.json
|
||||
jq '.declarative_net_request.rule_resources |= map(if .id == "annoyances-overlays" or .id == "annoyances-cookies" or .id == "annoyances-social" or .id == "annoyances-widgets" or .id == "annoyances-others" then .enabled = true else . end)' uBOLite.chromium.mv3/manifest.json > temp.json
|
||||
mv temp.json uBOLite.chromium.mv3/manifest.json
|
||||
|
||||
mkdir -p chromium-profile
|
||||
|
@@ -128,14 +128,6 @@ STATIC_URL = "/" + LD_CONTEXT_PATH + "static/"
|
||||
# Collect static files in static folder
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
# Resolve theme files from style source folder
|
||||
os.path.join(BASE_DIR, "bookmarks", "styles"),
|
||||
# Resolve downloaded files in dev environment
|
||||
os.path.join(BASE_DIR, "data", "favicons"),
|
||||
os.path.join(BASE_DIR, "data", "previews"),
|
||||
]
|
||||
|
||||
# REST framework
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
@@ -304,7 +296,7 @@ LD_SINGLEFILE_UBLOCK_OPTIONS = os.getenv(
|
||||
'--browser-arg="--headless=new"',
|
||||
'--browser-arg="--user-data-dir=./chromium-profile"',
|
||||
'--browser-arg="--no-sandbox"',
|
||||
'--browser-arg="--load-extension=uBlock0.chromium"',
|
||||
'--browser-arg="--load-extension=uBOLite.chromium.mv3"',
|
||||
]
|
||||
),
|
||||
)
|
||||
|
@@ -20,6 +20,14 @@ INTERNAL_IPS = [
|
||||
# Allow access through ngrok
|
||||
CSRF_TRUSTED_ORIGINS = ["https://*.ngrok-free.app"]
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
# Resolve theme files from style source folder
|
||||
os.path.join(BASE_DIR, "bookmarks", "styles"),
|
||||
# Resolve downloaded files in dev environment
|
||||
os.path.join(BASE_DIR, "data", "favicons"),
|
||||
os.path.join(BASE_DIR, "data", "previews"),
|
||||
]
|
||||
|
||||
# Enable debug logging
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
|
@@ -40,6 +40,13 @@ class LinkdingLoginView(auth_views.LoginView):
|
||||
return response
|
||||
|
||||
|
||||
class LinkdingPasswordChangeView(auth_views.PasswordChangeView):
|
||||
def form_invalid(self, form):
|
||||
response = super().form_invalid(form)
|
||||
response.status_code = 422
|
||||
return response
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", linkding_admin_site.urls),
|
||||
path(
|
||||
@@ -50,7 +57,7 @@ urlpatterns = [
|
||||
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
|
||||
path(
|
||||
"change-password/",
|
||||
auth_views.PasswordChangeView.as_view(),
|
||||
LinkdingPasswordChangeView.as_view(),
|
||||
name="change_password",
|
||||
),
|
||||
path(
|
||||
|
@@ -1 +1 @@
|
||||
1.35.0
|
||||
1.36.0
|
||||
|
Reference in New Issue
Block a user