From 8b1b01e508bf5827fd8d98a9cd1cdaf028d09a15 Mon Sep 17 00:00:00 2001 From: jim winstead Date: Mon, 25 Mar 2024 17:46:01 -0700 Subject: [PATCH] Update to Django 4.2.x, now in LTS until April 2026 --- archivebox/core/__init__.py | 1 - archivebox/core/admin.py | 177 ++++++++++++++++++++---------------- archivebox/core/apps.py | 2 - archivebox/core/settings.py | 4 - archivebox/core/urls.py | 4 +- pyproject.toml | 4 +- 6 files changed, 105 insertions(+), 87 deletions(-) diff --git a/archivebox/core/__init__.py b/archivebox/core/__init__.py index 9cd0ce16..ac3ec769 100644 --- a/archivebox/core/__init__.py +++ b/archivebox/core/__init__.py @@ -1,3 +1,2 @@ __package__ = 'archivebox.core' -default_app_config = 'archivebox.core.apps.CoreConfig' diff --git a/archivebox/core/admin.py b/archivebox/core/admin.py index 65baa52b..172a8caf 100644 --- a/archivebox/core/admin.py +++ b/archivebox/core/admin.py @@ -48,6 +48,60 @@ GLOBAL_CONTEXT = {'VERSION': VERSION, 'VERSIONS_AVAILABLE': VERSIONS_AVAILABLE, # TODO: https://stackoverflow.com/questions/40760880/add-custom-button-to-django-admin-panel +class ArchiveBoxAdmin(admin.AdminSite): + site_header = 'ArchiveBox' + index_title = 'Links' + site_title = 'Index' + namespace = 'admin' + + def get_urls(self): + return [ + path('core/snapshot/add/', self.add_view, name='Add'), + ] + super().get_urls() + + def add_view(self, request): + if not request.user.is_authenticated: + return redirect(f'/admin/login/?next={request.path}') + + request.current_app = self.name + context = { + **self.each_context(request), + 'title': 'Add URLs', + } + + if request.method == 'GET': + context['form'] = AddLinkForm() + + elif request.method == 'POST': + form = AddLinkForm(request.POST) + if form.is_valid(): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + else: + context["form"] = form + + return render(template_name='add.html', request=request, context=context) + +archivebox_admin = ArchiveBoxAdmin() +archivebox_admin.register(get_user_model()) +archivebox_admin.disable_action('delete_selected') + class ArchiveResultInline(admin.TabularInline): model = ArchiveResult @@ -57,11 +111,11 @@ class TagInline(admin.TabularInline): from django.contrib.admin.helpers import ActionForm from django.contrib.admin.widgets import AutocompleteSelectMultiple -# WIP: broken by Django 3.1.2 -> 4.0 migration class AutocompleteTags: model = Tag search_fields = ['name'] name = 'tags' + remote_field = TagInline class AutocompleteTagsAdminStub: name = 'admin' @@ -71,7 +125,6 @@ class SnapshotActionForm(ActionForm): tags = forms.ModelMultipleChoiceField( queryset=Tag.objects.all(), required=False, - # WIP: broken by Django 3.1.2 -> 4.0 migration widget=AutocompleteSelectMultiple( AutocompleteTags(), AutocompleteTagsAdminStub(), @@ -90,6 +143,7 @@ class SnapshotActionForm(ActionForm): # ) +@admin.register(Snapshot, site=archivebox_admin) class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): list_display = ('added', 'title_str', 'files', 'size', 'url_str') sort_fields = ('title_str', 'url_str', 'added', 'files') @@ -176,6 +230,10 @@ class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): obj.id, ) + @admin.display( + description='Title', + ordering='title', + ) def title_str(self, obj): canon = obj.as_link().canonical_outputs() tags = ''.join( @@ -197,12 +255,17 @@ class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): urldecode(htmldecode(obj.latest_title or obj.title or ''))[:128] or 'Pending...' ) + mark_safe(f' {tags}') + @admin.display( + description='Files Saved', + ordering='archiveresult_count', + ) def files(self, obj): return snapshot_icons(obj) - files.admin_order_field = 'archiveresult_count' - files.short_description = 'Files Saved' + @admin.display( + ordering='archiveresult_count' + ) def size(self, obj): archive_size = (Path(obj.link_dir) / 'index.html').exists() and obj.archive_size if archive_size: @@ -217,8 +280,11 @@ class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): size_txt, ) - size.admin_order_field = 'archiveresult_count' + @admin.display( + description='Original URL', + ordering='url', + ) def url_str(self, obj): return format_html( '{}', @@ -255,65 +321,76 @@ class SnapshotAdmin(SearchResultsAdminMixin, admin.ModelAdmin): # print('[*] Got request', request.method, request.POST) # return super().changelist_view(request, extra_context=None) + @admin.action( + description="Pull" + ) def update_snapshots(self, request, queryset): archive_links([ snapshot.as_link() for snapshot in queryset ], out_dir=OUTPUT_DIR) - update_snapshots.short_description = "Pull" + @admin.action( + description="⬇️ Title" + ) def update_titles(self, request, queryset): archive_links([ snapshot.as_link() for snapshot in queryset ], overwrite=True, methods=('title','favicon'), out_dir=OUTPUT_DIR) - update_titles.short_description = "⬇️ Title" + @admin.action( + description="Re-Snapshot" + ) def resnapshot_snapshot(self, request, queryset): for snapshot in queryset: timestamp = datetime.now(timezone.utc).isoformat('T', 'seconds') new_url = snapshot.url.split('#')[0] + f'#{timestamp}' add(new_url, tag=snapshot.tags_str()) - resnapshot_snapshot.short_description = "Re-Snapshot" + @admin.action( + description="Reset" + ) def overwrite_snapshots(self, request, queryset): archive_links([ snapshot.as_link() for snapshot in queryset ], overwrite=True, out_dir=OUTPUT_DIR) - overwrite_snapshots.short_description = "Reset" + @admin.action( + description="Delete" + ) def delete_snapshots(self, request, queryset): remove(snapshots=queryset, yes=True, delete=True, out_dir=OUTPUT_DIR) - delete_snapshots.short_description = "Delete" + @admin.action( + description="+" + ) def add_tags(self, request, queryset): tags = request.POST.getlist('tags') print('[+] Adding tags', tags, 'to Snapshots', queryset) for obj in queryset: obj.tags.add(*tags) - add_tags.short_description = "+" + @admin.action( + description="–" + ) def remove_tags(self, request, queryset): tags = request.POST.getlist('tags') print('[-] Removing tags', tags, 'to Snapshots', queryset) for obj in queryset: obj.tags.remove(*tags) - remove_tags.short_description = "–" - title_str.short_description = 'Title' - url_str.short_description = 'Original URL' - - title_str.admin_order_field = 'title' - url_str.admin_order_field = 'url' + +@admin.register(Tag, site=archivebox_admin) class TagAdmin(admin.ModelAdmin): list_display = ('slug', 'name', 'num_snapshots', 'snapshots', 'id') sort_fields = ('id', 'name', 'slug') @@ -344,6 +421,7 @@ class TagAdmin(admin.ModelAdmin): ) + (f'
and {total_count-10} more...' if obj.snapshot_set.count() > 10 else '')) +@admin.register(ArchiveResult, site=archivebox_admin) class ArchiveResultAdmin(admin.ModelAdmin): list_display = ('id', 'start_ts', 'extractor', 'snapshot_str', 'tags_str', 'cmd_str', 'status', 'output_str') sort_fields = ('start_ts', 'extractor', 'status') @@ -356,6 +434,9 @@ class ArchiveResultAdmin(admin.ModelAdmin): ordering = ['-start_ts'] list_per_page = SNAPSHOTS_PER_PAGE + @admin.display( + description='snapshot' + ) def snapshot_str(self, obj): return format_html( '[{}]
' @@ -365,6 +446,9 @@ class ArchiveResultAdmin(admin.ModelAdmin): obj.snapshot.url[:128], ) + @admin.display( + description='tags' + ) def tags_str(self, obj): return obj.snapshot.tags_str() @@ -381,62 +465,3 @@ class ArchiveResultAdmin(admin.ModelAdmin): obj.output if (obj.status == 'succeeded') and obj.extractor not in ('title', 'archive_org') else 'index.html', obj.output, ) - - tags_str.short_description = 'tags' - snapshot_str.short_description = 'snapshot' - -class ArchiveBoxAdmin(admin.AdminSite): - site_header = 'ArchiveBox' - index_title = 'Links' - site_title = 'Index' - - def get_urls(self): - return [ - path('core/snapshot/add/', self.add_view, name='Add'), - ] + super().get_urls() - - def add_view(self, request): - if not request.user.is_authenticated: - return redirect(f'/admin/login/?next={request.path}') - - request.current_app = self.name - context = { - **self.each_context(request), - 'title': 'Add URLs', - } - - if request.method == 'GET': - context['form'] = AddLinkForm() - - elif request.method == 'POST': - form = AddLinkForm(request.POST) - if form.is_valid(): - url = form.cleaned_data["url"] - print(f'[+] Adding URL: {url}') - depth = 0 if form.cleaned_data["depth"] == "0" else 1 - input_kwargs = { - "urls": url, - "depth": depth, - "update_all": False, - "out_dir": OUTPUT_DIR, - } - add_stdout = StringIO() - with redirect_stdout(add_stdout): - add(**input_kwargs) - print(add_stdout.getvalue()) - - context.update({ - "stdout": ansi_to_html(add_stdout.getvalue().strip()), - "form": AddLinkForm() - }) - else: - context["form"] = form - - return render(template_name='add.html', request=request, context=context) - -admin.site = ArchiveBoxAdmin() -admin.site.register(get_user_model()) -admin.site.register(Snapshot, SnapshotAdmin) -admin.site.register(Tag, TagAdmin) -admin.site.register(ArchiveResult, ArchiveResultAdmin) -admin.site.disable_action('delete_selected') diff --git a/archivebox/core/apps.py b/archivebox/core/apps.py index 32088de4..f3e35dbd 100644 --- a/archivebox/core/apps.py +++ b/archivebox/core/apps.py @@ -3,8 +3,6 @@ from django.apps import AppConfig class CoreConfig(AppConfig): name = 'core' - # WIP: broken by Django 3.1.2 -> 4.0 migration - default_auto_field = 'django.db.models.UUIDField' def ready(self): from .auth import register_signals diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py index 06e798ab..9b80c336 100644 --- a/archivebox/core/settings.py +++ b/archivebox/core/settings.py @@ -269,9 +269,6 @@ AUTH_PASSWORD_VALIDATORS = [ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ] -# WIP: broken by Django 3.1.2 -> 4.0 migration -DEFAULT_AUTO_FIELD = 'django.db.models.UUIDField' - ################################################################################ ### Shell Settings ################################################################################ @@ -290,7 +287,6 @@ if IS_SHELL: LANGUAGE_CODE = 'en-us' USE_I18N = True -USE_L10N = True USE_TZ = True DATETIME_FORMAT = 'Y-m-d g:iA' SHORT_DATETIME_FORMAT = 'Y-m-d h:iA' diff --git a/archivebox/core/urls.py b/archivebox/core/urls.py index 1111ead4..ce38af32 100644 --- a/archivebox/core/urls.py +++ b/archivebox/core/urls.py @@ -1,4 +1,4 @@ -from django.contrib import admin +from .admin import archivebox_admin from django.urls import path, include from django.views import static @@ -29,7 +29,7 @@ urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), - path('admin/', admin.site.urls), + path('admin/', archivebox_admin.urls), path('health/', HealthCheckView.as_view(), name='healthcheck'), path('error/', lambda _: 1/0), diff --git a/pyproject.toml b/pyproject.toml index eedea90c..969b6318 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ dependencies = [ # pdm update [--unconstrained] "croniter>=0.3.34", "dateparser>=1.0.0", - "django-extensions>=3.0.3", - "django>=3.1.3,<3.2", + "django-extensions>=3.2.3", + "django>=4.2.0,<5.0", "feedparser>=6.0.11", "ipython>5.0.0", "mypy-extensions>=0.4.3",