Format and lint with ruff (#1263)

This commit is contained in:
Sascha Ißbrücker
2026-01-04 12:13:48 +01:00
committed by GitHub
parent 4d82fefa4e
commit 3b26190df5
178 changed files with 601 additions and 739 deletions

View File

@@ -14,8 +14,11 @@ tasks:
test: test:
uv run pytest -n auto uv run pytest -n auto
lint:
uv run ruff check bookmarks
format: format:
uv run black bookmarks uv run ruff format bookmarks
uv run djlint bookmarks/templates --reformat --quiet --warn uv run djlint bookmarks/templates --reformat --quiet --warn
npx prettier bookmarks/frontend --write npx prettier bookmarks/frontend --write
npx prettier bookmarks/styles --write npx prettier bookmarks/styles --write

View File

@@ -96,9 +96,17 @@ Run all tests with pytest:
make test make test
``` ```
### Linting
Run linting with ruff:
```
make lint
```
### Formatting ### Formatting
Format Python code with black, Django templates with djlint, and JavaScript code with prettier: Format Python code with ruff, Django templates with djlint, and JavaScript code with prettier:
``` ```
make format make format
``` ```

View File

@@ -1,4 +1,5 @@
import os import os
from django import forms from django import forms
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite
@@ -8,7 +9,7 @@ from django.core.paginator import Paginator
from django.db.models import Count, QuerySet from django.db.models import Count, QuerySet
from django.shortcuts import render from django.shortcuts import render
from django.urls import path from django.urls import path
from django.utils.translation import ngettext, gettext from django.utils.translation import gettext, ngettext
from huey.contrib.djhuey import HUEY as huey from huey.contrib.djhuey import HUEY as huey
from bookmarks.models import ( from bookmarks.models import (
@@ -16,10 +17,10 @@ from bookmarks.models import (
Bookmark, Bookmark,
BookmarkAsset, BookmarkAsset,
BookmarkBundle, BookmarkBundle,
Tag,
UserProfile,
Toast,
FeedToken, FeedToken,
Tag,
Toast,
UserProfile,
) )
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
@@ -45,12 +46,14 @@ class TaskPaginator(Paginator):
# Copied from Huey's SqliteStorage with some modifications to allow pagination # Copied from Huey's SqliteStorage with some modifications to allow pagination
def enqueued_items(self, limit, offset): def enqueued_items(self, limit, offset):
to_bytes = lambda b: bytes(b) if not isinstance(b, bytes) else b def to_bytes(b):
return bytes(b) if not isinstance(b, bytes) else b
sql = "select data from task where queue=? order by priority desc, id limit ? offset ?" sql = "select data from task where queue=? order by priority desc, id limit ? offset ?"
params = (huey.storage.name, limit, offset) params = (huey.storage.name, limit, offset)
serialized_tasks = [ serialized_tasks = [
to_bytes(i) for i, in huey.storage.sql(sql, params, results=True) to_bytes(i) for (i,) in huey.storage.sql(sql, params, results=True)
] ]
return [huey.deserialize_task(task) for task in serialized_tasks] return [huey.deserialize_task(task) for task in serialized_tasks]
@@ -295,7 +298,7 @@ class AdminCustomUser(UserAdmin):
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
if not obj: if not obj:
return list() return list()
return super(AdminCustomUser, self).get_inline_instances(request, obj) return super().get_inline_instances(request, obj)
class AdminToast(admin.ModelAdmin): class AdminToast(admin.ModelAdmin):

View File

@@ -33,6 +33,6 @@ class LinkdingTokenAuthentication(TokenAuthentication):
msg = _( msg = _(
"Invalid token header. Token string should not contain invalid characters." "Invalid token header. Token string should not contain invalid characters."
) )
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg) from None
return self.authenticate_credentials(token) return self.authenticate_credentials(token)

View File

@@ -4,29 +4,29 @@ import os
from django.conf import settings from django.conf import settings
from django.http import Http404, StreamingHttpResponse from django.http import Http404, StreamingHttpResponse
from rest_framework import viewsets, mixins, status from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.routers import DefaultRouter, SimpleRouter
from bookmarks import queries from bookmarks import queries
from bookmarks.api.serializers import ( from bookmarks.api.serializers import (
BookmarkSerializer,
BookmarkAssetSerializer, BookmarkAssetSerializer,
BookmarkBundleSerializer,
BookmarkSerializer,
TagSerializer, TagSerializer,
UserProfileSerializer, UserProfileSerializer,
BookmarkBundleSerializer,
) )
from bookmarks.models import ( from bookmarks.models import (
Bookmark, Bookmark,
BookmarkAsset, BookmarkAsset,
BookmarkBundle,
BookmarkSearch, BookmarkSearch,
Tag, Tag,
User, User,
BookmarkBundle,
) )
from bookmarks.services import assets, bookmarks, bundles, auto_tagging, website_loader from bookmarks.services import assets, auto_tagging, bookmarks, bundles, website_loader
from bookmarks.type_defs import HttpRequest from bookmarks.type_defs import HttpRequest
from bookmarks.views import access from bookmarks.views import access
@@ -197,7 +197,7 @@ class BookmarkAssetViewSet(
file_stream = ( file_stream = (
gzip.GzipFile(file_path, mode="rb") gzip.GzipFile(file_path, mode="rb")
if asset.gzip if asset.gzip
else open(file_path, "rb") else open(file_path, "rb") # noqa: SIM115
) )
response = StreamingHttpResponse(file_stream, content_type=content_type) response = StreamingHttpResponse(file_stream, content_type=content_type)
response["Content-Disposition"] = ( response["Content-Disposition"] = (
@@ -205,7 +205,7 @@ class BookmarkAssetViewSet(
) )
return response return response
except FileNotFoundError: except FileNotFoundError:
raise Http404("Asset file does not exist") raise Http404("Asset file does not exist") from None
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Failed to download asset. bookmark_id={bookmark_id}, asset_id={pk}", f"Failed to download asset. bookmark_id={bookmark_id}, asset_id={pk}",

View File

@@ -1,4 +1,4 @@
from django.db.models import Max, prefetch_related_objects from django.db.models import prefetch_related_objects
from django.templatetags.static import static from django.templatetags.static import static
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import ListSerializer from rest_framework.serializers import ListSerializer
@@ -6,10 +6,10 @@ from rest_framework.serializers import ListSerializer
from bookmarks.models import ( from bookmarks.models import (
Bookmark, Bookmark,
BookmarkAsset, BookmarkAsset,
Tag,
build_tag_string,
UserProfile,
BookmarkBundle, BookmarkBundle,
Tag,
UserProfile,
build_tag_string,
) )
from bookmarks.services import bookmarks, bundles from bookmarks.services import bookmarks, bundles
from bookmarks.services.tags import get_or_create_tag from bookmarks.services.tags import get_or_create_tag
@@ -56,7 +56,7 @@ class BookmarkBundleSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
bundle = BookmarkBundle(**validated_data) bundle = BookmarkBundle(**validated_data)
bundle.order = validated_data["order"] if "order" in validated_data else None bundle.order = validated_data.get("order", None)
return bundles.create_bundle(bundle, self.context["user"]) return bundles.create_bundle(bundle, self.context["user"])

View File

@@ -6,4 +6,5 @@ class BookmarksConfig(AppConfig):
def ready(self): def ready(self):
# Register signal handlers # Register signal handlers
import bookmarks.signals # noinspection PyUnusedImports
import bookmarks.signals # noqa: F401

View File

@@ -50,10 +50,7 @@ class BaseBookmarksFeed(Feed):
def items(self, context: FeedContext): def items(self, context: FeedContext):
limit = context.request.GET.get("limit", 100) limit = context.request.GET.get("limit", 100)
if limit: data = context.query_set[: int(limit)] if limit else list(context.query_set)
data = context.query_set[: int(limit)]
else:
data = list(context.query_set)
prefetch_related_objects(data, "tags") prefetch_related_objects(data, "tags")
return data return data

View File

@@ -167,7 +167,9 @@ class TagMergeForm(forms.Form):
try: try:
target_tag = Tag.objects.get(name__iexact=target_tag_name, owner=self.user) target_tag = Tag.objects.get(name__iexact=target_tag_name, owner=self.user)
except Tag.DoesNotExist: except Tag.DoesNotExist:
raise forms.ValidationError(f'Tag "{target_tag_name}" does not exist.') raise forms.ValidationError(
f'Tag "{target_tag_name}" does not exist.'
) from None
return target_tag return target_tag
@@ -184,7 +186,9 @@ class TagMergeForm(forms.Form):
tag = Tag.objects.get(name__iexact=tag_name, owner=self.user) tag = Tag.objects.get(name__iexact=tag_name, owner=self.user)
merge_tags.append(tag) merge_tags.append(tag)
except Tag.DoesNotExist: except Tag.DoesNotExist:
raise forms.ValidationError(f'Tag "{tag_name}" does not exist.') raise forms.ValidationError(
f'Tag "{tag_name}" does not exist.'
) from None
target_tag = self.cleaned_data.get("target_tag") target_tag = self.cleaned_data.get("target_tag")
if target_tag and target_tag in merge_tags: if target_tag and target_tag in merge_tags:

View File

@@ -1,5 +1,5 @@
import sqlite3
import os import os
import sqlite3
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@@ -14,7 +14,7 @@ class Command(BaseCommand):
destination = options["destination"] destination = options["destination"]
def progress(status, remaining, total): def progress(status, remaining, total):
self.stdout.write(f"Copied {total-remaining} of {total} pages...") self.stdout.write(f"Copied {total - remaining} of {total} pages...")
source_db = sqlite3.connect(os.path.join("data", "db.sqlite3")) source_db = sqlite3.connect(os.path.join("data", "db.sqlite3"))
backup_db = sqlite3.connect(destination) backup_db = sqlite3.connect(destination)

View File

@@ -1,8 +1,8 @@
import os
import logging import logging
import os
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,5 +1,5 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):

View File

@@ -1,5 +1,5 @@
import sqlite3
import os import os
import sqlite3
import tempfile import tempfile
import zipfile import zipfile
@@ -65,7 +65,7 @@ class Command(BaseCommand):
def backup_database(self, backup_db_file): def backup_database(self, backup_db_file):
def progress(status, remaining, total): def progress(status, remaining, total):
self.stdout.write(f"Copied {total-remaining} of {total} pages...") self.stdout.write(f"Copied {total - remaining} of {total} pages...")
source_db = sqlite3.connect(os.path.join("data", "db.sqlite3")) source_db = sqlite3.connect(os.path.join("data", "db.sqlite3"))
backup_db = sqlite3.connect(backup_db_file) backup_db = sqlite3.connect(backup_db_file)

View File

@@ -4,7 +4,6 @@ import os
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.management.utils import get_random_secret_key from django.core.management.utils import get_random_secret_key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -15,10 +14,10 @@ class Command(BaseCommand):
secret_key_file = os.path.join("data", "secretkey.txt") secret_key_file = os.path.join("data", "secretkey.txt")
if os.path.exists(secret_key_file): if os.path.exists(secret_key_file):
logger.info(f"Secret key file already exists") logger.info("Secret key file already exists")
return return
secret_key = get_random_secret_key() secret_key = get_random_secret_key()
with open(secret_key_file, "w") as f: with open(secret_key_file, "w") as f:
f.write(secret_key) f.write(secret_key)
logger.info(f"Generated secret key file") logger.info("Generated secret key file")

View File

@@ -1,7 +1,7 @@
import importlib
import json import json
import os import os
import sqlite3 import sqlite3
import importlib
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand

View File

@@ -1,7 +1,7 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.middleware import RemoteUserMiddleware from django.contrib.auth.middleware import RemoteUserMiddleware
from bookmarks.models import UserProfile, GlobalSettings from bookmarks.models import GlobalSettings, UserProfile
class CustomRemoteUserMiddleware(RemoteUserMiddleware): class CustomRemoteUserMiddleware(RemoteUserMiddleware):
@@ -22,7 +22,7 @@ class LinkdingMiddleware:
# add global settings to request # add global settings to request
try: try:
global_settings = GlobalSettings.get() global_settings = GlobalSettings.get()
except: except Exception:
global_settings = default_global_settings global_settings = default_global_settings
request.global_settings = global_settings request.global_settings = global_settings

View File

@@ -1,12 +1,11 @@
# Generated by Django 2.2.2 on 2019-06-28 23:49 # Generated by Django 2.2.2 on 2019-06-28 23:49
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

View File

@@ -1,12 +1,11 @@
# Generated by Django 2.2.2 on 2019-06-29 23:03 # Generated by Django 2.2.2 on 2019-06-29 23:03
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("bookmarks", "0001_initial"), ("bookmarks", "0001_initial"),

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0002_auto_20190629_2303"), ("bookmarks", "0002_auto_20190629_2303"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0003_auto_20200913_0656"), ("bookmarks", "0003_auto_20200913_0656"),
] ]

View File

@@ -1,11 +1,11 @@
# Generated by Django 2.2.13 on 2021-01-03 12:12 # Generated by Django 2.2.13 on 2021-01-03 12:12
import bookmarks.validators
from django.db import migrations, models from django.db import migrations, models
import bookmarks.validators
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0004_auto_20200926_1028"), ("bookmarks", "0004_auto_20200926_1028"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0005_auto_20210103_1212"), ("bookmarks", "0005_auto_20210103_1212"),
] ]

View File

@@ -1,8 +1,8 @@
# Generated by Django 2.2.18 on 2021-03-26 22:39 # Generated by Django 2.2.18 on 2021-03-26 22:39
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
def forwards(apps, schema_editor): def forwards(apps, schema_editor):

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0007_userprofile"), ("bookmarks", "0007_userprofile"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0008_userprofile_bookmark_date_display"), ("bookmarks", "0008_userprofile_bookmark_date_display"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0009_bookmark_web_archive_snapshot_url"), ("bookmarks", "0009_bookmark_web_archive_snapshot_url"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0010_userprofile_bookmark_link_target"), ("bookmarks", "0010_userprofile_bookmark_link_target"),
] ]

View File

@@ -1,12 +1,11 @@
# Generated by Django 3.2.6 on 2022-01-08 19:24 # Generated by Django 3.2.6 on 2022-01-08 19:24
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("bookmarks", "0011_userprofile_web_archive_integration"), ("bookmarks", "0011_userprofile_web_archive_integration"),

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.2.6 on 2022-01-08 19:27 # Generated by Django 3.2.6 on 2022-01-08 19:27
from django.db import migrations
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import migrations
from bookmarks.models import Toast from bookmarks.models import Toast

View File

@@ -1,12 +1,11 @@
# Generated by Django 3.2.13 on 2022-07-23 20:35 # Generated by Django 3.2.13 on 2022-07-23 20:35
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("bookmarks", "0014_alter_bookmark_unread"), ("bookmarks", "0014_alter_bookmark_unread"),

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0015_feedtoken"), ("bookmarks", "0015_feedtoken"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0016_bookmark_shared"), ("bookmarks", "0016_bookmark_shared"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0017_userprofile_enable_sharing"), ("bookmarks", "0017_userprofile_enable_sharing"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0018_bookmark_favicon_file"), ("bookmarks", "0018_bookmark_favicon_file"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0019_userprofile_enable_favicons"), ("bookmarks", "0019_userprofile_enable_favicons"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0020_userprofile_tag_search"), ("bookmarks", "0020_userprofile_tag_search"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0021_userprofile_display_url"), ("bookmarks", "0021_userprofile_display_url"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0022_bookmark_notes"), ("bookmarks", "0022_bookmark_notes"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0023_userprofile_permanent_notes"), ("bookmarks", "0023_userprofile_permanent_notes"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0024_userprofile_enable_public_sharing"), ("bookmarks", "0024_userprofile_enable_public_sharing"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0025_userprofile_search_preferences"), ("bookmarks", "0025_userprofile_search_preferences"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0026_userprofile_custom_css"), ("bookmarks", "0026_userprofile_custom_css"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0027_userprofile_bookmark_description_display_and_more"), ("bookmarks", "0027_userprofile_bookmark_description_display_and_more"),
] ]

View File

@@ -1,7 +1,7 @@
# Generated by Django 5.0.2 on 2024-03-29 21:25 # Generated by Django 5.0.2 on 2024-03-29 21:25
from django.db import migrations
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import migrations
from bookmarks.models import Toast from bookmarks.models import Toast
@@ -9,7 +9,6 @@ User = get_user_model()
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
for user in User.objects.all(): for user in User.objects.all():
toast = Toast( toast = Toast(
key="bookmark_list_actions_hint", key="bookmark_list_actions_hint",
@@ -24,7 +23,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0028_userprofile_display_archive_bookmark_action_and_more"), ("bookmarks", "0028_userprofile_display_archive_bookmark_action_and_more"),
] ]

View File

@@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0029_bookmark_list_actions_toast"), ("bookmarks", "0029_bookmark_list_actions_toast"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0030_bookmarkasset"), ("bookmarks", "0030_bookmarkasset"),
] ]

View File

@@ -1,7 +1,7 @@
# Generated by Django 5.0.2 on 2024-04-01 12:17 # Generated by Django 5.0.2 on 2024-04-01 12:17
from django.db import migrations
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import migrations
from bookmarks.models import Toast from bookmarks.models import Toast
@@ -9,7 +9,6 @@ User = get_user_model()
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
for user in User.objects.all(): for user in User.objects.all():
toast = Toast( toast = Toast(
key="html_snapshots_hint", key="html_snapshots_hint",
@@ -24,7 +23,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0031_userprofile_enable_automatic_html_snapshots"), ("bookmarks", "0031_userprofile_enable_automatic_html_snapshots"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0032_html_snapshots_hint_toast"), ("bookmarks", "0032_html_snapshots_hint_toast"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0033_userprofile_default_mark_unread"), ("bookmarks", "0033_userprofile_default_mark_unread"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0034_bookmark_preview_image_file_and_more"), ("bookmarks", "0034_bookmark_preview_image_file_and_more"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0035_userprofile_tag_grouping"), ("bookmarks", "0035_userprofile_tag_grouping"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0036_userprofile_auto_tagging_rules"), ("bookmarks", "0036_userprofile_auto_tagging_rules"),
] ]

View File

@@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0037_globalsettings"), ("bookmarks", "0037_globalsettings"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0038_globalsettings_guest_profile_user"), ("bookmarks", "0038_globalsettings_guest_profile_user"),
] ]

View File

@@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0039_globalsettings_enable_link_prefetch"), ("bookmarks", "0039_globalsettings_enable_link_prefetch"),
] ]

View File

@@ -10,9 +10,9 @@ from bookmarks.models import Bookmark
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
Bookmark.objects.filter( Bookmark.objects.filter(
Q(title__isnull=True) | Q(title__exact=""), Q(title__isnull=True) | Q(title__exact=""),
).extra( ).extra(where=["website_title IS NOT NULL"]).update(
where=["website_title IS NOT NULL"] title=RawSQL("website_title", ())
).update(title=RawSQL("website_title", ())) )
Bookmark.objects.filter( Bookmark.objects.filter(
Q(description__isnull=True) | Q(description__exact=""), Q(description__isnull=True) | Q(description__exact=""),
@@ -26,7 +26,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0040_userprofile_items_per_page_and_more"), ("bookmarks", "0040_userprofile_items_per_page_and_more"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0041_merge_metadata"), ("bookmarks", "0041_merge_metadata"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0042_userprofile_custom_css_hash"), ("bookmarks", "0042_userprofile_custom_css_hash"),
] ]

View File

@@ -25,7 +25,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0043_userprofile_collapse_side_panel"), ("bookmarks", "0043_userprofile_collapse_side_panel"),
] ]

View File

@@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0044_bookmark_latest_snapshot"), ("bookmarks", "0044_bookmark_latest_snapshot"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0045_userprofile_hide_bundles_bookmarkbundle"), ("bookmarks", "0045_userprofile_hide_bundles_bookmarkbundle"),
] ]

View File

@@ -1,6 +1,7 @@
# Generated by Django 5.2.3 on 2025-08-22 08:28 # Generated by Django 5.2.3 on 2025-08-22 08:28
from django.db import migrations, transaction from django.db import migrations, transaction
from bookmarks.utils import normalize_url from bookmarks.utils import normalize_url
@@ -25,7 +26,6 @@ def reverse_populate_url_normalized(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0046_add_url_normalized_field"), ("bookmarks", "0046_add_url_normalized_field"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0047_populate_url_normalized_field"), ("bookmarks", "0047_populate_url_normalized_field"),
] ]

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0048_userprofile_default_mark_shared"), ("bookmarks", "0048_userprofile_default_mark_shared"),
] ]

View File

@@ -9,7 +9,6 @@ User = get_user_model()
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
for user in User.objects.all(): for user in User.objects.all():
toast = Toast( toast = Toast(
key="new_search_toast", key="new_search_toast",
@@ -24,7 +23,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0049_userprofile_legacy_search"), ("bookmarks", "0049_userprofile_legacy_search"),
] ]

View File

@@ -1,6 +1,7 @@
# Generated by Django 5.2.5 on 2025-10-11 08:46 # Generated by Django 5.2.5 on 2025-10-11 08:46
from django.db import migrations from django.db import migrations
from bookmarks.utils import normalize_url from bookmarks.utils import normalize_url
@@ -21,7 +22,6 @@ def reverse_fix_url_normalized(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0050_new_search_toast"), ("bookmarks", "0050_new_search_toast"),
] ]

View File

@@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0051_fix_normalized_url"), ("bookmarks", "0051_fix_normalized_url"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),

View File

@@ -21,7 +21,6 @@ def migrate_tokens_reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bookmarks", "0052_apitoken"), ("bookmarks", "0052_apitoken"),
("authtoken", "0004_alter_tokenproxy_options"), ("authtoken", "0004_alter_tokenproxy_options"),

View File

@@ -1,20 +1,19 @@
import binascii
import hashlib import hashlib
import logging import logging
import os import os
from typing import List
import binascii
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.http import QueryDict from django.http import QueryDict
from bookmarks.utils import unique, normalize_url from bookmarks.utils import normalize_url, unique
from bookmarks.validators import BookmarkURLValidator from bookmarks.validators import BookmarkURLValidator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -48,7 +47,7 @@ def parse_tag_string(tag_string: str, delimiter: str = ","):
return names return names
def build_tag_string(tag_names: List[str], delimiter: str = ","): def build_tag_string(tag_names: list[str], delimiter: str = ","):
return delimiter.join(tag_names) return delimiter.join(tag_names)
@@ -354,8 +353,8 @@ class BookmarkSearchForm(forms.Form):
def __init__( def __init__(
self, self,
search: BookmarkSearch, search: BookmarkSearch,
editable_fields: List[str] = None, editable_fields: list[str] = None,
users: List[User] = None, users: list[User] = None,
): ):
super().__init__() super().__init__()
editable_fields = editable_fields or [] editable_fields = editable_fields or []
@@ -640,7 +639,7 @@ class GlobalSettings(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.pk and GlobalSettings.objects.exists(): if not self.pk and GlobalSettings.objects.exists():
raise Exception("There is already one instance of GlobalSettings") raise Exception("There is already one instance of GlobalSettings")
return super(GlobalSettings, self).save(*args, **kwargs) return super().save(*args, **kwargs)
class GlobalSettingsForm(forms.ModelForm): class GlobalSettingsForm(forms.ModelForm):
@@ -649,5 +648,5 @@ class GlobalSettingsForm(forms.ModelForm):
fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"] fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(GlobalSettingsForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["guest_profile_user"].empty_label = "Standard profile" self.fields["guest_profile_user"].empty_label = "Standard profile"

View File

@@ -1,9 +1,9 @@
from typing import Optional import contextlib
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q, QuerySet, Exists, OuterRef, Case, When, CharField from django.db.models import Case, CharField, Exists, OuterRef, Q, QuerySet, When
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.functions import Lower from django.db.models.functions import Lower
@@ -16,16 +16,16 @@ from bookmarks.models import (
parse_tag_string, parse_tag_string,
) )
from bookmarks.services.search_query_parser import ( from bookmarks.services.search_query_parser import (
parse_search_query,
SearchExpression,
TermExpression,
TagExpression,
SpecialKeywordExpression,
AndExpression, AndExpression,
OrExpression,
NotExpression, NotExpression,
OrExpression,
SearchExpression,
SearchQueryParseError, SearchQueryParseError,
SpecialKeywordExpression,
TagExpression,
TermExpression,
extract_tag_names_from_query, extract_tag_names_from_query,
parse_search_query,
) )
from bookmarks.utils import unique from bookmarks.utils import unique
@@ -45,7 +45,7 @@ def query_archived_bookmarks(
def query_shared_bookmarks( def query_shared_bookmarks(
user: Optional[User], user: User | None,
profile: UserProfile, profile: UserProfile,
search: BookmarkSearch, search: BookmarkSearch,
public_only: bool, public_only: bool,
@@ -215,7 +215,7 @@ def _filter_bundle(query_set: QuerySet, bundle: BookmarkBundle) -> QuerySet:
def _base_bookmarks_query( def _base_bookmarks_query(
user: Optional[User], user: User | None,
profile: UserProfile, profile: UserProfile,
search: BookmarkSearch, search: BookmarkSearch,
) -> QuerySet: ) -> QuerySet:
@@ -227,19 +227,15 @@ def _base_bookmarks_query(
# Filter by modified_since if provided # Filter by modified_since if provided
if search.modified_since: if search.modified_since:
try: # If the date format is invalid, ignore the filter
with contextlib.suppress(ValidationError):
query_set = query_set.filter(date_modified__gt=search.modified_since) query_set = query_set.filter(date_modified__gt=search.modified_since)
except ValidationError:
# If the date format is invalid, ignore the filter
pass
# Filter by added_since if provided # Filter by added_since if provided
if search.added_since: if search.added_since:
try: # If the date format is invalid, ignore the filter
with contextlib.suppress(ValidationError):
query_set = query_set.filter(date_added__gt=search.added_since) query_set = query_set.filter(date_added__gt=search.added_since)
except ValidationError:
# If the date format is invalid, ignore the filter
pass
# Filter by search query # Filter by search query
if profile.legacy_search: if profile.legacy_search:
@@ -320,7 +316,7 @@ def query_archived_bookmark_tags(
def query_shared_bookmark_tags( def query_shared_bookmark_tags(
user: Optional[User], user: User | None,
profile: UserProfile, profile: UserProfile,
search: BookmarkSearch, search: BookmarkSearch,
public_only: bool, public_only: bool,
@@ -360,7 +356,7 @@ def get_tags_for_query(user: User, profile: UserProfile, query: str) -> QuerySet
def get_shared_tags_for_query( def get_shared_tags_for_query(
user: Optional[User], profile: UserProfile, query: str, public_only: bool user: User | None, profile: UserProfile, query: str, public_only: bool
) -> QuerySet: ) -> QuerySet:
tag_names = extract_tag_names_from_query(query, profile) tag_names = extract_tag_names_from_query(query, profile)

View File

@@ -5,7 +5,7 @@ import shutil
from django.conf import settings from django.conf import settings
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.utils import timezone, formats from django.utils import formats, timezone
from bookmarks.models import Bookmark, BookmarkAsset from bookmarks.models import Bookmark, BookmarkAsset
from bookmarks.services import singlefile from bookmarks.services import singlefile

View File

@@ -1,5 +1,6 @@
from urllib.parse import urlparse, parse_qs
import re import re
from urllib.parse import parse_qs, urlparse
import idna import idna

View File

@@ -1,12 +1,9 @@
import logging import logging
from typing import Union
from django.utils import timezone from django.utils import timezone
from bookmarks.models import Bookmark, User, parse_tag_string from bookmarks.models import Bookmark, User, parse_tag_string
from bookmarks.services import auto_tagging from bookmarks.services import auto_tagging, tasks, website_loader
from bookmarks.services import tasks
from bookmarks.services import website_loader
from bookmarks.services.tags import get_or_create_tags from bookmarks.services.tags import get_or_create_tags
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -91,7 +88,7 @@ def archive_bookmark(bookmark: Bookmark):
return bookmark return bookmark
def archive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def archive_bookmarks(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -106,7 +103,7 @@ def unarchive_bookmark(bookmark: Bookmark):
return bookmark return bookmark
def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def unarchive_bookmarks(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -114,13 +111,13 @@ def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
) )
def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def delete_bookmarks(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).delete() Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).delete()
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User): def tag_bookmarks(bookmark_ids: [int | str], tag_string: str, current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmark_ids = Bookmark.objects.filter( owned_bookmark_ids = Bookmark.objects.filter(
owner=current_user, id__in=sanitized_bookmark_ids owner=current_user, id__in=sanitized_bookmark_ids
@@ -143,9 +140,7 @@ def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user
) )
def untag_bookmarks( def untag_bookmarks(bookmark_ids: [int | str], tag_string: str, current_user: User):
bookmark_ids: [Union[int, str]], tag_string: str, current_user: User
):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmark_ids = Bookmark.objects.filter( owned_bookmark_ids = Bookmark.objects.filter(
owner=current_user, id__in=sanitized_bookmark_ids owner=current_user, id__in=sanitized_bookmark_ids
@@ -165,7 +160,7 @@ def untag_bookmarks(
) )
def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User): def mark_bookmarks_as_read(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -173,7 +168,7 @@ def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User):
) )
def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User): def mark_bookmarks_as_unread(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -181,7 +176,7 @@ def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User
) )
def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def share_bookmarks(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -189,7 +184,7 @@ def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
) )
def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def unshare_bookmarks(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -197,7 +192,7 @@ def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
) )
def refresh_bookmarks_metadata(bookmark_ids: [Union[int, str]], current_user: User): def refresh_bookmarks_metadata(bookmark_ids: [int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmarks = Bookmark.objects.filter( owned_bookmarks = Bookmark.objects.filter(
owner=current_user, id__in=sanitized_bookmark_ids owner=current_user, id__in=sanitized_bookmark_ids
@@ -208,7 +203,7 @@ def refresh_bookmarks_metadata(bookmark_ids: [Union[int, str]], current_user: Us
tasks.load_preview_image(current_user, bookmark) tasks.load_preview_image(current_user, bookmark)
def create_html_snapshots(bookmark_ids: list[Union[int, str]], current_user: User): def create_html_snapshots(bookmark_ids: list[int | str], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmarks = Bookmark.objects.filter( owned_bookmarks = Bookmark.objects.filter(
owner=current_user, id__in=sanitized_bookmark_ids owner=current_user, id__in=sanitized_bookmark_ids
@@ -246,6 +241,6 @@ def _update_bookmark_tags(bookmark: Bookmark, tag_string: str, user: User):
bookmark.tags.set(tags) bookmark.tags.set(tags)
def _sanitize_id_list(bookmark_ids: [Union[int, str]]) -> [int]: def _sanitize_id_list(bookmark_ids: [int | str]) -> [int]:
# Convert string ids to int if necessary # Convert string ids to int if necessary
return [int(bm_id) if isinstance(bm_id, str) else bm_id for bm_id in bookmark_ids] return [int(bm_id) if isinstance(bm_id, str) else bm_id for bm_id in bookmark_ids]

View File

@@ -1,12 +1,11 @@
import html import html
from typing import List
from bookmarks.models import Bookmark from bookmarks.models import Bookmark
BookmarkDocument = List[str] BookmarkDocument = list[str]
def export_netscape_html(bookmarks: List[Bookmark]): def export_netscape_html(bookmarks: list[Bookmark]):
doc = [] doc = []
append_header(doc) append_header(doc)
append_list_start(doc) append_list_start(doc)

View File

@@ -1,13 +1,12 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import List
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
from bookmarks.models import Bookmark, Tag from bookmarks.models import Bookmark, Tag
from bookmarks.services import tasks from bookmarks.services import tasks
from bookmarks.services.parser import parse, NetscapeBookmark from bookmarks.services.parser import NetscapeBookmark, parse
from bookmarks.utils import normalize_url, parse_timestamp from bookmarks.utils import normalize_url, parse_timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,13 +40,13 @@ class TagCache:
else: else:
return None return None
def get_all(self, tag_names: List[str]): def get_all(self, tag_names: list[str]):
result = [] result = []
for tag_name in tag_names: for tag_name in tag_names:
tag = self.get(tag_name) tag = self.get(tag_name)
# Tag may not have been created if tag name exceeded maximum length # Tag may not have been created if tag name exceeded maximum length
# Prevent returning duplicates # Prevent returning duplicates
if tag and not (tag in result): if tag and tag not in result:
result.append(tag) result.append(tag)
return result return result
@@ -57,14 +56,16 @@ class TagCache:
def import_netscape_html( def import_netscape_html(
html: str, user: User, options: ImportOptions = ImportOptions() html: str, user: User, options: ImportOptions | None = None
) -> ImportResult: ) -> ImportResult:
if options is None:
options = ImportOptions()
result = ImportResult() result = ImportResult()
import_start = timezone.now() import_start = timezone.now()
try: try:
netscape_bookmarks = parse(html) netscape_bookmarks = parse(html)
except: except Exception:
logging.exception("Could not read bookmarks file.") logging.exception("Could not read bookmarks file.")
raise raise
@@ -91,7 +92,7 @@ def import_netscape_html(
return result return result
def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User): def _create_missing_tags(netscape_bookmarks: list[NetscapeBookmark], user: User):
tag_cache = TagCache(user) tag_cache = TagCache(user)
tags_to_create = [] tags_to_create = []
@@ -114,7 +115,7 @@ def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User)
Tag.objects.bulk_create(tags_to_create) Tag.objects.bulk_create(tags_to_create)
def _get_batches(items: List, batch_size: int): def _get_batches(items: list, batch_size: int):
batches = [] batches = []
offset = 0 offset = 0
num_items = len(items) num_items = len(items)
@@ -129,7 +130,7 @@ def _get_batches(items: List, batch_size: int):
def _import_batch( def _import_batch(
netscape_bookmarks: List[NetscapeBookmark], netscape_bookmarks: list[NetscapeBookmark],
user: User, user: User,
options: ImportOptions, options: ImportOptions,
tag_cache: TagCache, tag_cache: TagCache,
@@ -172,7 +173,7 @@ def _import_batch(
bookmarks_to_create.append(bookmark) bookmarks_to_create.append(bookmark)
result.success = result.success + 1 result.success = result.success + 1
except: except Exception:
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..." shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..."
logging.exception("Error importing bookmark: " + shortened_bookmark_tag_str) logging.exception("Error importing bookmark: " + shortened_bookmark_tag_str)
result.failed = result.failed + 1 result.failed = result.failed + 1

View File

@@ -1,7 +1,7 @@
import gzip import gzip
import os
import shutil import shutil
import subprocess import subprocess
import os
from django.conf import settings from django.conf import settings
@@ -30,4 +30,4 @@ def create_snapshot(url: str, filepath: str):
os.remove(temp_filepath) os.remove(temp_filepath)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise MonolithError(f"Failed to create snapshot: {error.stderr}") raise MonolithError(f"Failed to create snapshot: {error.stderr}") from error

View File

@@ -1,6 +1,6 @@
import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from html.parser import HTMLParser from html.parser import HTMLParser
from typing import Dict, List
from bookmarks.models import parse_tag_string from bookmarks.models import parse_tag_string
@@ -13,7 +13,7 @@ class NetscapeBookmark:
notes: str notes: str
date_added: str date_added: str
date_modified: str date_modified: str
tag_names: List[str] tag_names: list[str]
to_read: bool to_read: bool
private: bool private: bool
archived: bool archived: bool
@@ -56,17 +56,15 @@ class BookmarkParser(HTMLParser):
def handle_end_dl(self): def handle_end_dl(self):
self.add_bookmark() self.add_bookmark()
def handle_start_dt(self, attrs: Dict[str, str]): def handle_start_dt(self, attrs: dict[str, str]):
self.add_bookmark() self.add_bookmark()
def handle_start_a(self, attrs: Dict[str, str]): def handle_start_a(self, attrs: dict[str, str]):
vars(self).update(attrs) vars(self).update(attrs)
tag_names = parse_tag_string(self.tags) tag_names = parse_tag_string(self.tags)
archived = "linkding:bookmarks.archived" in self.tags archived = "linkding:bookmarks.archived" in self.tags
try: with contextlib.suppress(ValueError):
tag_names.remove("linkding:bookmarks.archived") tag_names.remove("linkding:bookmarks.archived")
except ValueError:
pass
self.bookmark = NetscapeBookmark( self.bookmark = NetscapeBookmark(
href=self.href, href=self.href,
@@ -109,7 +107,7 @@ class BookmarkParser(HTMLParser):
self.private = "" self.private = ""
def parse(html: str) -> List[NetscapeBookmark]: def parse(html: str) -> list[NetscapeBookmark]:
parser = BookmarkParser() parser = BookmarkParser()
parser.feed(html) parser.feed(html)
return parser.bookmarks return parser.bookmarks

View File

@@ -1,11 +1,12 @@
import hashlib
import logging import logging
import mimetypes import mimetypes
import os.path import os.path
import hashlib
from pathlib import Path from pathlib import Path
import requests import requests
from django.conf import settings from django.conf import settings
from bookmarks.services import website_loader from bookmarks.services import website_loader
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,6 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import List, Optional
from bookmarks.models import UserProfile from bookmarks.models import UserProfile
@@ -124,7 +123,7 @@ class SearchQueryTokenizer:
return keyword return keyword
def tokenize(self) -> List[Token]: def tokenize(self) -> list[Token]:
"""Convert the query string into a list of tokens.""" """Convert the query string into a list of tokens."""
tokens = [] tokens = []
@@ -221,7 +220,7 @@ class SearchQueryParseError(Exception):
class SearchQueryParser: class SearchQueryParser:
def __init__(self, tokens: List[Token]): def __init__(self, tokens: list[Token]):
self.tokens = tokens self.tokens = tokens
self.position = 0 self.position = 0
self.current_token = tokens[0] if tokens else Token(TokenType.EOF, "", 0) self.current_token = tokens[0] if tokens else Token(TokenType.EOF, "", 0)
@@ -244,7 +243,7 @@ class SearchQueryParser:
self.current_token.position, self.current_token.position,
) )
def parse(self) -> Optional[SearchExpression]: def parse(self) -> SearchExpression | None:
"""Parse the tokens into an AST.""" """Parse the tokens into an AST."""
if not self.tokens or ( if not self.tokens or (
len(self.tokens) == 1 and self.tokens[0].type == TokenType.EOF len(self.tokens) == 1 and self.tokens[0].type == TokenType.EOF
@@ -283,7 +282,6 @@ class SearchQueryParser:
TokenType.LPAREN, TokenType.LPAREN,
TokenType.NOT, TokenType.NOT,
]: ]:
if self.current_token.type == TokenType.AND: if self.current_token.type == TokenType.AND:
self.advance() # consume explicit AND self.advance() # consume explicit AND
# else: implicit AND (don't advance token) # else: implicit AND (don't advance token)
@@ -328,7 +326,7 @@ class SearchQueryParser:
) )
def parse_search_query(query: str) -> Optional[SearchExpression]: def parse_search_query(query: str) -> SearchExpression | None:
if not query or not query.strip(): if not query or not query.strip():
return None return None
@@ -342,9 +340,9 @@ def _needs_parentheses(expr: SearchExpression, parent_type: type) -> bool:
if isinstance(expr, OrExpression) and parent_type == AndExpression: if isinstance(expr, OrExpression) and parent_type == AndExpression:
return True return True
# AndExpression or OrExpression needs parentheses when inside NotExpression # AndExpression or OrExpression needs parentheses when inside NotExpression
if isinstance(expr, (AndExpression, OrExpression)) and parent_type == NotExpression: return (
return True isinstance(expr, (AndExpression, OrExpression)) and parent_type == NotExpression
return False )
def _is_simple_expression(expr: SearchExpression) -> bool: def _is_simple_expression(expr: SearchExpression) -> bool:
@@ -412,15 +410,15 @@ def _expression_to_string(expr: SearchExpression, parent_type: type = None) -> s
raise ValueError(f"Unknown expression type: {type(expr)}") raise ValueError(f"Unknown expression type: {type(expr)}")
def expression_to_string(expr: Optional[SearchExpression]) -> str: def expression_to_string(expr: SearchExpression | None) -> str:
if expr is None: if expr is None:
return "" return ""
return _expression_to_string(expr) return _expression_to_string(expr)
def _strip_tag_from_expression( def _strip_tag_from_expression(
expr: Optional[SearchExpression], tag_name: str, enable_lax_search: bool = False expr: SearchExpression | None, tag_name: str, enable_lax_search: bool = False
) -> Optional[SearchExpression]: ) -> SearchExpression | None:
if expr is None: if expr is None:
return None return None
@@ -511,8 +509,8 @@ def strip_tag_from_query(
def _extract_tag_names_from_expression( def _extract_tag_names_from_expression(
expr: Optional[SearchExpression], enable_lax_search: bool = False expr: SearchExpression | None, enable_lax_search: bool = False
) -> List[str]: ) -> list[str]:
if expr is None: if expr is None:
return [] return []
@@ -546,7 +544,7 @@ def _extract_tag_names_from_expression(
def extract_tag_names_from_query( def extract_tag_names_from_query(
query: str, user_profile: UserProfile | None = None query: str, user_profile: UserProfile | None = None
) -> List[str]: ) -> list[str]:
try: try:
ast = parse_search_query(query) ast = parse_search_query(query)
except SearchQueryParseError: except SearchQueryParseError:

View File

@@ -38,12 +38,12 @@ def create_snapshot(url: str, filepath: str):
) )
process.terminate() process.terminate()
process.wait(timeout=20) process.wait(timeout=20)
raise SingleFileError("Timeout expired while creating snapshot") raise SingleFileError("Timeout expired while creating snapshot") from None
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
# Kill the whole process group, which should also clean up any chromium # Kill the whole process group, which should also clean up any chromium
# processes spawned by single-file # processes spawned by single-file
logger.error("Timeout expired while terminating. Killing process...") logger.error("Timeout expired while terminating. Killing process...")
os.killpg(os.getpgid(process.pid), signal.SIGTERM) os.killpg(os.getpgid(process.pid), signal.SIGTERM)
raise SingleFileError("Timeout expired while creating snapshot") raise SingleFileError("Timeout expired while creating snapshot") from None
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise SingleFileError(f"Failed to create snapshot: {error.stderr}") raise SingleFileError(f"Failed to create snapshot: {error.stderr}") from error

View File

@@ -1,6 +1,5 @@
import logging import logging
import operator import operator
from typing import List
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
@@ -11,7 +10,7 @@ from bookmarks.utils import unique
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_or_create_tags(tag_names: List[str], user: User): def get_or_create_tags(tag_names: list[str], user: User):
tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names] tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names]
return unique(tags, operator.attrgetter("id")) return unique(tags, operator.attrgetter("id"))
@@ -28,10 +27,10 @@ def get_or_create_tag(name: str, user: User):
# Legacy databases might contain duplicate tags with different capitalization # Legacy databases might contain duplicate tags with different capitalization
first_tag = Tag.objects.filter(name__iexact=name, owner=user).first() first_tag = Tag.objects.filter(name__iexact=name, owner=user).first()
message = ( message = (
"Found multiple tags for the name '{0}' with different capitalization. " f"Found multiple tags for the name '{name}' with different capitalization. "
"Using the first tag with the name '{1}'. " f"Using the first tag with the name '{first_tag.name}'. "
"Since v.1.2 tags work case-insensitive, which means duplicates of the same name are not allowed anymore. " "Since v.1.2 tags work case-insensitive, which means duplicates of the same name are not allowed anymore. "
"To solve this error remove the duplicate tag in admin." "To solve this error remove the duplicate tag in admin."
).format(name, first_tag.name) )
logger.error(message) logger.error(message)
return first_tag return first_tag

View File

@@ -1,6 +1,5 @@
import functools import functools
import logging import logging
from typing import List
import waybackpy import waybackpy
from django.conf import settings from django.conf import settings
@@ -10,7 +9,7 @@ from django.utils import timezone
from huey import crontab from huey import crontab
from huey.contrib.djhuey import HUEY as huey from huey.contrib.djhuey import HUEY as huey
from huey.exceptions import TaskLockedException from huey.exceptions import TaskLockedException
from waybackpy.exceptions import WaybackError, TooManyRequestsError from waybackpy.exceptions import TooManyRequestsError, WaybackError
from bookmarks.models import Bookmark, BookmarkAsset, UserProfile from bookmarks.models import Bookmark, BookmarkAsset, UserProfile
from bookmarks.services import assets, favicon_loader, preview_image_loader from bookmarks.services import assets, favicon_loader, preview_image_loader
@@ -263,7 +262,7 @@ def create_html_snapshot(bookmark: Bookmark):
asset.save() asset.save()
def create_html_snapshots(bookmark_list: List[Bookmark]): def create_html_snapshots(bookmark_list: list[Bookmark]):
if not is_html_snapshot_feature_active(): if not is_html_snapshot_feature_active():
return return

View File

@@ -81,10 +81,12 @@ def _load_website_metadata(url: str):
end = timezone.now() end = timezone.now()
logger.debug(f"Parsing duration: {end - start}") logger.debug(f"Parsing duration: {end - start}")
finally: except Exception:
return WebsiteMetadata( pass
url=url, title=title, description=description, preview_image=preview_image
) return WebsiteMetadata(
url=url, title=title, description=description, preview_image=preview_image
)
CHUNK_SIZE = 50 * 1024 CHUNK_SIZE = 50 * 1024
@@ -101,15 +103,12 @@ def load_page(url: str):
for chunk in r.iter_content(chunk_size=CHUNK_SIZE): for chunk in r.iter_content(chunk_size=CHUNK_SIZE):
size += len(chunk) size += len(chunk)
iteration = iteration + 1 iteration = iteration + 1
if content is None: content = chunk if content is None else content + chunk
content = chunk
else:
content = content + chunk
logger.debug(f"Loaded chunk (iteration={iteration}, total={size / 1024})") logger.debug(f"Loaded chunk (iteration={iteration}, total={size / 1024})")
# Stop reading if we have parsed end of head tag # Stop reading if we have parsed end of head tag
end_of_head = "</head>".encode("utf-8") end_of_head = b"</head>"
if end_of_head in content: if end_of_head in content:
logger.debug(f"Found closing head tag after {size} bytes") logger.debug(f"Found closing head tag after {size} bytes")
content = content.split(end_of_head)[0] + end_of_head content = content.split(end_of_head)[0] + end_of_head

View File

@@ -1,4 +1,5 @@
# Use dev settings as default, use production if dev settings do not exist # Use dev settings as default, use production if dev settings do not exist
# ruff: noqa
try: try:
from .dev import * from .dev import *
except: except:

View File

@@ -2,6 +2,8 @@
Development settings for linkding webapp Development settings for linkding webapp
""" """
# ruff: noqa
# Start from development settings # Start from development settings
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from .base import * from .base import *

View File

@@ -2,6 +2,8 @@
Production settings for linkding webapp Production settings for linkding webapp
""" """
# ruff: noqa
# Start from development settings # Start from development settings
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import os import os

View File

@@ -1,2 +1,3 @@
# Expose task modules to Huey Django extension # Expose task modules to Huey Django extension
import bookmarks.services.tasks # noinspection PyUnusedImports
import bookmarks.services.tasks # noqa: F401

View File

@@ -2,12 +2,11 @@ import re
import bleach import bleach
import markdown import markdown
from bleach_allowlist import markdown_tags, markdown_attrs from bleach_allowlist import markdown_attrs, markdown_tags
from django import template from django import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from bookmarks import utils from bookmarks import utils
from bookmarks.models import UserProfile
register = template.Library() register = template.Library()
@@ -82,7 +81,7 @@ class HtmlMinNode(template.Node):
def render_markdown(context, markdown_text): def render_markdown(context, markdown_text):
# naive approach to reusing the renderer for a single request # naive approach to reusing the renderer for a single request
# works for bookmark list for now # works for bookmark list for now
if not ("markdown_renderer" in context): if "markdown_renderer" not in context:
renderer = markdown.Markdown(extensions=["fenced_code", "nl2br"]) renderer = markdown.Markdown(extensions=["fenced_code", "nl2br"])
context["markdown_renderer"] = renderer context["markdown_renderer"] = renderer
else: else:

View File

@@ -5,7 +5,6 @@ import random
import shutil import shutil
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from typing import List
from unittest import TestCase from unittest import TestCase
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -307,7 +306,7 @@ class HtmlTestMixin:
class BookmarkListTestMixin(TestCase, HtmlTestMixin): class BookmarkListTestMixin(TestCase, HtmlTestMixin):
def assertVisibleBookmarks( def assertVisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank" self, response, bookmarks: list[Bookmark], link_target: str = "_blank"
): ):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
bookmark_list = soup.select_one( bookmark_list = soup.select_one(
@@ -325,7 +324,7 @@ class BookmarkListTestMixin(TestCase, HtmlTestMixin):
self.assertIsNotNone(bookmark_item) self.assertIsNotNone(bookmark_item)
def assertInvisibleBookmarks( def assertInvisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank" self, response, bookmarks: list[Bookmark], link_target: str = "_blank"
): ):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
@@ -337,7 +336,7 @@ class BookmarkListTestMixin(TestCase, HtmlTestMixin):
class TagCloudTestMixin(TestCase, HtmlTestMixin): class TagCloudTestMixin(TestCase, HtmlTestMixin):
def assertVisibleTags(self, response, tags: List[Tag]): def assertVisibleTags(self, response, tags: list[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_cloud = soup.select_one("div.tag-cloud") tag_cloud = soup.select_one("div.tag-cloud")
self.assertIsNotNone(tag_cloud) self.assertIsNotNone(tag_cloud)
@@ -350,7 +349,7 @@ class TagCloudTestMixin(TestCase, HtmlTestMixin):
for tag in tags: for tag in tags:
self.assertTrue(tag.name in tag_item_names) self.assertTrue(tag.name in tag_item_names)
def assertInvisibleTags(self, response, tags: List[Tag]): def assertInvisibleTags(self, response, tags: list[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_items = soup.select("a[data-is-tag-item]") tag_items = soup.select("a[data-is-tag-item]")
@@ -359,7 +358,7 @@ class TagCloudTestMixin(TestCase, HtmlTestMixin):
for tag in tags: for tag in tags:
self.assertFalse(tag.name in tag_item_names) self.assertFalse(tag.name in tag_item_names)
def assertSelectedTags(self, response, tags: List[Tag]): def assertSelectedTags(self, response, tags: list[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
selected_tags = soup.select_one("p.selected-tags") selected_tags = soup.select_one("p.selected-tags")
self.assertIsNotNone(selected_tags) self.assertIsNotNone(selected_tags)
@@ -433,18 +432,18 @@ class ImportTestMixin:
def render_tag(self, tag: BookmarkHtmlTag): def render_tag(self, tag: BookmarkHtmlTag):
return f""" return f"""
<DT> <DT>
<A {f'HREF="{tag.href}"' if tag.href else ''} <A {f'HREF="{tag.href}"' if tag.href else ""}
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''} {f'ADD_DATE="{tag.add_date}"' if tag.add_date else ""}
{f'LAST_MODIFIED="{tag.last_modified}"' if tag.last_modified else ''} {f'LAST_MODIFIED="{tag.last_modified}"' if tag.last_modified else ""}
{f'TAGS="{tag.tags}"' if tag.tags else ''} {f'TAGS="{tag.tags}"' if tag.tags else ""}
TOREAD="{1 if tag.to_read else 0}" TOREAD="{1 if tag.to_read else 0}"
PRIVATE="{1 if tag.private else 0}"> PRIVATE="{1 if tag.private else 0}">
{tag.title if tag.title else ''} {tag.title if tag.title else ""}
</A> </A>
{f'<DD>{tag.description}' if tag.description else ''} {f"<DD>{tag.description}" if tag.description else ""}
""" """
def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ""): def render_html(self, tags: list[BookmarkHtmlTag] = None, tags_html: str = ""):
if tags: if tags:
rendered_tags = [self.render_tag(tag) for tag in tags] rendered_tags = [self.render_tag(tag) for tag in tags]
tags_html = "\n".join(rendered_tags) tags_html = "\n".join(rendered_tags)

View File

@@ -2,6 +2,7 @@ import datetime
import gzip import gzip
import os import os
from datetime import timedelta from datetime import timedelta
from pathlib import Path
from unittest import mock from unittest import mock
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
@@ -14,7 +15,6 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
class AssetServiceTestCase(TestCase, BookmarkFactoryMixin): class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def setUp(self) -> None:
self.setup_temp_assets_dir() self.setup_temp_assets_dir()
self.get_or_create_test_user() self.get_or_create_test_user()
@@ -27,7 +27,7 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
self.mock_singlefile_create_snapshot_patcher.start() self.mock_singlefile_create_snapshot_patcher.start()
) )
self.mock_singlefile_create_snapshot.side_effect = lambda url, filepath: ( self.mock_singlefile_create_snapshot.side_effect = lambda url, filepath: (
open(filepath, "w").write(self.html_content) Path(filepath).write_text(self.html_content)
) )
def tearDown(self) -> None: def tearDown(self) -> None:
@@ -55,16 +55,14 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertIsNone(asset.id) self.assertIsNone(asset.id)
def test_create_snapshot(self): def test_create_snapshot(self):
initial_modified = timezone.datetime( initial_modified = timezone.datetime(2025, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
2025, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
)
bookmark = self.setup_bookmark( bookmark = self.setup_bookmark(
url="https://example.com", modified=initial_modified url="https://example.com", modified=initial_modified
) )
asset = assets.create_snapshot_asset(bookmark) asset = assets.create_snapshot_asset(bookmark)
asset.save() asset.save()
asset.date_created = timezone.datetime( asset.date_created = timezone.datetime(
2023, 8, 11, 21, 45, 11, tzinfo=datetime.timezone.utc 2023, 8, 11, 21, 45, 11, tzinfo=datetime.UTC
) )
assets.create_snapshot(asset) assets.create_snapshot(asset)
@@ -104,9 +102,11 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
asset = assets.create_snapshot_asset(bookmark) asset = assets.create_snapshot_asset(bookmark)
asset.save() asset.save()
self.mock_singlefile_create_snapshot.side_effect = Exception self.mock_singlefile_create_snapshot.side_effect = RuntimeError(
"Snapshot failed"
)
with self.assertRaises(Exception): with self.assertRaises(RuntimeError):
assets.create_snapshot(asset) assets.create_snapshot(asset)
asset.refresh_from_db() asset.refresh_from_db()
@@ -128,9 +128,7 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(saved_file.endswith("aaaa.html.gz")) self.assertTrue(saved_file.endswith("aaaa.html.gz"))
def test_upload_snapshot(self): def test_upload_snapshot(self):
initial_modified = timezone.datetime( initial_modified = timezone.datetime(2025, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
2025, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
)
bookmark = self.setup_bookmark( bookmark = self.setup_bookmark(
url="https://example.com", modified=initial_modified url="https://example.com", modified=initial_modified
) )
@@ -167,9 +165,9 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
# make gzip.open raise an exception # make gzip.open raise an exception
with mock.patch("gzip.open") as mock_gzip_open: with mock.patch("gzip.open") as mock_gzip_open:
mock_gzip_open.side_effect = Exception mock_gzip_open.side_effect = OSError("File operation failed")
with self.assertRaises(Exception): with self.assertRaises(OSError):
assets.upload_snapshot(bookmark, b"invalid content") assets.upload_snapshot(bookmark, b"invalid content")
# asset is not saved to the database # asset is not saved to the database
@@ -190,9 +188,7 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
@disable_logging @disable_logging
def test_upload_asset(self): def test_upload_asset(self):
initial_modified = timezone.datetime( initial_modified = timezone.datetime(2025, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
2025, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
)
bookmark = self.setup_bookmark(modified=initial_modified) bookmark = self.setup_bookmark(modified=initial_modified)
file_content = b"test content" file_content = b"test content"
upload_file = SimpleUploadedFile( upload_file = SimpleUploadedFile(
@@ -229,9 +225,7 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
@disable_logging @disable_logging
def test_upload_gzip_asset(self): def test_upload_gzip_asset(self):
initial_modified = timezone.datetime( initial_modified = timezone.datetime(2025, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
2025, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
)
bookmark = self.setup_bookmark(modified=initial_modified) bookmark = self.setup_bookmark(modified=initial_modified)
file_content = gzip.compress(b"<html>test content</html>") file_content = gzip.compress(b"<html>test content</html>")
upload_file = SimpleUploadedFile( upload_file = SimpleUploadedFile(
@@ -292,9 +286,9 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
# make open raise an exception # make open raise an exception
with mock.patch("builtins.open") as mock_open: with mock.patch("builtins.open") as mock_open:
mock_open.side_effect = Exception mock_open.side_effect = OSError("File operation failed")
with self.assertRaises(Exception): with self.assertRaises(OSError):
assets.upload_asset(bookmark, upload_file) assets.upload_asset(bookmark, upload_file)
# asset is not saved to the database # asset is not saved to the database
@@ -344,12 +338,12 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
failing_asset.save() failing_asset.save()
# Make the snapshot creation fail # Make the snapshot creation fail
self.mock_singlefile_create_snapshot.side_effect = Exception( self.mock_singlefile_create_snapshot.side_effect = RuntimeError(
"Snapshot creation failed" "Snapshot creation failed"
) )
# Attempt to create a snapshot (which will fail) # Attempt to create a snapshot (which will fail)
with self.assertRaises(Exception): with self.assertRaises(RuntimeError):
assets.create_snapshot(failing_asset) assets.create_snapshot(failing_asset)
# Verify that the bookmark's latest_snapshot is still the initial snapshot # Verify that the bookmark's latest_snapshot is still the initial snapshot
@@ -365,10 +359,10 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
# Make the gzip.open function fail # Make the gzip.open function fail
with mock.patch("gzip.open") as mock_gzip_open: with mock.patch("gzip.open") as mock_gzip_open:
mock_gzip_open.side_effect = Exception("Upload failed") mock_gzip_open.side_effect = OSError("Upload failed")
# Attempt to upload a snapshot (which will fail) # Attempt to upload a snapshot (which will fail)
with self.assertRaises(Exception): with self.assertRaises(OSError):
assets.upload_snapshot(bookmark, b"New content") assets.upload_snapshot(bookmark, b"New content")
# Verify that the bookmark's latest_snapshot is still the initial snapshot # Verify that the bookmark's latest_snapshot is still the initial snapshot
@@ -474,9 +468,7 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
@disable_logging @disable_logging
def test_remove_asset(self): def test_remove_asset(self):
initial_modified = timezone.datetime( initial_modified = timezone.datetime(2025, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
2025, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
)
bookmark = self.setup_bookmark(modified=initial_modified) bookmark = self.setup_bookmark(modified=initial_modified)
file_content = b"test content for removal" file_content = b"test content for removal"
upload_file = SimpleUploadedFile( upload_file = SimpleUploadedFile(

View File

@@ -1,11 +1,10 @@
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin, LinkdingApiTestCase
class AuthApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): class AuthApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def authenticate(self, keyword): def authenticate(self, keyword):
self.api_token = self.setup_api_token() self.api_token = self.setup_api_token()
self.client.credentials(HTTP_AUTHORIZATION=f"{keyword} {self.api_token.key}") self.client.credentials(HTTP_AUTHORIZATION=f"{keyword} {self.api_token.key}")

View File

@@ -1,9 +1,10 @@
from unittest.mock import patch, PropertyMock from unittest.mock import PropertyMock, patch
from django.test import TestCase, modify_settings from django.test import TestCase, modify_settings
from django.urls import reverse from django.urls import reverse
from bookmarks.models import User
from bookmarks.middlewares import CustomRemoteUserMiddleware from bookmarks.middlewares import CustomRemoteUserMiddleware
from bookmarks.models import User
class AuthProxySupportTest(TestCase): class AuthProxySupportTest(TestCase):

View File

@@ -1,6 +1,7 @@
from bookmarks.services import auto_tagging
from django.test import TestCase from django.test import TestCase
from bookmarks.services import auto_tagging
class AutoTaggingTestCase(TestCase): class AutoTaggingTestCase(TestCase):
def test_auto_tag_by_domain(self): def test_auto_tag_by_domain(self):

View File

@@ -19,7 +19,6 @@ from bookmarks.tests.helpers import (
class BookmarkActionViewTestCase( class BookmarkActionViewTestCase(
TestCase, BookmarkFactoryMixin, BookmarkListTestMixin, TagCloudTestMixin TestCase, BookmarkFactoryMixin, BookmarkListTestMixin, TagCloudTestMixin
): ):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)

View File

@@ -15,7 +15,6 @@ from bookmarks.tests.helpers import (
class BookmarkArchivedViewTestCase( class BookmarkArchivedViewTestCase(
TestCase, BookmarkFactoryMixin, BookmarkListTestMixin, TagCloudTestMixin TestCase, BookmarkFactoryMixin, BookmarkListTestMixin, TagCloudTestMixin
): ):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)
@@ -305,7 +304,7 @@ class BookmarkArchivedViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_unarchive">Unarchive</option> <option value="bulk_unarchive">Unarchive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@@ -326,7 +325,7 @@ class BookmarkArchivedViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_unarchive">Unarchive</option> <option value="bulk_unarchive">Unarchive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@@ -351,7 +350,7 @@ class BookmarkArchivedViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_unarchive">Unarchive</option> <option value="bulk_unarchive">Unarchive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@@ -378,7 +377,7 @@ class BookmarkArchivedViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_unarchive">Unarchive</option> <option value="bulk_unarchive">Unarchive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>

View File

@@ -11,7 +11,6 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
class BookmarkArchivedViewPerformanceTestCase( class BookmarkArchivedViewPerformanceTestCase(
TransactionTestCase, BookmarkFactoryMixin, HtmlTestMixin TransactionTestCase, BookmarkFactoryMixin, HtmlTestMixin
): ):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)

View File

@@ -6,7 +6,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from bookmarks.models import BookmarkAsset from bookmarks.models import BookmarkAsset
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin, LinkdingApiTestCase
class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):

View File

@@ -262,9 +262,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
self.assertIsNotNone(image) self.assertIsNotNone(image)
def test_internet_archive_link_with_fallback_url(self): def test_internet_archive_link_with_fallback_url(self):
date_added = timezone.datetime( date_added = timezone.datetime(2023, 8, 11, 21, 45, 11, tzinfo=datetime.UTC)
2023, 8, 11, 21, 45, 11, tzinfo=datetime.timezone.utc
)
bookmark = self.setup_bookmark(url="https://example.com/", added=date_added) bookmark = self.setup_bookmark(url="https://example.com/", added=date_added)
fallback_web_archive_url = ( fallback_web_archive_url = (
"https://web.archive.org/web/20230811214511/https://example.com/" "https://web.archive.org/web/20230811214511/https://example.com/"

View File

@@ -7,7 +7,6 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin): class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)

View File

@@ -299,7 +299,7 @@ class BookmarkIndexViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_archive">Archive</option> <option value="bulk_archive">Archive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@@ -320,7 +320,7 @@ class BookmarkIndexViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_archive">Archive</option> <option value="bulk_archive">Archive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@@ -345,7 +345,7 @@ class BookmarkIndexViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_archive">Archive</option> <option value="bulk_archive">Archive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@@ -372,7 +372,7 @@ class BookmarkIndexViewTestCase(
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
f""" """
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_archive">Archive</option> <option value="bulk_archive">Archive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>

Some files were not shown because too many files have changed in this diff Show More