from django import forms from django.forms.utils import ErrorList 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 CustomErrorList(ErrorList): template_name = "shared/error_list.html" 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"), "tag_string": request.GET.get("tags"), "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, error_class=CustomErrorList ) @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(" ", ",")