mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-07 18:58:30 +02:00
Move more logic into bookmark form
This commit is contained in:
94
bookmarks/forms.py
Normal file
94
bookmarks/forms.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from bookmarks.models import Bookmark, build_tag_string
|
||||||
|
from bookmarks.validators import BookmarkURLValidator
|
||||||
|
from bookmarks.type_defs import HttpRequest
|
||||||
|
from bookmarks.services.bookmarks import create_bookmark, update_bookmark
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkForm(forms.ModelForm):
|
||||||
|
# Use URLField for URL
|
||||||
|
url = forms.CharField(validators=[BookmarkURLValidator()])
|
||||||
|
tag_string = forms.CharField(required=False)
|
||||||
|
# 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)
|
||||||
|
# Hidden field that determines whether to close window/tab after saving the bookmark
|
||||||
|
auto_close = forms.CharField(required=False)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
"auto_close": "auto_close" in request.GET,
|
||||||
|
"unread": request.user_profile.default_mark_unread,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
@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.objects.filter(owner=self.instance.owner, url=url)
|
||||||
|
.exclude(pk=self.instance.pk)
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
if is_duplicate:
|
||||||
|
raise forms.ValidationError("A bookmark with this URL already exists.")
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
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(" ", ",")
|
@@ -150,56 +150,6 @@ def bookmark_asset_deleted(sender, instance, **kwargs):
|
|||||||
logger.error(f"Failed to delete asset file: {filepath}", exc_info=error)
|
logger.error(f"Failed to delete asset file: {filepath}", exc_info=error)
|
||||||
|
|
||||||
|
|
||||||
class BookmarkForm(forms.ModelForm):
|
|
||||||
# Use URLField for URL
|
|
||||||
url = forms.CharField(validators=[BookmarkURLValidator()])
|
|
||||||
tag_string = forms.CharField(required=False)
|
|
||||||
# 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)
|
|
||||||
# Hidden field that determines whether to close window/tab after saving the bookmark
|
|
||||||
auto_close = forms.CharField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Bookmark
|
|
||||||
fields = [
|
|
||||||
"url",
|
|
||||||
"tag_string",
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"notes",
|
|
||||||
"unread",
|
|
||||||
"shared",
|
|
||||||
"auto_close",
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_notes(self):
|
|
||||||
return self.initial.get("notes", None) or (
|
|
||||||
self.instance and self.instance.notes
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_url(self):
|
|
||||||
# When creating a bookmark, the service logic prevents duplicate URLs by
|
|
||||||
# updating the existing bookmark instead, which is also communicated in
|
|
||||||
# the form's UI. When editing a bookmark, there is no assumption that
|
|
||||||
# it would update a different bookmark if the URL is a duplicate, so
|
|
||||||
# raise a validation error in that case.
|
|
||||||
url = self.cleaned_data["url"]
|
|
||||||
if self.instance.pk:
|
|
||||||
is_duplicate = (
|
|
||||||
Bookmark.objects.filter(owner=self.instance.owner, url=url)
|
|
||||||
.exclude(pk=self.instance.pk)
|
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
if is_duplicate:
|
|
||||||
raise forms.ValidationError("A bookmark with this URL already exists.")
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
class BookmarkSearch:
|
class BookmarkSearch:
|
||||||
SORT_ADDED_ASC = "added_asc"
|
SORT_ADDED_ASC = "added_asc"
|
||||||
SORT_ADDED_DESC = "added_desc"
|
SORT_ADDED_DESC = "added_desc"
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
{% extends 'bookmarks/layout.html' %}
|
{% extends 'bookmarks/layout.html' %}
|
||||||
{% load bookmarks %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="bookmarks-form-page">
|
<div class="bookmarks-form-page">
|
||||||
@@ -9,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<form action="{% url 'linkding:bookmarks.edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post"
|
<form action="{% url 'linkding:bookmarks.edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post"
|
||||||
novalidate>
|
novalidate>
|
||||||
{% bookmark_form form return_url bookmark_id %}
|
{% include 'bookmarks/form.html' %}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -91,12 +91,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="form-group d-flex justify-between">
|
<div class="form-group d-flex justify-between">
|
||||||
{% if auto_close %}
|
{% if form.is_auto_close %}
|
||||||
<input type="submit" value="Save and close" class="btn btn-primary btn-wide">
|
<input type="submit" value="Save and close" class="btn btn-primary btn-wide">
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="submit" value="Save" class="btn btn-primary btn btn-primary btn-wide">
|
<input type="submit" value="Save" class="btn btn-primary btn btn-primary btn-wide">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ cancel_url }}" class="btn">Nevermind</a>
|
<a href="{{ return_url }}" class="btn">Nevermind</a>
|
||||||
</div>
|
</div>
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
||||||
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
||||||
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
|
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
|
||||||
const editedBookmarkId = {{ bookmark_id }};
|
const editedBookmarkId = {{ form.instance.id|default:0 }};
|
||||||
let isTitleModified = !!titleInput.value;
|
let isTitleModified = !!titleInput.value;
|
||||||
let isDescriptionModified = !!descriptionInput.value;
|
let isDescriptionModified = !!descriptionInput.value;
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
{% extends 'bookmarks/layout.html' %}
|
{% extends 'bookmarks/layout.html' %}
|
||||||
{% load bookmarks %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="bookmarks-form-page">
|
<div class="bookmarks-form-page">
|
||||||
@@ -8,7 +7,7 @@
|
|||||||
<h2>New bookmark</h2>
|
<h2>New bookmark</h2>
|
||||||
</div>
|
</div>
|
||||||
<form action="{% url 'linkding:bookmarks.new' %}" method="post" novalidate>
|
<form action="{% url 'linkding:bookmarks.new' %}" method="post" novalidate>
|
||||||
{% bookmark_form form return_url auto_close=auto_close %}
|
{% include 'bookmarks/form.html' %}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,7 +3,6 @@ from typing import List
|
|||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
from bookmarks.models import (
|
from bookmarks.models import (
|
||||||
BookmarkForm,
|
|
||||||
BookmarkSearch,
|
BookmarkSearch,
|
||||||
BookmarkSearchForm,
|
BookmarkSearchForm,
|
||||||
User,
|
User,
|
||||||
@@ -12,23 +11,6 @@ from bookmarks.models import (
|
|||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("bookmarks/form.html", name="bookmark_form", takes_context=True)
|
|
||||||
def bookmark_form(
|
|
||||||
context,
|
|
||||||
form: BookmarkForm,
|
|
||||||
cancel_url: str,
|
|
||||||
bookmark_id: int = 0,
|
|
||||||
auto_close: bool = False,
|
|
||||||
):
|
|
||||||
return {
|
|
||||||
"request": context["request"],
|
|
||||||
"form": form,
|
|
||||||
"auto_close": auto_close,
|
|
||||||
"bookmark_id": bookmark_id,
|
|
||||||
"cancel_url": cancel_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag(
|
@register.inclusion_tag(
|
||||||
"bookmarks/search.html", name="bookmark_search", takes_context=True
|
"bookmarks/search.html", name="bookmark_search", takes_context=True
|
||||||
)
|
)
|
||||||
|
@@ -147,7 +147,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
|
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
'<input type="hidden" name="auto_close" id="id_auto_close">',
|
'<input type="hidden" name="auto_close" value="False" id="id_auto_close">',
|
||||||
html,
|
html,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
|
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
|
||||||
|
|
||||||
def test_auto_close_should_redirect_to_close_view(self):
|
def test_auto_close_should_redirect_to_close_view(self):
|
||||||
form_data = self.create_form_data({"auto_close": "true"})
|
form_data = self.create_form_data({"auto_close": "True"})
|
||||||
|
|
||||||
response = self.client.post(reverse("linkding:bookmarks.new"), form_data)
|
response = self.client.post(reverse("linkding:bookmarks.new"), form_data)
|
||||||
|
|
||||||
|
@@ -2,8 +2,10 @@ import datetime
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookmarks.models import BookmarkForm, Bookmark
|
from bookmarks.forms import BookmarkForm
|
||||||
|
from bookmarks.models import Bookmark
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
|
||||||
ENABLED_URL_VALIDATION_TEST_CASES = [
|
ENABLED_URL_VALIDATION_TEST_CASES = [
|
||||||
@@ -62,12 +64,15 @@ class BookmarkValidationTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
|
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
|
||||||
|
|
||||||
def test_bookmark_form_should_validate_required_fields(self):
|
def test_bookmark_form_should_validate_required_fields(self):
|
||||||
form = BookmarkForm(data={"url": ""})
|
rf = RequestFactory()
|
||||||
|
request = rf.post("/", data={"url": ""})
|
||||||
|
form = BookmarkForm(request)
|
||||||
|
|
||||||
self.assertEqual(len(form.errors), 1)
|
self.assertEqual(len(form.errors), 1)
|
||||||
self.assertIn("required", str(form.errors))
|
self.assertIn("required", str(form.errors))
|
||||||
|
|
||||||
form = BookmarkForm(data={"url": None})
|
request = rf.post("/", data={})
|
||||||
|
form = BookmarkForm(request)
|
||||||
|
|
||||||
self.assertEqual(len(form.errors), 1)
|
self.assertEqual(len(form.errors), 1)
|
||||||
self.assertIn("required", str(form.errors))
|
self.assertIn("required", str(form.errors))
|
||||||
@@ -102,7 +107,9 @@ class BookmarkValidationTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def _run_bookmark_form_url_validity_checks(self, cases):
|
def _run_bookmark_form_url_validity_checks(self, cases):
|
||||||
for case in cases:
|
for case in cases:
|
||||||
url, expectation = case
|
url, expectation = case
|
||||||
form = BookmarkForm(data={"url": url})
|
rf = RequestFactory()
|
||||||
|
request = rf.post("/", data={"url": url})
|
||||||
|
form = BookmarkForm(request)
|
||||||
|
|
||||||
if expectation:
|
if expectation:
|
||||||
self.assertEqual(len(form.errors), 0)
|
self.assertEqual(len(form.errors), 0)
|
||||||
|
@@ -12,16 +12,13 @@ from django.shortcuts import render
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from bookmarks import queries, utils
|
from bookmarks import queries, utils
|
||||||
|
from bookmarks.forms import BookmarkForm
|
||||||
from bookmarks.models import (
|
from bookmarks.models import (
|
||||||
Bookmark,
|
Bookmark,
|
||||||
BookmarkForm,
|
|
||||||
BookmarkSearch,
|
BookmarkSearch,
|
||||||
build_tag_string,
|
|
||||||
)
|
)
|
||||||
from bookmarks.services import assets as asset_actions, tasks
|
from bookmarks.services import assets as asset_actions, tasks
|
||||||
from bookmarks.services.bookmarks import (
|
from bookmarks.services.bookmarks import (
|
||||||
create_bookmark,
|
|
||||||
update_bookmark,
|
|
||||||
archive_bookmark,
|
archive_bookmark,
|
||||||
archive_bookmarks,
|
archive_bookmarks,
|
||||||
unarchive_bookmark,
|
unarchive_bookmark,
|
||||||
@@ -151,37 +148,17 @@ def convert_tag_string(tag_string: str):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def new(request: HttpRequest):
|
def new(request: HttpRequest):
|
||||||
initial_auto_close = True if "auto_close" in request.GET else None
|
form = BookmarkForm(request)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = BookmarkForm(request.POST)
|
|
||||||
auto_close = form.data["auto_close"]
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
current_user = request.user
|
form.save()
|
||||||
tag_string = convert_tag_string(form.data["tag_string"])
|
if form.is_auto_close:
|
||||||
create_bookmark(form.save(commit=False), tag_string, current_user)
|
|
||||||
if auto_close:
|
|
||||||
return HttpResponseRedirect(reverse("linkding:bookmarks.close"))
|
return HttpResponseRedirect(reverse("linkding:bookmarks.close"))
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect(reverse("linkding:bookmarks.index"))
|
return HttpResponseRedirect(reverse("linkding:bookmarks.index"))
|
||||||
else:
|
|
||||||
form = BookmarkForm(
|
|
||||||
initial={
|
|
||||||
"url": request.GET.get("url"),
|
|
||||||
"title": request.GET.get("title"),
|
|
||||||
"description": request.GET.get("description"),
|
|
||||||
"notes": request.GET.get("notes"),
|
|
||||||
"auto_close": initial_auto_close,
|
|
||||||
"unread": request.user_profile.default_mark_unread,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
||||||
context = {
|
context = {"form": form, "return_url": reverse("linkding:bookmarks.index")}
|
||||||
"form": form,
|
|
||||||
"auto_close": initial_auto_close,
|
|
||||||
"return_url": reverse("linkding:bookmarks.index"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, "bookmarks/new.html", context, status=status)
|
return render(request, "bookmarks/new.html", context, status=status)
|
||||||
|
|
||||||
@@ -189,20 +166,15 @@ def new(request: HttpRequest):
|
|||||||
@login_required
|
@login_required
|
||||||
def edit(request: HttpRequest, bookmark_id: int):
|
def edit(request: HttpRequest, bookmark_id: int):
|
||||||
bookmark = access.bookmark_write(request, bookmark_id)
|
bookmark = access.bookmark_write(request, bookmark_id)
|
||||||
|
form = BookmarkForm(request, instance=bookmark)
|
||||||
return_url = get_safe_return_url(
|
return_url = get_safe_return_url(
|
||||||
request.GET.get("return_url"), reverse("linkding:bookmarks.index")
|
request.GET.get("return_url"), reverse("linkding:bookmarks.index")
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = BookmarkForm(request.POST, instance=bookmark)
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
tag_string = convert_tag_string(form.data["tag_string"])
|
form.save()
|
||||||
update_bookmark(form.save(commit=False), tag_string, request.user)
|
|
||||||
return HttpResponseRedirect(return_url)
|
return HttpResponseRedirect(return_url)
|
||||||
else:
|
|
||||||
form = BookmarkForm(instance=bookmark)
|
|
||||||
|
|
||||||
form.fields["tag_string"].initial = build_tag_string(bookmark.tag_names, " ")
|
|
||||||
|
|
||||||
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
||||||
context = {"form": form, "bookmark_id": bookmark_id, "return_url": return_url}
|
context = {"form": form, "bookmark_id": bookmark_id, "return_url": return_url}
|
||||||
|
Reference in New Issue
Block a user