Skip to content

Commit

Permalink
Add ModelAdmin for Url and Link models
Browse files Browse the repository at this point in the history
  • Loading branch information
timobrembeck committed Apr 13, 2023
1 parent 3d6188e commit d644041
Show file tree
Hide file tree
Showing 16 changed files with 975 additions and 135 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Unreleased

* Migrate linkcheck views to `ModelAdmin` (Timo Ludwig, #186)
* Add `ModelAdmin` for Url and Link models
* Fix internal redirect checker (Timo Ludwig, #180)
* Fix SSL status of unreachable domains (Timo Ludwig, #184)
* Fix URL message for internal server errorrs (Timo Ludwig, #182)
Expand Down
13 changes: 9 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ django-linkcheck
A fairly flexible app that will analyze and report on links in any model that
you register with it.

.. image:: https://github.com/DjangoAdminHackers/django-linkcheck/raw/master/linkcheck.jpg
.. image:: examples/linkcheck.jpg

Links can be bare (urls or image and file fields) or
embedded in HTML (linkcheck handles the parsing). It's fairly easy to override
Expand Down Expand Up @@ -46,11 +46,16 @@ Basic usage

#. Run ``./manage.py migrate``.

#. Add to your root url config::
#. Register linkcheck models in your admin::

path('admin/linkcheck/', include('linkcheck.urls'))
from django.contrib import admin
from linkcheck.models import Link, Url
from linkcheck.admin import LinkAdmin, UrlAdmin

#. View ``/admin/linkcheck/`` from your browser.
admin.site.register(Url, UrlAdmin)
admin.site.register(Link, LinkAdmin)

#. View ``/admin/linkcheck/url/`` from your browser.

We are aware that this documentation is on the brief side of things so any
suggestions for elaboration or clarification would be gratefully accepted.
Expand Down
Binary file modified examples/linkcheck.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed linkcheck.jpg
Binary file not shown.
6 changes: 6 additions & 0 deletions linkcheck/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .link_admin import LinkAdmin
from .url_admin import UrlAdmin

__all__ = [
"LinkAdmin", "UrlAdmin"
]
81 changes: 81 additions & 0 deletions linkcheck/admin/link_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from django.contrib import admin, messages
from django.template.defaultfilters import yesno
from django.utils.translation import gettext_lazy as _

from ..models import Link, Url
from ..templatetags.linkcheck_admin_tags import (
linkcheck_source,
linkcheck_status_icon,
linkcheck_url,
)


class LinkAdmin(admin.ModelAdmin):
list_display = [
'list_url',
'list_status',
'text',
'list_content_object',
'content_type',
'list_field',
'list_ignore',
]
actions = ['ignore', 'unignore']

@admin.display(ordering='url', description=Url._meta.get_field('url').verbose_name)
def list_url(self, link):
return linkcheck_url(link.url)

@admin.display(ordering='url__status', description=Url._meta.get_field('status').verbose_name)
def list_status(self, link):
return linkcheck_status_icon(link.url)

@admin.display(ordering='object_id', description=_('source'))
def list_content_object(self, link):
return linkcheck_source(link)

@admin.display(ordering='field', description=Link._meta.get_field('field').verbose_name)
def list_field(self, link):
return type(link.content_object)._meta.get_field(link.field).verbose_name

@admin.display(ordering='ignore', description=Link._meta.get_field('ignore').verbose_name)
def list_ignore(self, link):
return yesno(link.ignore)

def has_add_permission(self, request, obj=None):
return False

def has_change_permission(self, request, obj=None):
return False

@admin.action(description=_('Ignore selected links'))
def ignore(self, request, queryset):
queryset.update(ignore=True)
messages.success(
request,
_('The selected links are now ignored.'),
)

@admin.action(description=_('No longer ignore selected links'))
def unignore(self, request, queryset):
queryset.update(ignore=False)
messages.success(
request,
_('The selected links are no longer ignored.'),
)

def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
if request.GET.get('ignore__exact') == '1':
title = _('Ignored links')
elif request.GET.get('ignore__exact') == '0':
title = _('Not ignored links')
else:
title = _('Links')
extra_context['title'] = title
return super().changelist_view(request, extra_context=extra_context)

class Media:
css = {
'all': ['linkcheck/css/style.css'],
}
211 changes: 211 additions & 0 deletions linkcheck/admin/url_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
from django.contrib import admin, messages
from django.db.models import Count, Exists, OuterRef
from django.template.defaultfilters import yesno
from django.utils.translation import gettext_lazy as _

from ..models import TYPE_CHOICES, Link, Url
from ..templatetags.linkcheck_admin_tags import (
linkcheck_anchor_icon,
linkcheck_links,
linkcheck_ssl_icon,
linkcheck_status_code,
linkcheck_status_icon,
linkcheck_url,
)


class StatusFilter(admin.BooleanFieldListFilter):
"""
A custom status filter to include the ignore-status of links
"""
title = _('status')
parameter_name = 'status'
ignore_kwarg = 'ignore'

def __init__(self, field, request, params, model, model_admin, field_path):
self.ignore_val = params.get(self.ignore_kwarg)
super().__init__(field, request, params, model, model_admin, field_path)

def expected_parameters(self):
return super().expected_parameters() + [self.ignore_kwarg]

def choices(self, changelist):
qs = Url.objects.annotate(
ignore=Exists(Link.objects.filter(url=OuterRef('pk'), ignore=True))
)
field_choices = dict(self.field.flatchoices)
return [
{
'selected': self.lookup_val is None and self.ignore_val is None and not self.lookup_val2,
'query_string': changelist.get_query_string(
{self.lookup_kwarg: None},
[self.lookup_kwarg2, self.ignore_kwarg]
),
'display': _('All') + f' ({Url.objects.count()})',
},
{
'selected': self.lookup_val == '1' and self.ignore_val == 'False' and not self.lookup_val2,
'query_string': changelist.get_query_string(
{self.lookup_kwarg: '1', self.ignore_kwarg: 'False'},
[self.lookup_kwarg2]
),
'display': field_choices.get(True) + f' ({qs.filter(status=True, ignore=False).count()})',
},
{
'selected': self.lookup_val == '0' and self.ignore_val == 'False' and not self.lookup_val2,
'query_string': changelist.get_query_string(
{self.lookup_kwarg: '0', self.ignore_kwarg: 'False'},
[self.lookup_kwarg2]
),
'display': field_choices.get(False) + f' ({qs.filter(status=False, ignore=False).count()})',
},
{
'selected': self.lookup_val2 == 'True' and self.ignore_val == 'False' and not self.lookup_val,
'query_string': changelist.get_query_string(
{self.lookup_kwarg2: 'True', self.ignore_kwarg: 'False'},
[self.lookup_kwarg]
),
'display': field_choices.get(None) + f' ({qs.filter(status=None, ignore=False).count()})',
},
{
'selected': self.ignore_val == 'True' and not self.lookup_val and not self.lookup_val2,
'query_string': changelist.get_query_string(
{self.ignore_kwarg: 'True'},
[self.lookup_kwarg, self.lookup_kwarg2]
),
'display': _('Ignored') + f' ({qs.filter(ignore=True).count()})',
}
]


class TypeFilter(admin.SimpleListFilter):
title = _('type')
parameter_name = 'type'

def lookups(self, request, model_admin):
return TYPE_CHOICES

def queryset(self, request, queryset):
if not self.value():
return queryset
urls = [url.pk for url in queryset if url.type == self.value()]
return queryset.filter(pk__in=urls)


class UrlAdmin(admin.ModelAdmin):
list_display = [
'list_url',
'list_status',
'list_ssl_status',
'list_anchor_status',
'list_status_code',
'list_get_message',
'list_type',
'list_links',
'list_ignore',
]
list_display_links = None
list_filter = [('status', StatusFilter), TypeFilter]
sortable_by = [
'list_url',
'list_status',
'list_ssl_status',
'list_anchor_status',
'list_status_code',
'list_links',
]
actions = ['recheck', 'ignore', 'unignore']
empty_value_display = ''

@admin.display(ordering='url', description=Url._meta.get_field('url').verbose_name)
def list_url(self, url):
return linkcheck_url(url)

@admin.display(ordering='status', description=Url._meta.get_field('status').verbose_name)
def list_status(self, url):
return linkcheck_status_icon(url)

@admin.display(ordering='ssl_status', description=_('SSL'))
def list_ssl_status(self, url):
return linkcheck_ssl_icon(url)

@admin.display(ordering='anchor_status', description=_('Anchor'))
def list_anchor_status(self, url):
return linkcheck_anchor_icon(url)

@admin.display(ordering='status_code', description=Url._meta.get_field('status_code').verbose_name)
def list_status_code(self, url):
return linkcheck_status_code(url)

@admin.display(description=_('message'))
def list_get_message(self, url):
return url.get_message

@admin.display(ordering='type', description=_('type'))
def list_type(self, url):
return url.get_type_display()

@admin.display(ordering='links__count', description=Link._meta.verbose_name_plural)
def list_links(self, url):
return linkcheck_links(url)

@admin.display(description=Link._meta.get_field('ignore').verbose_name)
def list_ignore(self, url):
return yesno(url.ignore)

def has_add_permission(self, request, obj=None):
return False

def has_change_permission(self, request, obj=None):
return False

@admin.action(description=_('Recheck selected URLs'))
def recheck(self, request, queryset):
for url in queryset:
url.check_url(external_recheck_interval=0)
messages.success(
request,
_('The selected URLs were rechecked.'),
)

@admin.action(description=_('Ignore selected URLs'))
def ignore(self, request, queryset):
Link.objects.filter(url__in=queryset).update(ignore=True)
messages.success(
request,
_('The selected URLs are now ignored.'),
)

@admin.action(description=_('No longer ignore selected URLs'))
def unignore(self, request, queryset):
Link.objects.filter(url__in=queryset).update(ignore=False)
messages.success(
request,
_('The selected URLs are no longer ignored.'),
)

def get_queryset(self, request):
return super().get_queryset(request).annotate(
Count('links'),
ignore=Exists(Link.objects.filter(url=OuterRef('pk'), ignore=True))
)

def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
if request.GET.get('status__exact') == '1':
title = _('Valid URLs')
elif request.GET.get('status__exact') == '0':
title = _('Invalid URLs')
elif request.GET.get('status__isnull') == 'True':
title = _('Unchecked URLs')
elif request.GET.get('ignore') == 'True':
title = _('Ignored URLs')
else:
title = _('URLs')
extra_context['title'] = title
return super().changelist_view(request, extra_context=extra_context)

class Media:
css = {
'all': ['linkcheck/css/style.css'],
}
Loading

0 comments on commit d644041

Please sign in to comment.