Added a bunch of new features:

- Debug mode is now configurable in the configuration file. This way, we don't have to edit versioned files to disable it on production systems.
- Recent correspondents filter (enable in configuration file)
- Document actions: Edit tags and correspondents on multiple documents at once
- Replaced month list filter with date drilldown
- Sortable document count columns on Tag and Correspondent admin
- Last correspondence column on Correspondent admin
- Save and edit next functionality for document editing
This commit is contained in:
Jonas Winkler
2018-09-13 15:19:25 +02:00
parent 2edf65dd1e
commit fb6f2e07c9
8 changed files with 301 additions and 42 deletions

108
src/documents/actions.py Normal file
View File

@@ -0,0 +1,108 @@
from django.contrib import messages
from django.contrib.admin import helpers
from django.contrib.admin.utils import model_ngettext
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from documents.models import Tag, Correspondent
def select_action(modeladmin, request, queryset, title, action, modelclass, success_message="", document_action=None, queryset_action=None):
opts = modeladmin.model._meta
app_label = opts.app_label
if not modeladmin.has_change_permission(request):
raise PermissionDenied
if request.POST.get('post'):
n = queryset.count()
selected_object = modelclass.objects.get(id=request.POST.get('obj_id'))
if n:
for document in queryset:
if document_action:
document_action(document, selected_object)
document_display = str(document)
modeladmin.log_change(request, document, document_display)
if queryset_action:
queryset_action(queryset, selected_object)
modeladmin.message_user(request, success_message % {
"selected_object": selected_object.name, "count": n, "items": model_ngettext(modeladmin.opts, n)
}, messages.SUCCESS)
# Return None to display the change list page again.
return None
context = dict(
modeladmin.admin_site.each_context(request),
title=title,
queryset=queryset,
opts=opts,
action_checkbox_name=helpers.ACTION_CHECKBOX_NAME,
media=modeladmin.media,
action=action,
objects=modelclass.objects.all(),
itemname=model_ngettext(modelclass, 1)
)
request.current_app = modeladmin.admin_site.name
return TemplateResponse(request, "admin/%s/%s/select_object.html" % (app_label, opts.model_name), context)
def simple_action(modeladmin, request, queryset, success_message="", document_action=None, queryset_action=None):
if not modeladmin.has_change_permission(request):
raise PermissionDenied
n = queryset.count()
if n:
for document in queryset:
if document_action:
document_action(document)
document_display = str(document)
modeladmin.log_change(request, document, document_display)
if queryset_action:
queryset_action(queryset)
modeladmin.message_user(request, success_message % {
"count": n, "items": model_ngettext(modeladmin.opts, n)
}, messages.SUCCESS)
# Return None to display the change list page again.
return None
def add_tag_to_selected(modeladmin, request, queryset):
return select_action(modeladmin=modeladmin, request=request, queryset=queryset,
title="Add tag to multiple documents",
action="add_tag_to_selected",
modelclass=Tag,
success_message="Successfully added tag %(selected_object)s to %(count)d %(items)s.",
document_action=lambda doc, tag: doc.tags.add(tag))
add_tag_to_selected.short_description = "Add tag to selected documents"
def remove_tag_from_selected(modeladmin, request, queryset):
return select_action(modeladmin=modeladmin, request=request, queryset=queryset,
title="Remove tag from multiple documents",
action="remove_tag_from_selected",
modelclass=Tag,
success_message="Successfully removed tag %(selected_object)s from %(count)d %(items)s.",
document_action=lambda doc, tag: doc.tags.remove(tag))
remove_tag_from_selected.short_description = "Remove tag from selected documents"
def set_correspondent_on_selected(modeladmin, request, queryset):
return select_action(modeladmin=modeladmin, request=request, queryset=queryset,
title="Set correspondent on multiple documents",
action="set_correspondent_on_selected",
modelclass=Correspondent,
success_message="Successfully set correspondent %(selected_object)s on %(count)d %(items)s.",
queryset_action=lambda qs, correspondent: qs.update(correspondent=correspondent))
set_correspondent_on_selected.short_description = "Set correspondent on selected documents"
def remove_correspondent_from_selected(modeladmin, request, queryset):
return simple_action(modeladmin=modeladmin, request=request, queryset=queryset,
success_message="Successfully removed correspondent from %(count)d %(items)s.",
queryset_action=lambda qs: qs.update(correspondent=None))
remove_correspondent_from_selected.short_description = "Remove correspondent from selected documents"

View File

@@ -1,44 +1,25 @@
from datetime import datetime
from datetime import datetime, timedelta
from django.conf import settings
from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.auth.models import User, Group
from django.http import HttpResponseRedirect
try:
from django.core.urlresolvers import reverse
except ImportError:
from django.urls import reverse
from django.templatetags.static import static
from django.utils.safestring import mark_safe
from django.utils.html import format_html, format_html_join
from django.utils.http import urlquote
from django.utils.safestring import mark_safe
from django.db import models
from documents.actions import add_tag_to_selected, remove_tag_from_selected, set_correspondent_on_selected, \
remove_correspondent_from_selected
from .models import Correspondent, Tag, Document, Log
class MonthListFilter(admin.SimpleListFilter):
title = "Month"
# Parameter for the filter that will be used in the URL query.
parameter_name = "month"
def lookups(self, request, model_admin):
r = []
for document in Document.objects.all():
r.append((
document.created.strftime("%Y-%m"),
document.created.strftime("%B %Y")
))
return sorted(set(r), key=lambda x: x[0], reverse=True)
def queryset(self, request, queryset):
if not self.value():
return None
year, month = self.value().split("-")
return queryset.filter(created__year=year, created__month=month)
class FinancialYearFilter(admin.SimpleListFilter):
title = "Financial Year"
@@ -104,18 +85,43 @@ class FinancialYearFilter(admin.SimpleListFilter):
created__lte=self._fy_end(end))
class RecentCorrespondentFilter(admin.RelatedFieldListFilter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = "correspondent (recent)"
def field_choices(self, field, request, model_admin):
lookups = []
if settings.PAPERLESS_RECENT_CORRESPONDENT_YEARS and settings.PAPERLESS_RECENT_CORRESPONDENT_YEARS > 0:
date_limit = datetime.now() - timedelta(days=365*settings.PAPERLESS_RECENT_CORRESPONDENT_YEARS)
for c in Correspondent.objects.filter(documents__created__gte=date_limit).distinct():
lookups.append((c.id, c.name))
return lookups
class CommonAdmin(admin.ModelAdmin):
list_per_page = settings.PAPERLESS_LIST_PER_PAGE
class CorrespondentAdmin(CommonAdmin):
list_display = ("name", "match", "matching_algorithm", "document_count")
list_display = ("name", "match", "matching_algorithm", "document_count", "last_correspondence")
list_filter = ("matching_algorithm",)
list_editable = ("match", "matching_algorithm")
def get_queryset(self, request):
qs = super(CorrespondentAdmin, self).get_queryset(request)
qs = qs.annotate(document_count=models.Count("documents"), last_correspondence=models.Max("documents__created"))
return qs
def document_count(self, obj):
return obj.documents.count()
return obj.document_count
document_count.admin_order_field = "document_count"
def last_correspondence(self, obj):
return obj.last_correspondence
last_correspondence.admin_order_field = "last_correspondence"
class TagAdmin(CommonAdmin):
@@ -125,8 +131,14 @@ class TagAdmin(CommonAdmin):
list_filter = ("colour", "matching_algorithm")
list_editable = ("colour", "match", "matching_algorithm")
def get_queryset(self, request):
qs = super(TagAdmin, self).get_queryset(request)
qs = qs.annotate(document_count=models.Count("documents"))
return qs
def document_count(self, obj):
return obj.documents.count()
return obj.document_count
document_count.admin_order_field = "document_count"
class DocumentAdmin(CommonAdmin):
@@ -140,12 +152,18 @@ class DocumentAdmin(CommonAdmin):
readonly_fields = ("added",)
list_display = ("title", "created", "added", "thumbnail", "correspondent",
"tags_")
list_filter = ("tags", "correspondent", FinancialYearFilter,
MonthListFilter)
list_filter = ("tags", ('correspondent', RecentCorrespondentFilter), "correspondent", FinancialYearFilter)
filter_horizontal = ("tags",)
ordering = ["-created", "correspondent"]
actions = [add_tag_to_selected, remove_tag_from_selected, set_correspondent_on_selected, remove_correspondent_from_selected]
date_hierarchy = 'created'
document_queue = None
def has_add_permission(self, request):
return False
@@ -153,6 +171,56 @@ class DocumentAdmin(CommonAdmin):
return obj.created.date().strftime("%Y-%m-%d")
created_.short_description = "Created"
def changelist_view(self, request, extra_context=None):
response = super().changelist_view(request, extra_context)
if request.method == 'GET':
cl = self.get_changelist_instance(request)
self.document_queue = [doc.id for doc in cl.queryset]
return response
def change_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
doc = Document.objects.get(id=object_id)
if self.document_queue and object_id and int(object_id) in self.document_queue:
# There is a queue of documents
current_index = self.document_queue.index(int(object_id))
if current_index < len(self.document_queue) - 1:
# ... and there are still documents in the queue
extra_context['next_object'] = self.document_queue[current_index + 1]
return super(DocumentAdmin, self).change_view(
request, object_id, form_url, extra_context=extra_context,
)
def response_change(self, request, obj):
# This is mostly copied from ModelAdmin.response_change()
opts = self.model._meta
preserved_filters = self.get_preserved_filters(request)
msg_dict = {
'name': opts.verbose_name,
'obj': format_html('<a href="{}">{}</a>', urlquote(request.path), obj),
}
if "_saveandeditnext" in request.POST:
msg = format_html(
'The {name} "{obj}" was changed successfully. Editing next object.',
**msg_dict
)
self.message_user(request, msg, messages.SUCCESS)
redirect_url = reverse('admin:%s_%s_change' %
(opts.app_label, opts.model_name),
args=(request.POST['_next_object'],),
current_app=self.admin_site.name)
redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
response = HttpResponseRedirect(redirect_url)
else:
response = super().response_change(request, obj)
return response
@mark_safe
def thumbnail(self, obj):
return self._html_tag(
"a",
@@ -165,8 +233,8 @@ class DocumentAdmin(CommonAdmin):
),
href=obj.download_url
)
thumbnail.allow_tags = True
@mark_safe
def tags_(self, obj):
r = ""
for tag in obj.tags.all():
@@ -183,10 +251,11 @@ class DocumentAdmin(CommonAdmin):
)
}
)
return mark_safe(r)
tags_.allow_tags = True
return r
@mark_safe
def document(self, obj):
# TODO: is this method even used anymore?
return self._html_tag(
"a",
self._html_tag(
@@ -199,7 +268,6 @@ class DocumentAdmin(CommonAdmin):
),
href=obj.download_url
)
document.allow_tags = True
@staticmethod
def _html_tag(kind, inside=None, **kwargs):

View File

@@ -1,5 +1,21 @@
{% extends 'admin/change_form.html' %}
{% block content %}
{{ block.super }}
{% if next_object %}
<script type="text/javascript">//<![CDATA[
(function($){
$('<input type="submit" value="Save and edit next" name="_saveandeditnext" />')
.prependTo('div.submit-row');
$('<input type="hidden" value="{{next_object}}" name="_next_object" />')
.prependTo('div.submit-row');
})(django.jQuery);
//]]></script>
{% endif %}
{% endblock content %}
{% block footer %}
@@ -10,4 +26,4 @@
django.jQuery(".field-created input").first().attr("type", "date")
</script>
{% endblock footer %}
{% endblock footer %}

View File

@@ -0,0 +1,46 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n admin_urls static %}
{% load staticfiles %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script type="text/javascript" src="{% static 'admin/js/cancel.js' %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {{title}}
</div>
{% endblock %}
{% block content %}
<p>Please select the {{itemname}}.</p>
<form method="post">{% csrf_token %}
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}"/>
{% endfor %}
<p>
<select name="obj_id">
{% for obj in objects %}
<option value="{{obj.id}}">{{obj.name}}</option>
{% endfor %}
</select>
</p>
<input type="hidden" name="action" value="{{action}}"/>
<input type="hidden" name="post" value="yes"/>
<p>
<input type="submit" value="{% trans "Confirm" %}" />
<a href="#" class="button cancel-link">{% trans "Go back" %}</a>
</p>
</div>
</form>
{% endblock %}