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:
uv run pytest -n auto
lint:
uv run ruff check bookmarks
format:
uv run black bookmarks
uv run ruff format bookmarks
uv run djlint bookmarks/templates --reformat --quiet --warn
npx prettier bookmarks/frontend --write
npx prettier bookmarks/styles --write

View File

@@ -96,9 +96,17 @@ Run all tests with pytest:
make test
```
### Linting
Run linting with ruff:
```
make lint
```
### 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
```

View File

@@ -1,4 +1,5 @@
import os
from django import forms
from django.contrib import admin, messages
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.shortcuts import render
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 bookmarks.models import (
@@ -16,10 +17,10 @@ from bookmarks.models import (
Bookmark,
BookmarkAsset,
BookmarkBundle,
Tag,
UserProfile,
Toast,
FeedToken,
Tag,
Toast,
UserProfile,
)
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
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 ?"
params = (huey.storage.name, limit, offset)
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]
@@ -295,7 +298,7 @@ class AdminCustomUser(UserAdmin):
def get_inline_instances(self, request, obj=None):
if not obj:
return list()
return super(AdminCustomUser, self).get_inline_instances(request, obj)
return super().get_inline_instances(request, obj)
class AdminToast(admin.ModelAdmin):

View File

@@ -33,6 +33,6 @@ class LinkdingTokenAuthentication(TokenAuthentication):
msg = _(
"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)

View File

@@ -4,29 +4,29 @@ import os
from django.conf import settings
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.permissions import AllowAny
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.api.serializers import (
BookmarkSerializer,
BookmarkAssetSerializer,
BookmarkBundleSerializer,
BookmarkSerializer,
TagSerializer,
UserProfileSerializer,
BookmarkBundleSerializer,
)
from bookmarks.models import (
Bookmark,
BookmarkAsset,
BookmarkBundle,
BookmarkSearch,
Tag,
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.views import access
@@ -197,7 +197,7 @@ class BookmarkAssetViewSet(
file_stream = (
gzip.GzipFile(file_path, mode="rb")
if asset.gzip
else open(file_path, "rb")
else open(file_path, "rb") # noqa: SIM115
)
response = StreamingHttpResponse(file_stream, content_type=content_type)
response["Content-Disposition"] = (
@@ -205,7 +205,7 @@ class BookmarkAssetViewSet(
)
return response
except FileNotFoundError:
raise Http404("Asset file does not exist")
raise Http404("Asset file does not exist") from None
except Exception as e:
logger.error(
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 rest_framework import serializers
from rest_framework.serializers import ListSerializer
@@ -6,10 +6,10 @@ from rest_framework.serializers import ListSerializer
from bookmarks.models import (
Bookmark,
BookmarkAsset,
Tag,
build_tag_string,
UserProfile,
BookmarkBundle,
Tag,
UserProfile,
build_tag_string,
)
from bookmarks.services import bookmarks, bundles
from bookmarks.services.tags import get_or_create_tag
@@ -56,7 +56,7 @@ class BookmarkBundleSerializer(serializers.ModelSerializer):
def create(self, 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"])

View File

@@ -6,4 +6,5 @@ class BookmarksConfig(AppConfig):
def ready(self):
# 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):
limit = context.request.GET.get("limit", 100)
if limit:
data = context.query_set[: int(limit)]
else:
data = list(context.query_set)
data = context.query_set[: int(limit)] if limit else list(context.query_set)
prefetch_related_objects(data, "tags")
return data

View File

@@ -167,7 +167,9 @@ class TagMergeForm(forms.Form):
try:
target_tag = Tag.objects.get(name__iexact=target_tag_name, owner=self.user)
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
@@ -184,7 +186,9 @@ class TagMergeForm(forms.Form):
tag = Tag.objects.get(name__iexact=tag_name, owner=self.user)
merge_tags.append(tag)
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")
if target_tag and target_tag in merge_tags:

View File

@@ -1,5 +1,5 @@
import sqlite3
import os
import sqlite3
from django.core.management.base import BaseCommand
@@ -14,7 +14,7 @@ class Command(BaseCommand):
destination = options["destination"]
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"))
backup_db = sqlite3.connect(destination)

View File

@@ -1,8 +1,8 @@
import os
import logging
import os
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
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.core.management.base import BaseCommand
class Command(BaseCommand):

View File

@@ -1,5 +1,5 @@
import sqlite3
import os
import sqlite3
import tempfile
import zipfile
@@ -65,7 +65,7 @@ class Command(BaseCommand):
def backup_database(self, backup_db_file):
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"))
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.utils import get_random_secret_key
logger = logging.getLogger(__name__)
@@ -15,10 +14,10 @@ class Command(BaseCommand):
secret_key_file = os.path.join("data", "secretkey.txt")
if os.path.exists(secret_key_file):
logger.info(f"Secret key file already exists")
logger.info("Secret key file already exists")
return
secret_key = get_random_secret_key()
with open(secret_key_file, "w") as f:
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 os
import sqlite3
import importlib
from django.core.management.base import BaseCommand

View File

@@ -1,7 +1,7 @@
from django.conf import settings
from django.contrib.auth.middleware import RemoteUserMiddleware
from bookmarks.models import UserProfile, GlobalSettings
from bookmarks.models import GlobalSettings, UserProfile
class CustomRemoteUserMiddleware(RemoteUserMiddleware):
@@ -22,7 +22,7 @@ class LinkdingMiddleware:
# add global settings to request
try:
global_settings = GlobalSettings.get()
except:
except Exception:
global_settings = default_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
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# 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.db import migrations
from bookmarks.models import Toast

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("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
from django.db import migrations
from django.contrib.auth import get_user_model
from django.db import migrations
from bookmarks.models import Toast
@@ -9,7 +9,6 @@ User = get_user_model()
def forwards(apps, schema_editor):
for user in User.objects.all():
toast = Toast(
key="bookmark_list_actions_hint",
@@ -24,7 +23,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("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):
dependencies = [
("bookmarks", "0029_bookmark_list_actions_toast"),
]

View File

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

View File

@@ -1,7 +1,7 @@
# 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.db import migrations
from bookmarks.models import Toast
@@ -9,7 +9,6 @@ User = get_user_model()
def forwards(apps, schema_editor):
for user in User.objects.all():
toast = Toast(
key="html_snapshots_hint",
@@ -24,7 +23,6 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("bookmarks", "0031_userprofile_enable_automatic_html_snapshots"),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,19 @@
import binascii
import hashlib
import logging
import os
from typing import List
import binascii
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.db import models
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.http import QueryDict
from bookmarks.utils import unique, normalize_url
from bookmarks.utils import normalize_url, unique
from bookmarks.validators import BookmarkURLValidator
logger = logging.getLogger(__name__)
@@ -48,7 +47,7 @@ def parse_tag_string(tag_string: str, delimiter: str = ","):
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)
@@ -354,8 +353,8 @@ class BookmarkSearchForm(forms.Form):
def __init__(
self,
search: BookmarkSearch,
editable_fields: List[str] = None,
users: List[User] = None,
editable_fields: list[str] = None,
users: list[User] = None,
):
super().__init__()
editable_fields = editable_fields or []
@@ -640,7 +639,7 @@ class GlobalSettings(models.Model):
def save(self, *args, **kwargs):
if not self.pk and GlobalSettings.objects.exists():
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):
@@ -649,5 +648,5 @@ class GlobalSettingsForm(forms.ModelForm):
fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]
def __init__(self, *args, **kwargs):
super(GlobalSettingsForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
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.contrib.auth.models import User
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.functions import Lower
@@ -16,16 +16,16 @@ from bookmarks.models import (
parse_tag_string,
)
from bookmarks.services.search_query_parser import (
parse_search_query,
SearchExpression,
TermExpression,
TagExpression,
SpecialKeywordExpression,
AndExpression,
OrExpression,
NotExpression,
OrExpression,
SearchExpression,
SearchQueryParseError,
SpecialKeywordExpression,
TagExpression,
TermExpression,
extract_tag_names_from_query,
parse_search_query,
)
from bookmarks.utils import unique
@@ -45,7 +45,7 @@ def query_archived_bookmarks(
def query_shared_bookmarks(
user: Optional[User],
user: User | None,
profile: UserProfile,
search: BookmarkSearch,
public_only: bool,
@@ -215,7 +215,7 @@ def _filter_bundle(query_set: QuerySet, bundle: BookmarkBundle) -> QuerySet:
def _base_bookmarks_query(
user: Optional[User],
user: User | None,
profile: UserProfile,
search: BookmarkSearch,
) -> QuerySet:
@@ -227,19 +227,15 @@ def _base_bookmarks_query(
# Filter by modified_since if provided
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)
except ValidationError:
# If the date format is invalid, ignore the filter
pass
# Filter by added_since if provided
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)
except ValidationError:
# If the date format is invalid, ignore the filter
pass
# Filter by search query
if profile.legacy_search:
@@ -320,7 +316,7 @@ def query_archived_bookmark_tags(
def query_shared_bookmark_tags(
user: Optional[User],
user: User | None,
profile: UserProfile,
search: BookmarkSearch,
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(
user: Optional[User], profile: UserProfile, query: str, public_only: bool
user: User | None, profile: UserProfile, query: str, public_only: bool
) -> QuerySet:
tag_names = extract_tag_names_from_query(query, profile)

View File

@@ -5,7 +5,7 @@ import shutil
from django.conf import settings
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.services import singlefile

View File

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

View File

@@ -1,12 +1,9 @@
import logging
from typing import Union
from django.utils import timezone
from bookmarks.models import Bookmark, User, parse_tag_string
from bookmarks.services import auto_tagging
from bookmarks.services import tasks
from bookmarks.services import website_loader
from bookmarks.services import auto_tagging, tasks, website_loader
from bookmarks.services.tags import get_or_create_tags
logger = logging.getLogger(__name__)
@@ -91,7 +88,7 @@ def archive_bookmark(bookmark: 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)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
@@ -106,7 +103,7 @@ def unarchive_bookmark(bookmark: 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)
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)
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)
owned_bookmark_ids = Bookmark.objects.filter(
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(
bookmark_ids: [Union[int, str]], tag_string: str, current_user: User
):
def untag_bookmarks(bookmark_ids: [int | str], tag_string: str, current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmark_ids = Bookmark.objects.filter(
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)
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)
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)
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)
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)
owned_bookmarks = Bookmark.objects.filter(
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)
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)
owned_bookmarks = Bookmark.objects.filter(
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)
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
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
from typing import List
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 = []
append_header(doc)
append_list_start(doc)

View File

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

View File

@@ -1,7 +1,7 @@
import gzip
import os
import shutil
import subprocess
import os
from django.conf import settings
@@ -30,4 +30,4 @@ def create_snapshot(url: str, filepath: str):
os.remove(temp_filepath)
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 html.parser import HTMLParser
from typing import Dict, List
from bookmarks.models import parse_tag_string
@@ -13,7 +13,7 @@ class NetscapeBookmark:
notes: str
date_added: str
date_modified: str
tag_names: List[str]
tag_names: list[str]
to_read: bool
private: bool
archived: bool
@@ -56,17 +56,15 @@ class BookmarkParser(HTMLParser):
def handle_end_dl(self):
self.add_bookmark()
def handle_start_dt(self, attrs: Dict[str, str]):
def handle_start_dt(self, attrs: dict[str, str]):
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)
tag_names = parse_tag_string(self.tags)
archived = "linkding:bookmarks.archived" in self.tags
try:
with contextlib.suppress(ValueError):
tag_names.remove("linkding:bookmarks.archived")
except ValueError:
pass
self.bookmark = NetscapeBookmark(
href=self.href,
@@ -109,7 +107,7 @@ class BookmarkParser(HTMLParser):
self.private = ""
def parse(html: str) -> List[NetscapeBookmark]:
def parse(html: str) -> list[NetscapeBookmark]:
parser = BookmarkParser()
parser.feed(html)
return parser.bookmarks

View File

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

View File

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

View File

@@ -38,12 +38,12 @@ def create_snapshot(url: str, filepath: str):
)
process.terminate()
process.wait(timeout=20)
raise SingleFileError("Timeout expired while creating snapshot")
raise SingleFileError("Timeout expired while creating snapshot") from None
except subprocess.TimeoutExpired:
# Kill the whole process group, which should also clean up any chromium
# processes spawned by single-file
logger.error("Timeout expired while terminating. Killing process...")
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:
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 operator
from typing import List
from django.contrib.auth.models import User
from django.utils import timezone
@@ -11,7 +10,7 @@ from bookmarks.utils import unique
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]
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
first_tag = Tag.objects.filter(name__iexact=name, owner=user).first()
message = (
"Found multiple tags for the name '{0}' with different capitalization. "
"Using the first tag with the name '{1}'. "
f"Found multiple tags for the name '{name}' with different capitalization. "
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. "
"To solve this error remove the duplicate tag in admin."
).format(name, first_tag.name)
)
logger.error(message)
return first_tag

View File

@@ -1,6 +1,5 @@
import functools
import logging
from typing import List
import waybackpy
from django.conf import settings
@@ -10,7 +9,7 @@ from django.utils import timezone
from huey import crontab
from huey.contrib.djhuey import HUEY as huey
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.services import assets, favicon_loader, preview_image_loader
@@ -263,7 +262,7 @@ def create_html_snapshot(bookmark: Bookmark):
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():
return

View File

@@ -81,10 +81,12 @@ def _load_website_metadata(url: str):
end = timezone.now()
logger.debug(f"Parsing duration: {end - start}")
finally:
return WebsiteMetadata(
url=url, title=title, description=description, preview_image=preview_image
)
except Exception:
pass
return WebsiteMetadata(
url=url, title=title, description=description, preview_image=preview_image
)
CHUNK_SIZE = 50 * 1024
@@ -101,15 +103,12 @@ def load_page(url: str):
for chunk in r.iter_content(chunk_size=CHUNK_SIZE):
size += len(chunk)
iteration = iteration + 1
if content is None:
content = chunk
else:
content = content + chunk
content = chunk if content is None else content + chunk
logger.debug(f"Loaded chunk (iteration={iteration}, total={size / 1024})")
# 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:
logger.debug(f"Found closing head tag after {size} bytes")
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
# ruff: noqa
try:
from .dev import *
except:

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
# 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 markdown
from bleach_allowlist import markdown_tags, markdown_attrs
from bleach_allowlist import markdown_attrs, markdown_tags
from django import template
from django.utils.safestring import mark_safe
from bookmarks import utils
from bookmarks.models import UserProfile
register = template.Library()
@@ -82,7 +81,7 @@ class HtmlMinNode(template.Node):
def render_markdown(context, markdown_text):
# naive approach to reusing the renderer for a single request
# 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"])
context["markdown_renderer"] = renderer
else:

View File

@@ -5,7 +5,6 @@ import random
import shutil
import tempfile
from datetime import datetime
from typing import List
from unittest import TestCase
from bs4 import BeautifulSoup
@@ -307,7 +306,7 @@ class HtmlTestMixin:
class BookmarkListTestMixin(TestCase, HtmlTestMixin):
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())
bookmark_list = soup.select_one(
@@ -325,7 +324,7 @@ class BookmarkListTestMixin(TestCase, HtmlTestMixin):
self.assertIsNotNone(bookmark_item)
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())
@@ -337,7 +336,7 @@ class BookmarkListTestMixin(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())
tag_cloud = soup.select_one("div.tag-cloud")
self.assertIsNotNone(tag_cloud)
@@ -350,7 +349,7 @@ class TagCloudTestMixin(TestCase, HtmlTestMixin):
for tag in tags:
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())
tag_items = soup.select("a[data-is-tag-item]")
@@ -359,7 +358,7 @@ class TagCloudTestMixin(TestCase, HtmlTestMixin):
for tag in tags:
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())
selected_tags = soup.select_one("p.selected-tags")
self.assertIsNotNone(selected_tags)
@@ -433,18 +432,18 @@ class ImportTestMixin:
def render_tag(self, tag: BookmarkHtmlTag):
return f"""
<DT>
<A {f'HREF="{tag.href}"' if tag.href else ''}
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
{f'LAST_MODIFIED="{tag.last_modified}"' if tag.last_modified else ''}
{f'TAGS="{tag.tags}"' if tag.tags else ''}
<A {f'HREF="{tag.href}"' if tag.href else ""}
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ""}
{f'LAST_MODIFIED="{tag.last_modified}"' if tag.last_modified else ""}
{f'TAGS="{tag.tags}"' if tag.tags else ""}
TOREAD="{1 if tag.to_read else 0}"
PRIVATE="{1 if tag.private else 0}">
{tag.title if tag.title else ''}
{tag.title if tag.title else ""}
</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:
rendered_tags = [self.render_tag(tag) for tag in tags]
tags_html = "\n".join(rendered_tags)

View File

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

View File

@@ -1,11 +1,10 @@
from django.urls import reverse
from rest_framework import status
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
from bookmarks.tests.helpers import BookmarkFactoryMixin, LinkdingApiTestCase
class AuthApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def authenticate(self, keyword):
self.api_token = self.setup_api_token()
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.urls import reverse
from bookmarks.models import User
from bookmarks.middlewares import CustomRemoteUserMiddleware
from bookmarks.models import User
class AuthProxySupportTest(TestCase):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -262,9 +262,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
self.assertIsNotNone(image)
def test_internet_archive_link_with_fallback_url(self):
date_added = timezone.datetime(
2023, 8, 11, 21, 45, 11, tzinfo=datetime.timezone.utc
)
date_added = timezone.datetime(2023, 8, 11, 21, 45, 11, tzinfo=datetime.UTC)
bookmark = self.setup_bookmark(url="https://example.com/", added=date_added)
fallback_web_archive_url = (
"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):
def setUp(self) -> None:
user = self.get_or_create_test_user()
self.client.force_login(user)

View File

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

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