mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-27 22:43:15 +08:00
Format and lint with ruff (#1263)
This commit is contained in:
5
Makefile
5
Makefile
@@ -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
|
||||
|
||||
10
README.md
10
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ class BookmarksConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
# Register signal handlers
|
||||
import bookmarks.signals
|
||||
# noinspection PyUnusedImports
|
||||
import bookmarks.signals # noqa: F401
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import importlib
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0002_auto_20190629_2303"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0003_auto_20200913_0656"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0005_auto_20210103_1212"),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0007_userprofile"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0008_userprofile_bookmark_date_display"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0009_bookmark_web_archive_snapshot_url"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0010_userprofile_bookmark_link_target"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0015_feedtoken"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0016_bookmark_shared"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0017_userprofile_enable_sharing"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0018_bookmark_favicon_file"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0019_userprofile_enable_favicons"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0020_userprofile_tag_search"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0021_userprofile_display_url"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0022_bookmark_notes"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0023_userprofile_permanent_notes"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0024_userprofile_enable_public_sharing"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0025_userprofile_search_preferences"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0026_userprofile_custom_css"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0027_userprofile_bookmark_description_display_and_more"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0029_bookmark_list_actions_toast"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0030_bookmarkasset"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0032_html_snapshots_hint_toast"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0033_userprofile_default_mark_unread"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0034_bookmark_preview_image_file_and_more"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0035_userprofile_tag_grouping"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0036_userprofile_auto_tagging_rules"),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0038_globalsettings_guest_profile_user"),
|
||||
]
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0039_globalsettings_enable_link_prefetch"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0041_merge_metadata"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0042_userprofile_custom_css_hash"),
|
||||
]
|
||||
|
||||
@@ -25,7 +25,6 @@ def reverse(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0043_userprofile_collapse_side_panel"),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0045_userprofile_hide_bundles_bookmarkbundle"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0047_populate_url_normalized_field"),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0048_userprofile_default_mark_shared"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -21,7 +21,6 @@ def migrate_tokens_reverse(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookmarks", "0052_apitoken"),
|
||||
("authtoken", "0004_alter_tokenproxy_options"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import re
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import idna
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Use dev settings as default, use production if dev settings do not exist
|
||||
# ruff: noqa
|
||||
try:
|
||||
from .dev import *
|
||||
except:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Development settings for linkding webapp
|
||||
"""
|
||||
|
||||
# ruff: noqa
|
||||
|
||||
# Start from development settings
|
||||
# noinspection PyUnresolvedReferences
|
||||
from .base import *
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Production settings for linkding webapp
|
||||
"""
|
||||
|
||||
# ruff: noqa
|
||||
|
||||
# Start from development settings
|
||||
# noinspection PyUnresolvedReferences
|
||||
import os
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# Expose task modules to Huey Django extension
|
||||
import bookmarks.services.tasks
|
||||
# noinspection PyUnusedImports
|
||||
import bookmarks.services.tasks # noqa: F401
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user