mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-27 22:43:15 +08:00
366 lines
13 KiB
Python
366 lines
13 KiB
Python
from django import forms
|
|
from django.contrib.auth.models import User
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
from bookmarks.models import (
|
|
Bookmark,
|
|
BookmarkBundle,
|
|
BookmarkSearch,
|
|
GlobalSettings,
|
|
Tag,
|
|
UserProfile,
|
|
build_tag_string,
|
|
parse_tag_string,
|
|
sanitize_tag_name,
|
|
)
|
|
from bookmarks.services.bookmarks import create_bookmark, update_bookmark
|
|
from bookmarks.type_defs import HttpRequest
|
|
from bookmarks.validators import BookmarkURLValidator
|
|
from bookmarks.widgets import (
|
|
FormCheckbox,
|
|
FormErrorList,
|
|
FormInput,
|
|
FormNumberInput,
|
|
FormSelect,
|
|
FormTextarea,
|
|
TagAutocomplete,
|
|
)
|
|
|
|
|
|
class BookmarkForm(forms.ModelForm):
|
|
# Use URLField for URL
|
|
url = forms.CharField(validators=[BookmarkURLValidator()], widget=FormInput)
|
|
tag_string = forms.CharField(required=False, widget=TagAutocomplete)
|
|
# Do not require title and description as they may be empty
|
|
title = forms.CharField(max_length=512, required=False, widget=FormInput)
|
|
description = forms.CharField(required=False, widget=FormTextarea)
|
|
notes = forms.CharField(required=False, widget=FormTextarea)
|
|
unread = forms.BooleanField(required=False, widget=FormCheckbox)
|
|
shared = forms.BooleanField(required=False, widget=FormCheckbox)
|
|
# Hidden field that determines whether to close window/tab after saving the bookmark
|
|
auto_close = forms.CharField(required=False, widget=forms.HiddenInput)
|
|
|
|
class Meta:
|
|
model = Bookmark
|
|
fields = [
|
|
"url",
|
|
"tag_string",
|
|
"title",
|
|
"description",
|
|
"notes",
|
|
"unread",
|
|
"shared",
|
|
"auto_close",
|
|
]
|
|
|
|
def __init__(self, request: HttpRequest, instance: Bookmark = None):
|
|
self.request = request
|
|
|
|
initial = None
|
|
if instance is None and request.method == "GET":
|
|
initial = {
|
|
"url": request.GET.get("url"),
|
|
"title": request.GET.get("title"),
|
|
"description": request.GET.get("description"),
|
|
"notes": request.GET.get("notes"),
|
|
"tag_string": request.GET.get("tags"),
|
|
"auto_close": "auto_close" in request.GET,
|
|
"unread": request.user_profile.default_mark_unread,
|
|
"shared": request.user_profile.default_mark_shared,
|
|
}
|
|
if instance is not None and request.method == "GET":
|
|
initial = {"tag_string": build_tag_string(instance.tag_names, " ")}
|
|
data = request.POST if request.method == "POST" else None
|
|
super().__init__(
|
|
data, instance=instance, initial=initial, error_class=FormErrorList
|
|
)
|
|
|
|
@property
|
|
def is_auto_close(self):
|
|
return self.data.get("auto_close", False) == "True" or self.initial.get(
|
|
"auto_close", False
|
|
)
|
|
|
|
@property
|
|
def has_notes(self):
|
|
return self.initial.get("notes", None) or (
|
|
self.instance and self.instance.notes
|
|
)
|
|
|
|
def save(self, commit=False):
|
|
tag_string = convert_tag_string(self.data["tag_string"])
|
|
bookmark = super().save(commit=False)
|
|
if self.instance.pk:
|
|
return update_bookmark(bookmark, tag_string, self.request.user)
|
|
else:
|
|
return create_bookmark(bookmark, tag_string, self.request.user)
|
|
|
|
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.query_existing(self.instance.owner, url)
|
|
.exclude(pk=self.instance.pk)
|
|
.exists()
|
|
)
|
|
if is_duplicate:
|
|
raise forms.ValidationError("A bookmark with this URL already exists.")
|
|
|
|
return url
|
|
|
|
|
|
def convert_tag_string(tag_string: str):
|
|
# Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
|
|
# strings
|
|
return tag_string.replace(" ", ",")
|
|
|
|
|
|
class TagForm(forms.ModelForm):
|
|
name = forms.CharField(widget=FormInput)
|
|
|
|
class Meta:
|
|
model = Tag
|
|
fields = ["name"]
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
super().__init__(*args, **kwargs, error_class=FormErrorList)
|
|
self.user = user
|
|
|
|
def clean_name(self):
|
|
name = self.cleaned_data.get("name", "").strip()
|
|
|
|
name = sanitize_tag_name(name)
|
|
|
|
queryset = Tag.objects.filter(name__iexact=name, owner=self.user)
|
|
if self.instance.pk:
|
|
queryset = queryset.exclude(pk=self.instance.pk)
|
|
|
|
if queryset.exists():
|
|
raise forms.ValidationError(f'Tag "{name}" already exists.')
|
|
|
|
return name
|
|
|
|
def save(self, commit=True):
|
|
tag = super().save(commit=False)
|
|
if not self.instance.pk:
|
|
tag.owner = self.user
|
|
tag.date_added = timezone.now()
|
|
else:
|
|
tag.date_modified = timezone.now()
|
|
if commit:
|
|
tag.save()
|
|
return tag
|
|
|
|
|
|
class TagMergeForm(forms.Form):
|
|
target_tag = forms.CharField(widget=TagAutocomplete)
|
|
merge_tags = forms.CharField(widget=TagAutocomplete)
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
super().__init__(*args, **kwargs, error_class=FormErrorList)
|
|
self.user = user
|
|
|
|
def clean_target_tag(self):
|
|
target_tag_name = self.cleaned_data.get("target_tag", "")
|
|
|
|
target_tag_names = parse_tag_string(target_tag_name, " ")
|
|
if len(target_tag_names) != 1:
|
|
raise forms.ValidationError(
|
|
"Please enter only one tag name for the target tag."
|
|
)
|
|
|
|
target_tag_name = target_tag_names[0]
|
|
|
|
try:
|
|
target_tag = Tag.objects.get(name__iexact=target_tag_name, owner=self.user)
|
|
except Tag.DoesNotExist:
|
|
raise forms.ValidationError(
|
|
f'Tag "{target_tag_name}" does not exist.'
|
|
) from None
|
|
|
|
return target_tag
|
|
|
|
def clean_merge_tags(self):
|
|
merge_tags_string = self.cleaned_data.get("merge_tags", "")
|
|
|
|
merge_tag_names = parse_tag_string(merge_tags_string, " ")
|
|
if not merge_tag_names:
|
|
raise forms.ValidationError("Please enter at least one tag to merge.")
|
|
|
|
merge_tags = []
|
|
for tag_name in merge_tag_names:
|
|
try:
|
|
tag = Tag.objects.get(name__iexact=tag_name, owner=self.user)
|
|
merge_tags.append(tag)
|
|
except Tag.DoesNotExist:
|
|
raise forms.ValidationError(
|
|
f'Tag "{tag_name}" does not exist.'
|
|
) from None
|
|
|
|
target_tag = self.cleaned_data.get("target_tag")
|
|
if target_tag and target_tag in merge_tags:
|
|
raise forms.ValidationError(
|
|
"The target tag cannot be selected for merging."
|
|
)
|
|
|
|
return merge_tags
|
|
|
|
|
|
class BookmarkBundleForm(forms.ModelForm):
|
|
name = forms.CharField(max_length=256, widget=FormInput)
|
|
search = forms.CharField(max_length=256, required=False, widget=FormInput)
|
|
any_tags = forms.CharField(required=False, widget=TagAutocomplete)
|
|
all_tags = forms.CharField(required=False, widget=TagAutocomplete)
|
|
excluded_tags = forms.CharField(required=False, widget=TagAutocomplete)
|
|
|
|
class Meta:
|
|
model = BookmarkBundle
|
|
fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs, error_class=FormErrorList)
|
|
|
|
|
|
class BookmarkSearchForm(forms.Form):
|
|
SORT_CHOICES = [
|
|
(BookmarkSearch.SORT_ADDED_ASC, "Added ↑"),
|
|
(BookmarkSearch.SORT_ADDED_DESC, "Added ↓"),
|
|
(BookmarkSearch.SORT_TITLE_ASC, "Title ↑"),
|
|
(BookmarkSearch.SORT_TITLE_DESC, "Title ↓"),
|
|
]
|
|
FILTER_SHARED_CHOICES = [
|
|
(BookmarkSearch.FILTER_SHARED_OFF, "Off"),
|
|
(BookmarkSearch.FILTER_SHARED_SHARED, "Shared"),
|
|
(BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"),
|
|
]
|
|
FILTER_UNREAD_CHOICES = [
|
|
(BookmarkSearch.FILTER_UNREAD_OFF, "Off"),
|
|
(BookmarkSearch.FILTER_UNREAD_YES, "Unread"),
|
|
(BookmarkSearch.FILTER_UNREAD_NO, "Read"),
|
|
]
|
|
|
|
q = forms.CharField()
|
|
user = forms.ChoiceField(required=False, widget=FormSelect)
|
|
bundle = forms.CharField(required=False)
|
|
sort = forms.ChoiceField(choices=SORT_CHOICES, widget=FormSelect)
|
|
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
|
|
unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect)
|
|
modified_since = forms.CharField(required=False)
|
|
added_since = forms.CharField(required=False)
|
|
|
|
def __init__(
|
|
self,
|
|
search: BookmarkSearch,
|
|
editable_fields: list[str] = None,
|
|
users: list[User] = None,
|
|
):
|
|
super().__init__()
|
|
editable_fields = editable_fields or []
|
|
self.editable_fields = editable_fields
|
|
|
|
# set choices for user field if users are provided
|
|
if users:
|
|
user_choices = [(user.username, user.username) for user in users]
|
|
user_choices.insert(0, ("", "Everyone"))
|
|
self.fields["user"].choices = user_choices
|
|
|
|
for param in search.params:
|
|
# set initial values for modified params
|
|
value = search.__dict__.get(param)
|
|
if isinstance(value, models.Model):
|
|
self.fields[param].initial = value.id
|
|
else:
|
|
self.fields[param].initial = value
|
|
|
|
# Mark non-editable modified fields as hidden. That way, templates
|
|
# rendering a form can just loop over hidden_fields to ensure that
|
|
# all necessary search options are kept when submitting the form.
|
|
if search.is_modified(param) and param not in editable_fields:
|
|
self.fields[param].widget = forms.HiddenInput()
|
|
|
|
|
|
class UserProfileForm(forms.ModelForm):
|
|
class Meta:
|
|
model = UserProfile
|
|
fields = [
|
|
"theme",
|
|
"bookmark_date_display",
|
|
"bookmark_description_display",
|
|
"bookmark_description_max_lines",
|
|
"bookmark_link_target",
|
|
"web_archive_integration",
|
|
"tag_search",
|
|
"tag_grouping",
|
|
"enable_sharing",
|
|
"enable_public_sharing",
|
|
"enable_favicons",
|
|
"enable_preview_images",
|
|
"enable_automatic_html_snapshots",
|
|
"display_url",
|
|
"display_view_bookmark_action",
|
|
"display_edit_bookmark_action",
|
|
"display_archive_bookmark_action",
|
|
"display_remove_bookmark_action",
|
|
"permanent_notes",
|
|
"default_mark_unread",
|
|
"default_mark_shared",
|
|
"custom_css",
|
|
"auto_tagging_rules",
|
|
"items_per_page",
|
|
"sticky_pagination",
|
|
"collapse_side_panel",
|
|
"hide_bundles",
|
|
"legacy_search",
|
|
]
|
|
widgets = {
|
|
"theme": FormSelect,
|
|
"bookmark_date_display": FormSelect,
|
|
"bookmark_description_display": FormSelect,
|
|
"bookmark_description_max_lines": FormNumberInput,
|
|
"bookmark_link_target": FormSelect,
|
|
"web_archive_integration": FormSelect,
|
|
"tag_search": FormSelect,
|
|
"tag_grouping": FormSelect,
|
|
"auto_tagging_rules": FormTextarea,
|
|
"custom_css": FormTextarea,
|
|
"items_per_page": FormNumberInput,
|
|
"display_url": FormCheckbox,
|
|
"permanent_notes": FormCheckbox,
|
|
"display_view_bookmark_action": FormCheckbox,
|
|
"display_edit_bookmark_action": FormCheckbox,
|
|
"display_archive_bookmark_action": FormCheckbox,
|
|
"display_remove_bookmark_action": FormCheckbox,
|
|
"sticky_pagination": FormCheckbox,
|
|
"collapse_side_panel": FormCheckbox,
|
|
"hide_bundles": FormCheckbox,
|
|
"legacy_search": FormCheckbox,
|
|
"enable_favicons": FormCheckbox,
|
|
"enable_preview_images": FormCheckbox,
|
|
"enable_sharing": FormCheckbox,
|
|
"enable_public_sharing": FormCheckbox,
|
|
"enable_automatic_html_snapshots": FormCheckbox,
|
|
"default_mark_unread": FormCheckbox,
|
|
"default_mark_shared": FormCheckbox,
|
|
}
|
|
|
|
|
|
class GlobalSettingsForm(forms.ModelForm):
|
|
class Meta:
|
|
model = GlobalSettings
|
|
fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]
|
|
widgets = {
|
|
"landing_page": FormSelect,
|
|
"guest_profile_user": FormSelect,
|
|
"enable_link_prefetch": FormCheckbox,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields["guest_profile_user"].empty_label = "Standard profile"
|