mirror of
				https://github.com/sissbruecker/linkding.git
				synced 2025-11-04 13:04:00 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			466 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import re
 | 
						|
import urllib.parse
 | 
						|
from typing import Set, List
 | 
						|
 | 
						|
from django.conf import settings
 | 
						|
from django.core.handlers.wsgi import WSGIRequest
 | 
						|
from django.core.paginator import Paginator
 | 
						|
from django.db import models
 | 
						|
from django.http import Http404
 | 
						|
from django.urls import reverse
 | 
						|
 | 
						|
from bookmarks import queries
 | 
						|
from bookmarks import utils
 | 
						|
from bookmarks.models import (
 | 
						|
    Bookmark,
 | 
						|
    BookmarkAsset,
 | 
						|
    BookmarkSearch,
 | 
						|
    User,
 | 
						|
    UserProfile,
 | 
						|
    Tag,
 | 
						|
)
 | 
						|
from bookmarks.services.wayback import generate_fallback_webarchive_url
 | 
						|
 | 
						|
CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
 | 
						|
 | 
						|
 | 
						|
class RequestContext:
 | 
						|
    index_view = "bookmarks:index"
 | 
						|
    action_view = "bookmarks:index.action"
 | 
						|
 | 
						|
    def __init__(self, request: WSGIRequest):
 | 
						|
        self.request = request
 | 
						|
        self.index_url = reverse(self.index_view)
 | 
						|
        self.action_url = reverse(self.action_view)
 | 
						|
        self.query_params = request.GET.copy()
 | 
						|
        self.query_params.pop("details", None)
 | 
						|
 | 
						|
    def get_url(self, view_url: str, add: dict = None, remove: dict = None) -> str:
 | 
						|
        query_params = self.query_params.copy()
 | 
						|
        if add:
 | 
						|
            query_params.update(add)
 | 
						|
        if remove:
 | 
						|
            for key in remove:
 | 
						|
                query_params.pop(key, None)
 | 
						|
        encoded_params = query_params.urlencode()
 | 
						|
        return view_url + "?" + encoded_params if encoded_params else view_url
 | 
						|
 | 
						|
    def index(self, add: dict = None, remove: dict = None) -> str:
 | 
						|
        return self.get_url(self.index_url, add=add, remove=remove)
 | 
						|
 | 
						|
    def action(self, add: dict = None, remove: dict = None) -> str:
 | 
						|
        return self.get_url(self.action_url, add=add, remove=remove)
 | 
						|
 | 
						|
    def details(self, bookmark_id: int) -> str:
 | 
						|
        return self.get_url(self.index_url, add={"details": bookmark_id})
 | 
						|
 | 
						|
    def get_bookmark_query_set(self, search: BookmarkSearch):
 | 
						|
        raise NotImplementedError("Must be implemented by subclass")
 | 
						|
 | 
						|
    def get_tag_query_set(self, search: BookmarkSearch):
 | 
						|
        raise NotImplementedError("Must be implemented by subclass")
 | 
						|
 | 
						|
 | 
						|
class ActiveBookmarksContext(RequestContext):
 | 
						|
    index_view = "bookmarks:index"
 | 
						|
    action_view = "bookmarks:index.action"
 | 
						|
 | 
						|
    def get_bookmark_query_set(self, search: BookmarkSearch):
 | 
						|
        return queries.query_bookmarks(
 | 
						|
            self.request.user, self.request.user_profile, search
 | 
						|
        )
 | 
						|
 | 
						|
    def get_tag_query_set(self, search: BookmarkSearch):
 | 
						|
        return queries.query_bookmark_tags(
 | 
						|
            self.request.user, self.request.user_profile, search
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class ArchivedBookmarksContext(RequestContext):
 | 
						|
    index_view = "bookmarks:archived"
 | 
						|
    action_view = "bookmarks:archived.action"
 | 
						|
 | 
						|
    def get_bookmark_query_set(self, search: BookmarkSearch):
 | 
						|
        return queries.query_archived_bookmarks(
 | 
						|
            self.request.user, self.request.user_profile, search
 | 
						|
        )
 | 
						|
 | 
						|
    def get_tag_query_set(self, search: BookmarkSearch):
 | 
						|
        return queries.query_archived_bookmark_tags(
 | 
						|
            self.request.user, self.request.user_profile, search
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class SharedBookmarksContext(RequestContext):
 | 
						|
    index_view = "bookmarks:shared"
 | 
						|
    action_view = "bookmarks:shared.action"
 | 
						|
 | 
						|
    def get_bookmark_query_set(self, search: BookmarkSearch):
 | 
						|
        user = User.objects.filter(username=search.user).first()
 | 
						|
        public_only = not self.request.user.is_authenticated
 | 
						|
        return queries.query_shared_bookmarks(
 | 
						|
            user, self.request.user_profile, search, public_only
 | 
						|
        )
 | 
						|
 | 
						|
    def get_tag_query_set(self, search: BookmarkSearch):
 | 
						|
        user = User.objects.filter(username=search.user).first()
 | 
						|
        public_only = not self.request.user.is_authenticated
 | 
						|
        return queries.query_shared_bookmark_tags(
 | 
						|
            user, self.request.user_profile, search, public_only
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class BookmarkItem:
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        context: RequestContext,
 | 
						|
        bookmark: Bookmark,
 | 
						|
        user: User,
 | 
						|
        profile: UserProfile,
 | 
						|
    ) -> None:
 | 
						|
        self.bookmark = bookmark
 | 
						|
 | 
						|
        is_editable = bookmark.owner == user
 | 
						|
        self.is_editable = is_editable
 | 
						|
 | 
						|
        self.id = bookmark.id
 | 
						|
        self.url = bookmark.url
 | 
						|
        self.title = bookmark.resolved_title
 | 
						|
        self.description = bookmark.resolved_description
 | 
						|
        self.notes = bookmark.notes
 | 
						|
        self.tag_names = bookmark.tag_names
 | 
						|
        self.web_archive_snapshot_url = bookmark.web_archive_snapshot_url
 | 
						|
        if not self.web_archive_snapshot_url:
 | 
						|
            self.web_archive_snapshot_url = generate_fallback_webarchive_url(
 | 
						|
                bookmark.url, bookmark.date_added
 | 
						|
            )
 | 
						|
        self.favicon_file = bookmark.favicon_file
 | 
						|
        self.preview_image_file = bookmark.preview_image_file
 | 
						|
        self.is_archived = bookmark.is_archived
 | 
						|
        self.unread = bookmark.unread
 | 
						|
        self.owner = bookmark.owner
 | 
						|
        self.details_url = context.details(bookmark.id)
 | 
						|
 | 
						|
        css_classes = []
 | 
						|
        if bookmark.unread:
 | 
						|
            css_classes.append("unread")
 | 
						|
        if bookmark.shared:
 | 
						|
            css_classes.append("shared")
 | 
						|
 | 
						|
        self.css_classes = " ".join(css_classes)
 | 
						|
 | 
						|
        if profile.bookmark_date_display == UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE:
 | 
						|
            self.display_date = utils.humanize_relative_date(bookmark.date_added)
 | 
						|
        elif (
 | 
						|
            profile.bookmark_date_display == UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE
 | 
						|
        ):
 | 
						|
            self.display_date = utils.humanize_absolute_date(bookmark.date_added)
 | 
						|
 | 
						|
        self.show_notes_button = bookmark.notes and not profile.permanent_notes
 | 
						|
        self.show_mark_as_read = is_editable and bookmark.unread
 | 
						|
        self.show_unshare = is_editable and bookmark.shared and profile.enable_sharing
 | 
						|
 | 
						|
        self.has_extra_actions = (
 | 
						|
            self.show_notes_button or self.show_mark_as_read or self.show_unshare
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class BookmarkListContext:
 | 
						|
    request_context = RequestContext
 | 
						|
 | 
						|
    def __init__(self, request: WSGIRequest) -> None:
 | 
						|
        request_context = self.request_context(request)
 | 
						|
        user = request.user
 | 
						|
        user_profile = request.user_profile
 | 
						|
 | 
						|
        self.request = request
 | 
						|
        self.search = BookmarkSearch.from_request(
 | 
						|
            self.request.GET, user_profile.search_preferences
 | 
						|
        )
 | 
						|
 | 
						|
        query_set = request_context.get_bookmark_query_set(self.search)
 | 
						|
        page_number = request.GET.get("page")
 | 
						|
        paginator = Paginator(query_set, user_profile.items_per_page)
 | 
						|
        bookmarks_page = paginator.get_page(page_number)
 | 
						|
        # Prefetch related objects, this avoids n+1 queries when accessing fields in templates
 | 
						|
        models.prefetch_related_objects(bookmarks_page.object_list, "owner", "tags")
 | 
						|
 | 
						|
        self.items = [
 | 
						|
            BookmarkItem(request_context, bookmark, user, user_profile)
 | 
						|
            for bookmark in bookmarks_page
 | 
						|
        ]
 | 
						|
        self.is_empty = paginator.count == 0
 | 
						|
        self.bookmarks_page = bookmarks_page
 | 
						|
        self.bookmarks_total = paginator.count
 | 
						|
 | 
						|
        self.return_url = request_context.index()
 | 
						|
        self.action_url = request_context.action()
 | 
						|
 | 
						|
        self.link_target = user_profile.bookmark_link_target
 | 
						|
        self.date_display = user_profile.bookmark_date_display
 | 
						|
        self.description_display = user_profile.bookmark_description_display
 | 
						|
        self.description_max_lines = user_profile.bookmark_description_max_lines
 | 
						|
        self.show_url = user_profile.display_url
 | 
						|
        self.show_view_action = user_profile.display_view_bookmark_action
 | 
						|
        self.show_edit_action = user_profile.display_edit_bookmark_action
 | 
						|
        self.show_archive_action = user_profile.display_archive_bookmark_action
 | 
						|
        self.show_remove_action = user_profile.display_remove_bookmark_action
 | 
						|
        self.show_favicons = user_profile.enable_favicons
 | 
						|
        self.show_preview_images = user_profile.enable_preview_images
 | 
						|
        self.show_notes = user_profile.permanent_notes
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None):
 | 
						|
        query_params = search.query_params
 | 
						|
        if page is not None:
 | 
						|
            query_params["page"] = page
 | 
						|
        query_string = urllib.parse.urlencode(query_params)
 | 
						|
 | 
						|
        return base_url if query_string == "" else base_url + "?" + query_string
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def generate_action_url(
 | 
						|
        search: BookmarkSearch, base_action_url: str, return_url: str
 | 
						|
    ):
 | 
						|
        query_params = search.query_params
 | 
						|
        query_params["return_url"] = return_url
 | 
						|
        query_string = urllib.parse.urlencode(query_params)
 | 
						|
 | 
						|
        return (
 | 
						|
            base_action_url
 | 
						|
            if query_string == ""
 | 
						|
            else base_action_url + "?" + query_string
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class ActiveBookmarkListContext(BookmarkListContext):
 | 
						|
    request_context = ActiveBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class ArchivedBookmarkListContext(BookmarkListContext):
 | 
						|
    request_context = ArchivedBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class SharedBookmarkListContext(BookmarkListContext):
 | 
						|
    request_context = SharedBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class TagGroup:
 | 
						|
    def __init__(self, char: str):
 | 
						|
        self.tags = []
 | 
						|
        self.char = char
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return f"<{self.char} TagGroup>"
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def create_tag_groups(mode: str, tags: Set[Tag]):
 | 
						|
        if mode == UserProfile.TAG_GROUPING_ALPHABETICAL:
 | 
						|
            return TagGroup._create_tag_groups_alphabetical(tags)
 | 
						|
        elif mode == UserProfile.TAG_GROUPING_DISABLED:
 | 
						|
            return TagGroup._create_tag_groups_disabled(tags)
 | 
						|
        else:
 | 
						|
            raise ValueError(f"{mode} is not a valid tag grouping mode")
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _create_tag_groups_alphabetical(tags: Set[Tag]):
 | 
						|
        # Ensure groups, as well as tags within groups, are ordered alphabetically
 | 
						|
        sorted_tags = sorted(tags, key=lambda x: str.lower(x.name))
 | 
						|
        group = None
 | 
						|
        groups = []
 | 
						|
        cjk_used = False
 | 
						|
        cjk_group = TagGroup("Ideographic")
 | 
						|
 | 
						|
        # Group tags that start with a different character than the previous one
 | 
						|
        for tag in sorted_tags:
 | 
						|
            tag_char = tag.name[0].lower()
 | 
						|
            if CJK_RE.match(tag_char):
 | 
						|
                cjk_used = True
 | 
						|
                cjk_group.tags.append(tag)
 | 
						|
            elif not group or group.char != tag_char:
 | 
						|
                group = TagGroup(tag_char)
 | 
						|
                groups.append(group)
 | 
						|
                group.tags.append(tag)
 | 
						|
            else:
 | 
						|
                group.tags.append(tag)
 | 
						|
 | 
						|
        if cjk_used:
 | 
						|
            groups.append(cjk_group)
 | 
						|
        return groups
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _create_tag_groups_disabled(tags: Set[Tag]):
 | 
						|
        if len(tags) == 0:
 | 
						|
            return []
 | 
						|
 | 
						|
        sorted_tags = sorted(tags, key=lambda x: str.lower(x.name))
 | 
						|
        group = TagGroup("Ungrouped")
 | 
						|
        for tag in sorted_tags:
 | 
						|
            group.tags.append(tag)
 | 
						|
 | 
						|
        return [group]
 | 
						|
 | 
						|
 | 
						|
class TagCloudContext:
 | 
						|
    request_context = RequestContext
 | 
						|
 | 
						|
    def __init__(self, request: WSGIRequest) -> None:
 | 
						|
        request_context = self.request_context(request)
 | 
						|
        user_profile = request.user_profile
 | 
						|
 | 
						|
        self.request = request
 | 
						|
        self.search = BookmarkSearch.from_request(
 | 
						|
            self.request.GET, user_profile.search_preferences
 | 
						|
        )
 | 
						|
 | 
						|
        query_set = request_context.get_tag_query_set(self.search)
 | 
						|
        tags = list(query_set)
 | 
						|
        selected_tags = self.get_selected_tags(tags)
 | 
						|
        unique_tags = utils.unique(tags, key=lambda x: str.lower(x.name))
 | 
						|
        unique_selected_tags = utils.unique(
 | 
						|
            selected_tags, key=lambda x: str.lower(x.name)
 | 
						|
        )
 | 
						|
        has_selected_tags = len(unique_selected_tags) > 0
 | 
						|
        unselected_tags = set(unique_tags).symmetric_difference(unique_selected_tags)
 | 
						|
        groups = TagGroup.create_tag_groups(user_profile.tag_grouping, unselected_tags)
 | 
						|
 | 
						|
        self.tags = unique_tags
 | 
						|
        self.groups = groups
 | 
						|
        self.selected_tags = unique_selected_tags
 | 
						|
        self.has_selected_tags = has_selected_tags
 | 
						|
 | 
						|
    def get_selected_tags(self, tags: List[Tag]):
 | 
						|
        parsed_query = queries.parse_query_string(self.search.q)
 | 
						|
        tag_names = parsed_query["tag_names"]
 | 
						|
        if self.request.user_profile.tag_search == UserProfile.TAG_SEARCH_LAX:
 | 
						|
            tag_names = tag_names + parsed_query["search_terms"]
 | 
						|
        tag_names = [tag_name.lower() for tag_name in tag_names]
 | 
						|
 | 
						|
        return [tag for tag in tags if tag.name.lower() in tag_names]
 | 
						|
 | 
						|
 | 
						|
class ActiveTagCloudContext(TagCloudContext):
 | 
						|
    request_context = ActiveBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class ArchivedTagCloudContext(TagCloudContext):
 | 
						|
    request_context = ArchivedBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class SharedTagCloudContext(TagCloudContext):
 | 
						|
    request_context = SharedBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class BookmarkAssetItem:
 | 
						|
    def __init__(self, asset: BookmarkAsset):
 | 
						|
        self.asset = asset
 | 
						|
 | 
						|
        self.id = asset.id
 | 
						|
        self.display_name = asset.display_name
 | 
						|
        self.asset_type = asset.asset_type
 | 
						|
        self.content_type = asset.content_type
 | 
						|
        self.file = asset.file
 | 
						|
        self.file_size = asset.file_size
 | 
						|
        self.status = asset.status
 | 
						|
 | 
						|
        icon_classes = []
 | 
						|
        text_classes = []
 | 
						|
        if asset.status == BookmarkAsset.STATUS_PENDING:
 | 
						|
            icon_classes.append("text-tertiary")
 | 
						|
            text_classes.append("text-tertiary")
 | 
						|
        elif asset.status == BookmarkAsset.STATUS_FAILURE:
 | 
						|
            icon_classes.append("text-error")
 | 
						|
            text_classes.append("text-error")
 | 
						|
        else:
 | 
						|
            icon_classes.append("icon-color")
 | 
						|
 | 
						|
        self.icon_classes = " ".join(icon_classes)
 | 
						|
        self.text_classes = " ".join(text_classes)
 | 
						|
 | 
						|
 | 
						|
class BookmarkDetailsContext:
 | 
						|
    request_context = RequestContext
 | 
						|
 | 
						|
    def __init__(self, request: WSGIRequest, bookmark: Bookmark):
 | 
						|
        request_context = self.request_context(request)
 | 
						|
 | 
						|
        user = request.user
 | 
						|
        user_profile = request.user_profile
 | 
						|
 | 
						|
        self.edit_return_url = request_context.details(bookmark.id)
 | 
						|
        self.action_url = request_context.action(add={"details": bookmark.id})
 | 
						|
        self.delete_url = request_context.action()
 | 
						|
        self.close_url = request_context.index()
 | 
						|
 | 
						|
        self.bookmark = bookmark
 | 
						|
        self.profile = request.user_profile
 | 
						|
        self.is_editable = bookmark.owner == user
 | 
						|
        self.sharing_enabled = user_profile.enable_sharing
 | 
						|
        self.preview_image_enabled = user_profile.enable_preview_images
 | 
						|
        self.show_link_icons = user_profile.enable_favicons and bookmark.favicon_file
 | 
						|
        # For now hide files section if snapshots are not supported
 | 
						|
        self.show_files = settings.LD_ENABLE_SNAPSHOTS
 | 
						|
 | 
						|
        self.web_archive_snapshot_url = bookmark.web_archive_snapshot_url
 | 
						|
        if not self.web_archive_snapshot_url:
 | 
						|
            self.web_archive_snapshot_url = generate_fallback_webarchive_url(
 | 
						|
                bookmark.url, bookmark.date_added
 | 
						|
            )
 | 
						|
 | 
						|
        self.assets = [
 | 
						|
            BookmarkAssetItem(asset) for asset in bookmark.bookmarkasset_set.all()
 | 
						|
        ]
 | 
						|
        self.has_pending_assets = any(
 | 
						|
            asset.status == BookmarkAsset.STATUS_PENDING for asset in self.assets
 | 
						|
        )
 | 
						|
        self.latest_snapshot = next(
 | 
						|
            (
 | 
						|
                asset
 | 
						|
                for asset in self.assets
 | 
						|
                if asset.asset.asset_type == BookmarkAsset.TYPE_SNAPSHOT
 | 
						|
                and asset.status == BookmarkAsset.STATUS_COMPLETE
 | 
						|
            ),
 | 
						|
            None,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class ActiveBookmarkDetailsContext(BookmarkDetailsContext):
 | 
						|
    request_context = ActiveBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class ArchivedBookmarkDetailsContext(BookmarkDetailsContext):
 | 
						|
    request_context = ArchivedBookmarksContext
 | 
						|
 | 
						|
 | 
						|
class SharedBookmarkDetailsContext(BookmarkDetailsContext):
 | 
						|
    request_context = SharedBookmarksContext
 | 
						|
 | 
						|
 | 
						|
def get_details_context(
 | 
						|
    request: WSGIRequest, context_type
 | 
						|
) -> BookmarkDetailsContext | None:
 | 
						|
    bookmark_id = request.GET.get("details")
 | 
						|
    if not bookmark_id:
 | 
						|
        return None
 | 
						|
 | 
						|
    try:
 | 
						|
        bookmark = Bookmark.objects.get(pk=int(bookmark_id))
 | 
						|
    except Bookmark.DoesNotExist:
 | 
						|
        # just ignore, might end up in a situation where the bookmark was deleted
 | 
						|
        # in between navigating back and forth
 | 
						|
        return None
 | 
						|
 | 
						|
    is_owner = bookmark.owner == request.user
 | 
						|
    is_shared = (
 | 
						|
        request.user.is_authenticated
 | 
						|
        and bookmark.shared
 | 
						|
        and bookmark.owner.profile.enable_sharing
 | 
						|
    )
 | 
						|
    is_public_shared = bookmark.shared and bookmark.owner.profile.enable_public_sharing
 | 
						|
    if not is_owner and not is_shared and not is_public_shared:
 | 
						|
        raise Http404("Bookmark does not exist")
 | 
						|
    if request.method == "POST" and not is_owner:
 | 
						|
        raise Http404("Bookmark does not exist")
 | 
						|
 | 
						|
    return context_type(request, bookmark)
 |