Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 183 additions & 17 deletions bookmarks/forms.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
from django import forms
from django.forms.utils import ErrorList
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


class CustomErrorList(ErrorList):
template_name = "shared/error_list.html"
from bookmarks.widgets import (
FormCheckbox,
FormErrorList,
FormInput,
FormNumberInput,
FormSelect,
FormTextarea,
TagAutocomplete,
)


class BookmarkForm(forms.ModelForm):
# Use URLField for URL
url = forms.CharField(validators=[BookmarkURLValidator()])
tag_string = forms.CharField(required=False)
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)
description = forms.CharField(required=False, widget=forms.Textarea())
unread = forms.BooleanField(required=False)
shared = forms.BooleanField(required=False)
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)
auto_close = forms.CharField(required=False, widget=forms.HiddenInput)

class Meta:
model = Bookmark
Expand Down Expand Up @@ -62,7 +73,7 @@ def __init__(self, request: HttpRequest, instance: Bookmark = None):
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=CustomErrorList
data, instance=instance, initial=initial, error_class=FormErrorList
)

@property
Expand Down Expand Up @@ -111,12 +122,14 @@ def convert_tag_string(tag_string: str):


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=CustomErrorList)
super().__init__(*args, **kwargs, error_class=FormErrorList)
self.user = user

def clean_name(self):
Expand Down Expand Up @@ -146,11 +159,11 @@ def save(self, commit=True):


class TagMergeForm(forms.Form):
target_tag = forms.CharField()
merge_tags = forms.CharField()
target_tag = forms.CharField(widget=TagAutocomplete)
merge_tags = forms.CharField(widget=TagAutocomplete)

def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs, error_class=CustomErrorList)
super().__init__(*args, **kwargs, error_class=FormErrorList)
self.user = user

def clean_target_tag(self):
Expand Down Expand Up @@ -197,3 +210,156 @@ def clean_merge_tags(self):
)

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"
3 changes: 2 additions & 1 deletion bookmarks/frontend/components/tag-autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class TagAutocomplete extends TurboLitElement {
inputId: { type: String, attribute: "input-id" },
inputName: { type: String, attribute: "input-name" },
inputValue: { type: String, attribute: "input-value" },
inputClass: { type: String, attribute: "input-class" },
inputPlaceholder: { type: String, attribute: "input-placeholder" },
inputAriaDescribedBy: { type: String, attribute: "input-aria-describedby" },
variant: { type: String },
Expand Down Expand Up @@ -160,7 +161,7 @@ export class TagAutocomplete extends TurboLitElement {
name="${this.inputName || nothing}"
.value="${this.inputValue || ""}"
placeholder="${this.inputPlaceholder || " "}"
class="form-input"
class="form-input ${this.inputClass || ""}"
type="text"
autocomplete="off"
autocapitalize="off"
Expand Down
8 changes: 4 additions & 4 deletions bookmarks/frontend/utils/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ document.addEventListener("turbo:render", () => {
});

document.addEventListener("turbo:before-morph-element", (event) => {
if (event.target instanceof TurboLitElement) {
// Prevent Turbo from morphing Lit elements, which would remove rendered
// contents. For now this means that any Lit element / widget can not be
// updated from the server when using morphing.
const parent = event.target?.parentElement;
if (parent instanceof TurboLitElement) {
// Prevent Turbo from morphing Lit elements contents, which would remove
// elements rendered on the client side.
event.preventDefault();
}
});
Expand Down
Loading