mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-28 06:53:12 +08:00
Align form usages in templates (#1267)
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
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,
|
||||
@@ -12,23 +17,29 @@ from bookmarks.models import (
|
||||
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
|
||||
@@ -62,7 +73,7 @@ class BookmarkForm(forms.ModelForm):
|
||||
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
|
||||
@@ -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):
|
||||
@@ -146,11 +159,11 @@ class TagForm(forms.ModelForm):
|
||||
|
||||
|
||||
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):
|
||||
@@ -197,3 +210,156 @@ class TagMergeForm(forms.Form):
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
@@ -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 },
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator
|
||||
@@ -195,12 +194,6 @@ class BookmarkBundle(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class BookmarkBundleForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BookmarkBundle
|
||||
fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"]
|
||||
|
||||
|
||||
class BookmarkSearch:
|
||||
SORT_ADDED_ASC = "added_asc"
|
||||
SORT_ADDED_DESC = "added_desc"
|
||||
@@ -323,64 +316,6 @@ class BookmarkSearch:
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
bundle = forms.CharField(required=False)
|
||||
sort = forms.ChoiceField(choices=SORT_CHOICES)
|
||||
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 UserProfile(models.Model):
|
||||
THEME_AUTO = "auto"
|
||||
THEME_LIGHT = "light"
|
||||
@@ -507,41 +442,6 @@ class UserProfile(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
@@ -640,13 +540,3 @@ class GlobalSettings(models.Model):
|
||||
if not self.pk and GlobalSettings.objects.exists():
|
||||
raise Exception("There is already one instance of GlobalSettings")
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class GlobalSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = GlobalSettings
|
||||
fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["guest_profile_user"].empty_label = "Standard profile"
|
||||
|
||||
@@ -39,7 +39,6 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"widget_tweaks",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"huey.contrib.djhuey",
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.is-error) {
|
||||
background: var(--error-color-shade);
|
||||
border-color: var(--error-color);
|
||||
|
||||
&.is-focused {
|
||||
outline-color: var(--error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
|
||||
@@ -135,6 +135,10 @@ textarea.form-input {
|
||||
.is-error + & {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Form element: Select */
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
<div class="bookmarks-form">
|
||||
{% csrf_token %}
|
||||
{{ form.auto_close|attr:"type:hidden" }}
|
||||
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
|
||||
{{ form.auto_close }}
|
||||
<div class="form-group">
|
||||
{% formlabel form.url "URL" %}
|
||||
<div class="has-icon-right">
|
||||
{{ form.url|form_field:"validation"|add_class:"form-input"|attr:"autofocus" }}
|
||||
{% formfield form.url autofocus=True %}
|
||||
<i class="form-icon loading"></i>
|
||||
</div>
|
||||
{% if form.url.errors %}<div class="form-input-hint">{{ form.url.errors }}</div>{% endif %}
|
||||
{{ form.url.errors }}
|
||||
<div class="form-input-hint bookmark-exists">
|
||||
This URL is already bookmarked.
|
||||
The form has been pre-filled with the existing bookmark, and saving the form will update the existing bookmark.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tag_string.id_for_label }}" class="form-label">Tags</label>
|
||||
<ld-tag-autocomplete input-id="{{ form.tag_string.auto_id }}"
|
||||
input-name="{{ form.tag_string.html_name }}"
|
||||
input-value="{{ form.tag_string.value|default_if_none:'' }}"
|
||||
input-aria-describedby="{{ form.tag_string.auto_id }}_help">
|
||||
</ld-tag-autocomplete>
|
||||
<div id="{{ form.tag_string.auto_id }}_help" class="form-input-hint">
|
||||
{% formlabel form.tag_string "Tags" %}
|
||||
{% formfield form.tag_string has_help=True %}
|
||||
{% formhelp form.tag_string %}
|
||||
Enter any number of tags separated by space and <strong>without</strong> the hash (#).
|
||||
If a tag does not exist it will be automatically created.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
<div class="form-input-hint auto-tags"></div>
|
||||
{{ form.tag_string.errors }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex justify-between align-baseline">
|
||||
<label for="{{ form.title.id_for_label }}" class="form-label">Title</label>
|
||||
{% formlabel form.title "Title" %}
|
||||
<div class="flex">
|
||||
<button id="refresh-button" class="btn btn-link suffix-button" type="button">Refresh from website</button>
|
||||
<ld-clear-button data-for="{{ form.title.id_for_label }}">
|
||||
@@ -40,18 +34,16 @@
|
||||
</ld-clear-button>
|
||||
</div>
|
||||
</div>
|
||||
{{ form.title|add_class:"form-input"|attr:"autocomplete:off" }}
|
||||
{{ form.title.errors }}
|
||||
{% formfield form.title %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex justify-between align-baseline">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
|
||||
{% formlabel form.description "Description" %}
|
||||
<ld-clear-button data-for="{{ form.description.id_for_label }}">
|
||||
<button class="btn btn-link suffix-button" type="button">Clear</button>
|
||||
</ld-clear-button>
|
||||
</div>
|
||||
{{ form.description|add_class:"form-input"|attr:"rows:3" }}
|
||||
{{ form.description.errors }}
|
||||
{% formfield form.description rows="3" %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<details class="notes"{% if form.has_notes %} open{% endif %}>
|
||||
@@ -59,35 +51,28 @@
|
||||
<span class="form-label d-inline-block">Notes</span>
|
||||
</summary>
|
||||
<label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label>
|
||||
{{ form.notes|form_field:"help"|add_class:"form-input"|attr:"rows:8" }}
|
||||
<div id="{{ form.notes.auto_id }}_help" class="form-input-hint">Additional notes, supports Markdown.</div>
|
||||
{% formfield form.notes rows="8" has_help=True %}
|
||||
{% formhelp form.notes %}
|
||||
Additional notes, supports Markdown.
|
||||
{% endformhelp %}
|
||||
</details>
|
||||
{{ form.notes.errors }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-checkbox">
|
||||
{{ form.unread|form_field:"help" }}
|
||||
<i class="form-icon"></i>
|
||||
<label for="{{ form.unread.id_for_label }}">Mark as unread</label>
|
||||
</div>
|
||||
<div id="{{ form.unread.auto_id }}_help" class="form-input-hint">
|
||||
{% formfield form.unread label="Mark as unread" has_help=True %}
|
||||
{% formhelp form.unread %}
|
||||
Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
{% if request.user_profile.enable_sharing %}
|
||||
<div class="form-group">
|
||||
<div class="form-checkbox">
|
||||
{{ form.shared|form_field:"help" }}
|
||||
<i class="form-icon"></i>
|
||||
<label for="{{ form.shared.id_for_label }}">Share</label>
|
||||
</div>
|
||||
<div id="{{ form.shared.auto_id }}_help" class="form-input-hint">
|
||||
{% formfield form.shared label="Share" has_help=True %}
|
||||
{% formhelp form.shared %}
|
||||
{% if request.user_profile.enable_public_sharing %}
|
||||
Share this bookmark with other registered users and anonymous users.
|
||||
{% else %}
|
||||
Share this bookmark with other registered users.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load static widget_tweaks %}
|
||||
{% load static shared %}
|
||||
<div class="search-container">
|
||||
<form id="search" action="" method="get" role="search">
|
||||
<ld-search-autocomplete input-name="q"
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="form-group">
|
||||
<label for="{{ preferences_form.sort.id_for_label }}"
|
||||
class="form-label{% if 'sort' in search.modified_params %} text-bold{% endif %}">Sort by</label>
|
||||
{{ preferences_form.sort|add_class:"form-select select-sm" }}
|
||||
{% formfield preferences_form.sort class='select-sm' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'shared' in preferences_form.editable_fields %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load shared %}
|
||||
<section aria-labelledby="user-heading">
|
||||
<div class="section-header">
|
||||
<h2 id="user-heading">User</h2>
|
||||
@@ -9,7 +9,7 @@
|
||||
{% for hidden_field in user_list.form.hidden_fields %}{{ hidden_field }}{% endfor %}
|
||||
<div class="form-group">
|
||||
<div class="d-flex">
|
||||
{% render_field user_list.form.user class+="form-select" data-submit-on-change="" %}
|
||||
{% formfield user_list.form.user data_submit_on_change="" %}
|
||||
<noscript>
|
||||
<button type="submit" class="btn btn-link ml-2">Apply</button>
|
||||
</noscript>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block head %}
|
||||
{% with page_title="Edit bundle - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="form-group {% if form.name.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">Name</label>
|
||||
{{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }}
|
||||
{% if form.name.errors %}<div class="form-input-hint">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="form-group {% if form.search.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.search.id_for_label }}" class="form-label">Search terms</label>
|
||||
{{ form.search|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }}
|
||||
{% if form.search.errors %}<div class="form-input-hint">{{ form.search.errors }}</div>{% endif %}
|
||||
<div class="form-input-hint">All of these search terms must be present in a bookmark to match.</div>
|
||||
{% load shared %}
|
||||
<div class="form-group">
|
||||
{% formlabel form.name "Name" %}
|
||||
{% formfield form.name %}
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.any_tags.id_for_label }}" class="form-label">Tags</label>
|
||||
<ld-tag-autocomplete input-id="{{ form.any_tags.auto_id }}"
|
||||
input-name="{{ form.any_tags.html_name }}"
|
||||
input-value="{{ form.any_tags.value|default_if_none:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">At least one of these tags must be present in a bookmark to match.</div>
|
||||
{% formlabel form.search "Search terms" %}
|
||||
{% formfield form.search has_help=True %}
|
||||
{{ form.search.errors }}
|
||||
{% formhelp form.search %}
|
||||
All of these search terms must be present in a bookmark to match.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.all_tags.id_for_label }}" class="form-label">Required tags</label>
|
||||
<ld-tag-autocomplete input-id="{{ form.all_tags.auto_id }}"
|
||||
input-name="{{ form.all_tags.html_name }}"
|
||||
input-value="{{ form.all_tags.value|default_if_none:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">All of these tags must be present in a bookmark to match.</div>
|
||||
{% formlabel form.any_tags "Tags" %}
|
||||
{% formfield form.any_tags has_help=True %}
|
||||
{% formhelp form.any_tags %}
|
||||
At least one of these tags must be present in a bookmark to match.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.excluded_tags.id_for_label }}" class="form-label">Excluded tags</label>
|
||||
<ld-tag-autocomplete input-id="{{ form.excluded_tags.auto_id }}"
|
||||
input-name="{{ form.excluded_tags.html_name }}"
|
||||
input-value="{{ form.excluded_tags.value|default_if_none:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">None of these tags must be present in a bookmark to match.</div>
|
||||
{% formlabel form.all_tags "Required tags" %}
|
||||
{% formfield form.all_tags has_help=True %}
|
||||
{% formhelp form.all_tags %}
|
||||
All of these tags must be present in a bookmark to match.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{% formlabel form.excluded_tags "Excluded tags" %}
|
||||
{% formfield form.excluded_tags has_help=True %}
|
||||
{% formhelp form.excluded_tags %}
|
||||
None of these tags must be present in a bookmark to match.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-footer d-flex mt-4">
|
||||
<input type="submit"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block head %}
|
||||
{% with page_title="New bundle - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block head %}
|
||||
{% with page_title="Registration complete - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block head %}
|
||||
{% with page_title="Registration - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
@@ -14,22 +13,22 @@
|
||||
{% csrf_token %}
|
||||
<div class="form-group {% if form.errors.username %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
|
||||
{{ form.username|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{{ form.username }}
|
||||
<div class="form-input-hint">{{ form.errors.username }}</div>
|
||||
</div>
|
||||
<div class="form-group {% if form.errors.email %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.email.id_for_label }}">Email</label>
|
||||
{{ form.email|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{{ form.email }}
|
||||
<div class="form-input-hint">{{ form.errors.email }}</div>
|
||||
</div>
|
||||
<div class="form-group {% if form.errors.password1 %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.password1.id_for_label }}">Password</label>
|
||||
{{ form.password1|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{{ form.password1 }}
|
||||
<div class="form-input-hint">{{ form.errors.password1 }}</div>
|
||||
</div>
|
||||
<div class="form-group {% if form.errors.password2 %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.password2.id_for_label }}">Confirm Password</label>
|
||||
{{ form.password2|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{{ form.password2 }}
|
||||
<div class="form-input-hint">{{ form.errors.password2 }}</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load shared %}
|
||||
{% block head %}
|
||||
{% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
@@ -11,17 +11,15 @@
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="form-group has-error">
|
||||
<p class="form-input-hint">Your username and password didn't match. Please try again.</p>
|
||||
</div>
|
||||
<p class="form-input-hint is-error">Your username and password didn't match. Please try again.</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
|
||||
{{ form.username|add_class:'form-input'|attr:'placeholder: ' }}
|
||||
{% formlabel form.username 'Username' %}
|
||||
{% formfield form.username class='form-input' %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="{{ form.password.id_for_label }}">Password</label>
|
||||
{{ form.password|add_class:'form-input'|attr:'placeholder: ' }}
|
||||
{% formlabel form.password 'Password' %}
|
||||
{% formfield form.password class='form-input' %}
|
||||
</div>
|
||||
<br />
|
||||
<div class="d-flex justify-between">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block head %}
|
||||
{% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load shared %}
|
||||
{% block head %}
|
||||
{% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
@@ -10,20 +10,20 @@
|
||||
</div>
|
||||
<form method="post" action="{% url 'change_password' %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group {% if form.old_password.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.old_password.id_for_label }}">Old password</label>
|
||||
{{ form.old_password|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.old_password.errors %}<div class="form-input-hint">{{ form.old_password.errors }}</div>{% endif %}
|
||||
<div class="form-group">
|
||||
{% formlabel form.old_password 'Old password' %}
|
||||
{% formfield form.old_password class='form-input' %}
|
||||
{{ form.old_password.errors }}
|
||||
</div>
|
||||
<div class="form-group {% if form.new_password1.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.new_password1.id_for_label }}">New password</label>
|
||||
{{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.new_password1.errors %}<div class="form-input-hint">{{ form.new_password1.errors }}</div>{% endif %}
|
||||
<div class="form-group">
|
||||
{% formlabel form.new_password1 'New password' %}
|
||||
{% formfield form.new_password1 class='form-input' %}
|
||||
{{ form.new_password1.errors }}
|
||||
</div>
|
||||
<div class="form-group {% if form.new_password2.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.new_password2.id_for_label }}">Confirm new password</label>
|
||||
{{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.new_password2.errors %}<div class="form-input-hint">{{ form.new_password2.errors }}</div>{% endif %}
|
||||
<div class="form-group">
|
||||
{% formlabel form.new_password2 'Confirm new password' %}
|
||||
{% formfield form.new_password2 class='form-input' %}
|
||||
{{ form.new_password2.errors }}
|
||||
</div>
|
||||
<br />
|
||||
<input type="submit"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "shared/layout.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load shared %}
|
||||
{% block head %}
|
||||
{% with page_title="Settings - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
@@ -20,154 +20,117 @@
|
||||
data-turbo="false">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
|
||||
{{ form.theme|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.theme "Theme" %}
|
||||
{% formfield form.theme has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.theme %}
|
||||
Whether to use a light or dark theme, or automatically adjust the theme based on your system's settings.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_date_display.id_for_label }}"
|
||||
class="form-label">Bookmark date format</label>
|
||||
{{ form.bookmark_date_display|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.bookmark_date_display "Bookmark date format" %}
|
||||
{% formfield form.bookmark_date_display has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.bookmark_date_display %}
|
||||
Whether to show bookmark dates as relative (how long ago), or as absolute dates. Alternatively the date can
|
||||
be hidden.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_description_display.id_for_label }}"
|
||||
class="form-label">
|
||||
Bookmark
|
||||
description
|
||||
</label>
|
||||
{{ form.bookmark_description_display|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.bookmark_description_display "Bookmark description" %}
|
||||
{% formfield form.bookmark_description_display has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.bookmark_description_display %}
|
||||
Whether to show bookmark descriptions and tags in the same line, or as separate blocks.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group {% if request.user_profile.bookmark_description_display == 'inline' %}d-hide{% endif %}">
|
||||
<label for="{{ form.bookmark_description_max_lines.id_for_label }}"
|
||||
class="form-label">
|
||||
Bookmark description
|
||||
max lines
|
||||
</label>
|
||||
{{ form.bookmark_description_max_lines|add_class:"form-input width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">Limits the number of lines that are displayed for the bookmark description.</div>
|
||||
{% formlabel form.bookmark_description_max_lines "Bookmark description max lines" %}
|
||||
{% formfield form.bookmark_description_max_lines has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.bookmark_description_max_lines %}
|
||||
Limits the number of lines that are displayed for the bookmark description.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.display_url.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_url }}
|
||||
<i class="form-icon"></i> Show bookmark URL
|
||||
</label>
|
||||
<div class="form-input-hint">When enabled, this setting displays the bookmark URL below the title.</div>
|
||||
{% formfield form.display_url label="Show bookmark URL" has_help=True %}
|
||||
{% formhelp form.display_url %}
|
||||
When enabled, this setting displays the bookmark URL below the title.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.permanent_notes.id_for_label }}" class="form-checkbox">
|
||||
{{ form.permanent_notes }}
|
||||
<i class="form-icon"></i> Show notes permanently
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.permanent_notes label="Show notes permanently" has_help=True %}
|
||||
{% formhelp form.permanent_notes %}
|
||||
Whether to show bookmark notes permanently, without having to toggle them individually.
|
||||
Alternatively the keyboard shortcut <code>e</code> can be used to temporarily show all notes.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Bookmark actions</label>
|
||||
<label for="{{ form.display_view_bookmark_action.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.display_view_bookmark_action }}
|
||||
<i class="form-icon"></i> View
|
||||
</label>
|
||||
<label for="{{ form.display_edit_bookmark_action.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.display_edit_bookmark_action }}
|
||||
<i class="form-icon"></i> Edit
|
||||
</label>
|
||||
<label for="{{ form.display_archive_bookmark_action.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.display_archive_bookmark_action }}
|
||||
<i class="form-icon"></i> Archive
|
||||
</label>
|
||||
<label for="{{ form.display_remove_bookmark_action.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.display_remove_bookmark_action }}
|
||||
<i class="form-icon"></i> Remove
|
||||
</label>
|
||||
<span class="form-label">Bookmark actions</span>
|
||||
{% formfield form.display_view_bookmark_action label="View" %}
|
||||
{% formfield form.display_edit_bookmark_action label="Edit" %}
|
||||
{% formfield form.display_archive_bookmark_action label="Archive" %}
|
||||
{% formfield form.display_remove_bookmark_action label="Remove" %}
|
||||
<div class="form-input-hint">Which actions to display for each bookmark.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
|
||||
{{ form.bookmark_link_target|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">Whether to open bookmarks a new page or in the same page.</div>
|
||||
</div>
|
||||
<div class="form-group{% if form.items_per_page.errors %} has-error{% endif %}">
|
||||
<label for="{{ form.items_per_page.id_for_label }}" class="form-label">Items per page</label>
|
||||
{{ form.items_per_page|add_class:"form-input width-25 width-sm-100"|attr:"min:10" }}
|
||||
{% if form.items_per_page.errors %}
|
||||
<div class="form-input-hint is-error">{{ form.items_per_page.errors }}</div>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
<div class="form-input-hint">The number of bookmarks to display per page.</div>
|
||||
{% formlabel form.bookmark_link_target "Open bookmarks in" %}
|
||||
{% formfield form.bookmark_link_target has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.bookmark_link_target %}
|
||||
Whether to open bookmarks a new page or in the same page.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.sticky_pagination.id_for_label }}" class="form-checkbox">
|
||||
{{ form.sticky_pagination }}
|
||||
<i class="form-icon"></i> Sticky pagination
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.items_per_page "Items per page" %}
|
||||
{% formfield form.items_per_page has_help=True class="width-25 width-sm-100" min="10" %}
|
||||
{{ form.items_per_page.errors }}
|
||||
{% formhelp form.items_per_page %}
|
||||
The number of bookmarks to display per page.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{% formfield form.sticky_pagination label="Sticky pagination" has_help=True %}
|
||||
{% formhelp form.sticky_pagination %}
|
||||
When enabled, the pagination controls will stick to the bottom of the screen, so that they are always
|
||||
visible without having to scroll to the end of the page first.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.collapse_side_panel.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.collapse_side_panel }}
|
||||
<i class="form-icon"></i> Collapse side panel
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.collapse_side_panel label="Collapse side panel" has_help=True %}
|
||||
{% formhelp form.collapse_side_panel %}
|
||||
When enabled, the tags side panel will be collapsed by default to give more space to the bookmark list.
|
||||
Instead, the tags are shown in an expandable drawer.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.hide_bundles.id_for_label }}" class="form-checkbox">
|
||||
{{ form.hide_bundles }}
|
||||
<i class="form-icon"></i> Hide bundles
|
||||
</label>
|
||||
<div class="form-input-hint">Allows to hide the bundles in the side panel if you don't intend to use them.</div>
|
||||
{% formfield form.hide_bundles label="Hide bundles" has_help=True %}
|
||||
{% formhelp form.hide_bundles %}
|
||||
Allows to hide the bundles in the side panel if you don't intend to use them.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
|
||||
{{ form.tag_search|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.tag_search "Tag search" %}
|
||||
{% formfield form.tag_search has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.tag_search %}
|
||||
In strict mode, tags must be prefixed with a hash character (#).
|
||||
In lax mode, tags can also be searched without the hash character.
|
||||
Note that tags without the hash character are indistinguishable from search terms, which means the search
|
||||
result will also include bookmarks where a search term matches otherwise.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.legacy_search.id_for_label }}" class="form-checkbox">
|
||||
{{ form.legacy_search }}
|
||||
<i class="form-icon"></i> Enable legacy search
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.legacy_search label="Enable legacy search" has_help=True %}
|
||||
{% formhelp form.legacy_search %}
|
||||
Since version 1.44.0, linkding has a new search engine that supports logical expressions (and, or, not).
|
||||
If you run into any issues with the new search, you can enable this option to temporarily switch back to the old search.
|
||||
Please report any issues you encounter with the new search on <a href="https://github.com/sissbruecker/linkding/issues"
|
||||
target="_blank">GitHub</a> so they can be addressed.
|
||||
This option will be removed in a future version.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tag_grouping.id_for_label }}" class="form-label">Tag grouping</label>
|
||||
{{ form.tag_grouping|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.tag_grouping "Tag grouping" %}
|
||||
{% formfield form.tag_grouping has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.tag_grouping %}
|
||||
In alphabetical mode, tags will be grouped by the first letter.
|
||||
If disabled, tags will not be grouped.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<details {% if form.auto_tagging_rules.value %}open{% endif %}>
|
||||
@@ -176,21 +139,18 @@
|
||||
</summary>
|
||||
<label for="{{ form.auto_tagging_rules.id_for_label }}"
|
||||
class="text-assistive">Auto Tagging</label>
|
||||
<div>{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }}</div>
|
||||
<div>{% formfield form.auto_tagging_rules has_help=True class="monospace" rows="6" %}</div>
|
||||
</details>
|
||||
<div class="form-input-hint">
|
||||
{% formhelp form.auto_tagging_rules %}
|
||||
Automatically adds tags to bookmarks based on predefined rules.
|
||||
Each line is a single rule that maps a URL to one or more tags. For example:
|
||||
<pre>youtube.com video
|
||||
reddit.com/r/Music music reddit</pre>
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_favicons.id_for_label }}" class="form-checkbox">
|
||||
{{ form.enable_favicons }}
|
||||
<i class="form-icon"></i> Enable Favicons
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.enable_favicons label="Enable Favicons" has_help=True %}
|
||||
{% formhelp form.enable_favicons %}
|
||||
Automatically loads favicons for bookmarked websites and displays them next to each bookmark.
|
||||
Enabling this feature automatically downloads all missing favicons.
|
||||
By default, this feature uses a <b>Google service</b> to download favicons.
|
||||
@@ -198,95 +158,68 @@ reddit.com/r/Music music reddit</pre>
|
||||
<a href="https://linkding.link/options/#ld_favicon_provider"
|
||||
target="_blank">options documentation</a> on how to configure a custom favicon provider.
|
||||
Icons are downloaded in the background, and it may take a while for them to show up.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
{% if request.user_profile.enable_favicons and enable_refresh_favicons %}
|
||||
<button class="btn mt-2" name="refresh_favicons">Refresh Favicons</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_preview_images.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.enable_preview_images }}
|
||||
<i class="form-icon"></i> Enable Preview Images
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.enable_preview_images label="Enable Preview Images" has_help=True %}
|
||||
{% formhelp form.enable_preview_images %}
|
||||
Automatically loads preview images for bookmarked websites and displays them next to each bookmark.
|
||||
Enabling this feature automatically downloads all missing preview images.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.web_archive_integration.id_for_label }}"
|
||||
class="form-label">
|
||||
Internet Archive
|
||||
integration
|
||||
</label>
|
||||
{{ form.web_archive_integration|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel form.web_archive_integration "Internet Archive integration" %}
|
||||
{% formfield form.web_archive_integration has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp form.web_archive_integration %}
|
||||
Enabling this feature will automatically create snapshots of bookmarked websites on the
|
||||
<a href="https://web.archive.org/" target="_blank" rel="noopener">Internet Archive Wayback Machine</a>.
|
||||
This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in
|
||||
case it goes offline or its content is modified.
|
||||
Please consider donating to the <a href="https://archive.org/donate" target="_blank" rel="noopener">Internet Archive</a> if you make use of this feature.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_sharing.id_for_label }}" class="form-checkbox">
|
||||
{{ form.enable_sharing }}
|
||||
<i class="form-icon"></i> Enable bookmark sharing
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.enable_sharing label="Enable bookmark sharing" has_help=True %}
|
||||
{% formhelp form.enable_sharing %}
|
||||
Allows to share bookmarks with other users, and to view shared bookmarks.
|
||||
Disabling this feature will hide all previously shared bookmarks from other users.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_public_sharing.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.enable_public_sharing }}
|
||||
<i class="form-icon"></i> Enable public bookmark sharing
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.enable_public_sharing label="Enable public bookmark sharing" has_help=True %}
|
||||
{% formhelp form.enable_public_sharing %}
|
||||
Makes shared bookmarks publicly accessible, without requiring a login.
|
||||
That means that anyone with a link to this instance can view shared bookmarks via the <a href="{% url 'linkding:bookmarks.shared' %}">shared bookmarks page</a>.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
{% if has_snapshot_support %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_automatic_html_snapshots.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.enable_automatic_html_snapshots }}
|
||||
<i class="form-icon"></i> Automatically create HTML snapshots
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.enable_automatic_html_snapshots label="Automatically create HTML snapshots" has_help=True %}
|
||||
{% formhelp form.enable_automatic_html_snapshots %}
|
||||
Automatically creates HTML snapshots when adding bookmarks. Alternatively, when disabled, snapshots can be
|
||||
created manually in the details view of a bookmark.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
<button class="btn mt-2" name="create_missing_html_snapshots">Create missing HTML snapshots</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.default_mark_unread.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.default_mark_unread }}
|
||||
<i class="form-icon"></i> Create bookmarks as unread by default
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.default_mark_unread label="Create bookmarks as unread by default" has_help=True %}
|
||||
{% formhelp form.default_mark_unread %}
|
||||
Sets the default state for the "Mark as unread" option when creating a new bookmark.
|
||||
Setting this option will make all new bookmarks default to unread.
|
||||
This can be overridden when creating each new bookmark.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.default_mark_shared.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ form.default_mark_shared }}
|
||||
<i class="form-icon"></i> Create bookmarks as shared by default
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield form.default_mark_shared label="Create bookmarks as shared by default" has_help=True %}
|
||||
{% formhelp form.default_mark_shared %}
|
||||
Sets the default state for the "Share" option when creating a new bookmark.
|
||||
Setting this option will make all new bookmarks default to shared.
|
||||
This can be overridden when creating each new bookmark.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<details {% if form.custom_css.value %}open{% endif %}>
|
||||
@@ -294,9 +227,11 @@ reddit.com/r/Music music reddit</pre>
|
||||
<span class="form-label d-inline-block">Custom CSS</span>
|
||||
</summary>
|
||||
<label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
|
||||
<div>{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}</div>
|
||||
<div>{% formfield form.custom_css has_help=True class="monospace" rows="6" %}</div>
|
||||
</details>
|
||||
<div class="form-input-hint">Allows to add custom CSS to the page.</div>
|
||||
{% formhelp form.custom_css %}
|
||||
Allows to add custom CSS to the page.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit"
|
||||
@@ -316,34 +251,27 @@ reddit.com/r/Music music reddit</pre>
|
||||
data-turbo="false">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="{{ global_settings_form.landing_page.id_for_label }}"
|
||||
class="form-label">Landing page</label>
|
||||
{{ global_settings_form.landing_page|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">The page that unauthenticated users are redirected to when accessing the root URL.</div>
|
||||
{% formlabel global_settings_form.landing_page "Landing page" %}
|
||||
{% formfield global_settings_form.landing_page has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp global_settings_form.landing_page %}
|
||||
The page that unauthenticated users are redirected to when accessing the root URL.
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ global_settings_form.guest_profile_user.id_for_label }}"
|
||||
class="form-label">
|
||||
Guest user
|
||||
profile
|
||||
</label>
|
||||
{{ global_settings_form.guest_profile_user|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
{% formlabel global_settings_form.guest_profile_user "Guest user profile" %}
|
||||
{% formfield global_settings_form.guest_profile_user has_help=True class="width-25 width-sm-100" %}
|
||||
{% formhelp global_settings_form.guest_profile_user %}
|
||||
The user profile to use for users that are not logged in. This will affect how publicly shared bookmarks
|
||||
are displayed regarding theme, bookmark list settings, etc. You can either use your own profile or create
|
||||
a dedicated user for this purpose. By default, a standard profile with fixed settings is used.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ global_settings_form.enable_link_prefetch.id_for_label }}"
|
||||
class="form-checkbox">
|
||||
{{ global_settings_form.enable_link_prefetch }}
|
||||
<i class="form-icon"></i> Enable prefetching links on hover
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
{% formfield global_settings_form.enable_link_prefetch label="Enable prefetching links on hover" has_help=True %}
|
||||
{% formhelp global_settings_form.enable_link_prefetch %}
|
||||
Prefetches internal links when hovering over them. This can improve the perceived performance when
|
||||
navigating application, but also increases the load on the server as well as bandwidth usage.
|
||||
</div>
|
||||
{% endformhelp %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit"
|
||||
@@ -367,10 +295,13 @@ reddit.com/r/Music music reddit</pre>
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="import_map_private_flag" class="form-checkbox">
|
||||
<input type="checkbox" id="import_map_private_flag" name="map_private_flag">
|
||||
<input type="checkbox"
|
||||
id="import_map_private_flag"
|
||||
name="map_private_flag"
|
||||
aria-describedby="import_map_private_flag_help">
|
||||
<i class="form-icon"></i> Import public bookmarks as shared
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
<div id="import_map_private_flag_help" class="form-input-hint">
|
||||
When importing bookmarks from a service that supports marking bookmarks as public or private (using the
|
||||
<code>PRIVATE</code> attribute), enabling this option will import all bookmarks that are marked as not
|
||||
private as shared bookmarks.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{# Force rendering validation errors in English language to align with the rest of the app #}
|
||||
{% language 'en-us' %}
|
||||
{% if errors %}
|
||||
<ul class="{{ error_class }}"
|
||||
<ul class="{{ error_class }} form-input-hint is-error"
|
||||
{% if errors.field_id %}id="{{ errors.field_id }}_error"{% endif %}>
|
||||
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="form-group {% if form.name.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.name.id_for_label }}">Name</label>
|
||||
{{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: "|attr:"autofocus" }}
|
||||
<div class="form-input-hint">
|
||||
{% load shared %}
|
||||
<div class="form-group">
|
||||
{% formlabel form.name "Name" %}
|
||||
{% formfield form.name has_help=True autofocus=True %}
|
||||
{% formhelp form.name %}
|
||||
Tag names are case-insensitive and cannot contain spaces (spaces will be replaced with hyphens).
|
||||
</div>
|
||||
{% if form.name.errors %}<div class="form-input-hint">{{ form.name.errors }}</div>{% endif %}
|
||||
{% endformhelp %}
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load shared %}
|
||||
<turbo-frame id="tag-modal">
|
||||
<form method="post"
|
||||
action="{% url 'linkding:tags.merge' %}"
|
||||
@@ -23,28 +23,22 @@
|
||||
<li>The merged tags are deleted</li>
|
||||
</ol>
|
||||
</details>
|
||||
<div class="form-group {% if form.target_tag.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.target_tag.id_for_label }}" class="form-label">Target tag</label>
|
||||
<ld-tag-autocomplete input-id="{{ form.target_tag.auto_id }}"
|
||||
input-name="{{ form.target_tag.html_name }}"
|
||||
input-value="{{ form.target_tag.value|default_if_none:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">
|
||||
<div class="form-group">
|
||||
{% formlabel form.target_tag "Target tag" %}
|
||||
{% formfield form.target_tag has_help=True %}
|
||||
{% formhelp form.target_tag %}
|
||||
Enter the name of the tag you want to keep. The tags entered below will be merged into this one.
|
||||
</div>
|
||||
{% if form.target_tag.errors %}<div class="form-input-hint">{{ form.target_tag.errors }}</div>{% endif %}
|
||||
{% endformhelp %}
|
||||
{{ form.target_tag.errors }}
|
||||
</div>
|
||||
<div class="form-group {% if form.merge_tags.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.merge_tags.id_for_label }}" class="form-label">Tags to merge</label>
|
||||
<ld-tag-autocomplete input-id="{{ form.merge_tags.auto_id }}"
|
||||
input-name="{{ form.merge_tags.html_name }}"
|
||||
input-value="{{ form.merge_tags.value|default_if_none:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">
|
||||
<div class="form-group">
|
||||
{% formlabel form.merge_tags "Tags to merge" %}
|
||||
{% formfield form.merge_tags has_help=True %}
|
||||
{% formhelp form.merge_tags %}
|
||||
Enter the names of tags to merge into the target tag, separated by spaces.
|
||||
These tags will be deleted after merging.
|
||||
</div>
|
||||
{% if form.merge_tags.errors %}<div class="form-input-hint">{{ form.merge_tags.errors }}</div>{% endif %}
|
||||
{% endformhelp %}
|
||||
{{ form.merge_tags.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from django import template
|
||||
|
||||
from bookmarks.models import (
|
||||
BookmarkSearch,
|
||||
BookmarkSearchForm,
|
||||
)
|
||||
from bookmarks.forms import BookmarkSearchForm
|
||||
from bookmarks.models import BookmarkSearch
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from bookmarks import utils
|
||||
from bookmarks.widgets import FormCheckbox
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -102,20 +103,60 @@ def append_attr(widget, attr, value):
|
||||
attrs[attr] = value
|
||||
|
||||
|
||||
@register.filter("form_field")
|
||||
def form_field(field, modifier_string):
|
||||
modifiers = modifier_string.split(",")
|
||||
@register.simple_tag
|
||||
def formlabel(field, label_text):
|
||||
return mark_safe(
|
||||
f'<label for="{field.id_for_label}" class="form-label">{label_text}</label>'
|
||||
)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def formfield(field, **kwargs):
|
||||
widget = field.field.widget
|
||||
|
||||
label = kwargs.pop("label", None)
|
||||
if label and isinstance(widget, FormCheckbox):
|
||||
widget.label = label
|
||||
|
||||
if kwargs.pop("has_help", False):
|
||||
append_attr(widget, "aria-describedby", field.auto_id + "_help")
|
||||
|
||||
has_errors = hasattr(field, "errors") and field.errors
|
||||
|
||||
if "validation" in modifiers and has_errors:
|
||||
append_attr(field.field.widget, "aria-describedby", field.auto_id + "_error")
|
||||
if "help" in modifiers:
|
||||
append_attr(field.field.widget, "aria-describedby", field.auto_id + "_help")
|
||||
|
||||
# Some assistive technologies announce a field as invalid when it has the
|
||||
# required attribute, even if the user has not interacted with the field
|
||||
# yet. Set aria-invalid false to prevent this behavior.
|
||||
if has_errors:
|
||||
append_attr(widget, "class", "is-error")
|
||||
append_attr(widget, "aria-describedby", field.auto_id + "_error")
|
||||
if field.field.required and not has_errors:
|
||||
append_attr(field.field.widget, "aria-invalid", "false")
|
||||
append_attr(widget, "aria-invalid", "false")
|
||||
|
||||
return field
|
||||
for attr, value in kwargs.items():
|
||||
attr = attr.replace("_", "-")
|
||||
if attr == "class":
|
||||
append_attr(widget, "class", value)
|
||||
else:
|
||||
widget.attrs[attr] = value
|
||||
|
||||
return field.as_widget()
|
||||
|
||||
|
||||
@register.tag
|
||||
def formhelp(parser, token):
|
||||
try:
|
||||
tag_name, field_var = token.split_contents()
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
f"{token.contents.split()[0]!r} tag requires a single argument (form field)"
|
||||
) from None
|
||||
nodelist = parser.parse(("endformhelp",))
|
||||
parser.delete_first_token()
|
||||
return FormHelpNode(nodelist, field_var)
|
||||
|
||||
|
||||
class FormHelpNode(template.Node):
|
||||
def __init__(self, nodelist, field_var):
|
||||
self.nodelist = nodelist
|
||||
self.field_var = template.Variable(field_var)
|
||||
|
||||
def render(self, context):
|
||||
field = self.field_var.resolve(context)
|
||||
content = self.nodelist.render(context)
|
||||
return f'<div id="{field.auto_id}_help" class="form-input-hint">{content}</div>'
|
||||
|
||||
@@ -113,7 +113,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="text" name="url" aria-invalid="false" autofocus class="form-input" required id="id_url" value="{bookmark.url}">
|
||||
<input type="text" name="url" aria-invalid="false" autocomplete="off" autofocus class="form-input" required id="id_url" value="{bookmark.url}">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<input type="text" name="url" aria-invalid="false" autofocus class="form-input" required id="id_url" value="http://example.com">
|
||||
<input type="text" name="url" aria-invalid="false" autocomplete="off" autofocus class="form-input" required id="id_url" value="http://example.com">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks.models import BookmarkSearch, BookmarkSearchForm
|
||||
from bookmarks.forms import BookmarkSearchForm
|
||||
from bookmarks.models import BookmarkSearch
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="text" name="name" value="{bundle.name}"
|
||||
autocomplete="off" placeholder=" " class="form-input"
|
||||
maxlength="256" required id="id_name">
|
||||
autocomplete="off" class="form-input"
|
||||
maxlength="256" aria-invalid="false" required id="id_name">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
@@ -65,8 +65,8 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<input type="text" name="search" value="{bundle.search}"
|
||||
autocomplete="off" placeholder=" " class="form-input"
|
||||
maxlength="256" id="id_search">
|
||||
autocomplete="off" class="form-input"
|
||||
maxlength="256" aria-describedby="id_search_help" id="id_search">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<ld-tag-autocomplete input-name="any_tags" input-value="{bundle.any_tags}"
|
||||
input-id="id_any_tags">
|
||||
input-aria-describedby="id_any_tags_help" input-id="id_any_tags">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
@@ -82,7 +82,7 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<ld-tag-autocomplete input-name="all_tags" input-value="{bundle.all_tags}"
|
||||
input-id="id_all_tags">
|
||||
input-aria-describedby="id_all_tags_help" input-id="id_all_tags">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
@@ -90,7 +90,7 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<ld-tag-autocomplete input-name="excluded_tags" input-value="{bundle.excluded_tags}"
|
||||
input-id="id_excluded_tags">
|
||||
input-aria-describedby="id_excluded_tags_help" input-id="id_excluded_tags">
|
||||
""",
|
||||
html,
|
||||
)
|
||||
|
||||
@@ -410,7 +410,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<input type="checkbox" name="enable_automatic_html_snapshots" id="id_enable_automatic_html_snapshots" checked="">
|
||||
<input type="checkbox" name="enable_automatic_html_snapshots"
|
||||
aria-describedby="id_enable_automatic_html_snapshots_help"
|
||||
id="id_enable_automatic_html_snapshots" checked="">
|
||||
""",
|
||||
html,
|
||||
count=0,
|
||||
@@ -425,7 +427,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<input type="checkbox" name="enable_automatic_html_snapshots" id="id_enable_automatic_html_snapshots" checked="">
|
||||
<input type="checkbox" name="enable_automatic_html_snapshots"
|
||||
aria-describedby="id_enable_automatic_html_snapshots_help"
|
||||
id="id_enable_automatic_html_snapshots" checked="">
|
||||
""",
|
||||
html,
|
||||
count=1,
|
||||
|
||||
@@ -29,6 +29,10 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
return autocomplete_element.find_parent("div", class_="form-group")
|
||||
return None
|
||||
|
||||
def get_autocomplete(self, response, input_name):
|
||||
soup = self.make_soup(response.content.decode())
|
||||
return soup.find("ld-tag-autocomplete", {"input-name": input_name})
|
||||
|
||||
def test_merge_tags(self):
|
||||
target_tag = self.setup_tag(name="target_tag")
|
||||
merge_tag1 = self.setup_tag(name="merge_tag1")
|
||||
@@ -134,6 +138,9 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
self.assertIn("This field is required", self.get_text(target_tag_group))
|
||||
self.assertTrue(Tag.objects.filter(id=merge_tag.id).exists())
|
||||
|
||||
autocomplete = self.get_autocomplete(response, "target_tag")
|
||||
self.assertIn("is-error", autocomplete.get("input-class", ""))
|
||||
|
||||
def test_validate_missing_merge_tags(self):
|
||||
self.setup_tag(name="target_tag")
|
||||
|
||||
@@ -145,6 +152,9 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
merge_tags_group = self.get_form_group(response, "merge_tags")
|
||||
self.assertIn("This field is required", self.get_text(merge_tags_group))
|
||||
|
||||
autocomplete = self.get_autocomplete(response, "merge_tags")
|
||||
self.assertIn("is-error", autocomplete.get("input-class", ""))
|
||||
|
||||
def test_validate_nonexistent_target_tag(self):
|
||||
self.setup_tag(name="merge_tag")
|
||||
|
||||
|
||||
@@ -160,7 +160,15 @@ if settings.LD_ENABLE_OIDC:
|
||||
|
||||
# Registration
|
||||
if settings.ALLOW_REGISTRATION:
|
||||
urlpatterns.append(path("", include("django_registration.backends.one_step.urls")))
|
||||
from django_registration.backends.one_step.views import RegistrationView
|
||||
|
||||
urlpatterns.append(
|
||||
path(
|
||||
"accounts/register/",
|
||||
RegistrationView.as_view(),
|
||||
name="django_registration_register",
|
||||
)
|
||||
)
|
||||
|
||||
# Context path
|
||||
if settings.LD_CONTEXT_PATH:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from bookmarks.widgets import FormErrorList
|
||||
|
||||
|
||||
class LinkdingLoginView(auth_views.LoginView):
|
||||
"""
|
||||
@@ -8,6 +10,11 @@ class LinkdingLoginView(auth_views.LoginView):
|
||||
Allows to override settings in tests
|
||||
"""
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.error_class = FormErrorList
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
@@ -27,6 +34,11 @@ class LinkdingLoginView(auth_views.LoginView):
|
||||
|
||||
|
||||
class LinkdingPasswordChangeView(auth_views.PasswordChangeView):
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.error_class = FormErrorList
|
||||
return form
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""
|
||||
Hotwired Turbo requires a non 2xx status code to handle failed form
|
||||
|
||||
@@ -4,7 +4,8 @@ from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import BookmarkBundle, BookmarkBundleForm, BookmarkSearch
|
||||
from bookmarks.forms import BookmarkBundleForm
|
||||
from bookmarks.models import BookmarkBundle, BookmarkSearch
|
||||
from bookmarks.queries import parse_query_string
|
||||
from bookmarks.services import bundles
|
||||
from bookmarks.views import access
|
||||
|
||||
@@ -8,12 +8,12 @@ from django.http import Http404
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks import queries, utils
|
||||
from bookmarks.forms import BookmarkSearchForm
|
||||
from bookmarks.models import (
|
||||
Bookmark,
|
||||
BookmarkAsset,
|
||||
BookmarkBundle,
|
||||
BookmarkSearch,
|
||||
BookmarkSearchForm,
|
||||
Tag,
|
||||
User,
|
||||
UserProfile,
|
||||
|
||||
@@ -13,13 +13,12 @@ from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from bookmarks.forms import GlobalSettingsForm, UserProfileForm
|
||||
from bookmarks.models import (
|
||||
ApiToken,
|
||||
Bookmark,
|
||||
FeedToken,
|
||||
GlobalSettings,
|
||||
GlobalSettingsForm,
|
||||
UserProfileForm,
|
||||
)
|
||||
from bookmarks.services import exporter, importer, tasks
|
||||
from bookmarks.type_defs import HttpRequest
|
||||
@@ -301,10 +300,11 @@ def bookmark_export(request: HttpRequest):
|
||||
|
||||
return response
|
||||
except Exception:
|
||||
return render(
|
||||
return general(
|
||||
request,
|
||||
"settings/general.html",
|
||||
{"export_error": "An error occurred during bookmark export."},
|
||||
context_overrides={
|
||||
"export_error": "An error occurred during bookmark export."
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
83
bookmarks/widgets.py
Normal file
83
bookmarks/widgets.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from django import forms
|
||||
from django.forms.utils import ErrorList
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
class FormErrorList(ErrorList):
|
||||
template_name = "shared/error_list.html"
|
||||
|
||||
|
||||
class FormInput(forms.TextInput):
|
||||
def __init__(self, attrs=None):
|
||||
default_attrs = {"class": "form-input", "autocomplete": "off"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(default_attrs)
|
||||
|
||||
|
||||
class FormNumberInput(forms.NumberInput):
|
||||
def __init__(self, attrs=None):
|
||||
default_attrs = {"class": "form-input"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(default_attrs)
|
||||
|
||||
|
||||
class FormSelect(forms.Select):
|
||||
def __init__(self, attrs=None):
|
||||
default_attrs = {"class": "form-select"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(default_attrs)
|
||||
|
||||
|
||||
class FormTextarea(forms.Textarea):
|
||||
def __init__(self, attrs=None):
|
||||
default_attrs = {"class": "form-input"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(default_attrs)
|
||||
|
||||
|
||||
class FormCheckbox(forms.CheckboxInput):
|
||||
def __init__(self, attrs=None):
|
||||
super().__init__(attrs)
|
||||
self.label = "Label"
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
checkbox_html = super().render(name, value, attrs, renderer)
|
||||
input_id = attrs.get("id") if attrs else None
|
||||
return format_html(
|
||||
'<div class="form-checkbox">'
|
||||
"{}"
|
||||
'<i class="form-icon"></i>'
|
||||
'<label for="{}">{}</label>'
|
||||
"</div>",
|
||||
checkbox_html,
|
||||
input_id,
|
||||
self.label,
|
||||
)
|
||||
|
||||
|
||||
class TagAutocomplete(forms.TextInput):
|
||||
def __init__(self, attrs=None):
|
||||
super().__init__(attrs)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
# Merge self.attrs with passed attrs
|
||||
final_attrs = self.build_attrs(self.attrs, attrs)
|
||||
|
||||
input_id = final_attrs.get("id")
|
||||
input_value = escape(value) if value else ""
|
||||
aria_describedby = final_attrs.get("aria-describedby")
|
||||
input_class = final_attrs.get("class")
|
||||
|
||||
html = f'<ld-tag-autocomplete input-id="{input_id}" input-name="{name}" input-value="{input_value}"'
|
||||
if aria_describedby:
|
||||
html += f' input-aria-describedby="{aria_describedby}"'
|
||||
if input_class:
|
||||
html += f' input-class="{input_class}"'
|
||||
html += "></ld-tag-autocomplete>"
|
||||
|
||||
return mark_safe(html)
|
||||
Reference in New Issue
Block a user