From 30acfdd3f12c5709189b2c302ed3861497f16ba9 Mon Sep 17 00:00:00 2001
From: Jonas Winkler <jonas.winkler@jpwinkler.de>
Date: Thu, 26 Nov 2020 14:18:10 +0100
Subject: [PATCH 1/3] tests for the classifier and fixes for edge cases with
 minimal data.

---
 src/documents/classifier.py            |  45 +++++--
 src/documents/tests/test_classifier.py | 155 ++++++++++++++++++++++++-
 2 files changed, 189 insertions(+), 11 deletions(-)

diff --git a/src/documents/classifier.py b/src/documents/classifier.py
index 6e0d6f946..b0d7d87bb 100755
--- a/src/documents/classifier.py
+++ b/src/documents/classifier.py
@@ -6,7 +6,8 @@ import re
 
 from sklearn.feature_extraction.text import CountVectorizer
 from sklearn.neural_network import MLPClassifier
-from sklearn.preprocessing import MultiLabelBinarizer
+from sklearn.preprocessing import MultiLabelBinarizer, LabelBinarizer
+from sklearn.utils.multiclass import type_of_target
 
 from documents.models import Document, MatchingModel
 from paperless import settings
@@ -27,7 +28,7 @@ def preprocess_content(content):
 
 class DocumentClassifier(object):
 
-    FORMAT_VERSION = 5
+    FORMAT_VERSION = 6
 
     def __init__(self):
         # mtime of the model file on disk. used to prevent reloading when
@@ -54,6 +55,8 @@ class DocumentClassifier(object):
                         "Cannor load classifier, incompatible versions.")
                 else:
                     if self.classifier_version > 0:
+                        # Don't be confused by this check. It's simply here
+                        # so that we wont log anything on initial reload.
                         logger.info("Classifier updated on disk, "
                                     "reloading classifier models")
                     self.data_hash = pickle.load(f)
@@ -122,9 +125,14 @@ class DocumentClassifier(object):
         labels_tags_unique = set([tag for tags in labels_tags for tag in tags])
 
         num_tags = len(labels_tags_unique)
+
         # substract 1 since -1 (null) is also part of the classes.
-        num_correspondents = len(set(labels_correspondent)) - 1
-        num_document_types = len(set(labels_document_type)) - 1
+
+        # union with {-1} accounts for cases where all documents have
+        # correspondents and types assigned, so -1 isnt part of labels_x, which
+        # it usually is.
+        num_correspondents = len(set(labels_correspondent) | {-1}) - 1
+        num_document_types = len(set(labels_document_type) | {-1}) - 1
 
         logging.getLogger(__name__).debug(
             "{} documents, {} tag(s), {} correspondent(s), "
@@ -145,12 +153,23 @@ class DocumentClassifier(object):
         )
         data_vectorized = self.data_vectorizer.fit_transform(data)
 
-        self.tags_binarizer = MultiLabelBinarizer()
-        labels_tags_vectorized = self.tags_binarizer.fit_transform(labels_tags)
-
         # Step 3: train the classifiers
         if num_tags > 0:
             logging.getLogger(__name__).debug("Training tags classifier...")
+
+            if num_tags == 1:
+                # Special case where only one tag has auto:
+                # Fallback to binary classification.
+                labels_tags = [label[0] if len(label) == 1 else -1
+                               for label in labels_tags]
+                self.tags_binarizer = LabelBinarizer()
+                labels_tags_vectorized = self.tags_binarizer.fit_transform(
+                    labels_tags).ravel()
+            else:
+                self.tags_binarizer = MultiLabelBinarizer()
+                labels_tags_vectorized = self.tags_binarizer.fit_transform(
+                    labels_tags)
+
             self.tags_classifier = MLPClassifier(tol=0.01)
             self.tags_classifier.fit(data_vectorized, labels_tags_vectorized)
         else:
@@ -222,6 +241,16 @@ class DocumentClassifier(object):
             X = self.data_vectorizer.transform([preprocess_content(content)])
             y = self.tags_classifier.predict(X)
             tags_ids = self.tags_binarizer.inverse_transform(y)[0]
-            return tags_ids
+            if type_of_target(y).startswith('multilabel'):
+                # the usual case when there are multiple tags.
+                return list(tags_ids)
+            elif type_of_target(y) == 'binary' and tags_ids != -1:
+                # This is for when we have binary classification with only one
+                # tag and the result is to assign this tag.
+                return [tags_ids]
+            else:
+                # Usually binary as well with -1 as the result, but we're
+                # going to catch everything else here as well.
+                return []
         else:
             return []
diff --git a/src/documents/tests/test_classifier.py b/src/documents/tests/test_classifier.py
index 4ae672ac2..0f421bb32 100644
--- a/src/documents/tests/test_classifier.py
+++ b/src/documents/tests/test_classifier.py
@@ -1,8 +1,10 @@
 import tempfile
+from time import sleep
+from unittest import mock
 
 from django.test import TestCase, override_settings
 
-from documents.classifier import DocumentClassifier
+from documents.classifier import DocumentClassifier, IncompatibleClassifierVersionError
 from documents.models import Correspondent, Document, Tag, DocumentType
 
 
@@ -15,10 +17,12 @@ class TestClassifier(TestCase):
     def generate_test_data(self):
         self.c1 = Correspondent.objects.create(name="c1", matching_algorithm=Correspondent.MATCH_AUTO)
         self.c2 = Correspondent.objects.create(name="c2")
+        self.c3 = Correspondent.objects.create(name="c3", matching_algorithm=Correspondent.MATCH_AUTO)
         self.t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
         self.t2 = Tag.objects.create(name="t2", matching_algorithm=Tag.MATCH_ANY, pk=34, is_inbox_tag=True)
         self.t3 = Tag.objects.create(name="t3", matching_algorithm=Tag.MATCH_AUTO, pk=45)
         self.dt = DocumentType.objects.create(name="dt", matching_algorithm=DocumentType.MATCH_AUTO)
+        self.dt2 = DocumentType.objects.create(name="dt2", matching_algorithm=DocumentType.MATCH_AUTO)
 
         self.doc1 = Document.objects.create(title="doc1", content="this is a document from c1", correspondent=self.c1, checksum="A", document_type=self.dt)
         self.doc2 = Document.objects.create(title="doc1", content="this is another document, but from c2", correspondent=self.c2, checksum="B")
@@ -59,8 +63,8 @@ class TestClassifier(TestCase):
         self.classifier.train()
         self.assertEqual(self.classifier.predict_correspondent(self.doc1.content), self.c1.pk)
         self.assertEqual(self.classifier.predict_correspondent(self.doc2.content), None)
-        self.assertTupleEqual(self.classifier.predict_tags(self.doc1.content), (self.t1.pk,))
-        self.assertTupleEqual(self.classifier.predict_tags(self.doc2.content), (self.t1.pk, self.t3.pk))
+        self.assertListEqual(self.classifier.predict_tags(self.doc1.content), [self.t1.pk])
+        self.assertListEqual(self.classifier.predict_tags(self.doc2.content), [self.t1.pk, self.t3.pk])
         self.assertEqual(self.classifier.predict_document_type(self.doc1.content), self.dt.pk)
         self.assertEqual(self.classifier.predict_document_type(self.doc2.content), None)
 
@@ -71,6 +75,42 @@ class TestClassifier(TestCase):
         self.assertTrue(self.classifier.train())
         self.assertFalse(self.classifier.train())
 
+    def testVersionIncreased(self):
+
+        self.generate_test_data()
+        self.assertTrue(self.classifier.train())
+        self.assertFalse(self.classifier.train())
+
+        classifier2 = DocumentClassifier()
+
+        current_ver = DocumentClassifier.FORMAT_VERSION
+        with mock.patch("documents.classifier.DocumentClassifier.FORMAT_VERSION", current_ver+1):
+            # assure that we won't load old classifiers.
+            self.assertRaises(IncompatibleClassifierVersionError, self.classifier.reload)
+
+            self.classifier.save_classifier()
+
+            # assure that we can load the classifier after saving it.
+            classifier2.reload()
+
+    def testReload(self):
+
+        self.generate_test_data()
+        self.assertTrue(self.classifier.train())
+        self.classifier.save_classifier()
+
+        classifier2 = DocumentClassifier()
+        classifier2.reload()
+        v1 = classifier2.classifier_version
+
+        # change the classifier after some time.
+        sleep(1)
+        self.classifier.save_classifier()
+
+        classifier2.reload()
+        v2 = classifier2.classifier_version
+        self.assertNotEqual(v1, v2)
+
     @override_settings(DATA_DIR=tempfile.mkdtemp())
     def testSaveClassifier(self):
 
@@ -83,3 +123,112 @@ class TestClassifier(TestCase):
         new_classifier = DocumentClassifier()
         new_classifier.reload()
         self.assertFalse(new_classifier.train())
+
+    def test_one_correspondent_predict(self):
+        c1 = Correspondent.objects.create(name="c1", matching_algorithm=Correspondent.MATCH_AUTO)
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", correspondent=c1, checksum="A")
+
+        self.classifier.train()
+        self.assertEqual(self.classifier.predict_correspondent(doc1.content), c1.pk)
+
+    def test_one_correspondent_predict_manydocs(self):
+        c1 = Correspondent.objects.create(name="c1", matching_algorithm=Correspondent.MATCH_AUTO)
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", correspondent=c1, checksum="A")
+        doc2 = Document.objects.create(title="doc2", content="this is a document from noone", checksum="B")
+
+        self.classifier.train()
+        self.assertEqual(self.classifier.predict_correspondent(doc1.content), c1.pk)
+        self.assertIsNone(self.classifier.predict_correspondent(doc2.content))
+
+    def test_one_type_predict(self):
+        dt = DocumentType.objects.create(name="dt", matching_algorithm=DocumentType.MATCH_AUTO)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1",
+                                            checksum="A", document_type=dt)
+
+        self.classifier.train()
+        self.assertEqual(self.classifier.predict_document_type(doc1.content), dt.pk)
+
+    def test_one_type_predict_manydocs(self):
+        dt = DocumentType.objects.create(name="dt", matching_algorithm=DocumentType.MATCH_AUTO)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1",
+                                            checksum="A", document_type=dt)
+
+        doc2 = Document.objects.create(title="doc1", content="this is a document from c2",
+                                            checksum="B")
+
+        self.classifier.train()
+        self.assertEqual(self.classifier.predict_document_type(doc1.content), dt.pk)
+        self.assertIsNone(self.classifier.predict_document_type(doc2.content))
+
+    def test_one_tag_predict(self):
+        t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", checksum="A")
+
+        doc1.tags.add(t1)
+        self.classifier.train()
+        self.assertListEqual(self.classifier.predict_tags(doc1.content), [t1.pk])
+
+    def test_one_tag_predict_unassigned(self):
+        t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", checksum="A")
+
+        self.classifier.train()
+        self.assertListEqual(self.classifier.predict_tags(doc1.content), [])
+
+    def test_two_tags_predict_singledoc(self):
+        t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
+        t2 = Tag.objects.create(name="t2", matching_algorithm=Tag.MATCH_AUTO, pk=121)
+
+        doc4 = Document.objects.create(title="doc1", content="this is a document from c4", checksum="D")
+
+        doc4.tags.add(t1)
+        doc4.tags.add(t2)
+        self.classifier.train()
+        self.assertListEqual(self.classifier.predict_tags(doc4.content), [t1.pk, t2.pk])
+
+    def test_two_tags_predict(self):
+        t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
+        t2 = Tag.objects.create(name="t2", matching_algorithm=Tag.MATCH_AUTO, pk=121)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", checksum="A")
+        doc2 = Document.objects.create(title="doc1", content="this is a document from c2", checksum="B")
+        doc3 = Document.objects.create(title="doc1", content="this is a document from c3", checksum="C")
+        doc4 = Document.objects.create(title="doc1", content="this is a document from c4", checksum="D")
+
+        doc1.tags.add(t1)
+        doc2.tags.add(t2)
+
+        doc4.tags.add(t1)
+        doc4.tags.add(t2)
+        self.classifier.train()
+        self.assertListEqual(self.classifier.predict_tags(doc1.content), [t1.pk])
+        self.assertListEqual(self.classifier.predict_tags(doc2.content), [t2.pk])
+        self.assertListEqual(self.classifier.predict_tags(doc3.content), [])
+        self.assertListEqual(self.classifier.predict_tags(doc4.content), [t1.pk, t2.pk])
+
+    def test_one_tag_predict_multi(self):
+        t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", checksum="A")
+        doc2 = Document.objects.create(title="doc2", content="this is a document from c2", checksum="B")
+
+        doc1.tags.add(t1)
+        doc2.tags.add(t1)
+        self.classifier.train()
+        self.assertListEqual(self.classifier.predict_tags(doc1.content), [t1.pk])
+        self.assertListEqual(self.classifier.predict_tags(doc2.content), [t1.pk])
+
+    def test_one_tag_predict_multi_2(self):
+        t1 = Tag.objects.create(name="t1", matching_algorithm=Tag.MATCH_AUTO, pk=12)
+
+        doc1 = Document.objects.create(title="doc1", content="this is a document from c1", checksum="A")
+        doc2 = Document.objects.create(title="doc2", content="this is a document from c2", checksum="B")
+
+        doc1.tags.add(t1)
+        self.classifier.train()
+        self.assertListEqual(self.classifier.predict_tags(doc1.content), [t1.pk])
+        self.assertListEqual(self.classifier.predict_tags(doc2.content), [])

From 43b473dc531d22e5de7ee62f0a98430630b64f31 Mon Sep 17 00:00:00 2001
From: Jonas Winkler <jonas.winkler@jpwinkler.de>
Date: Thu, 26 Nov 2020 15:55:13 +0100
Subject: [PATCH 2/3] Test cases for the API

---
 src/documents/tests/samples/simple.pdf | Bin 0 -> 22926 bytes
 src/documents/tests/samples/simple.zip | Bin 0 -> 17396 bytes
 src/documents/tests/test_api.py        |  39 +++++++++++++++++++++++++
 3 files changed, 39 insertions(+)
 create mode 100644 src/documents/tests/samples/simple.pdf
 create mode 100644 src/documents/tests/samples/simple.zip

diff --git a/src/documents/tests/samples/simple.pdf b/src/documents/tests/samples/simple.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..e450de48269ce43785b8344c63e233a1794abae6
GIT binary patch
literal 22926
zcmeFZ1ymeg@;^!l!6mrE;Lb3(yL)iAVQ_bM2@U~*y9EgZNJ4OTf?IHc27(3mH{|=>
z-S7V7z4P{+J?EYO***+?`*z*Bb*rj-daCNvG^&!)EFe~HWSZ{c?w0P)-Fe9D05*W5
znGLd_AW#wFVCiNB;DGk10i~_&+#oJMX**Llh$IB;Xbuq;Ms{^`ftcDOdu6l44<m=N
zy_CAVj@HFjbg(Oe7w@<V!ZVP4ZkWPaJUbaT4mi6c*02CY@}0t_oq-Echb@V6Q5vhw
zjA4VRSm=}A>>H?6H;@?p^vKWPs#P#ZGbM{;Bcf{BGr+ELKNlvYYRNcNgtX+7@aLXT
zMQN!S?3XnMOGXzd6?Y;rsx^sOB+DXSS48V9%8C_*Nre0Ge09*fJ;tB)nym>uKSOw1
z2i!o0IGFz_FtqiwM&zfZJvBhw+)rnJ_woEU1@Qha3iwk&AOMJsm!0je>e%A*b|c=(
zS#^}IgXn*zCa)u*nWXP(wA20EkL1j%VyEC?M)$S`K=_(QzmCRCLHrFiECjSQ<SL6K
zQy}`exS1v4vl}6H6B;v3gt(`#s!8x$CWV{)goc`80(oolm%f3kWl4o$`}V634I3jP
zM{Vtn!UE+z8n3MeIoG2&`3YYOR93tYAE-eJo}6f9Fm+Ywl@3$E?7H94@$>tYn`iKg
zf}%nOaWK%_&+Ku&A#j>Q@-?@j><QDlD&T4{LZ38@IcdDFi4Dtx<>#2p9dZv4QKhun
z=@em(Dge&env$D{x9Q_-*cI_>U>>Rgrg4#rb67eijW{P8;mu->2nuC92$yD~)|^om
zof)g{JNi%po%qS2uXL^$$;LVc720v6ksjPB{pbm!yHQ(d{s&oogF>puBi3^YH8K8~
ztf=^&Z>QNYfr%PP%}Ba_X=avrD9bVAkH*pka_wzWhja;v5}TSX<y}tL7*UA~#hXSd
zZ}1c4ff$68A^;9!lM68yl51-|k;ZU~UyW&zOH%Pkfg=4ek&16B+1`>TYZnCH!OGA`
z3&Wr_z7-7B5)oa9ALHmvT?5AkgZZZC23wcJ>T-OElbRVKU0r;Baq_`7Pq-kT3Z{JZ
znzD>tk*w~s6hTUEMXXn7y`Gwr?fjkxs;nIJ_~l-gs<>$-h<<qzKHvV#g?#o*PTJKX
z{W|9kvefRfl(C4}m>Ro53Nw-;(BpU?f_z^C3`oT3wrR`6@gqyKrECgMXzc67xJHqs
zAT-Dx8^>$LdmKT)E37b`Q9HMosc9RLS$SU}H%%K8sPn~!;@wJl8+r3Ni~|WNKE`!R
z=<|F3*4*42fu`oqj{85Inim%J8su5@xg8h26nNi<@6U2i3+$78s?@4}*VfUtWWkk6
zeAe{ldtn!>Qb4X=udArd3&K0rj2b-D!=Pmdh8w@li!_F%!*}lAmIHJV5!u@`n11Hu
z#F}Fagcv7kuQ4S`TnuB$$LG*7l+ct^QSXK;nPa<~;%{0m9&|Yq?448ky<0xS-kd^R
z=`@)^j=-TZt1p0$iI!&iVt%=D96Ou<>au%fn$^mpvHOmuK3obBkAk|UuHVvh2G0bh
zVd#_TTdGX6Jv<Kg7a?_JbNe<ti*~{?2i%Q4E2BEaaTzJ63!8oxYOlw~2gOW3dpK-w
zZ}_l(ubF^9=<J{|%D~7!L_NyPXq4lg9;iGx__fEh%5Az$(Kx$u_{R3#Om$mTUUfaI
zn3nebLDz^eVkubU{;)A6NPMtz_Ay*lb$h{l_jF+w;sJhdX|d$dDYTJ?L}HvxR*T|u
zJ5r@3fbD787}Xy`8xIur7-w#FycqNeB;dYEnO^ocbkRNPHOi>;Kv{=TJ2sA4{=8zx
zVGa>A?xEGeV@B7Sw<v<}dKpksy&jlqUEyR+#60bmbeX*}A64OVB~<POY$;mu>bkd+
z`Yz5K<s@m&EQWaCYu%ZM=8|>(Xo}_Tt4>o8W?%ftQ37A^?FYCHR{9eQ0jBvmGBcLV
z7USBIYAT_SguJkPyK>e<C$dEOxSb5gbo}t)FQ&(f2qi1g4sp-fVg%C;CWQk;*=Hr-
z>Tf=DHgI?IA7lk=OMias-*WM_{oKsStX;f1tbxPT*rG)H@JdR-qiMbg%YftI!VPiy
zZR^}EJtn@&S8k+jFr_tRn+KzvT{naNdgcjyzj@^<Nsl`Zt=Cs>-Cw4W{vM74GX3aG
zA8%4J&>|DQ4h1z-uCB}oY#P?Uy|GYrt+1K%w)kn}x`2wFUsXPfXf!%<tE!p+*BKYo
z>W(eEb!UUN;={~aG}&ptOzqXF$UaFB(W2)RzJSXYod?!X>MwuK0cJ@kv?_Z)Wq0~*
zGOg&X#OHioX*4tz8_S6BMI3fc-aPx9SV>#!LJ6SP0Y&o|8J_vzMoHtuuMdn&y(V1R
zK3q=dZ`GNZv1=&=LdVU94vAbHoVU;;EGI@!=NH-SOr^m`dwB(Y2hn07Nsh@#q^8!b
zog){pP5B33E|Gl<Chf$=M&z*!N7m7%U;FDj^{pH&_Y^yeG%onjoNLNA<=f`Zyo%A+
z->)J?KO2>I$`2g4eMdGHjsV|+;o9-(THn7OA24?NE{GgWdW$|4i-A%#Om9y@vU~Gu
zf_#`FM|CtNfv^t=Vv#jFC!namky9zp<6{Wl8^lNw%}gptv8L=)vGr7JU$w5d0xfO@
zO`Hb6y3uS@5GCb|O^vME)$Um$SdSk5l-cGS^vgLtmnCt;I?6gFaT1e^Kycs3X~0)8
z#@Ld>x3EadXZ=fh*Sy_b)t4{;X-ds?7e@fOp<U1G_nm=&P=A!qgKaOK4^9n*U#M0Z
zm^+r7;mJG7O`XyUk;~x32JKDF`zD~e!LnW~ILRR^1eoSd<dx?9S}$cUdJKKeq!riB
zBN$~l?lMoE!@L#Ei)L&(jfIADOr#VX3|zZ!gKbNpTKbTX@|i%)GE7~X*ECE&)+r7%
zA}&ZWc;a$|c%6@m1k&;a(<jyh63c1G-Q^#P3$}TKxt9S0>dJ~0__})@Tj!i~EmyhR
zMIaQ*Gq&r}C!;53!hbq4PU6b(^$S5J$HvCwPj~NadHT7-)7`vvRWj>x(94OQT=<Di
zk`@1QEBf#ui_m#hM}6Od&Nc%CkI=6s#<~bxChw|(;(#%htXvJ{@gK8T+FJ_Ow?0|S
z7;XVO`ob-Z27ou_3EnJ1s?QkulNZU4JwqUyAZrl@U=1TH1H(JU4?1ZqCs%KiTqGGQ
z1%>S)QiT2GGZDghdV$l(WmRmJFIsV5<7Q&=*@b_>z0*3@5vvn##f4+iAtctFB4n-0
zwal!;jo%)3jY*cxR)?YS9BGm&4jLFzMgE%Zds9|GHgwt$G;dYa(PPb(`E&Rb*J(?S
z_{*t4;H1me92Saibz9)2`y4ae<uYpAoy>aEOjeuoRE9t$Nj#&&W$5r|$}8Eg86;nv
zY>xf(Dh_F-t`;Xn<iV1aUpWXE<NU<h6OQwDu93?Nr#C9ct<m^H`^zfnYv43#I0JQ%
zBAJ3?v_sml1XBT;T?RA*sZJ>c;xxNV!5UqHMfq0Mn~fae3Tz`4iS{D8W|NQbS!2j1
zFH<*9e-3L`+3Q8VSR14DPu+Z%TC5kTag`HZQN$w}xA&Ek)xR!ydk{s_4Go>SZMbzn
zL_!NQZ`ynqXsi}XRqLZv8&^~H(aUdJUdVX!Wb3r=2iHsE=MP+Ky3f0aysXerYl3sR
z5~I>gd=d9wF?6mJ6Nf#spfYIGs^W}}v(3s@?XPuWV*1IXJ)gFtnL^COB6#`zXTs8I
zjVGs@;mP!J+c-_!s)%4fjqBG1$)mOSoCb_f^J1>2>yVNogSmjmRHb3NgQCNlWix+|
z$sg8^D~-jq)%N)JT%m5PZNtW+B61^Nnib_);Fa7z$&cGqY6z0urs1<5oo6tjMwHBh
zLT5Uxy+ebokmIfyM`}Yfy!<3ZTCpOuLq1}?{DPAe)JsR}5mWMY)a^u$BWv&snUh|Z
z@w5RenTyjt7*A*MW61mAPy2v&IL4vgK6m^sl*=XlPajm@ruzpjPB@b8&6E8!ZhOcJ
zGVt6uW;rN|yGwpNKIh=k;PasC_;_3KzGd&CgJ!hc@&E-d)yGrgbLM<zZ!Vw492V7&
zL@m6xDVu30ekF1xER#NdFRrEd4W=TD$U!TXu;1WVXq_^xvam8hSW+}M+mqXU()<!O
zB+z?<J<4lbv+!lrc>kskg>WWifmaok5-jbv%Y8R!_ZR!a*c(d+@u|ReL8tA^_wR4s
z(=t^yBG?}G5mo>c1}UuHA&WLhsuxu1Sd>%h>@wQo;x$#s>+9K^UCE#8x!0)JX4ePV
z1sCD*+67yq^suZo1ogxA!0I2<=;p7$hP*h#OSg2PPx(e#C$Lc>?`kEZI6BfLi$SJO
zjJt)G59v2|f<xnV4=I3k+O$(*68&_YdE)LlJs0RWx8HvA73nqe5^j8_eOgCl35Tbz
ze9Wx={>qDU8FJ>>^UfrMHs7BPnfo3i=cG%(VW<A=w?0FfQpFYWHIw>P5TAq^)XV21
z>6;rtTl<x&!IeMHou!xH1zbF(ySO{8rRAOnNNa1gzuEkj1LO#{<Pm;r<eqED#+mwE
zUaywWX=3W+`K(|!a&t$k*R>(aT+79zB=gbYc&^^nVu<_A&2Xe&RJh8r#PMxAtj2=F
z)%fPs)dVAg;B8O)`^^>5hk(N#67s$PyzgN`w1&2-_?TSfoYwM!!g0AwmnPyNNUxEU
zGdjQ_KG?fTY-8g)N^Je5hwqTkvrHD?oUyNz02zSybtl5ozu44-iuMSpv>)lG6f4(H
zCxLhD77fEc*}vi;X4N!6E_&~1A$gs;Yve||em_#RSFR3h6Yjd>=9CVaFK(>8{5wA!
zKjaDD@8=nN@71b1-k<$AYe<?krJL=Py_=-13VCJU!?=5F`)-1G`1+R-xns{j!=?rM
zn2Ri3&{?D+n<Bn3_ucZhuLdJ=pNMwU**+*ui1J8Lw8CHKpkbi17+kH^thk+ZWj;)I
zMLynjFVbcLb<mC;Ph#EIH*RxK*3$y)ifS`Kv&na%1p7QCrfPasYPczQ8ky6OK6VnR
zBIWS@NnzvKm3J&1S4{DBHe-<JRX@fu@-Qy8MIOFn<xI+SK*P_*JnKTg!c^^1naqIa
zyBdrfO>e6E82M|*{myK$S7Dr2yYe_m6F!X?Kf@arw%S@Lck%O4NiFh)aHP0`uSyE}
zzVq<aAm2{FU?{}E+pkQPaM#(ScAHN9G6PHG^v~t%L-cLN`E5wZ@-(&8U!|sLtE#T-
zFpbyrfYFVYcQ(#_rRHh6_%T^ucG8RIB9ccPe?|mG9lK(-F?Hoo#=eFj!(5PhXX&y<
z`A!mX%o2_qlY%CMS{xfpbtHjlgm0=0m+|e3QCR-FblKru4zsbC<By0UIO}ZKi&S0G
zm|?LA=hg@^QpYiDi6^f{Iae(*JzulM9hPL0v}OEg`wF{ePW#gGaH3KfF0RffqWv!2
z`;dWi2jHP4BtviMIz%#-a;m!>rj;R7Q2We;?xXxoRme}!`><gVUmJbrmD>0F&jz_Z
z+4Z{~oZjNYI|?`TY}u5vk?2_o@$&Ar9*%ca``_lrUe4KB*;HE_UKP0{o(jKXVAguS
zm??JaKRl`d!4R~I&*nvK@8{E#-!86{)6FQCt^CTf1*~W8O+HjJB&3v1?$@@eqAvj#
zm4QzUQCjSS%UFWQaZ+DoVm5ZeGN8b*u$csVpm6H0!J?!S61UqK9D1U)Ta4)gZU=`a
zU77grJahcUgf4TnJ1<Bd>nQvW4@gP6LXRM6#^`!0#*5iP7e2R<sjf9HWG&VwEb}Tx
zeXccfjFjHq3ubL=D>2vXlZDxQ&NV}PPF(@o>dC__(T55|`~t+14O#brDBA9x;pGlC
zIt@R7J(&;skAWXW9<%BL2Mzt0YUGt3VXHfjly1aZ{T4%F3{r4IP9N?n$87sn305g*
z7{!MgE!4V!RL?bXL?rn!f&C2#-is$IQOC-DS+C0ASC!-G1LXpdJ-XDFi0=5hTEAkP
zv}<{5TvjT1XZ;GuxUBxJsX~<cg*Pl`6ALb&?=ky0);8FR`?KLU-e)wP3~VyL2Ge0r
zz3_50OmK%tR86=TXjj$N1PiHF)z|!7XuKtZxvrX>b71ikv8*)L#9xQV1jh`h9Y+0p
zbeVCkVQ>QJ4%^~<31eR8ncCGnt2xE^h0nnDQmmJWO-5eB<HhzMzh)?GrK*KN@QiuX
z9_@=!D!ds_9Ns*?cl-q%M0N81ioqxYYUlgN1_P}H50$X^N#~jPxH<}~4D@PiW-hv7
zecDLYj9vvo>|}CM)u=DRO#rwrrNJlnQ3GDQG*v`~ES$4E(<r31^H-9xRxvc+Yb61=
zQBwQJi=+6qI5*>xmH<sAyO{V{cNuAMYuy5ol-IH6;eBPauu<;^Csa8J<Pp)={i04!
z3=1URP{z5VxuaQT><ml45f_!Ef$bbO2h_^Ej>{pAOhk;6c~h<m8<q5qO=r%S&I`6&
zm6HapP|OriInTk3M~DXcZbhGjobSYp+b0Xml9znbIY58@wrZF*Q9bTK!^%;19#ghN
zd{E~jSE8gb!8cKAB86Gx+peUE4<CSuI3We*=7P~9N%vii{ID+1(-k<07Gr2HBWys2
zwUYFN@sj=F)JnP&cxvmS(X>K@@-&o1!BJ87^L2x~HgP5i7gB1bZi0mJ$O00#VQur~
zfSb@JH+r2cP*wTMj95A|U)|XFO)E;A=)!tYHa4t`*Tv&{yordhX1yQmk|wAt5%I)d
zBNU>~X;9)}m=@!^yXE2bz)a03_R(GxS~8NNg@;U2MYQs82FveyQQ|F1oD#;+u#KBA
z{v3$%W5!b|uu(*W3r*+MV&dlKlDiFqC__R}Vrk6=VpQPNtRU>V_{t^UiL*!jZ!czE
zq|C;eS?-P3oeq1I-=8E-Ccu4S=bMN-NSK^es(qNGzG-kr5pu3dAP&XG3D%?F$ly(j
zMqvh1=gQC0P+_~*_I#=DUshU>3ya;z>AIE2q7<WCBNY`48mEs+;{B$ArN|ho5nLs{
zXXD57#%D69L6l#WG2#ws8ef*U3u(G=45pr#>Xlk#yW~gzl26iMH!1Y|86GU+i>iS*
zEN(?Bh%T-N$i;cDD8GDF`|8Xd?hKb$mrDJLDU4l=mf|JCiZNuP@~W92WyFODR1NPD
zn7gh0EiRuXSd+h(mvd4M@1Cr<SM_Lkk0RgTwL35>`(<~?2;(QA*I4(8L*1O#G-@OI
zDRO9j@%?0Hyb(7=Rmp^duPm~ZB^O*!*FUID4S!fc!V^pU0dY|%+!^y&JGCszlWIu2
zUh3S%QDHY?`YuOP<MtI^2M1I<srR#6_T4I5=<}*vu~H>V<_fF$wP_^dJsDNE=kmsd
zBmG7+@)hcp)4s7n$s>+9{)o(NZfRrZEFbt^=*&#z<Vre355`ma!v<b3RoY=yzPaEk
ztSa)i(~L-srOPl3{@HUW%#910ZFpzTw}0}jqIkzbF+)B0M;~T<Ei=2j+lQ5LMR8^|
z<QMZp<YIA1+XAHe=}Iy|=iJ3-{Jrc0(@gKo<%{AuqC>yT8;qZmsoD5o9%3)&*@6r^
zm%bGXWtg1My4uO;e(bc1|6!wEj?}Bpzk&3b{9y3HGWM%}Q=yo}*OM1q<ls+o3`St)
zLcl|@KyL1Pf<5KhfZ{oIpG@_<a)Y25p3>dkZ`CPkW*O%DR3o%(%G_(6lTi~BdHmdw
zjg=M}x*YEEBctv534OShxFd$-QFhG91mK3co1$-;JgM~o967uFLhrH;@SPz6OY&P`
zr_AG;t`E5xE5Na@ltOWsJ?hN4)Rhp@VN;T8q1}S4+=8iguB9Lf|4}aewQu~e6tFKN
zoYkbO*60LG(^0#$>IkX*6b5X2J&C`c86sp2at_%`-{AG|Q20|F=4knSURw^mWbvvz
zR#uoU3d3Gie&Go)iXj?X$DUUzXOS&AI(Y5*y;;J$Iq9&gL7}sOBHr<Pq@Q3|!-pLt
zRk{APX+p0<R7A_QOqGsPB*j3dtLKOJ*E6$`+bScjLPu!rm*c*E_rCeQ7@V6QA3pAU
zd~~}zo0#$rZ}JpVctDJ3P=4Rg)_A?S7HA&;PR#BZQJ&2^%gz>N$kWJ!3phqwv6>k}
zay@rFY5dW+v!nd=>zH+E9`7C8<y3!;*7C+|Ki__BE_jY);~pdE@Y(+J>>Z=W)}?i-
zd;v3|?}A;cuAVvPhv+fq)dJdjD$9I<be?|Bhx0kT@Ru9o&zH?PW}S9G0TValv&i$o
z)<2G|dl#cTF;+k=j-AATJ7UgEE6TfXXHUCV!wMHi8fKO}X6z9h)%zW@+fI(-?#|F6
z9oNVfe9eGqXH}p33b|&j`iX2)t%yeOuR8_ZLRU)csb(sd9!v-MW5w|?X7nTZwe*L3
zCdIgDtHi}f9k=wa$b|c#&ueR^T>fM+pYV^=bG<R>ypcaT7$$t`|JF@zEJzdHWM^gX
z>vwzR5s>Bk=~F$pRddetz{x{!?!=<sm(;tP)QSATRb*3meVgXercB>O-|ny^XIrdD
z2V@1aoNqLZ<`NW#H<+*9@NUDc$#j{E-$+Iek;fj6izKqU{`ox@mtp@Y{%w<a+>RL`
z`oPTn8N)gHpf`d!JFi=<C*B%y8!<X5<@@2L&Axz|pT^t!Z$c4eanhwl+wEp$_526T
zD<TY4)kBYQgyp_!pkaB^O=-N@O3tLFc8-x}r+HTZYfW;jE8#7h?-R7poE#=hUr+m9
z@>6m&Uukv#8}xTxAj~k4Gs^#v_jt-^JS980*?D>Xp7`MVN8&@n+X(`cc64x)gt&rT
ztexB(p_w1(p|Ytx1Sl@{v@t1Kn?YPm-K-rQ)FCd`7RXPjAxl>PC$s^j6c=~&(r4k~
z<N&a6uyFuD>^wXGK3<R^P|nTN&KfM{U}*;duptA*T)_|rHvlgm2pRY^+0y|FJBS+@
zC}HX(3$eDea(fzw3{-c6*lPiJp+lb7L$gs3cED4rMhzL7c==oI2E_J{HT;>qV&`N3
zf5=@KCJj4u1E9GpzZb)9nAb_X7=v{ww6&X3<Q0n;PA1uuGX$n}$E#@>a{A>c;~5#g
z2n27yt~OOjgO%i(C}J0VHW*W0r^uTxs{5ju(-z)2C0J=o>_}p@9tgf5AX-d-FuZBd
z$qLEa`kvp9BKUf<*Y>#2XzTmNeBYN=zRMTS=K<=kG}R=?Zw1{C8;JoFb9EE7c3my8
zZ$&z9e~ND7So+~|*`ynkb$DJN7b;H4zjZf6`fy{+!Zh#lW5#l)h%;uAlVw(OGux|P
z!U%MfG}p6;>mrt2Hb6XUKA=CVnn${WvMI!|g)zeQi;Cl1urBS4ZZt1(Y{YUib4+Ne
z=&+%GXZ<d)k9OwkEid~GbtbOzpw8933=hBC#0~tRaeV0i;6WhJKQ#S!Zvm>io4Gx?
zj)sdn<ae*QsVn5kUjMt(Si88oNm!Y>JUN4+>7Q*77cx-W+T6{`Ri6jyINVUXa<Q=i
zo}7q-mk+?s#RlMny4KV2)Bfq))8}vPr!l|jx!8~m|8SUpI5ObL&()BD8jhL{)=zm%
zX!`EI`O#mppwiI%-qT|L+0p)%IfSN3pVkL;@ISb!x;TQ>A#VCWXl50t0r7G({3ri}
z5<Oja&{4nDqiSji0jiq1Jb69$69b?c#MRN=1q^Wofd0t4DnZPxO`k5NCn33^)9~=I
z8$z!J7k9ARA0u6#R43!&=<Wn%s}9w@se`N26Hl-=P(mGgRe4y0A!;(>Ksf+(6f|uM
zlz>VMozfNXTj;-DKb-#%P#M7Sw+t|d>$hzG_bK4_nVK3}hPQ`1dMZ4EZl2IT&jvSH
z&xW9nAppi4O8Kb#pdfnW*CEj%h>Y(L8R<;`@cHx~yxTp0p4?+xJld@y>_5cWjyBE;
zRojxW!dxwP`*nj&`C+=KzMK7wJ@?8nH1Gxu2f@4xLL~@}ZE&<psg8?#ab;|~&lmLY
zgX7I(%YNYM<s)-QUSJgv=H*3@t6!1Xcl0mELDyItg%9ha&n?c-eq9KxH6t)%y0Wc3
zbG$3My^A#66d}0+)<1NE&^>Mtn4iG_LC1j~#vV;p^V6MRt~AjvP#y%j1EWc96Oe>Y
zI!&G5bYebw%Co<I;n6{``^;$V`FAlk#OKbUzw%{*9#wF6@^CIy8lpGP<6`CGk<d}2
zlh@)B&UesWpIl-nJXjv&**z@Z8;^mXZ*DzMiAf5<sG{87iiE**q`-Xe^$C1b>3(=P
z^oDtP3WF+hcj>pu2cyA_aW?fh1GoPye-mchbEiF1<ps=K{PT{RE*@dpb>p~05##4<
zqOdrk&rv&`-F5m?kbRNl(<w$kMYxPWVtcOD0YCo?VGV96Xs3fu7ytto{PHDyVGw*z
z&{c4#nkdl#3?BdkCx}uNo>3I{-81)mq>n+|<9tpA<ODF?&ySRX7}%cUbztc{vo-Ne
zj1+Q$i44Maf@kf({qh`R4XGvQQcS$90|PEd^rc&(>aLj9Is%_mh}BEiL@<dgOcVf#
zIus`nd9veG;xl$JyaD*eH3DArU!ubFFyU*|^sr@}gsQ0WqJ;DaxcOKbp+u@Ed!j<T
z2t*y;<A|A}99GZX=c5_G9^^kac$Slo;ucCaj&jq%<wUv!^L&k#08YY$U<saTjg{a9
zc#X^prms`t7*R2#-vn9k8OIuf7raF$iPtlu5bt9m<{+Lm7B7UioxaD<szSJqG5z3;
zJG4JMYYQe{qnn3aT*IG#7TCc$|NQnPT<1$HaWMi!lywSN$&i7UFA5{yy`<`as}-$!
zDbVxGSxoRHum=N4R6LQ58hHZXEQ|Ff#2lb2OQsf-0>G8URg2^Vu*f1zhM!SL3?Q-x
zj{sZ;u<9c000INg_~H1(SQD8x(Y?hZDI5pze?`I;0O;Y6Ln-rR>4~DlzZbyKBk{wQ
zh|3hX(PIxqxD;%t3cJBFhDGO3se&|+QzdX!aW$ULh@GoGcY9_NqL&{tPV<hm3DsSE
zSrua%;aq|zKWiMoi5e&BSpeD<=0q0?p)FvrBJx2Nirmgm8yEZt(;EIZ|8gApBfNh|
zo2YSt<2Z&D_RWg}(ZB*Bf)2LWFc2F$Z3lHath%XmI-y1QH&fPhgzeB5CKNh2c%d{V
z3_5skBXLb}b<k#mKA9NmkaLGAnQ+X*evYs=^_|D`h%hw8m`AyYxL<?eB|s1FUZ>}U
zkB`J&H|NC_Mz-wwb`0XhU=32~DqA=Ef?6F^xvuwx%pr()-QtSU52+2+vrBv3=nFYn
zkYi`}F`^*yYGnU9(iKP$O(fKEJ?+@m3o`%#*v)h-bH#Co`+)A)wRnu)f^tM<0*4$d
zwT4LzM;h|1Gt5NF3GfB81@T!JTS!Ers4Rs!CNd%<j7S{2AZ}cOfKu`0HX>6POV_jY
z*G^(zs9IhMBL+&oq{P7tel6WYfrTma()u;3BpxMxQY5`74#g-y9y9edQID?V^Fqvt
z5Gx5c06(TSrvK~x*IBPAdJxTUPGC+DPY6J9UJ>aME#l0SD-<hK(3Hi?{ntmYm3n|Z
z<UM3q(uIjS)X(I^)RGiswW#UdAPgqph)*VB$yyf+sv(s}PfP9r&Xf9NIf`i}@qPrk
zi8oOQB?-$0n!jX6C5(!dq6H);Sr4F^qyOTqgV7CRmVQf7nfPhIbpT}m&z$%ddmYNR
zn4dB$lIRpifd0gd0l7`=O^!_x7qV;k#xMi10Lf~qb}F8v_M~R|Ao0Qv^svz(W}={?
z`BzKy0Vyw|Bi=|37UHNf(xauI%acQ5*=2N#eyYaPYp2i+A>alfONSQ~zB+klu0h0z
zof<kMfm^6tB>yV)o!&H`22};_Ong&FQ*=`VktkZhVg6wOSs_`Gg=+mf?RQesST--t
zk<M|?vCmP?@zKU*Iq{SvrwRxss;rbb$@LJGBoqoE6VX<hAMx}s?8CSv^a~Cql&yF^
z;xFKnhM`Ny6l{zS?sB@(^5gr%8iuh-Yv<dJpYKY$F*T7chaXBA%U~4BjDvS0cl&qC
zcKOZ(Z&+@GZm4`n?@3$Z9zvfHP{T)3n7$*hKt+f^pn8qjD{E#Bk|*Gb<(4J~ht&4W
zn~{L334pOcT14#ls6J{FHMSq=B#Oe@)TIRK!R4LV-JGVZW~|_9vuaZZGY4=V+eUhS
zdRaPt`dqqaI_`+&2+s(u1wws-l$o;**4t=Er;FM3hb|rGDo(9umC-X@dtjvxZ*>H!
zuxCm4<Ip-H%!)po%u~&~*`afj>xZ#)YMJeS*r{ci_prnNj3E$2+81xuUz<>?QVaYL
zGY|Pm<UxA-{1&A-Mq=#)FIIdsxYK$4%9)ZEDVtfDuq=40mvEig=`*iPHt{Gieh^Bp
z)CQIl!xzORkioM<)M^s#P}>f!4c7H0C;u;5UlzZBmZ+AnmzW*MF3}r8ZbNS4Z^IXR
zyS<om!z?@M*Behlyoij5YJ#-8Z8y+PEWC(Ys8-O9;xIZQ*TKh8SDb#_!WhP3z+Ub3
z{S&1tZa=y<j6m!_;y?_M;JnVUb^PPutAwkXE2TSfSjmo;ui+SjQ#+JiQoqJ5Mv8`W
z4`J<0H<9~5_vK{^VnEnSaU6=^MEL>AP0VP-k}#VtK5>qMoN=XHI#jGftUe_5u$?gP
zE`cthbtco~<48nIkR(=8@PPCt4Kg;(YZpn}LcEDYE9H+g{Fp+o1A1PX;edkErAKJD
zu~jgKqdxVV_Go>_H3K>a@rt*oWK#=MwNbXwRAbFWW%Y<qzYe2Gl2fEc$0C<Yp_Wpk
zE>Av_rKPr=q<AAR7>T18O-+-iCikt_Z_-+guRMNQ)`rSGsX7T>uBbS&*m@FWlJdv%
z9~?grtRqpAK<4ZjQ6pm8bW;P9<}`J*--7I<Khp>#w+=kpWOHG@M&wJ*mSLq#OR^or
zAF$tKbrJDS6qFB;%%y0jZl|ev)BdJmpc#icT(m%Kp1uVKGa5%KsZea9Ed7-o!zd0=
z9)>0xOGd81{M9dpGKSO?A9;?F&`Alx{8-gK1{HeO6saMEA^aiQEg)$Kx{3@mK)z9e
zU65R=UN}|Ekzb!*U*=kJT7Xg-Q>aj=P$nR)EvYT7Ei)(U8Fk3GjMjwNgy5#`=IR#X
z#^ko|rtj8#M(;x@L?wjK3e!r^O47>M%G!$5iq?wViti8bhx3Q^C-P^#m%CBB(Yukp
zQ8?57fv_vH+yA5Nhw*rOiE+_t{-e|j4dzsFNWm{vsds7`G!=L=uWh7+B+v72)Vs@1
zra5iUKPBqPzc1ldTPzov&YEtXmYFV_)}E%>quaySBZr539t_<=#e&4L#d5`RI)gex
zI)giNIuj^jDdH(oDIyCJ3sMT=3!(}_3)0r{y6L;&x?Lk)L|~xqqw1sH#u7*)Q=w1+
zi|C6`i;#<0ix`SnikONpz=&XGFfy1MOaW#9vw*R|2w?IJzy`qvxKFo_C$=(nl^~Eq
zOwo&$5gQ>HCK)anHkme=A{jRsI~ia7r8<r}yE^R@QYls`QK@vPN2yV%SShHqU(r?3
zF?}|@jpKlgDm`i>cEn($Yh-d{XoP>Hbi`w%dZcZ{bmU}2CL;lqXI5?QVB!F78xrjB
zOY;u7<~yZ4Wjy8CCdGq8NeWL2kC953%9IM1O6!g7&F#(U&7%pSNumj*$ty`Od0P^-
z#XijS*7Pj`^AvN9&RgBK4|esiS|m)VsiB&Qn$emWnvpXpGx0MyGkIJgTuEHX=Ww=2
zwk)<B`?#|KvzW7yRVuppI@&rsI-c_{?IgJ^xXHMQvPiP{vnaA~vpBMN^y&3U7GHhd
zu(O=6_(t@Nf4;gFtv;^)>xWj0+m6qjpS$Bt=xRCYPwQf8hwE+Yh-()=G}qnMMb^{R
zS=Qy503jm7D_twS!X3gx!h^#75ApXLx3ssy2=Sr8p)nF+5)l${-9g>a-5K4HRIyaK
zR5?_6MPWtpMd3v;;2?0y2B;q;o+chhvY7pnE;u<VIWoDAVXS=6dc=D8N8gVz!d~w%
zj>!7g^>0oEEd?yeckD+av&VSx`Q~^7T@Ia18{ZjZ8&De58SEKE7;qa@8w40684MbH
zGmtV!HP~t7|5EL|yzSF-p1sd9M^~-n&=I#|@qK$8Z$oQs_B;31=bej9^zZRo${VB5
z`(^cp!`9(;Td#kDZ>DdqZ}_dir7;;QMrvfTOqfi(OlW_4e|&!qT_RlsUFr_63P^=i
z1x1Btj3xJ0a7wVoTgyA(_~(h4H=$Rfw{;70%R-Aq3wn!D3q{LG3t`KPmaP`FmV=f#
zFBva7uN|*6uQ~6)Q;KVPzn^~Me(ZjGe$sxQ{SN(t{Xl*)*T1giulKGOu6(W|uE|cm
z{%YO0?YfL1A5>Yl+`<2zwm!GUv)!<9xrwozu%WU8Tm$Ynt{tr%?X-74ra#g=B0M<W
z1-|$IiwY|UYX%z)+XRaTI||zdn*=KZ7mt*U`3~C(cNdcY%ZvWlU@fPksl!n$1&{+M
z%YT>OlK(lsF~2B3!6eM2&ZK&cuv4#7tCOsgqBAn2K13m8KSV&>ilX9W!%HRESF+jz
zY6DUO#`N;^n)E8~G<LI9n^oJ#4GS0wybEv($i;8F;7svMpP5>lx|@PceNBPus_T;L
zB<oXMpSs?633YXMB|z`^Y2iF!o>ESfij;noZzwA$r71lqb15k(ktn}WmPy)3>Ph-Y
zMo5uGwY>BO{!%j6#sLm7GXfY{Dp+kO_W(XRoCch_XX#e8ySft!g>#Ze(aKS6c!t=^
zg`J$paiO&G>iIt=nXRiois~`5glfrF7IIBBSxbDB`N+56yTG%MOCT1jsi>-`rKm8X
zI3hQq!mP)v^fOE+T&GAUZJwppxmLFpQtN2u``ybK&soe_*O|rH)!EWH@Py#R?L_>9
z@r3ro;e_Hu<~y)Q<n?1>5cL2S305IiG?ow6Ec9!w6AKCJ8&(<3sJy|O-86#K^tU=G
zSCq9>QAy@0o5?9}rxQ6--BQkyW8Vrg>H>6WzA<}i{`$OGyva1kHORtHjMIwKkP?<s
zky4uyn$n<AtWhzIRE}IuTkcS9T8>n%tyZa~KZ$7VW9?v#WX)v_{vk0bJo#!;rUa$9
zvRJ>^w^*jQtk_r~@XZ5D=v&j2^|Yk76DgoH=Om<*sN}&nd@Qb7rH0#0BSIOL><a9w
z<`)pTx}SB0b@Fxabslw*5Gr$Zh{}N05clxKK>UF6AnQQD+vTM0A<!_=(BR;Y0o}pI
z;qU>PVbV0F<YvGe4G%RB1rPA@)nS^TUhXR+Q6nuQ{S~zpLt#l_xrcPWjJp@ttk<U3
za@XG1D!<xs*^oz&Ly&(VHzCg==OUXTGaz3hry_qvZbkOQDaBo(_tTu$q-LyRn#4)P
zjl&_u@y7+@aNwvg%rbmtLZ=s2%~m&fwWJ}=2%<k>7^Jsm(q(953TGsJHKbjkYNNHM
z*;L6{bf#r3VQrjIHB!P{P*rVL@w1BLU3PwUr9p+CSfl>+=U*wm3V${Isz!blE+M5T
zrP(XlD@P+rV?-lUBDvKfpEhKl%8)9R>Yi$-S;!b!@uecOqP(KsCf}xT4>ZF*BRxYl
zBTz9_;i%bO{){V{C?PW?voy0Lvsv$*Ui1ReC!0?(HupA{GZr&i)1Rj&_7wNT_mKDC
z_b&F@_CR~OdzRD0(*iS*GiB5GGwl^e)`?tSxTv`}xtO_@xVkvixR5z*I9aWi#tG)U
z%1>TxG|(z1D<zkfS6CPRs3I)RD9$L;E&o>Nt#q2ZU9}y({TWX>MoK|aL3dbZSdK}R
z>DB%ht#GZFS+4iKdi8otdW#DL_IUP~_7HopJ>7xm0ri34f&78iLBPT70`TM9$H4tH
zw|Tc7w`sQ#Hv@NZcQrR;Hyif_cZWU2-Im$w!um3W*{#{s*_qi-vqDwGC0XUZ(w8j`
z4Tq%5gv*fSJRf=?S|L**8X>R{T`QoKy4A1Mrj@((S!<-vsgLoQ$NAum;LYR>|IO!{
zl^f8_x0{lixSQ4+@J;;<(@i_76Z}v3@9=x@c<^NKnD9T~%g~5Wo6w|DEl}U0aidK{
zFh>+dR76-rI7FC6SVu@joJZtDG@zBCrJ)8A6~yYqO2%o#jl?d+cEwJ{Dih{#?6DJp
zGC_LW3+#Oa^+Z##GjS}jLa|h_9I;~rc|3<8GOhrJ7Mr_8IL9t)O6x*<Gw^qNEZa9=
zHHS$vmu2%M^Oen*!8V2*hW7H7@`Car>%<?JQ?OI~Q^Td*rBkH}rCAnM99kS84tEX(
z4q&}iJ#)QUy+ysP1;&U`hCgVLr;KBVN1Kg@-IE)GTLmOyZREHD3FyDexQ@PdL}__Z
z7M>CwAD$DQClw-<EA>gLxi`NzrB@&(@2!;rw}P{Rl7g**Bh-`z!^p!iZ+TegSXOlc
z8pQQ#btbB&-!s2&sWPi_sTz9Es)wedq{E`;-yp5Wp+8kM^ZtDmhJL@!j-IwogYN3b
z+>Zga_ucUJ-8NJXMb=>O1{mAXb*XfrwA*69V!`5@#Uh^Nur-r?rR}~6u~n9xzLmb?
zBE)yjW7gyAMGa|h{Hk*2==|sm>k?~s^?dbU_3B~4a>z1|Z?RCl&{w1Gm4%gl;T7Sr
zw!VkDd;8n63#JPJbZP`61aAZ?1pG+0NViC{NZQDN$VhY)^i_-k^jb13kz)6Oi^Ru*
zVu=@#qhx#hoIX~E<7E=Q@ftD|GPp9XGQfU<euI9-evf{Ne!_l?cmZW<IwQIlbYME3
zGQ=|GGWN0<WuBbU?6=vUb52H=M(ai+Mn^|YM+rx1Mp;IAvgvYG`5c?p489nc8=x9w
z8;ToF8p<2!8901VY-(}+=F;2f(m2#a)YSAPqS4~Z+osP=D{eV1h937@(oP%B$(|e@
zuC6g|!yc7R&70ht+#8?2U-ajEXI-oQ>2Me@FElDN);ildbzgm(eQSv^5IPvTUGT$n
zfBne0r)XfKMJBIbB0(yFJXc3WS%pyLxeET6(-`xZ%NX|<#Telj?pV8#n~}It1@smv
zY!qem!f2<(r-jU`_2MRgz(0NC!<lJl_h@7{h9xPhh~JOn@%6IEIrJkR^;W5tnO6Ij
z`_=?6d~f69!^_;`fc@4jiR1P2S7)H}le4Jv!PD~79~X@~r$d(A_*=t=Ob51W``?Mz
zvkvrsEUpWD7udi3hVcu0fqgl3BCrX4s5sQ#p4;ZxyqtFY>Nu9ynYUfD<#<HCnYV5H
zeRVHzIB+3wCXgqH2!NGOXoCNeAQ4ZMWE{(h$%Nm8%Y-M9h@R*jp%pm+ju+<#>SMDt
zleMXiPaXGwK0qztAYVd^LX1#sODrwK0YM1kh6EL%9!>#%A3*>?25tvV`-L$Q8Cn2t
z3uXxVI%*OM2<Zed3T+Uh9ODNXA%-`09j-j;7KSF$C**S!AuJ#Mk9=nqyKcKSyG8`y
zbe(kNbg+(&j+PE&e(OWCDX^QnYhpcn9a@OIZnS=~{$d?%{eC@peRy2}^9+*`GZXUy
zlLC_va|@Fc)05Ij(oV9FijQiGN{I?XhE@_!+URu)86WO7g$qwKg*>x7-4Fw#AR}D`
ze+AQ?(X{yVn3XtZ;79q7?rx%PHg0lm`e#mOc4t1Q>=9JbtbNpdOtci+=8E8vxO6&D
zCFr(xq!z38rnb6vqSm|iyw<-KyLPx%!1>I%(0Rc*-g(`b&zZ}4*%{;eN{@Yi@<#Xu
z*9Pf#iH-X2mEQ$6j(UMTjx>o_aM;t>Ke5EI*s&S0tErb1bY$lhscDgEE9e$zBB(8B
zwP-zPR}>6nm*gpFkZ7!_`KZHbAvBtF?leNOKFMcH8E=^1RHgn*CrQfAGDurWzN7bM
zxE{ldpfZy=i~m9vCHy9^cwP1}shp~k0T50j1ub~!Wul=dp)cWsux)t{`7ko3GHE`2
z(TQ-ed}97Y<<99Yd~QhuQeso0`bxpV%tH5``#qgK<$?dg;j7Z3H!^4mJ|uA>KEJG{
zCOV785~|CJrW&VGr`}IJpPHItniQBiDr=W}%n{2I8y^4a^!4>1Z;~?w7_ed8x1(rI
zt6geVs%ar=;bS3XVYz)j(vZ<!|4!FT_eR%2*I74QS4lTicdWk60$k@^uUT(jFHu(y
z{cYk~M_0FMd9?Lmb6_)l(`$!hkYUhztTF49-;ycq4O3=i)_x{&vVP)Xx-ZkE_u(9P
z9_-gY9si9SMc}RP&!4rn{iX5mNh1?nNwj$#yakWF`c5h~mix)cjD_2>`rptcTW0Pg
zr@hgB>&bUyw35}5;}_u<<yYiab60%Fah>+-XdAfcn3pd=;UWGQ)*lsyU8Zax2y!ud
z=&^WKBKlFZrsF-J4f=%r$)tEqv(tj2QT3y0&G>t^wqp%b+jZG>zx8doA-M&)*5pGv
zY~@^G?~~1(<Qf$R{0aOhd_o*=JQ`eF{7?9CxUCEee7#Qczfw*Zj?@{-hLjEY&ex_V
z%a^B^%OEOQc^4u%0Zlh<V?PF`DlOMIvMp0a=d+Q7l6^}bM$S1nEN5eCd+&OQdX;-8
zdSCQ@?d|Rb#st1qmA9gSr^%&}qoJW8rMXmCNxfr%Fr>14qDQ0u%KV<;fGLOZUL%5W
zgGC)Epn<39QCL-O_vI(#n+0EqLDkZSrOG>5Z(VQ8Yp?5DWYx}B1v;kcn0n4tg#~8$
z-@SVq<jqQCON>fnAZ8F}h|JdgP{~j$<7dWj#{G)Kilquy&F7k9m2H~Xl~NVw6+)HC
zl?s(L8VTC%HP5)RxrVqrxYoJ6x#GAaxPZ3Ywm)oXY~pPFZ4+j-W-exxX96m^r%&dr
zXB($`XL72WHF&fxs~s+x7j?tyxh*Be$V7Y}ey)8>v*g+Tu}`z7wCBG+wQsw3yH~b9
zH?}^Clrtr?6Ttez;78-AE5_J+P5_Y?(KHbWu^CY`(E?E)(F*s^TuXr(uayhGtLwW{
zr5(q-K>vr!7o{k5_{=~!<AK(3r}f!ze0*-=AH+1oO2qfX9K>VXD^`9hSFKS?)U)PG
zn~Nzc8dKA&XCJMLGUgR+x$Q0-gcl5!m*xbg?mmT&Q!bc|yL_Q}Us6$018Lf_Wh&78
zIFmoq%=P|L)2ABu((`?(?@|j=-7&)W2}V_o^MNNndL8XIxof#MxZ6L+es20qY+r9L
z!a8C2rp4TY;aKgW#5K*q(8t@lIj7dJbh%Eb_}<GGe0y*aej!3yMH)@&L7GGwPKqzk
z$N!c!bl4zmhE>2YzS+R<dT2`~ZLT`M2GL-m@w+$8mi$)g3KNGmH@{qtSq^Q^Lym6F
zc}@erv4?{v$bHcLn_IYN&aTY?(ShC`{PNQB-ZJYl(sIeN;c`N=h;gqmZljIY%#Kap
zOuaTjb=#MwneKV2Iq)*}iu(A}0>^Cq63XSTGn<R1bK1+dmltQsmkwu1x70U^x3IS^
zx2$K&hx$JkTl<`q&NHVzUD?viHB}3>U6kaFtL*a}ULK7dI_{G11uj4C@GrWj?#It<
zRO1^TwFrCP52JOVEud`>KjU$6$ZiMuflvAlVfup>f=)%dM89>+1k*hy<{9Lnam-vr
zZOrhjzuw;cTB3i<vZBAxAm~uCQqkysKfCn)y;!N(wODiKZl`)^8Uh`>2-<q2l|-M!
zGl@otI0-5V$H-2$u@AE!Zk>jl&X3R68`lpO6&r9J7FXjN{kJ`K4*DDBWMrwZsJ5vn
zsIIBrQ(a06FNrN<wiE^S?<jBQxOzq$F|T}U9(LE>s9s!7X`b<{S<Pu~78+u&fIbaz
z27Gio+uJ?cy#(EWGC*0i1GVblw0_#Ywk4{S<S&*1z_XPoU;9h4CH>}vR-n)7RlwRk
z_%iD#VI}Yp=3MkRxbt~C!Z!?7!7rz4%e;c4BGm$iLb-e^Y{G&M+Z0{{x2ih@bKPf=
z=NOS>tinryJ*Tp_NjpVF38TV9ejksm97BA?ZM$5*@AiC)>Wm7HYL&~CJCLgxJR7_j
zEEwz^yzg&Ga3>=V$UeDR+E`3jRTfkCQ}<C9R@YVEDLYhT5fT%`^fNo|d6YX$E~Nvj
z2rGh1rAp(<LQ79efyyH354@pyn0dA7NLhS*Qy>n$`iGQX8r$Vxr*_MFRhsig1@Et8
zPcpu9^jk)aG3K6&_@3D9@ZGwe?;MUz<>vVdpZPiCjr{t$_w{CUdxZOI{MYIc(;X{U
z3)ceIa92{-WLIP3@zz9t(@V+=sUI;b_+!R*K7$u(2PKQc(`lB*=&gRI$UeP~Lu;vz
zmEamZV<E3|=Ug@*pCcDJU!X;s@57I~ZQJcR#u`JTvX*Lpp(`KfXL@VKY&yrrylvx6
z(A&3n(ntDMt9pWZou>I_-sY=Yv|Hjho?OStRnC^i)9rixGyRDM3&-x2TtDBfgX9L0
z6;P{sz|OD3f#wCz(4!URPK6K!X$3I_y0;5yv}xRL%b^eY@xyY%Gs8S-_$v}eFII@#
zF#Pf!A0}_3E{VzCk>d#S2FO0Bp03YE_TqX;IrUo6c$JuxfFU+pw~QMVzbb}phW5VA
ztk0BiO=s?Ae$%^JDF1Z$smykf{F|_i-`QRIb?wQ<eE%nvFcq~tJCWW1#z(jNsk^e)
zF~2-CVFbU?$EAnslbs`Df_C~q=leGoTT5vI#w#`8x!$?=vqf_Xb2abV4OZ`mZZ#im
zJLc|c4vfhI@7p>*7SyeOc6KW=1-pUIq0gGrJd2LbtBmbve)?DF{`hwezl6?wCoZdx
z#@ZflI!`2SBgYb!$ngUOZ)#7H*UKhV=JKpXhyzyc8=K_jQ&v7MD$Z9ew0yd@Kxw3@
zgS6o`YPQTYoSWHtW;rgJ2v{{cHM{y+_}=381K%49x5wYx+;Uy-FFKa4R<sKRSUx!1
z+^=2kkTyZ@b2~ZHIj#bpo?4GD9|!K7uaeKoF2;`Yw%X_0u>$QM`X1lk`0gJq8cws?
z&$;keH27Wi9d?nmK*OAE_>J04bL~9s7|>g%(q;E$%;oKc@xjsK?NN^y#e=s&`il;V
z7cenMFclTgUZTSUeSZjqVZ#?${htbHo@y(eYC?E9dH(@I2R&7T{Iya<#of)x-A(Ry
z-3GMCLq<d07+PT>1_J(Z0$R~wZv9k?C1(yT?69`5hPcqXLTf$%a#9l9AU<wx7Isz;
z!&8No+aCj<bt)Fn8Y%$jsp3QJuO%RV!N32mQBi`pnVOrrnf|@FM@LEVZ-pV8e4M=h
zz`YBKcsZGZZ6R&|Gl-?NgAnCWb2}x#+FXcIn+y6}=p+HLvX=F6fvEc^Yk+-h!F=YF
zBEramUi@D6PWI4Q0ABWX4zB!OLX@UZC_aQA+J5Q=QVJsj1YOK6_|+t(|D=H)2~k?P
zxjFFzfu5e8tezaKjxLr!5FZ~Okc}P4&dvg*U~%<!a5ME{ad4%2V(|x$zZC*Obp?3h
zX=>)^?j}S@`6pv1a|_caiT}de)Y{tqH}|J=KnGW#wfS$6pbY<@=7(0>xCwIz0{?ml
zo$v4G!Hy0VjxP454q%9|_}|I?+WRCO@JXD%i1+_O<iE@7;L7@2cdTGX`zLXLAXbjQ
z2+I#OySY0U;vx*K-gAMdSXe+yg8)3NY`=y2$Jzfc;y{i0Ka1mK>I4;s?Qh~h>G&m}
zuaAJ%7HT*;+6imBSVLbhAqf1h{l69Y_fCi;^!*dU>}>38EFcgIh*tx|#m~;h&&kWg
z2I6OfuH(N@{x2)}L)lP&g8Cou?+*B+@&7Zn|19MHZ+85TMgC)vf0G9O<68eF#s14|
z|6`GVS#|%Wf&a0{ze%zGGTZ-H<X=|ZziHrqEb?zs?7z(RKNk6yRrhZi_#cb>n-u#m
zv;9BEBL9e=LGg@2l%7w~vHvMz_!pq23KTQv4Gmx+16A!z!4P|B@DC_%=Loj_E9&fT
z5yrov&i(+8N!dZ{p|DCQl+4t@+|<P!$^!5^7zk}x0{%d}0mT5IKjCr;PypILVcGry
z75g10{AaF`j$rpEX`Z-(e!oETuWS|ce<RBNnc;7;zs5ZO;HcpOfv7n;K5_aDlzTb`
zu>S$s1FAXNL6l5?1K<8!22d3WB>i*pzc-$=0aVccy}W;(^f#kFCslU0chyH`1Ly<(
zCY|A50;B&mBKw5hb4C7dfIVd3-w}Jz$m-uAd(c^d3fAVX`hRHl=}^Ma{Rueumnqo)
zE2vTe8h5pGw1lo4I@ccvL1dtc6U0Fb{1oif|7|4zP)An};NWg&_s2grDDQt(-QO*s
zXzE}|4{>18RA+$7Y3gDt2MtU@-QuYUG5-@D2%X_C!Dd(e-?3;lL+HBx2m}K_zj3#J
zmHRJn$v;DSg3?mb(rj#8JZ#V^CpI>2KIonkx`)CtdDx-G_*)?IPdZMCKWMn2w7==0
zbez!f+}wYDpyRnYpdV;Il!T1~%9mXVx@Uv(`aLC79Z#V_dP!(Fkev+#0`Y>l*x7!^
z1?kw>=otPYu9pkM0%~S<4rC~&KQ{n37Z(Q?zyk1R9}gD?)ZkAWz~TQY+=_7<hGF2I
zS9r->iIzwS=we$U&?!S+P!s`D6h(o=UGn;zf@Ux^bR~{QiN51tfzOz8%?u_+(c;KQ
zhSD}T@<HGN6HCtGa4WHVXhN{Lh;eroPF-CzlR7>LJ)eXGF>TK0gtN`{OwrawtjqNr
zu=Ty=!&Y~Iu{Jf(7qWchg&`<|%RL{t1SLLk>*XB}8ke?Pd;Gj%M2wgYKkM)?7506b
z)Cc-o(|s#&`Rh0lg~C4S#JPxi2#hpVu65C6obhw`Ur2K@=FM_GeSBSizlWDuDCw-V
kPGfa=d?`gW`dlS1xI->&7F}1o7(8T1_&&7T@9RN#cXxA{fdBvi

literal 0
HcmV?d00001

diff --git a/src/documents/tests/samples/simple.zip b/src/documents/tests/samples/simple.zip
new file mode 100644
index 0000000000000000000000000000000000000000..e96270508e35b295d4d833c33e3582e84472ab25
GIT binary patch
literal 17396
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
G3=9C@cO}gL

literal 0
HcmV?d00001

diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py
index b0318d2b3..c7e31e280 100644
--- a/src/documents/tests/test_api.py
+++ b/src/documents/tests/test_api.py
@@ -5,6 +5,7 @@ from unittest import mock
 
 from django.contrib.auth.models import User
 from django.test import override_settings
+from pathvalidate import ValidationError
 from rest_framework.test import APITestCase
 
 from documents.models import Document, Correspondent, DocumentType, Tag
@@ -215,3 +216,41 @@ class DocumentApiTest(APITestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.data['documents_total'], 3)
         self.assertEqual(response.data['documents_inbox'], 1)
+
+    @mock.patch("documents.forms.async_task")
+    def test_upload(self, m):
+
+        with open(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), "rb") as f:
+            response = self.client.post("/api/documents/post_document/", {"document": f})
+
+        self.assertEqual(response.status_code, 200)
+
+        m.assert_called_once()
+
+        self.assertEqual(m.call_args.kwargs['override_filename'], "simple.pdf")
+
+    @mock.patch("documents.forms.async_task")
+    def test_upload_invalid_form(self, m):
+
+        with open(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), "rb") as f:
+            response = self.client.post("/api/documents/post_document/", {"documenst": f})
+        self.assertEqual(response.status_code, 400)
+        m.assert_not_called()
+
+    @mock.patch("documents.forms.async_task")
+    def test_upload_invalid_file(self, m):
+
+        with open(os.path.join(os.path.dirname(__file__), "samples", "simple.zip"), "rb") as f:
+            response = self.client.post("/api/documents/post_document/", {"document": f})
+        self.assertEqual(response.status_code, 400)
+        m.assert_not_called()
+
+    @mock.patch("documents.forms.async_task")
+    @mock.patch("documents.forms.validate_filename")
+    def test_upload_invalid_filename(self, validate_filename, async_task):
+        validate_filename.side_effect = ValidationError()
+        with open(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), "rb") as f:
+            response = self.client.post("/api/documents/post_document/", {"document": f})
+        self.assertEqual(response.status_code, 400)
+
+        async_task.assert_not_called()

From 75390693b900f51ffd53283f2c5a07b0e58f668a Mon Sep 17 00:00:00 2001
From: jonaswinkler <jonas.winkler@jpwinkler.de>
Date: Thu, 26 Nov 2020 17:41:50 +0100
Subject: [PATCH 3/3] Apparently there was a very good reason to use inotify.
 fixes #46 complete with test cases for inotify and polling.

---
 Pipfile                                       |   1 +
 Pipfile.lock                                  |  22 +-
 .../management/commands/document_consumer.py  | 124 ++++++++----
 .../tests/test_management_consumer.py         | 188 ++++++++++++++++++
 4 files changed, 293 insertions(+), 42 deletions(-)
 create mode 100644 src/documents/tests/test_management_consumer.py

diff --git a/Pipfile b/Pipfile
index ad60e0905..a6169a2ba 100644
--- a/Pipfile
+++ b/Pipfile
@@ -35,6 +35,7 @@ scikit-learn="~=0.23.2"
 whitenoise = "~=5.2.0"
 watchdog = "*"
 whoosh="~=2.7.4"
+inotify-simple = "*"
 
 [dev-packages]
 coveralls = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 6ecca3c34..b10c414ed 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "ae2643b9cf0cf5741ae149fb6bc0c480de41329ce48e773eb4b5d760bc5e2244"
+            "sha256": "e9792119f687757dd388e73827ddd4216910327d5b65a8b950d4b202679c36eb"
         },
         "pipfile-spec": 6,
         "requires": {},
@@ -129,6 +129,14 @@
             "index": "pypi",
             "version": "==0.32.0"
         },
+        "inotify-simple": {
+            "hashes": [
+                "sha256:8440ffe49c4ae81a8df57c1ae1eb4b6bfa7acb830099bfb3e305b383005cc128",
+                "sha256:854f9ac752cc1fcff6ca34e9d3d875c9a94c9b7d6eb377f63be2d481a566c6ee"
+            ],
+            "index": "pypi",
+            "version": "==1.3.5"
+        },
         "joblib": {
             "hashes": [
                 "sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72",
@@ -663,11 +671,11 @@
         },
         "faker": {
             "hashes": [
-                "sha256:3f5d379e4b5ce92a8afe3c2ce59d7c43886370dd3bf9495a936b91888debfc81",
-                "sha256:8c0e8a06acef4b9312902e2ce18becabe62badd3a6632180bd0680c6ee111473"
+                "sha256:5398268e1d751ffdb3ed36b8a790ed98659200599b368eec38a02eed15bce997",
+                "sha256:d4183b8f57316de3be27cd6c3b40e9f9343d27c95c96179f027316c58c2c239e"
             ],
             "markers": "python_version >= '3.5'",
-            "version": "==4.17.0"
+            "version": "==4.17.1"
         },
         "filelock": {
             "hashes": [
@@ -999,11 +1007,11 @@
         },
         "virtualenv": {
             "hashes": [
-                "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2",
-                "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380"
+                "sha256:07cff122e9d343140366055f31be4dcd61fd598c69d11cd33a9d9c8df4546dd7",
+                "sha256:e0aac7525e880a429764cefd3aaaff54afb5d9f25c82627563603f5d7de5a6e5"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==20.1.0"
+            "version": "==20.2.1"
         }
     }
 }
diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py
index 05711ebd8..4bfd78e8f 100644
--- a/src/documents/management/commands/document_consumer.py
+++ b/src/documents/management/commands/document_consumer.py
@@ -1,11 +1,11 @@
 import logging
 import os
+from time import sleep
 
 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django_q.tasks import async_task
 from watchdog.events import FileSystemEventHandler
-from watchdog.observers import Observer
 from watchdog.observers.polling import PollingObserver
 
 try:
@@ -13,25 +13,54 @@ try:
 except ImportError:
     INotify = flags = None
 
+logger = logging.getLogger(__name__)
+
+
+def _consume(file):
+    try:
+        if os.path.isfile(file):
+            async_task("documents.tasks.consume_file",
+                       file,
+                       task_name=os.path.basename(file)[:100])
+        else:
+            logger.debug(
+                f"Not consuming file {file}: File has moved.")
+
+    except Exception as e:
+        # Catch all so that the consumer won't crash.
+        # This is also what the test case is listening for to check for
+        # errors.
+        logger.error(
+            "Error while consuming document: {}".format(e))
+
+
+def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
+    mtime = -1
+    current_try = 0
+    while current_try < num_tries:
+        try:
+            new_mtime = os.stat(file).st_mtime
+        except FileNotFoundError:
+            logger.debug(f"File {file} moved while waiting for it to remain "
+                         f"unmodified.")
+            return
+        if new_mtime == mtime:
+            _consume(file)
+            return
+        mtime = new_mtime
+        sleep(wait_time)
+        current_try += 1
+
+    logger.error(f"Timeout while waiting on file {file} to remain unmodified.")
+
 
 class Handler(FileSystemEventHandler):
 
-    def _consume(self, file):
-        if os.path.isfile(file):
-            try:
-                async_task("documents.tasks.consume_file",
-                           file,
-                           task_name=os.path.basename(file)[:100])
-            except Exception as e:
-                # Catch all so that the consumer won't crash.
-                logging.getLogger(__name__).error(
-                    "Error while consuming document: {}".format(e))
-
     def on_created(self, event):
-        self._consume(event.src_path)
+        _consume_wait_unmodified(event.src_path)
 
     def on_moved(self, event):
-        self._consume(event.src_path)
+        _consume_wait_unmodified(event.dest_path)
 
 
 class Command(BaseCommand):
@@ -40,12 +69,15 @@ class Command(BaseCommand):
     consumption directory.
     """
 
+    # This is here primarily for the tests and is irrelevant in production.
+    stop_flag = False
+
     def __init__(self, *args, **kwargs):
 
-        self.verbosity = 0
         self.logger = logging.getLogger(__name__)
 
         BaseCommand.__init__(self, *args, **kwargs)
+        self.observer = None
 
     def add_arguments(self, parser):
         parser.add_argument(
@@ -54,38 +86,60 @@ class Command(BaseCommand):
             nargs="?",
             help="The consumption directory."
         )
+        parser.add_argument(
+            "--oneshot",
+            action="store_true",
+            help="Run only once."
+        )
 
     def handle(self, *args, **options):
-
-        self.verbosity = options["verbosity"]
         directory = options["directory"]
 
         logging.getLogger(__name__).info(
-            "Starting document consumer at {}".format(
-                directory
-            )
-        )
+            f"Starting document consumer at {directory}")
 
-        # Consume all files as this is not done initially by the watchdog
         for entry in os.scandir(directory):
             if entry.is_file():
                 async_task("documents.tasks.consume_file",
                            entry.path,
                            task_name=os.path.basename(entry.path)[:100])
 
-        # Start the watchdog. Woof!
-        if settings.CONSUMER_POLLING > 0:
-            logging.getLogger(__name__).info(
-                "Using polling instead of file system notifications.")
-            observer = PollingObserver(timeout=settings.CONSUMER_POLLING)
+        if options["oneshot"]:
+            return
+
+        if settings.CONSUMER_POLLING == 0 and INotify:
+            self.handle_inotify(directory)
         else:
-            observer = Observer()
-        event_handler = Handler()
-        observer.schedule(event_handler, directory, recursive=True)
-        observer.start()
+            self.handle_polling(directory)
+
+        logger.debug("Consumer exiting.")
+
+    def handle_polling(self, directory):
+        logging.getLogger(__name__).info(
+            f"Polling directory for changes: {directory}")
+        self.observer = PollingObserver(timeout=settings.CONSUMER_POLLING)
+        self.observer.schedule(Handler(), directory, recursive=False)
+        self.observer.start()
         try:
-            while observer.is_alive():
-                observer.join(1)
+            while self.observer.is_alive():
+                self.observer.join(1)
+                if self.stop_flag:
+                    self.observer.stop()
         except KeyboardInterrupt:
-            observer.stop()
-        observer.join()
+            self.observer.stop()
+        self.observer.join()
+
+    def handle_inotify(self, directory):
+        logging.getLogger(__name__).info(
+            f"Using inotify to watch directory for changes: {directory}")
+
+        inotify = INotify()
+        inotify.add_watch(directory, flags.CLOSE_WRITE | flags.MOVED_TO)
+        try:
+            while not self.stop_flag:
+                for event in inotify.read(timeout=1000, read_delay=1000):
+                    file = os.path.join(directory, event.name)
+                    if os.path.isfile(file):
+                        _consume(file)
+        except KeyboardInterrupt:
+            pass
diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py
new file mode 100644
index 000000000..bfb7520ee
--- /dev/null
+++ b/src/documents/tests/test_management_consumer.py
@@ -0,0 +1,188 @@
+import filecmp
+import os
+import shutil
+import tempfile
+from threading import Thread
+from time import sleep
+from unittest import mock
+
+from django.conf import settings
+from django.test import TestCase, override_settings
+
+from documents.consumer import ConsumerError
+from documents.management.commands import document_consumer
+
+
+class ConsumerThread(Thread):
+
+    def __init__(self):
+        super().__init__()
+        self.cmd = document_consumer.Command()
+
+    def run(self) -> None:
+        self.cmd.handle(directory=settings.CONSUMPTION_DIR, oneshot=False)
+
+    def stop(self):
+        # Consumer checks this every second.
+        self.cmd.stop_flag = True
+
+
+def chunked(size, source):
+    for i in range(0, len(source), size):
+        yield source[i:i+size]
+
+
+class TestConsumer(TestCase):
+
+    sample_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
+
+    def setUp(self) -> None:
+        patcher = mock.patch("documents.management.commands.document_consumer.async_task")
+        self.task_mock = patcher.start()
+        self.addCleanup(patcher.stop)
+
+        self.consume_dir = tempfile.mkdtemp()
+
+        override_settings(CONSUMPTION_DIR=self.consume_dir).enable()
+
+    def t_start(self):
+        self.t = ConsumerThread()
+        self.t.start()
+        # give the consumer some time to do initial work
+        sleep(1)
+
+    def tearDown(self) -> None:
+        if self.t:
+            self.t.stop()
+
+    def wait_for_task_mock_call(self):
+        n = 0
+        while n < 100:
+            if self.task_mock.call_count > 0:
+                # give task_mock some time to finish and raise errors
+                sleep(1)
+                return
+            n += 1
+            sleep(0.1)
+        self.fail("async_task was never called")
+
+    # A bogus async_task that will simply check the file for
+    # completeness and raise an exception otherwise.
+    def bogus_task(self, func, filename, **kwargs):
+        eq = filecmp.cmp(filename, self.sample_file, shallow=False)
+        if not eq:
+            print("Consumed an INVALID file.")
+            raise ConsumerError("Incomplete File READ FAILED")
+        else:
+            print("Consumed a perfectly valid file.")
+
+    def slow_write_file(self, target, incomplete=False):
+        with open(self.sample_file, 'rb') as f:
+            pdf_bytes = f.read()
+
+        if incomplete:
+            pdf_bytes = pdf_bytes[:len(pdf_bytes) - 100]
+
+        with open(target, 'wb') as f:
+            # this will take 2 seconds, since the file is about 20k.
+            print("Start writing file.")
+            for b in chunked(1000, pdf_bytes):
+                f.write(b)
+                sleep(0.1)
+            print("file completed.")
+
+    def test_consume_file(self):
+        self.t_start()
+
+        f = os.path.join(self.consume_dir, "my_file.pdf")
+        shutil.copy(self.sample_file, f)
+
+        self.wait_for_task_mock_call()
+
+        self.task_mock.assert_called_once()
+        self.assertEqual(self.task_mock.call_args.args[1], f)
+
+    @override_settings(CONSUMER_POLLING=1)
+    def test_consume_file_polling(self):
+        self.test_consume_file()
+
+    def test_consume_existing_file(self):
+        f = os.path.join(self.consume_dir, "my_file.pdf")
+        shutil.copy(self.sample_file, f)
+
+        self.t_start()
+        self.task_mock.assert_called_once()
+        self.assertEqual(self.task_mock.call_args.args[1], f)
+
+    @override_settings(CONSUMER_POLLING=1)
+    def test_consume_existing_file_polling(self):
+        self.test_consume_existing_file()
+
+    @mock.patch("documents.management.commands.document_consumer.logger.error")
+    def test_slow_write_pdf(self, error_logger):
+
+        self.task_mock.side_effect = self.bogus_task
+
+        self.t_start()
+
+        fname = os.path.join(self.consume_dir, "my_file.pdf")
+
+        self.slow_write_file(fname)
+
+        self.wait_for_task_mock_call()
+
+        error_logger.assert_not_called()
+
+        self.task_mock.assert_called_once()
+
+        self.assertEqual(self.task_mock.call_args.args[1], fname)
+
+    @override_settings(CONSUMER_POLLING=1)
+    def test_slow_write_pdf_polling(self):
+        self.test_slow_write_pdf()
+
+    @mock.patch("documents.management.commands.document_consumer.logger.error")
+    def test_slow_write_and_move(self, error_logger):
+
+        self.task_mock.side_effect = self.bogus_task
+
+        self.t_start()
+
+        fname = os.path.join(self.consume_dir, "my_file.~df")
+        fname2 = os.path.join(self.consume_dir, "my_file.pdf")
+
+        self.slow_write_file(fname)
+        shutil.move(fname, fname2)
+
+        self.wait_for_task_mock_call()
+
+        self.task_mock.assert_called_once()
+        self.assertEqual(self.task_mock.call_args.args[1], fname2)
+
+        error_logger.assert_not_called()
+
+    @override_settings(CONSUMER_POLLING=1)
+    def test_slow_write_and_move_polling(self):
+        self.test_slow_write_and_move()
+
+    @mock.patch("documents.management.commands.document_consumer.logger.error")
+    def test_slow_write_incomplete(self, error_logger):
+
+        self.task_mock.side_effect = self.bogus_task
+
+        self.t_start()
+
+        fname = os.path.join(self.consume_dir, "my_file.pdf")
+        self.slow_write_file(fname, incomplete=True)
+
+        self.wait_for_task_mock_call()
+
+        self.task_mock.assert_called_once()
+        self.assertEqual(self.task_mock.call_args.args[1], fname)
+
+        # assert that we have an error logged with this invalid file.
+        error_logger.assert_called_once()
+
+    @override_settings(CONSUMER_POLLING=1)
+    def test_slow_write_incomplete_polling(self):
+        self.test_slow_write_incomplete()