mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Added pytest and broke up the consumer into file and mail
This commit is contained in:
		| @@ -12,10 +12,11 @@ from PIL import Image | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.utils import timezone | ||||
| from django.template.defaultfilters import slugify | ||||
|  | ||||
| from paperless.db import GnuPG | ||||
|  | ||||
| from ..models import Tag, Document | ||||
| from ..models import Sender, Tag, Document | ||||
| from ..languages import ISO639 | ||||
|  | ||||
|  | ||||
| @@ -31,6 +32,19 @@ class Consumer(object): | ||||
|     OCR = pyocr.get_available_tools()[0] | ||||
|     DEFAULT_OCR_LANGUAGE = settings.OCR_LANGUAGE | ||||
|  | ||||
|     REGEX_TITLE = re.compile( | ||||
|         r"^.*/(.*)\.(pdf|jpe?g|png|gif|tiff)$", | ||||
|         flags=re.IGNORECASE | ||||
|     ) | ||||
|     REGEX_SENDER_TITLE = re.compile( | ||||
|         r"^.*/(.*) - (.*)\.(pdf|jpe?g|png|gif|tiff)", | ||||
|         flags=re.IGNORECASE | ||||
|     ) | ||||
|     REGEX_SENDER_TITLE_TAGS = re.compile( | ||||
|         r"^.*/(.*) - (.*) - ([a-z\-,])\.(pdf|jpe?g|png|gif|tiff)", | ||||
|         flags=re.IGNORECASE | ||||
|     ) | ||||
|  | ||||
|     def __init__(self, verbosity=1): | ||||
|  | ||||
|         self.verbosity = verbosity | ||||
| @@ -105,13 +119,48 @@ class Consumer(object): | ||||
|         # Strip out excess white space to allow matching to go smoother | ||||
|         return re.sub(r"\s+", " ", r) | ||||
|  | ||||
|     def _guess_file_attributes(self, doc): | ||||
|         raise NotImplementedError( | ||||
|             "At the very least a consumer should determine the file type.") | ||||
|     def _guess_attributes_from_name(self, parseable): | ||||
|         """ | ||||
|         We use a crude naming convention to make handling the sender, title, and | ||||
|         tags easier: | ||||
|           "<sender> - <title> - <tags>.<suffix>" | ||||
|           "<sender> - <title>.<suffix>" | ||||
|           "<title>.<suffix>" | ||||
|         """ | ||||
|  | ||||
|         def get_sender(sender_name): | ||||
|             return Sender.objects.get_or_create( | ||||
|                 name=sender_name, defaults={"slug": slugify(sender_name)})[0] | ||||
|  | ||||
|         def get_tags(tags): | ||||
|             r = [] | ||||
|             for t in tags.split(","): | ||||
|                 r.append( | ||||
|                     Tag.objects.get_or_create(slug=t, defaults={"name": t})[0]) | ||||
|             return r | ||||
|  | ||||
|         # First attempt: "<sender> - <title> - <tags>.<suffix>" | ||||
|         m = re.match(self.REGEX_SENDER_TITLE_TAGS, parseable) | ||||
|         if m: | ||||
|             return ( | ||||
|                 get_sender(m.group(1)), | ||||
|                 m.group(2), | ||||
|                 get_tags(m.group(3)), | ||||
|                 m.group(4) | ||||
|             ) | ||||
|  | ||||
|         # Second attempt: "<sender> - <title>.<suffix>" | ||||
|         m = re.match(self.REGEX_SENDER_TITLE, parseable) | ||||
|         if m: | ||||
|             return get_sender(m.group(1)), m.group(2), [], m.group(3) | ||||
|  | ||||
|         # That didn't work, so we assume sender and tags are None | ||||
|         m = re.match(self.REGEX_TITLE, parseable) | ||||
|         return None, m.group(1), [], m.group(2) | ||||
|  | ||||
|     def _store(self, text, doc): | ||||
|  | ||||
|         sender, title, file_type = self._guess_file_attributes(doc) | ||||
|         sender, title, file_type = self._guess_attributes_from_name(doc) | ||||
|  | ||||
|         lower_text = text.lower() | ||||
|         relevant_tags = [t for t in Tag.objects.all() if t.matches(lower_text)] | ||||
|   | ||||
| @@ -2,10 +2,8 @@ import os | ||||
| import re | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.template.defaultfilters import slugify | ||||
|  | ||||
| from ..models import Sender | ||||
| from . import Consumer, OCRError | ||||
| from .base import Consumer, OCRError | ||||
|  | ||||
|  | ||||
| class FileConsumerError(Exception): | ||||
| @@ -16,11 +14,6 @@ class FileConsumer(Consumer): | ||||
|  | ||||
|     CONSUME = settings.CONSUMPTION_DIR | ||||
|  | ||||
|     PARSER_REGEX_TITLE = re.compile( | ||||
|         r"^.*/(.*)\.(pdf|jpe?g|png|gif|tiff)$", flags=re.IGNORECASE) | ||||
|     PARSER_REGEX_SENDER_TITLE = re.compile( | ||||
|         r"^.*/(.*) - (.*)\.(pdf|jpe?g|png|gif|tiff)", flags=re.IGNORECASE) | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|  | ||||
|         Consumer.__init__(self, *args, **kwargs) | ||||
| @@ -47,7 +40,7 @@ class FileConsumer(Consumer): | ||||
|             if not os.path.isfile(doc): | ||||
|                 continue | ||||
|  | ||||
|             if not re.match(self.PARSER_REGEX_TITLE, doc): | ||||
|             if not re.match(self.REGEX_TITLE, doc): | ||||
|                 continue | ||||
|  | ||||
|             if doc in self._ignore: | ||||
| @@ -85,22 +78,3 @@ class FileConsumer(Consumer): | ||||
|         self.stats[doc] = t | ||||
|  | ||||
|         return False | ||||
|  | ||||
|     def _guess_file_attributes(self, doc): | ||||
|         """ | ||||
|         We use a crude naming convention to make handling the sender and title | ||||
|         easier: | ||||
|           "<sender> - <title>.<suffix>" | ||||
|         """ | ||||
|  | ||||
|         # First we attempt "<sender> - <title>.<suffix>" | ||||
|         m = re.match(self.PARSER_REGEX_SENDER_TITLE, doc) | ||||
|         if m: | ||||
|             sender_name, title, file_type = m.group(1), m.group(2), m.group(3) | ||||
|             sender, __ = Sender.objects.get_or_create( | ||||
|                 name=sender_name, defaults={"slug": slugify(sender_name)}) | ||||
|             return sender, title, file_type | ||||
|  | ||||
|         # That didn't work, so we assume sender is None | ||||
|         m = re.match(self.PARSER_REGEX_TITLE, doc) | ||||
|         return None, m.group(1), m.group(2) | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| import datetime | ||||
| import email | ||||
| import imaplib | ||||
|  | ||||
| from base64 import b64decode | ||||
| from io import BytesIO | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
| from . import Consumer | ||||
| @@ -49,7 +53,8 @@ class MailConsumer(Consumer): | ||||
|     def consume(self): | ||||
|  | ||||
|         if self._enabled: | ||||
|             self.get_messages() | ||||
|             for message in self.get_messages(): | ||||
|                 pass | ||||
|  | ||||
|         self.last_checked = datetime.datetime.now() | ||||
|  | ||||
| @@ -58,12 +63,64 @@ class MailConsumer(Consumer): | ||||
|         self._connect() | ||||
|         self._login() | ||||
|  | ||||
|         for message in self._fetch(): | ||||
|             print(message)  # Now we have to do something with the attachment | ||||
|         messages = [] | ||||
|         for data in self._fetch(): | ||||
|             message = self._parse_message(data) | ||||
|             if message: | ||||
|                 messages.append(message) | ||||
|  | ||||
|         self._connection.expunge() | ||||
|         self._connection.close() | ||||
|         self._connection.logout() | ||||
|  | ||||
|     def _guess_file_attributes(self, doc): | ||||
|         return None, None, "jpg" | ||||
|         return messages | ||||
|  | ||||
|     @staticmethod | ||||
|     def _parse_message(data): | ||||
|         """ | ||||
|         Cribbed heavily from | ||||
|         https://www.ianlewis.org/en/parsing-email-attachments-python | ||||
|         """ | ||||
|  | ||||
|         r = [] | ||||
|         message = email.message_from_string(data) | ||||
|  | ||||
|         for part in message.walk(): | ||||
|  | ||||
|             content_disposition = part.get("Content-Disposition") | ||||
|             if not content_disposition: | ||||
|                 continue | ||||
|  | ||||
|             dispositions = content_disposition.strip().split(";") | ||||
|             if not dispositions[0].lower() == "attachment": | ||||
|                 continue | ||||
|  | ||||
|             file_data = part.get_payload() | ||||
|             attachment = BytesIO(b64decode(file_data)) | ||||
|             attachment.content_type = part.get_content_type() | ||||
|             attachment.size = len(file_data) | ||||
|             attachment.name = None | ||||
|             attachment.create_date = None | ||||
|             attachment.mod_date = None | ||||
|             attachment.read_date = None | ||||
|  | ||||
|             for param in dispositions[1:]: | ||||
|  | ||||
|                 name, value = param.split("=") | ||||
|                 name = name.lower() | ||||
|  | ||||
|                 if name == "filename": | ||||
|                     attachment.name = value | ||||
|                 elif name == "create-date": | ||||
|                     attachment.create_date = value | ||||
|                 elif name == "modification-date": | ||||
|                     attachment.mod_date = value | ||||
|                 elif name == "read-date": | ||||
|                     attachment.read_date = value | ||||
|  | ||||
|             r.append({ | ||||
|                 "subject": message.get("Subject"), | ||||
|                 "attachment": attachment, | ||||
|             }) | ||||
|  | ||||
|         return r | ||||
|   | ||||
| @@ -157,5 +157,9 @@ class Document(models.Model): | ||||
|     @property | ||||
|     def parseable_file_name(self): | ||||
|         if self.sender and self.title: | ||||
|             return "{} - {}.{}".format(self.sender, self.title, self.file_types) | ||||
|             tags = ",".join([t.slug for t in self.tags.all()]) | ||||
|             if tags: | ||||
|                 return "{} - {} - {}.{}".format( | ||||
|                     self.sender, self.title, tags, self.file_type) | ||||
|             return "{} - {}.{}".format(self.sender, self.title, self.file_type) | ||||
|         return os.path.basename(self.source_path) | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| from django.test import TestCase | ||||
|  | ||||
| # Create your tests here. | ||||
							
								
								
									
										0
									
								
								src/documents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/documents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/documents/tests/consumers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/documents/tests/consumers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										43
									
								
								src/documents/tests/consumers/mail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/documents/tests/consumers/mail.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import os | ||||
| import magic | ||||
|  | ||||
| from hashlib import md5 | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
|  | ||||
| from ...consumers.mail import MailConsumer | ||||
|  | ||||
|  | ||||
| class TestMailConsumer(TestCase): | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|  | ||||
|         TestCase.__init__(self, *args, **kwargs) | ||||
|         self.sample = os.path.join( | ||||
|             settings.BASE_DIR, | ||||
|             "documents", | ||||
|             "tests", | ||||
|             "consumers", | ||||
|             "samples", | ||||
|             "mail.txt" | ||||
|         ) | ||||
|  | ||||
|     def test_parse(self): | ||||
|         consumer = MailConsumer() | ||||
|         with open(self.sample) as f: | ||||
|  | ||||
|             messages = consumer._parse_message(f.read()) | ||||
|  | ||||
|             self.assertTrue(len(messages), 1) | ||||
|             self.assertEqual(messages[0]["subject"], "Test 0") | ||||
|  | ||||
|             attachment = messages[0]["attachment"] | ||||
|             data = attachment.read() | ||||
|  | ||||
|             self.assertEqual( | ||||
|                 md5(data).hexdigest(), "7c89655f9e9eb7dd8cde8568e8115d59") | ||||
|  | ||||
|             self.assertEqual(attachment.content_type, "application/pdf") | ||||
|             with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m: | ||||
|                 self.assertEqual(m.id_buffer(data), "application/pdf") | ||||
							
								
								
									
										208
									
								
								src/documents/tests/consumers/samples/mail.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								src/documents/tests/consumers/samples/mail.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| Return-Path: <sender@example.com> | ||||
| X-Original-To: sender@mailbox4.mailhost.com | ||||
| Delivered-To: sender@mailbox4.mailhost.com | ||||
| Received: from mx8.mailhost.com (mail8.mailhost.com [75.126.24.68]) | ||||
| 	by mailbox4.mailhost.com (Postfix) with ESMTP id B62BD5498001 | ||||
| 	for <sender@mailbox4.mailhost.com>; Thu,  4 Feb 2016 22:01:17 +0000 (UTC) | ||||
| Received: from localhost (localhost.localdomain [127.0.0.1]) | ||||
| 	by mx8.mailhost.com (Postfix) with ESMTP id B41796F190D | ||||
| 	for <sender@mailbox4.mailhost.com>; Thu,  4 Feb 2016 22:01:17 +0000 (UTC) | ||||
| X-Spam-Flag: NO | ||||
| X-Spam-Score: 0 | ||||
| X-Spam-Level:  | ||||
| X-Spam-Status: No, score=0 tagged_above=-999 required=3 | ||||
| 	tests=[RCVD_IN_DNSWL_NONE=-0.0001] | ||||
| Received: from mx8.mailhost.com ([127.0.0.1]) | ||||
| 	by localhost (mail8.mailhost.com [127.0.0.1]) (amavisd-new, port 10024) | ||||
| 	with ESMTP id 3cj6d28FXsS3 for <sender@mailbox4.mailhost.com>; | ||||
| 	Thu,  4 Feb 2016 22:01:17 +0000 (UTC) | ||||
| Received: from smtp.mailhost.com (smtp.mailhost.com [74.55.86.74]) | ||||
| 	by mx8.mailhost.com (Postfix) with ESMTP id 527D76F1529 | ||||
| 	for <paperless@example.com>; Thu,  4 Feb 2016 22:01:17 +0000 (UTC) | ||||
| Received: from [10.114.0.19] (nl3x.mullvad.net [46.166.136.162]) | ||||
| 	by smtp.mailhost.com (Postfix) with ESMTP id 9C52420C6FDA | ||||
| 	for <paperless@example.com>; Thu,  4 Feb 2016 22:01:16 +0000 (UTC) | ||||
| To: paperless@example.com | ||||
| From: Daniel Quinn <sender@example.com> | ||||
| Subject: Test 0 | ||||
| Message-ID: <56B3CA2A.6030806@example.com> | ||||
| Date: Thu, 4 Feb 2016 22:01:14 +0000 | ||||
| User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 | ||||
|  Thunderbird/38.5.0 | ||||
| MIME-Version: 1.0 | ||||
| Content-Type: multipart/mixed; | ||||
|  boundary="------------090701020702030809070008" | ||||
|  | ||||
| This is a multi-part message in MIME format. | ||||
| --------------090701020702030809070008 | ||||
| Content-Type: text/plain; charset=utf-8 | ||||
| Content-Transfer-Encoding: 7bit | ||||
|  | ||||
| This is the test body. | ||||
|  | ||||
| --------------090701020702030809070008 | ||||
| Content-Type: application/pdf; | ||||
|  name="test0.pdf" | ||||
| Content-Transfer-Encoding: base64 | ||||
| Content-Disposition: attachment; | ||||
|  filename="test0.pdf" | ||||
|  | ||||
| JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0 | ||||
| ZURlY29kZT4+CnN0cmVhbQp4nFWLQQvCMAyF7/kVOQutSdeuHZSA0+3gbVDwIN6c3gR38e/b | ||||
| bF4kkPfyvReyjB94IyFVF7pgG0ze4TLDZYevLamzPKEvEFqbMEZfq+WO+5GRHZbHNROLy+So | ||||
| UfFi6g7/RyusEpUl9VsQxQTlHR2oV3wUEzOdhOnXG1aw/o1yK2cYCkww4RdbUCevCmVuZHN0 | ||||
| cmVhbQplbmRvYmoKCjMgMCBvYmoKMTM5CmVuZG9iagoKNSAwIG9iago8PC9MZW5ndGggNiAw | ||||
| IFIvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aDEgMTA4MjQ+PgpzdHJlYW0KeJzlOWt0G9WZ | ||||
| 95uRbNmWLckPWY4SaRTFedmybI8T4rw8sS3ZiZ1YfqWSCbFkS7YEtiQkJSE8GlNeOQ5pUmh5 | ||||
| Zkt2l+XQNl3GhLaBpcWw0D19UGALLRRS0gM9nD0lxVBK9wCx97tXI0UJAc727L8d+c587/u9 | ||||
| 7p0rOZXYEyJaMkV4Io1OBuLOqmqBEPJLQqB0dG9K2NRTsQHhM4Rw/zkWH5+870e7PiRE9Rgh | ||||
| +Y+NT+wf+/b3e4YI0YYJKX41HAoEfxj6vUjIIgltrA0jYef8/nzEr0F8WXgydY2bP7QO8WOI | ||||
| SxOx0cDxxbUmxN9AfOlk4Jr4apWLI8SMKBGigcmQpYXrRBx9KtobjyVTQbJsgZDl91B+PBGK | ||||
| d9838hzipwjhjyIN8EMvLYJ5FOd4lTovX1NQWKQtLtGR/3eX+jCpIJ3qTURH4ux+wcWfIFXk | ||||
| XkIW3qXY+ft898LH/5deaNKPe8hD5DFymLxGrlAYbuIhEbIHKbnX0+QlpNLLQ4bId8n055g9 | ||||
| QU4hPy3nJ0doJJe8PORucpL8xwWzeMgkuQ59+QF5DRrIz7BVYuQD0JAbyXNo9QOkbb+UKa4E | ||||
| b2MMHMuhvk7u5w6RbdzbiNxLOZyT05NnyTHYjZZTGOfhbMQbP2P0NnID3vtJmOxFmF3qTZ/+ | ||||
| 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 | ||||
| l6zxXC7DQbm6n96l3iE576BMBocu984AfN13y+HDpHVJl9zY75X9S3xdchABiQJTCOiXzBhJ | ||||
| qy+ZTNWwC2pqEN6Dd1KzpwaJu5NpKsnySU0SkrhHJZkS1FCBNA54r6E8JFA9QO3dSUJvlFmT | ||||
| VqLaScUcU07fGGDa/T/LhW2oCmVuZHN0cmVhbQplbmRvYmoKCjYgMCBvYmoKNjI5MQplbmRv | ||||
| YmoKCjcgMCBvYmoKPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250TmFtZS9CQUFBQUErTGli | ||||
| ZXJhdGlvblNlcmlmCi9GbGFncyA0Ci9Gb250QkJveFstNTQzIC0zMDMgMTI3NyA5ODFdL0l0 | ||||
| YWxpY0FuZ2xlIDAKL0FzY2VudCA4OTEKL0Rlc2NlbnQgLTIxNgovQ2FwSGVpZ2h0IDk4MQov | ||||
| U3RlbVYgODAKL0ZvbnRGaWxlMiA1IDAgUgo+PgplbmRvYmoKCjggMCBvYmoKPDwvTGVuZ3Ro | ||||
| IDI5Mi9GaWx0ZXIvRmxhdGVEZWNvZGU+PgpzdHJlYW0KeJxdkctuwyAQRfd8Bct0EfmROA/J | ||||
| spQmseRFH6rbD3BgnCLVGGGy8N+XmUlbqQvQmZl7BxiSY3NqrAnJqx9VC0H2xmoP03jzCuQF | ||||
| rsaKLJfaqHCPaFdD50QSve08BRga249lKZK3WJuCn+XioMcLPIjkxWvwxl7l4uPYxri9OfcF | ||||
| A9ggU1FVUkMf+zx17rkbICHXstGxbMK8jJY/wfvsQOYUZ3wVNWqYXKfAd/YKokzTSpZ1XQmw | ||||
| +l8tK9hy6dVn56M0i9I0LdZV5Jx4s0NeMe+R18TbFXJBnKfIG9ZkyFvWUJ8d5wvkPTPlD8w1 | ||||
| 8iMz9Tyyl/Qnzp+Qz8xn5JrPPdOj7rfH5+H8f8Ym1c37ODL6JJoVTslY+P1HNzp00foG7l+O | ||||
| gwplbmRzdHJlYW0KZW5kb2JqCgo5IDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVU | ||||
| eXBlL0Jhc2VGb250L0JBQUFBQStMaWJlcmF0aW9uU2VyaWYKL0ZpcnN0Q2hhciAwCi9MYXN0 | ||||
| Q2hhciAxNQovV2lkdGhzWzc3NyA2MTAgNTAwIDI3NyAzODkgMjUwIDQ0MyAyNzcgNDQzIDUw | ||||
| MCA1MDAgNDQzIDUwMCA3NzcgNTAwIDI1MApdCi9Gb250RGVzY3JpcHRvciA3IDAgUgovVG9V | ||||
| bmljb2RlIDggMCBSCj4+CmVuZG9iagoKMTAgMCBvYmoKPDwvRjEgOSAwIFIKPj4KZW5kb2Jq | ||||
| CgoxMSAwIG9iago8PC9Gb250IDEwIDAgUgovUHJvY1NldFsvUERGL1RleHRdCj4+CmVuZG9i | ||||
| agoKMSAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDQgMCBSL1Jlc291cmNlcyAxMSAwIFIv | ||||
| TWVkaWFCb3hbMCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNl | ||||
| UkdCL0kgdHJ1ZT4+L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUv | ||||
| UGFnZXMKL1Jlc291cmNlcyAxMSAwIFIKL01lZGlhQm94WyAwIDAgNTk1IDg0MiBdCi9LaWRz | ||||
| WyAxIDAgUiBdCi9Db3VudCAxPj4KZW5kb2JqCgoxMiAwIG9iago8PC9UeXBlL0NhdGFsb2cv | ||||
| UGFnZXMgNCAwIFIKL09wZW5BY3Rpb25bMSAwIFIgL1hZWiBudWxsIG51bGwgMF0KL0xhbmco | ||||
| ZW4tR0IpCj4+CmVuZG9iagoKMTMgMCBvYmoKPDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5 | ||||
| MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAwNEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAw | ||||
| NjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzUwMDJFMDAzMD4KL0NyZWF0aW9uRGF0ZShEOjIw | ||||
| MTYwMjA0MjIwMDAyWicpPj4KZW5kb2JqCgp4cmVmCjAgMTQKMDAwMDAwMDAwMCA2NTUzNSBm | ||||
| IAowMDAwMDA3NTA5IDAwMDAwIG4gCjAwMDAwMDAwMTkgMDAwMDAgbiAKMDAwMDAwMDIyOSAw | ||||
| MDAwMCBuIAowMDAwMDA3NjUyIDAwMDAwIG4gCjAwMDAwMDAyNDkgMDAwMDAgbiAKMDAwMDAw | ||||
| NjYyNSAwMDAwMCBuIAowMDAwMDA2NjQ2IDAwMDAwIG4gCjAwMDAwMDY4NDEgMDAwMDAgbiAK | ||||
| MDAwMDAwNzIwMiAwMDAwMCBuIAowMDAwMDA3NDIyIDAwMDAwIG4gCjAwMDAwMDc0NTQgMDAw | ||||
| MDAgbiAKMDAwMDAwNzc1MSAwMDAwMCBuIAowMDAwMDA3ODQ4IDAwMDAwIG4gCnRyYWlsZXIK | ||||
| PDwvU2l6ZSAxNC9Sb290IDEyIDAgUgovSW5mbyAxMyAwIFIKL0lEIFsgPDRFN0ZCMEZCMjA4 | ||||
| ODBCNURBQkIzQTNEOTQxNDlBRTQ3Pgo8NEU3RkIwRkIyMDg4MEI1REFCQjNBM0Q5NDE0OUFF | ||||
| NDc+IF0KL0RvY0NoZWNrc3VtIC8yQTY0RDMzNzRFQTVEODMwNTRDNEI2RDFEMUY4QzU1RQo+ | ||||
| PgpzdGFydHhyZWYKODAxOAolJUVPRgo= | ||||
| --------------090701020702030809070008-- | ||||
							
								
								
									
										1
									
								
								src/documents/tests/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/documents/tests/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| from .consumers.mail import TestMailConsumer | ||||
		Reference in New Issue
	
	Block a user
	 Daniel Quinn
					Daniel Quinn