diff --git a/src/documents/actions.py b/src/documents/actions.py new file mode 100755 index 000000000..7f23baabd --- /dev/null +++ b/src/documents/actions.py @@ -0,0 +1,161 @@ +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 add_tag_to_selected(modeladmin, request, queryset): + 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() + tag = Tag.objects.get(id=request.POST.get('tag_id')) + if n: + for obj in queryset: + obj.tags.add(tag) + obj_display = str(obj) + modeladmin.log_change(request, obj, obj_display) + modeladmin.message_user(request, "Successfully added tag %(tag)s to %(count)d %(items)s." % { + "tag": tag.name, "count": n, "items": model_ngettext(modeladmin.opts, n) + }, messages.SUCCESS) + + # Return None to display the change list page again. + return None + + title = "Add tag to multiple documents" + + 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="add_tag_to_selected", + tags=Tag.objects.all() + ) + + request.current_app = modeladmin.admin_site.name + + return TemplateResponse(request, + "admin/%s/%s/mass_modify_tag.html" % (app_label, opts.model_name) + , context) + + +add_tag_to_selected.short_description = "Add tag to selected documents" + + +def remove_tag_from_selected(modeladmin, request, queryset): + 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() + tag = Tag.objects.get(id=request.POST.get('tag_id')) + if n: + for obj in queryset: + obj.tags.remove(tag) + obj_display = str(obj) + modeladmin.log_change(request, obj, obj_display) + modeladmin.message_user(request, "Successfully removed tag %(tag)s from %(count)d %(items)s." % { + "tag": tag.name, "count": n, "items": model_ngettext(modeladmin.opts, n) + }, messages.SUCCESS) + + # Return None to display the change list page again. + return None + + title = "Remove tag from multiple documents" + + 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="remove_tag_from_selected", + tags=Tag.objects.all() + ) + + request.current_app = modeladmin.admin_site.name + + return TemplateResponse(request, + "admin/%s/%s/mass_modify_tag.html" % (app_label, opts.model_name) + , context) + + +remove_tag_from_selected.short_description = "Remove tag from selected documents" + + +def set_correspondent_on_selected(modeladmin, request, queryset): + 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() + correspondent = Correspondent.objects.get(id=request.POST.get('correspondent_id')) + if n: + for obj in queryset: + obj_display = str(obj) + modeladmin.log_change(request, obj, obj_display) + queryset.update(correspondent=correspondent) + modeladmin.message_user(request, "Successfully set correspondent %(correspondent)s on %(count)d %(items)s." % { + "correspondent": correspondent.name, "count": n, "items": model_ngettext(modeladmin.opts, n) + }, messages.SUCCESS) + + # Return None to display the change list page again. + return None + + title = "Set correspondent on multiple documents" + + context = dict( + modeladmin.admin_site.each_context(request), + title=title, + queryset=queryset, + opts=opts, + action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, + media=modeladmin.media, + correspondents=Correspondent.objects.all() + ) + + request.current_app = modeladmin.admin_site.name + + return TemplateResponse(request, + "admin/%s/%s/set_correspondent.html" % (app_label, opts.model_name) + , context) + + +set_correspondent_on_selected.short_description = "Set correspondent on selected documents" + + +def remove_correspondent_from_selected(modeladmin, request, queryset): + if not modeladmin.has_change_permission(request): + raise PermissionDenied + + n = queryset.count() + if n: + for obj in queryset: + obj_display = str(obj) + modeladmin.log_change(request, obj, obj_display) + queryset.update(correspondent=None) + modeladmin.message_user(request, "Successfully removed correspondent from %(count)d %(items)s." % { + "count": n, "items": model_ngettext(modeladmin.opts, n) + }, messages.SUCCESS) + + return None + + +remove_correspondent_from_selected.short_description = "Remove correspondent from selected documents" diff --git a/src/documents/admin.py b/src/documents/admin.py index b8d64c434..a85b087a3 100755 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -7,6 +7,8 @@ from django.urls import reverse from django.templatetags.static import static from django.utils.safestring import mark_safe +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 @@ -110,6 +112,14 @@ class CorrespondentAdmin(CommonAdmin): list_filter = ("matching_algorithm",) list_editable = ("match", "matching_algorithm") + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + + for document in Document.objects.filter(correspondent__isnull=True).exclude(tags__is_archived_tag=True): + if obj.matches(document.content): + document.correspondent = obj + document.save(update_fields=("correspondent",)) + def document_count(self, obj): return obj.documents.count() @@ -121,6 +131,13 @@ class TagAdmin(CommonAdmin): list_filter = ("colour", "matching_algorithm") list_editable = ("colour", "match", "matching_algorithm") + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + + for document in Document.objects.all().exclude(tags__is_archived_tag=True): + if obj.matches(document.content): + document.tags.add(obj) + def document_count(self, obj): return obj.documents.count() @@ -130,17 +147,20 @@ class DocumentAdmin(CommonAdmin): class Media: css = { "all": ("paperless.css",) + } search_fields = ("correspondent__name", "title", "content", "tags__name") readonly_fields = ("added",) list_display = ("title", "created", "added", "thumbnail", "correspondent", - "tags_") + "tags_", "archive_serial_number") list_filter = ("tags", "correspondent", FinancialYearFilter, MonthListFilter) ordering = ["-created", "correspondent"] + actions = [add_tag_to_selected, remove_tag_from_selected, set_correspondent_on_selected, remove_correspondent_from_selected] + def has_add_permission(self, request): return False diff --git a/src/documents/management/commands/document_correspondents.py b/src/documents/management/commands/document_correspondents.py old mode 100644 new mode 100755 index 0709c49d2..d3b324bb1 --- a/src/documents/management/commands/document_correspondents.py +++ b/src/documents/management/commands/document_correspondents.py @@ -41,7 +41,7 @@ class Command(Renderable, BaseCommand): self.verbosity = options["verbosity"] - for document in Document.objects.filter(correspondent__isnull=True): + for document in Document.objects.filter(correspondent__isnull=True).exclude(tags__is_archived_tag=True): potential_correspondents = list( Correspondent.match_all(document.content)) diff --git a/src/documents/management/commands/document_retagger.py b/src/documents/management/commands/document_retagger.py old mode 100644 new mode 100755 index 8f56e1eea..d3fd83962 --- a/src/documents/management/commands/document_retagger.py +++ b/src/documents/management/commands/document_retagger.py @@ -22,7 +22,7 @@ class Command(Renderable, BaseCommand): self.verbosity = options["verbosity"] - for document in Document.objects.all(): + for document in Document.objects.all().exclude(tags__is_archived_tag=True): tags = Tag.objects.exclude( pk__in=document.tags.values_list("pk", flat=True)) diff --git a/src/documents/migrations/0022_workflow_improvements.py b/src/documents/migrations/0022_workflow_improvements.py new file mode 100755 index 000000000..534cb78e7 --- /dev/null +++ b/src/documents/migrations/0022_workflow_improvements.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-02-04 13:07 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0021_document_storage_type'), + ] + + operations = [ + + migrations.AddField( + model_name='document', + name='archive_serial_number', + field=models.IntegerField(unique=True, blank=True, null=True, db_index=True), + ), + + migrations.AddField( + model_name='tag', + name='is_inbox_tag', + field=models.BooleanField(default=False), + ), + + migrations.AddField( + model_name='tag', + name='is_archived_tag', + field=models.BooleanField(default=False), + ), + + ] diff --git a/src/documents/models.py b/src/documents/models.py index 780cddb0a..2da28da57 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -180,6 +180,14 @@ class Tag(MatchingModel): colour = models.PositiveIntegerField(choices=COLOURS, default=1) + is_inbox_tag = models.BooleanField( + default=False, + help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.") + + is_archived_tag = models.BooleanField( + default=False, + help_text="Marks this tag as an archive tag: All documents tagged with archive tags will never be modified automatically (i.e., modifying tags by matching rules)") + class Document(models.Model): @@ -247,6 +255,13 @@ class Document(models.Model): added = models.DateTimeField( default=timezone.now, editable=False, db_index=True) + archive_serial_number = models.IntegerField( + blank=True, + null=True, + unique=True, + db_index=True, + help_text="The position of this document in your physical document archive.") + class Meta: ordering = ("correspondent", "title") diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py old mode 100644 new mode 100755 index cdeaaba40..b3579b567 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -47,7 +47,7 @@ def set_correspondent(sender, document=None, logging_group=None, **kwargs): def set_tags(sender, document=None, logging_group=None, **kwargs): current_tags = set(document.tags.all()) - relevant_tags = set(Tag.match_all(document.content)) - current_tags + relevant_tags = (set(Tag.match_all(document.content)) | set(Tag.objects.filter(is_inbox_tag=True))) - current_tags if not relevant_tags: return diff --git a/src/documents/templates/admin/documents/document/change_form.html b/src/documents/templates/admin/documents/document/change_form.html old mode 100644 new mode 100755 diff --git a/src/documents/templates/admin/documents/document/change_list_results.html b/src/documents/templates/admin/documents/document/change_list_results.html old mode 100644 new mode 100755 index b33cd2927..d295ce601 --- a/src/documents/templates/admin/documents/document/change_list_results.html +++ b/src/documents/templates/admin/documents/document/change_list_results.html @@ -25,6 +25,7 @@ border-radius: 2%; overflow: hidden; height: 300px; + position: relative; } .result .header { padding: 5px; @@ -79,6 +80,15 @@ .result .image img { width: 100%; } + .result .footer { + position: absolute; + bottom: 0; + right: 0; + border-left: 1px solid #cccccc; + border-top: 1px solid #cccccc; + padding: 4px 10px 4px 10px; + background: white; + } .grid { margin-right: 260px; @@ -152,7 +162,8 @@ {# 4: Image #} {# 5: Correspondent #} {# 6: Tags #} - {# 7: Document edit url #} + {# 7: Archive serial number #} + {# 8: Document edit url #}
@@ -166,7 +177,7 @@ selection would not be possible with mouse click + drag. Instead, the underlying link would be dragged. {% endcomment %} - +
{{ result.0 }}
{{ result.5 }} @@ -178,6 +189,9 @@
{{ result.2 }}
{{ result.4 }}
+ {# Only show the archive serial number if it is set on the document. #} + {# checking for >-< (i.e., will a dash be displayed) doesn't feel like a very good solution to me. #} + {% if '>-<' not in result.7 %}{% endif %}
{% endfor %} diff --git a/src/documents/templates/admin/documents/document/mass_modify_tag.html b/src/documents/templates/admin/documents/document/mass_modify_tag.html new file mode 100755 index 000000000..71ef09ae8 --- /dev/null +++ b/src/documents/templates/admin/documents/document/mass_modify_tag.html @@ -0,0 +1,46 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n admin_urls static %} +{% load staticfiles %} + +{% block extrahead %} +{{ block.super }} +{{ media }} + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

Please select the tag.

+
{% csrf_token %} +
+ {% for obj in queryset %} + + {% endfor %} +

+ +

+ + + +

+ + {% trans "Go back" %} +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/documents/templates/admin/documents/document/set_correspondent.html b/src/documents/templates/admin/documents/document/set_correspondent.html new file mode 100755 index 000000000..22993421e --- /dev/null +++ b/src/documents/templates/admin/documents/document/set_correspondent.html @@ -0,0 +1,46 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n admin_urls static %} +{% load staticfiles %} + +{% block extrahead %} +{{ block.super }} +{{ media }} + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

Please select the correspondent.

+
{% csrf_token %} +
+ {% for obj in queryset %} + + {% endfor %} +

+ +

+ + + +

+ + {% trans "Go back" %} +

+
+
+{% endblock %} \ No newline at end of file