Browse Source

added post_office and crontab

master
Esther Kleinhenz 6 years ago
parent
commit
38a25437b6
70 changed files with 6526 additions and 8 deletions
  1. 15
    8
      application/views.py
  2. 10
    0
      mysite/settings.py
  3. 654
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/DESCRIPTION.rst
  4. 1
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/INSTALLER
  5. 685
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/METADATA
  6. 95
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/RECORD
  7. 6
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/WHEEL
  8. 1
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/metadata.json
  9. 1
    0
      thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/top_level.txt
  10. 121
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/DESCRIPTION.rst
  11. 1
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/INSTALLER
  12. 143
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/METADATA
  13. 19
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/RECORD
  14. 6
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/WHEEL
  15. 1
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/metadata.json
  16. 1
    0
      thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/top_level.txt
  17. 1
    0
      thesisenv/lib/python3.6/site-packages/jsonfield/__init__.py
  18. 58
    0
      thesisenv/lib/python3.6/site-packages/jsonfield/encoder.py
  19. 183
    0
      thesisenv/lib/python3.6/site-packages/jsonfield/fields.py
  20. 1
    0
      thesisenv/lib/python3.6/site-packages/jsonfield/models.py
  21. 62
    0
      thesisenv/lib/python3.6/site-packages/jsonfield/subclassing.py
  22. 392
    0
      thesisenv/lib/python3.6/site-packages/jsonfield/tests.py
  23. 5
    0
      thesisenv/lib/python3.6/site-packages/post_office/__init__.py
  24. 153
    0
      thesisenv/lib/python3.6/site-packages/post_office/admin.py
  25. 7
    0
      thesisenv/lib/python3.6/site-packages/post_office/apps.py
  26. 56
    0
      thesisenv/lib/python3.6/site-packages/post_office/backends.py
  27. 26
    0
      thesisenv/lib/python3.6/site-packages/post_office/cache.py
  28. 46
    0
      thesisenv/lib/python3.6/site-packages/post_office/compat.py
  29. 44
    0
      thesisenv/lib/python3.6/site-packages/post_office/connections.py
  30. 58
    0
      thesisenv/lib/python3.6/site-packages/post_office/fields.py
  31. BIN
      thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.mo
  32. 130
    0
      thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.po
  33. BIN
      thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.mo
  34. 130
    0
      thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.po
  35. BIN
      thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.mo
  36. 206
    0
      thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.po
  37. BIN
      thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.mo
  38. 211
    0
      thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.po
  39. 148
    0
      thesisenv/lib/python3.6/site-packages/post_office/lockfile.py
  40. 37
    0
      thesisenv/lib/python3.6/site-packages/post_office/logutils.py
  41. 305
    0
      thesisenv/lib/python3.6/site-packages/post_office/mail.py
  42. 0
    0
      thesisenv/lib/python3.6/site-packages/post_office/management/__init__.py
  43. 0
    0
      thesisenv/lib/python3.6/site-packages/post_office/management/commands/__init__.py
  44. 35
    0
      thesisenv/lib/python3.6/site-packages/post_office/management/commands/cleanup_mail.py
  45. 60
    0
      thesisenv/lib/python3.6/site-packages/post_office/management/commands/send_queued_mail.py
  46. 93
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0001_initial.py
  47. 89
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0002_add_i18n_and_backend_alias.py
  48. 25
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0003_longer_subject.py
  49. 120
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0004_auto_20160607_0901.py
  50. 19
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0005_auto_20170515_0013.py
  51. 19
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0006_attachment_mimetype.py
  52. 19
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/0007_auto_20170731_1342.py
  53. 0
    0
      thesisenv/lib/python3.6/site-packages/post_office/migrations/__init__.py
  54. 284
    0
      thesisenv/lib/python3.6/site-packages/post_office/models.py
  55. 95
    0
      thesisenv/lib/python3.6/site-packages/post_office/settings.py
  56. 84
    0
      thesisenv/lib/python3.6/site-packages/post_office/test_settings.py
  57. 6
    0
      thesisenv/lib/python3.6/site-packages/post_office/test_urls.py
  58. 8
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/__init__.py
  59. 113
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_backends.py
  60. 41
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_cache.py
  61. 150
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_commands.py
  62. 13
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_connections.py
  63. 75
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_lockfile.py
  64. 400
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_mail.py
  65. 332
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_models.py
  66. 203
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_utils.py
  67. 35
    0
      thesisenv/lib/python3.6/site-packages/post_office/tests/test_views.py
  68. 138
    0
      thesisenv/lib/python3.6/site-packages/post_office/utils.py
  69. 50
    0
      thesisenv/lib/python3.6/site-packages/post_office/validators.py
  70. 1
    0
      thesisenv/lib/python3.6/site-packages/post_office/views.py

+ 15
- 8
application/views.py View File

from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.db.models import Q
import sys import sys
import collections import collections
from taggit_templatetags2.views import TagCanvasListView from taggit_templatetags2.views import TagCanvasListView
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib import messages from django.contrib import messages
""" from post_office import mail
"""
from post_office.models import EmailTemplate
from post_office import mail





import logging import logging
def tag_cloud(request): def tag_cloud(request):
return render(request, 'tag_cloud.html', {}) return render(request, 'tag_cloud.html', {})


""" mail.send(

EmailTemplate.objects.create(
name='weekly-update',
subject='Hi' + User.objects.get(username=request.user) + '!',
content='How are you feeling today?' + Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date'),
html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?',
)

mail.send(
'kleinhenz.e@gmail.com', # List of email addresses also accepted 'kleinhenz.e@gmail.com', # List of email addresses also accepted
'esther.kleinhenz@web.de', 'esther.kleinhenz@web.de',
subject='My email',
message='Hi there!',
html_message='Hi <strong>there</strong>!',
) """
template='weekly-update',
context={'name': 'alice'},
)

+ 10
- 0
mysite/settings.py View File

'taggit', 'taggit',
'taggit_templatetags2', 'taggit_templatetags2',
'kombu.transport.django', 'kombu.transport.django',
'post_office',
] ]


MIDDLEWARE = [ MIDDLEWARE = [
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
'INTERCEPT_REDIRECTS': False, 'INTERCEPT_REDIRECTS': False,
} }

EMAIL_BACKEND = 'post_office.EmailBackend'

EMAIL_HOST = 'smtp.web.de'
EMAIL_HOST_USER = "esther.kleinhenz@web.de"
EMAIL_PORT = 25 # default smtp port
EMAIL_HOST_PASSWORD = "2mSchneeinMikkeli"
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'your.generic.test.email@web.de'

+ 654
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/DESCRIPTION.rst View File

==================
Django Post Office
==================

Django Post Office is a simple app to send and manage your emails in Django.
Some awesome features are:

* Allows you to send email asynchronously
* Multi backend support
* Supports HTML email
* Supports database based email templates
* Built in scheduling support
* Works well with task queues like `RQ <http://python-rq.org>`_ or `Celery <http://www.celeryproject.org>`_
* Uses multiprocessing (and threading) to send a large number of emails in parallel
* Supports multilingual email templates (i18n)


Dependencies
============

* `django >= 1.8 <http://djangoproject.com/>`_
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield>`_


Installation
============

|Build Status|


* Install from PyPI (or you `manually download from PyPI <http://pypi.python.org/pypi/django-post_office>`_)::

pip install django-post_office

* Add ``post_office`` to your INSTALLED_APPS in django's ``settings.py``:

.. code-block:: python

INSTALLED_APPS = (
# other apps
"post_office",
)

* Run ``migrate``::

python manage.py migrate

* Set ``post_office.EmailBackend`` as your ``EMAIL_BACKEND`` in django's ``settings.py``:

.. code-block:: python

EMAIL_BACKEND = 'post_office.EmailBackend'


Quickstart
==========

Send a simple email is really easy:

.. code-block:: python

from post_office import mail

mail.send(
'recipient@example.com', # List of email addresses also accepted
'from@example.com',
subject='My email',
message='Hi there!',
html_message='Hi <strong>there</strong>!',
)


If you want to use templates, ensure that Django's admin interface is enabled. Create an
``EmailTemplate`` instance via ``admin`` and do the following:

.. code-block:: python

from post_office import mail

mail.send(
'recipient@example.com', # List of email addresses also accepted
'from@example.com',
template='welcome_email', # Could be an EmailTemplate instance or name
context={'foo': 'bar'},
)

The above command will put your email on the queue so you can use the
command in your webapp without slowing down the request/response cycle too much.
To actually send them out, run ``python manage.py send_queued_mail``.
You can schedule this management command to run regularly via cron::

* * * * * (/usr/bin/python manage.py send_queued_mail >> send_mail.log 2>&1)

or, if you use uWSGI_ as application server, add this short snipped to the
project's ``wsgi.py`` file:

.. code-block:: python

from django.core.wsgi import get_wsgi_application

application = get_wsgi_application()

# add this block of code
try:
import uwsgidecorators
from django.core.management import call_command

@uwsgidecorators.timer(10)
def send_queued_mail(num):
"""Send queued mail every 10 seconds"""
call_command('send_queued_mail', processes=1)

except ImportError:
print("uwsgidecorators not found. Cron and timers are disabled")

Alternatively you can also use the decorator ``@uwsgidecorators.cron(minute, hour, day, month, weekday)``.
This will schedule a task at specific times. Use ``-1`` to signal any time, it corresponds to the ``*``
in cron.

Please note that ``uwsgidecorators`` are available only, if the application has been started
with **uWSGI**. However, Django's internal ``./manange.py runserver`` also access this file,
therefore wrap the block into an exception handler as shown above.

This configuration is very useful in environments, such as Docker containers, where you
don't have a running cron-daemon.


Usage
=====

mail.send()
-----------

``mail.send`` is the most important function in this library, it takes these
arguments:

+--------------------+----------+--------------------------------------------------+
| Argument | Required | Description |
+--------------------+----------+--------------------------------------------------+
| recipients | Yes | list of recipient email addresses |
+--------------------+----------+--------------------------------------------------+
| sender | No | Defaults to ``settings.DEFAULT_FROM_EMAIL``, |
| | | display name is allowed (``John <john@a.com>``) |
+--------------------+----------+--------------------------------------------------+
| subject | No | Email subject (if ``template`` is not specified) |
+--------------------+----------+--------------------------------------------------+
| message | No | Email content (if ``template`` is not specified) |
+--------------------+----------+--------------------------------------------------+
| html_message | No | HTML content (if ``template`` is not specified) |
+--------------------+----------+--------------------------------------------------+
| template | No | ``EmailTemplate`` instance or name |
+--------------------+----------+--------------------------------------------------+
| language | No | Language in which you want to send the email in |
| | | (if you have multilingual email templates.) |
+--------------------+----------+--------------------------------------------------+
| cc | No | list emails, will appear in ``cc`` field |
+--------------------+----------+--------------------------------------------------+
| bcc | No | list of emails, will appear in `bcc` field |
+--------------------+----------+--------------------------------------------------+
| attachments | No | Email attachments - A dictionary where the keys |
| | | are the filenames and the values are either: |
| | | |
| | | * files |
| | | * file-like objects |
| | | * full path of the file |
+--------------------+----------+--------------------------------------------------+
| context | No | A dictionary, used to render templated email |
+--------------------+----------+--------------------------------------------------+
| headers | No | A dictionary of extra headers on the message |
+--------------------+----------+--------------------------------------------------+
| scheduled_time | No | A date/datetime object indicating when the email |
| | | should be sent |
+--------------------+----------+--------------------------------------------------+
| priority | No | ``high``, ``medium``, ``low`` or ``now`` |
| | | (send_immediately) |
+--------------------+----------+--------------------------------------------------+
| backend | No | Alias of the backend you want to use. |
| | | ``default`` will be used if not specified. |
+--------------------+----------+--------------------------------------------------+
| render_on_delivery | No | Setting this to ``True`` causes email to be |
| | | lazily rendered during delivery. ``template`` |
| | | is required when ``render_on_delivery`` is True. |
| | | This way content is never stored in the DB. |
| | | May result in significant space savings. |
+--------------------+----------+--------------------------------------------------+


Here are a few examples.

If you just want to send out emails without using database templates. You can
call the ``send`` command without the ``template`` argument.

.. code-block:: python

from post_office import mail

mail.send(
['recipient1@example.com'],
'from@example.com',
subject='Welcome!',
message='Welcome home, {{ name }}!',
html_message='Welcome home, <b>{{ name }}</b>!',
headers={'Reply-to': 'reply@example.com'},
scheduled_time=date(2014, 1, 1),
context={'name': 'Alice'},
)

``post_office`` is also task queue friendly. Passing ``now`` as priority into
``send_mail`` will deliver the email right away (instead of queuing it),
regardless of how many emails you have in your queue:

.. code-block:: python

from post_office import mail

mail.send(
['recipient1@example.com'],
'from@example.com',
template='welcome_email',
context={'foo': 'bar'},
priority='now',
)

This is useful if you already use something like `django-rq <https://github.com/ui/django-rq>`_
to send emails asynchronously and only need to store email related activities and logs.

If you want to send an email with attachments:

.. code-block:: python

from django.core.files.base import ContentFile
from post_office import mail

mail.send(
['recipient1@example.com'],
'from@example.com',
template='welcome_email',
context={'foo': 'bar'},
priority='now',
attachments={
'attachment1.doc': '/path/to/file/file1.doc',
'attachment2.txt': ContentFile('file content'),
'attachment3.txt': { 'file': ContentFile('file content'), 'mimetype': 'text/plain'},
}
)

Template Tags and Variables
---------------------------

``post-office`` supports Django's template tags and variables.
For example, if you put "Hello, {{ name }}" in the subject line and pass in
``{'name': 'Alice'}`` as context, you will get "Hello, Alice" as subject:

.. code-block:: python

from post_office.models import EmailTemplate
from post_office import mail

EmailTemplate.objects.create(
name='morning_greeting',
subject='Morning, {{ name|capfirst }}',
content='Hi {{ name }}, how are you feeling today?',
html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?',
)

mail.send(
['recipient@example.com'],
'from@example.com',
template='morning_greeting',
context={'name': 'alice'},
)

# This will create an email with the following content:
subject = 'Morning, Alice',
content = 'Hi alice, how are you feeling today?'
content = 'Hi <strong>alice</strong>, how are you feeling today?'


Multilingual Email Templates
----------------------------

You can easily create email templates in various different languanges.
For example:

.. code-block:: python

template = EmailTemplate.objects.create(
name='hello',
subject='Hello world!',
)

# Add an Indonesian version of this template:
indonesian_template = template.translated_templates.create(
language='id',
subject='Halo Dunia!'
)

Sending an email using template in a non default languange is
also similarly easy:

.. code-block:: python

mail.send(
['recipient@example.com'],
'from@example.com',
template=template, # Sends using the default template
)

mail.send(
['recipient@example.com'],
'from@example.com',
template=template,
language='id', # Sends using Indonesian template
)

Custom Email Backends
---------------------

By default, ``post_office`` uses django's ``smtp.EmailBackend``. If you want to
use a different backend, you can do so by configuring ``BACKENDS``.

For example if you want to use `django-ses <https://github.com/hmarr/django-ses>`_::

POST_OFFICE = {
'BACKENDS': {
'default': 'smtp.EmailBackend',
'ses': 'django_ses.SESBackend',
}
}

You can then choose what backend you want to use when sending mail:

.. code-block:: python

# If you omit `backend_alias` argument, `default` will be used
mail.send(
['recipient@example.com'],
'from@example.com',
subject='Hello',
)

# If you want to send using `ses` backend
mail.send(
['recipient@example.com'],
'from@example.com',
subject='Hello',
backend='ses',
)


Management Commands
-------------------

* ``send_queued_mail`` - send queued emails, those aren't successfully sent
will be marked as ``failed``. Accepts the following arguments:

+---------------------------+--------------------------------------------------+
| Argument | Description |
+---------------------------+--------------------------------------------------+
| ``--processes`` or ``-p`` | Number of parallel processes to send email. |
| | Defaults to 1 |
+---------------------------+--------------------------------------------------+
| ``--lockfile`` or ``-L`` | Full path to file used as lock file. Defaults to |
| | ``/tmp/post_office.lock`` |
+---------------------------+--------------------------------------------------+


* ``cleanup_mail`` - delete all emails created before an X number of days
(defaults to 90).

+---------------------------+--------------------------------------------------+
| Argument | Description |
+---------------------------+--------------------------------------------------+
| ``--days`` or ``-d`` | Email older than this argument will be deleted. |
| | Defaults to 90 |
+---------------------------+--------------------------------------------------+
| ``--delete-attachments`` | Flag to delete orphaned attachment records and |
| or ``-da`` | files on disk. If flag is not set, |
| | on disk attachments files won't be deleted. |
+---------------------------+--------------------------------------------------+


You may want to set these up via cron to run regularly::

* * * * * (cd $PROJECT; python manage.py send_queued_mail --processes=1 >> $PROJECT/cron_mail.log 2>&1)
0 1 * * * (cd $PROJECT; python manage.py cleanup_mail --days=30 --delete-attachments >> $PROJECT/cron_mail_cleanup.log 2>&1)

Settings
========
This section outlines all the settings and configurations that you can put
in Django's ``settings.py`` to fine tune ``post-office``'s behavior.

Batch Size
----------

If you may want to limit the number of emails sent in a batch (sometimes useful
in a low memory environment), use the ``BATCH_SIZE`` argument to limit the
number of queued emails fetched in one batch.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'BATCH_SIZE': 50
}

Default Priority
----------------

The default priority for emails is ``medium``, but this can be altered by
setting ``DEFAULT_PRIORITY``. Integration with asynchronous email backends
(e.g. based on Celery) becomes trivial when set to ``now``.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'DEFAULT_PRIORITY': 'now'
}

Log Level
---------

The default log level is 2 (logs both successful and failed deliveries)
This behavior can be changed by setting ``LOG_LEVEL``.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'LOG_LEVEL': 1 # Log only failed deliveries
}

The different options are:

* ``0`` logs nothing
* ``1`` logs only failed deliveries
* ``2`` logs everything (both successful and failed delivery attempts)


Sending Order
-------------

The default sending order for emails is ``-priority``, but this can be altered by
setting ``SENDING_ORDER``. For example, if you want to send queued emails in FIFO order :

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'SENDING_ORDER': ['created']
}

Context Field Serializer
------------------------

If you need to store complex Python objects for deferred rendering
(i.e. setting ``render_on_delivery=True``), you can specify your own context
field class to store context variables. For example if you want to use
`django-picklefield <https://github.com/gintas/django-picklefield/tree/master/src/picklefield>`_:

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'CONTEXT_FIELD_CLASS': 'picklefield.fields.PickledObjectField'
}

``CONTEXT_FIELD_CLASS`` defaults to ``jsonfield.JSONField``.

Logging
-------

You can configure ``post-office``'s logging from Django's ``settings.py``. For
example:

.. code-block:: python

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"post_office": {
"format": "[%(levelname)s]%(asctime)s PID %(process)d: %(message)s",
"datefmt": "%d-%m-%Y %H:%M:%S",
},
},
"handlers": {
"post_office": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "post_office"
},
# If you use sentry for logging
'sentry': {
'level': 'ERROR',
'class': 'raven.contrib.django.handlers.SentryHandler',
},
},
'loggers': {
"post_office": {
"handlers": ["post_office", "sentry"],
"level": "INFO"
},
},
}


Threads
-------

``post-office`` >= 3.0 allows you to use multiple threads to dramatically speed up
the speed at which emails are sent. By default, ``post-office`` uses 5 threads per process.
You can tweak this setting by changing ``THREADS_PER_PROCESS`` setting.

This may dramatically increase the speed of bulk email delivery, depending on which email
backends you use. In my tests, multi threading speeds up email backends that use HTTP based
(REST) delivery mechanisms but doesn't seem to help SMTP based backends.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'THREADS_PER_PROCESS': 10
}


Performance
===========

Caching
-------

if Django's caching mechanism is configured, ``post_office`` will cache
``EmailTemplate`` instances . If for some reason you want to disable caching,
set ``POST_OFFICE_CACHE`` to ``False`` in ``settings.py``:

.. code-block:: python

## All cache key will be prefixed by post_office:template:
## To turn OFF caching, you need to explicitly set POST_OFFICE_CACHE to False in settings
POST_OFFICE_CACHE = False

## Optional: to use a non default cache backend, add a "post_office" entry in CACHES
CACHES = {
'post_office': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '127.0.0.1:11211',
}
}


send_many()
-----------

``send_many()`` is much more performant (generates less database queries) when
sending a large number of emails. ``send_many()`` is almost identical to ``mail.send()``,
with the exception that it accepts a list of keyword arguments that you'd
usually pass into ``mail.send()``:

.. code-block:: python

from post_office import mail

first_email = {
'sender': 'from@example.com',
'recipients': ['alice@example.com'],
'subject': 'Hi!',
'message': 'Hi Alice!'
}
second_email = {
'sender': 'from@example.com',
'recipients': ['bob@example.com'],
'subject': 'Hi!',
'message': 'Hi Bob!'
}
kwargs_list = [first_email, second_email]

mail.send_many(kwargs_list)

Attachments are not supported with ``mail.send_many()``.


Running Tests
=============

To run the test suite::

`which django-admin.py` test post_office --settings=post_office.test_settings --pythonpath=.

You can run the full test suite with::

tox

or::

python setup.py test


Changelog
=========

Version 3.1.0 (2018-07-24)
--------------------------
* Improvements to attachments are handled. Thanks @SeiryuZ!
* Added ``--delete-attachments`` flag to ``cleanup_mail`` management command. Thanks @Seiryuz!
* I18n improvements. Thanks @vsevolod-skripnik and @delneg!
* Django admin improvements. Thanks @kakulukia!


Version 3.0.4
-------------
* Added compatibility with Django 2.0. Thanks @PreActionTech and @PetrDlouhy!
* Added natural key support to `EmailTemplate` model. Thanks @maximlomakin!


Version 3.0.2
-------------
- Fixed memory leak when multiprocessing is used.
- Fixed a possible error when adding a new email from Django admin. Thanks @ivlevdenis!


Version 3.0.2
-------------
- `_send_bulk` now properly catches exceptions when preparing email messages.


Version 3.0.1
-------------
- Fixed an infinite loop bug in `send_queued_mail` management command.


Version 3.0.0
-------------
* `_send_bulk` now allows each process to use multiple threads to send emails.
* Added support for mimetypes in email attachments. Thanks @clickonchris!
* An `EmailTemplate` can now be used as defaults multiple times in one language. Thanks @sac7e!
* `send_queued_mail` management command will now check whether there are more queued emails to be sent before exiting.
* Drop support for Django < 1.8. Thanks @fendyh!


Full changelog can be found `here <https://github.com/ui/django-post_office/blob/master/CHANGELOG.md>`_.


Created and maintained by the cool guys at `Stamps <https://stamps.co.id>`_,
Indonesia's most elegant CRM/loyalty platform.


.. |Build Status| image:: https://travis-ci.org/ui/django-post_office.png?branch=master
:target: https://travis-ci.org/ui/django-post_office

.. _uWSGI: https://uwsgi-docs.readthedocs.org/en/latest/



+ 1
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/INSTALLER View File

pip

+ 685
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/METADATA View File

Metadata-Version: 2.0
Name: django-post-office
Version: 3.1.0
Summary: A Django app to monitor and send mail asynchronously, complete with template support.
Home-page: https://github.com/ui/django-post_office
Author: Selwin Ong
Author-email: selwin.ong@gmail.com
License: MIT
Description-Content-Type: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Dist: django (>=1.8)
Requires-Dist: jsonfield
Provides-Extra: test
Requires-Dist: tox (>=2.3); extra == 'test'

==================
Django Post Office
==================

Django Post Office is a simple app to send and manage your emails in Django.
Some awesome features are:

* Allows you to send email asynchronously
* Multi backend support
* Supports HTML email
* Supports database based email templates
* Built in scheduling support
* Works well with task queues like `RQ <http://python-rq.org>`_ or `Celery <http://www.celeryproject.org>`_
* Uses multiprocessing (and threading) to send a large number of emails in parallel
* Supports multilingual email templates (i18n)


Dependencies
============

* `django >= 1.8 <http://djangoproject.com/>`_
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield>`_


Installation
============

|Build Status|


* Install from PyPI (or you `manually download from PyPI <http://pypi.python.org/pypi/django-post_office>`_)::

pip install django-post_office

* Add ``post_office`` to your INSTALLED_APPS in django's ``settings.py``:

.. code-block:: python

INSTALLED_APPS = (
# other apps
"post_office",
)

* Run ``migrate``::

python manage.py migrate

* Set ``post_office.EmailBackend`` as your ``EMAIL_BACKEND`` in django's ``settings.py``:

.. code-block:: python

EMAIL_BACKEND = 'post_office.EmailBackend'


Quickstart
==========

Send a simple email is really easy:

.. code-block:: python

from post_office import mail

mail.send(
'recipient@example.com', # List of email addresses also accepted
'from@example.com',
subject='My email',
message='Hi there!',
html_message='Hi <strong>there</strong>!',
)


If you want to use templates, ensure that Django's admin interface is enabled. Create an
``EmailTemplate`` instance via ``admin`` and do the following:

.. code-block:: python

from post_office import mail

mail.send(
'recipient@example.com', # List of email addresses also accepted
'from@example.com',
template='welcome_email', # Could be an EmailTemplate instance or name
context={'foo': 'bar'},
)

The above command will put your email on the queue so you can use the
command in your webapp without slowing down the request/response cycle too much.
To actually send them out, run ``python manage.py send_queued_mail``.
You can schedule this management command to run regularly via cron::

* * * * * (/usr/bin/python manage.py send_queued_mail >> send_mail.log 2>&1)

or, if you use uWSGI_ as application server, add this short snipped to the
project's ``wsgi.py`` file:

.. code-block:: python

from django.core.wsgi import get_wsgi_application

application = get_wsgi_application()

# add this block of code
try:
import uwsgidecorators
from django.core.management import call_command

@uwsgidecorators.timer(10)
def send_queued_mail(num):
"""Send queued mail every 10 seconds"""
call_command('send_queued_mail', processes=1)

except ImportError:
print("uwsgidecorators not found. Cron and timers are disabled")

Alternatively you can also use the decorator ``@uwsgidecorators.cron(minute, hour, day, month, weekday)``.
This will schedule a task at specific times. Use ``-1`` to signal any time, it corresponds to the ``*``
in cron.

Please note that ``uwsgidecorators`` are available only, if the application has been started
with **uWSGI**. However, Django's internal ``./manange.py runserver`` also access this file,
therefore wrap the block into an exception handler as shown above.

This configuration is very useful in environments, such as Docker containers, where you
don't have a running cron-daemon.


Usage
=====

mail.send()
-----------

``mail.send`` is the most important function in this library, it takes these
arguments:

+--------------------+----------+--------------------------------------------------+
| Argument | Required | Description |
+--------------------+----------+--------------------------------------------------+
| recipients | Yes | list of recipient email addresses |
+--------------------+----------+--------------------------------------------------+
| sender | No | Defaults to ``settings.DEFAULT_FROM_EMAIL``, |
| | | display name is allowed (``John <john@a.com>``) |
+--------------------+----------+--------------------------------------------------+
| subject | No | Email subject (if ``template`` is not specified) |
+--------------------+----------+--------------------------------------------------+
| message | No | Email content (if ``template`` is not specified) |
+--------------------+----------+--------------------------------------------------+
| html_message | No | HTML content (if ``template`` is not specified) |
+--------------------+----------+--------------------------------------------------+
| template | No | ``EmailTemplate`` instance or name |
+--------------------+----------+--------------------------------------------------+
| language | No | Language in which you want to send the email in |
| | | (if you have multilingual email templates.) |
+--------------------+----------+--------------------------------------------------+
| cc | No | list emails, will appear in ``cc`` field |
+--------------------+----------+--------------------------------------------------+
| bcc | No | list of emails, will appear in `bcc` field |
+--------------------+----------+--------------------------------------------------+
| attachments | No | Email attachments - A dictionary where the keys |
| | | are the filenames and the values are either: |
| | | |
| | | * files |
| | | * file-like objects |
| | | * full path of the file |
+--------------------+----------+--------------------------------------------------+
| context | No | A dictionary, used to render templated email |
+--------------------+----------+--------------------------------------------------+
| headers | No | A dictionary of extra headers on the message |
+--------------------+----------+--------------------------------------------------+
| scheduled_time | No | A date/datetime object indicating when the email |
| | | should be sent |
+--------------------+----------+--------------------------------------------------+
| priority | No | ``high``, ``medium``, ``low`` or ``now`` |
| | | (send_immediately) |
+--------------------+----------+--------------------------------------------------+
| backend | No | Alias of the backend you want to use. |
| | | ``default`` will be used if not specified. |
+--------------------+----------+--------------------------------------------------+
| render_on_delivery | No | Setting this to ``True`` causes email to be |
| | | lazily rendered during delivery. ``template`` |
| | | is required when ``render_on_delivery`` is True. |
| | | This way content is never stored in the DB. |
| | | May result in significant space savings. |
+--------------------+----------+--------------------------------------------------+


Here are a few examples.

If you just want to send out emails without using database templates. You can
call the ``send`` command without the ``template`` argument.

.. code-block:: python

from post_office import mail

mail.send(
['recipient1@example.com'],
'from@example.com',
subject='Welcome!',
message='Welcome home, {{ name }}!',
html_message='Welcome home, <b>{{ name }}</b>!',
headers={'Reply-to': 'reply@example.com'},
scheduled_time=date(2014, 1, 1),
context={'name': 'Alice'},
)

``post_office`` is also task queue friendly. Passing ``now`` as priority into
``send_mail`` will deliver the email right away (instead of queuing it),
regardless of how many emails you have in your queue:

.. code-block:: python

from post_office import mail

mail.send(
['recipient1@example.com'],
'from@example.com',
template='welcome_email',
context={'foo': 'bar'},
priority='now',
)

This is useful if you already use something like `django-rq <https://github.com/ui/django-rq>`_
to send emails asynchronously and only need to store email related activities and logs.

If you want to send an email with attachments:

.. code-block:: python

from django.core.files.base import ContentFile
from post_office import mail

mail.send(
['recipient1@example.com'],
'from@example.com',
template='welcome_email',
context={'foo': 'bar'},
priority='now',
attachments={
'attachment1.doc': '/path/to/file/file1.doc',
'attachment2.txt': ContentFile('file content'),
'attachment3.txt': { 'file': ContentFile('file content'), 'mimetype': 'text/plain'},
}
)

Template Tags and Variables
---------------------------

``post-office`` supports Django's template tags and variables.
For example, if you put "Hello, {{ name }}" in the subject line and pass in
``{'name': 'Alice'}`` as context, you will get "Hello, Alice" as subject:

.. code-block:: python

from post_office.models import EmailTemplate
from post_office import mail

EmailTemplate.objects.create(
name='morning_greeting',
subject='Morning, {{ name|capfirst }}',
content='Hi {{ name }}, how are you feeling today?',
html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?',
)

mail.send(
['recipient@example.com'],
'from@example.com',
template='morning_greeting',
context={'name': 'alice'},
)

# This will create an email with the following content:
subject = 'Morning, Alice',
content = 'Hi alice, how are you feeling today?'
content = 'Hi <strong>alice</strong>, how are you feeling today?'


Multilingual Email Templates
----------------------------

You can easily create email templates in various different languanges.
For example:

.. code-block:: python

template = EmailTemplate.objects.create(
name='hello',
subject='Hello world!',
)

# Add an Indonesian version of this template:
indonesian_template = template.translated_templates.create(
language='id',
subject='Halo Dunia!'
)

Sending an email using template in a non default languange is
also similarly easy:

.. code-block:: python

mail.send(
['recipient@example.com'],
'from@example.com',
template=template, # Sends using the default template
)

mail.send(
['recipient@example.com'],
'from@example.com',
template=template,
language='id', # Sends using Indonesian template
)

Custom Email Backends
---------------------

By default, ``post_office`` uses django's ``smtp.EmailBackend``. If you want to
use a different backend, you can do so by configuring ``BACKENDS``.

For example if you want to use `django-ses <https://github.com/hmarr/django-ses>`_::

POST_OFFICE = {
'BACKENDS': {
'default': 'smtp.EmailBackend',
'ses': 'django_ses.SESBackend',
}
}

You can then choose what backend you want to use when sending mail:

.. code-block:: python

# If you omit `backend_alias` argument, `default` will be used
mail.send(
['recipient@example.com'],
'from@example.com',
subject='Hello',
)

# If you want to send using `ses` backend
mail.send(
['recipient@example.com'],
'from@example.com',
subject='Hello',
backend='ses',
)


Management Commands
-------------------

* ``send_queued_mail`` - send queued emails, those aren't successfully sent
will be marked as ``failed``. Accepts the following arguments:

+---------------------------+--------------------------------------------------+
| Argument | Description |
+---------------------------+--------------------------------------------------+
| ``--processes`` or ``-p`` | Number of parallel processes to send email. |
| | Defaults to 1 |
+---------------------------+--------------------------------------------------+
| ``--lockfile`` or ``-L`` | Full path to file used as lock file. Defaults to |
| | ``/tmp/post_office.lock`` |
+---------------------------+--------------------------------------------------+


* ``cleanup_mail`` - delete all emails created before an X number of days
(defaults to 90).

+---------------------------+--------------------------------------------------+
| Argument | Description |
+---------------------------+--------------------------------------------------+
| ``--days`` or ``-d`` | Email older than this argument will be deleted. |
| | Defaults to 90 |
+---------------------------+--------------------------------------------------+
| ``--delete-attachments`` | Flag to delete orphaned attachment records and |
| or ``-da`` | files on disk. If flag is not set, |
| | on disk attachments files won't be deleted. |
+---------------------------+--------------------------------------------------+


You may want to set these up via cron to run regularly::

* * * * * (cd $PROJECT; python manage.py send_queued_mail --processes=1 >> $PROJECT/cron_mail.log 2>&1)
0 1 * * * (cd $PROJECT; python manage.py cleanup_mail --days=30 --delete-attachments >> $PROJECT/cron_mail_cleanup.log 2>&1)

Settings
========
This section outlines all the settings and configurations that you can put
in Django's ``settings.py`` to fine tune ``post-office``'s behavior.

Batch Size
----------

If you may want to limit the number of emails sent in a batch (sometimes useful
in a low memory environment), use the ``BATCH_SIZE`` argument to limit the
number of queued emails fetched in one batch.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'BATCH_SIZE': 50
}

Default Priority
----------------

The default priority for emails is ``medium``, but this can be altered by
setting ``DEFAULT_PRIORITY``. Integration with asynchronous email backends
(e.g. based on Celery) becomes trivial when set to ``now``.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'DEFAULT_PRIORITY': 'now'
}

Log Level
---------

The default log level is 2 (logs both successful and failed deliveries)
This behavior can be changed by setting ``LOG_LEVEL``.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'LOG_LEVEL': 1 # Log only failed deliveries
}

The different options are:

* ``0`` logs nothing
* ``1`` logs only failed deliveries
* ``2`` logs everything (both successful and failed delivery attempts)


Sending Order
-------------

The default sending order for emails is ``-priority``, but this can be altered by
setting ``SENDING_ORDER``. For example, if you want to send queued emails in FIFO order :

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'SENDING_ORDER': ['created']
}

Context Field Serializer
------------------------

If you need to store complex Python objects for deferred rendering
(i.e. setting ``render_on_delivery=True``), you can specify your own context
field class to store context variables. For example if you want to use
`django-picklefield <https://github.com/gintas/django-picklefield/tree/master/src/picklefield>`_:

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'CONTEXT_FIELD_CLASS': 'picklefield.fields.PickledObjectField'
}

``CONTEXT_FIELD_CLASS`` defaults to ``jsonfield.JSONField``.

Logging
-------

You can configure ``post-office``'s logging from Django's ``settings.py``. For
example:

.. code-block:: python

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"post_office": {
"format": "[%(levelname)s]%(asctime)s PID %(process)d: %(message)s",
"datefmt": "%d-%m-%Y %H:%M:%S",
},
},
"handlers": {
"post_office": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "post_office"
},
# If you use sentry for logging
'sentry': {
'level': 'ERROR',
'class': 'raven.contrib.django.handlers.SentryHandler',
},
},
'loggers': {
"post_office": {
"handlers": ["post_office", "sentry"],
"level": "INFO"
},
},
}


Threads
-------

``post-office`` >= 3.0 allows you to use multiple threads to dramatically speed up
the speed at which emails are sent. By default, ``post-office`` uses 5 threads per process.
You can tweak this setting by changing ``THREADS_PER_PROCESS`` setting.

This may dramatically increase the speed of bulk email delivery, depending on which email
backends you use. In my tests, multi threading speeds up email backends that use HTTP based
(REST) delivery mechanisms but doesn't seem to help SMTP based backends.

.. code-block:: python

# Put this in settings.py
POST_OFFICE = {
'THREADS_PER_PROCESS': 10
}


Performance
===========

Caching
-------

if Django's caching mechanism is configured, ``post_office`` will cache
``EmailTemplate`` instances . If for some reason you want to disable caching,
set ``POST_OFFICE_CACHE`` to ``False`` in ``settings.py``:

.. code-block:: python

## All cache key will be prefixed by post_office:template:
## To turn OFF caching, you need to explicitly set POST_OFFICE_CACHE to False in settings
POST_OFFICE_CACHE = False

## Optional: to use a non default cache backend, add a "post_office" entry in CACHES
CACHES = {
'post_office': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '127.0.0.1:11211',
}
}


send_many()
-----------

``send_many()`` is much more performant (generates less database queries) when
sending a large number of emails. ``send_many()`` is almost identical to ``mail.send()``,
with the exception that it accepts a list of keyword arguments that you'd
usually pass into ``mail.send()``:

.. code-block:: python

from post_office import mail

first_email = {
'sender': 'from@example.com',
'recipients': ['alice@example.com'],
'subject': 'Hi!',
'message': 'Hi Alice!'
}
second_email = {
'sender': 'from@example.com',
'recipients': ['bob@example.com'],
'subject': 'Hi!',
'message': 'Hi Bob!'
}
kwargs_list = [first_email, second_email]

mail.send_many(kwargs_list)

Attachments are not supported with ``mail.send_many()``.


Running Tests
=============

To run the test suite::

`which django-admin.py` test post_office --settings=post_office.test_settings --pythonpath=.

You can run the full test suite with::

tox

or::

python setup.py test


Changelog
=========

Version 3.1.0 (2018-07-24)
--------------------------
* Improvements to attachments are handled. Thanks @SeiryuZ!
* Added ``--delete-attachments`` flag to ``cleanup_mail`` management command. Thanks @Seiryuz!
* I18n improvements. Thanks @vsevolod-skripnik and @delneg!
* Django admin improvements. Thanks @kakulukia!


Version 3.0.4
-------------
* Added compatibility with Django 2.0. Thanks @PreActionTech and @PetrDlouhy!
* Added natural key support to `EmailTemplate` model. Thanks @maximlomakin!


Version 3.0.2
-------------
- Fixed memory leak when multiprocessing is used.
- Fixed a possible error when adding a new email from Django admin. Thanks @ivlevdenis!


Version 3.0.2
-------------
- `_send_bulk` now properly catches exceptions when preparing email messages.


Version 3.0.1
-------------
- Fixed an infinite loop bug in `send_queued_mail` management command.


Version 3.0.0
-------------
* `_send_bulk` now allows each process to use multiple threads to send emails.
* Added support for mimetypes in email attachments. Thanks @clickonchris!
* An `EmailTemplate` can now be used as defaults multiple times in one language. Thanks @sac7e!
* `send_queued_mail` management command will now check whether there are more queued emails to be sent before exiting.
* Drop support for Django < 1.8. Thanks @fendyh!


Full changelog can be found `here <https://github.com/ui/django-post_office/blob/master/CHANGELOG.md>`_.


Created and maintained by the cool guys at `Stamps <https://stamps.co.id>`_,
Indonesia's most elegant CRM/loyalty platform.


.. |Build Status| image:: https://travis-ci.org/ui/django-post_office.png?branch=master
:target: https://travis-ci.org/ui/django-post_office

.. _uWSGI: https://uwsgi-docs.readthedocs.org/en/latest/



+ 95
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/RECORD View File

django_post_office-3.1.0.dist-info/DESCRIPTION.rst,sha256=fq6f9SdPAs7jJl7BWe_S9ko0vLKU4Yarl_f1aTu68_Q,22039
django_post_office-3.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
django_post_office-3.1.0.dist-info/METADATA,sha256=ax9BP4Si7dNXTUuIQ5bMgtr0u0JvkEQjvDKdP8qXavY,23265
django_post_office-3.1.0.dist-info/RECORD,,
django_post_office-3.1.0.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110
django_post_office-3.1.0.dist-info/metadata.json,sha256=EM2Mho7BIVj-ZWfq4Cl6uwNx6as_NqbCf1MMU0qaxLU,1353
django_post_office-3.1.0.dist-info/top_level.txt,sha256=UM3cswoGuzxOuDS20Qu9QVSXmMCn5t3ZZniWAtZ6Dn4,12
post_office/__init__.py,sha256=eVpmOKhL7n_V2TQ2upYQRcfmVbrKEFFYsM71Hv3F738,114
post_office/__pycache__/__init__.cpython-36.pyc,,
post_office/__pycache__/admin.cpython-36.pyc,,
post_office/__pycache__/apps.cpython-36.pyc,,
post_office/__pycache__/backends.cpython-36.pyc,,
post_office/__pycache__/cache.cpython-36.pyc,,
post_office/__pycache__/compat.cpython-36.pyc,,
post_office/__pycache__/connections.cpython-36.pyc,,
post_office/__pycache__/fields.cpython-36.pyc,,
post_office/__pycache__/lockfile.cpython-36.pyc,,
post_office/__pycache__/logutils.cpython-36.pyc,,
post_office/__pycache__/mail.cpython-36.pyc,,
post_office/__pycache__/models.cpython-36.pyc,,
post_office/__pycache__/settings.cpython-36.pyc,,
post_office/__pycache__/test_settings.cpython-36.pyc,,
post_office/__pycache__/test_urls.cpython-36.pyc,,
post_office/__pycache__/utils.cpython-36.pyc,,
post_office/__pycache__/validators.cpython-36.pyc,,
post_office/__pycache__/views.cpython-36.pyc,,
post_office/admin.py,sha256=wQqT_r9mBJ3E2t_aKSF2T8WHxgly6a4UmcyF3eyDA7Q,4922
post_office/apps.py,sha256=dv8AJIBvIec64Xy2BFKKd1R-8eEbLT0XBBiHXWQzS5Q,188
post_office/backends.py,sha256=5s2UTtFz2DIdaJMtzJV06bm4pP6IsaSrx1eHt3qK6yY,1855
post_office/cache.py,sha256=O39Foxqg8gxn2TBaTiT6wlyTKFE7Y6F-rSPLrB6ESkk,646
post_office/compat.py,sha256=5adNRlBcuvNqwDBjIDnbX2wVabGnj3fQTRr9e5PT-0I,1076
post_office/connections.py,sha256=REO_ns9KT42rhBFX4b8ABzBpkGiR8uO429j4DSOhKeU,1145
post_office/fields.py,sha256=9MgoIuXNm6mqrAuTqzMxfd4JiOSFx7DXWILXWnOU7ds,1973
post_office/locale/de/LC_MESSAGES/django.mo,sha256=JGLLjuEXSDqKzq0RnBjnIZX23aV0988oHNGCgD9J3sk,1632
post_office/locale/de/LC_MESSAGES/django.po,sha256=WBTJcJK_0SGC6O4nbE2DMaCLdrmQRHyE1RRRUbedmQE,2395
post_office/locale/it/LC_MESSAGES/django.mo,sha256=6hqUjb2Y3T21QnOya5bgY-gkMi4QhJxEoKv6QSAF9Mg,1611
post_office/locale/it/LC_MESSAGES/django.po,sha256=h-hoj6fTJwcgVB9vf9UKsEvzjWJ2nmYFoDj5JlYQro0,2379
post_office/locale/pl/LC_MESSAGES/django.mo,sha256=k_bOkQR787kAqHKUGxcRco5Mkggl2Hwr_bR3d05q6Ug,2698
post_office/locale/pl/LC_MESSAGES/django.po,sha256=tDDtQeMufPFoSYSgILfvgCSzqnpRG6V_Sngbwwhsv_4,4300
post_office/locale/ru_RU/LC_MESSAGES/django.mo,sha256=Op7j2z0Hii-uT6WVa7aqen60Rahz1RNXyp7GoHPnQ_E,3274
post_office/locale/ru_RU/LC_MESSAGES/django.po,sha256=UIsDRCnv0tc11sjFSUgJjOLcXGr-lHgIpp2ZOcAhrls,4901
post_office/lockfile.py,sha256=RS3c_b5jWi1z1pGH-7TsT8gwN5ueDdtHSpiIkbE64l4,4594
post_office/logutils.py,sha256=gTa5EeuZu3UHiyR-4XvGVl-LRJ1vM8lHoLW274wTDNs,1066
post_office/mail.py,sha256=O8kZsccs30rKpDAreIM6mC6YfdI72Mu0gv_Fb-ks8DY,10135
post_office/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
post_office/management/__pycache__/__init__.cpython-36.pyc,,
post_office/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
post_office/management/commands/__pycache__/__init__.cpython-36.pyc,,
post_office/management/commands/__pycache__/cleanup_mail.cpython-36.pyc,,
post_office/management/commands/__pycache__/send_queued_mail.cpython-36.pyc,,
post_office/management/commands/cleanup_mail.py,sha256=LKO3SWjbIMzlXsbUDychhkX3Lt9AO8eZdtAdYo0Nin0,1412
post_office/management/commands/send_queued_mail.py,sha256=ALkDylHVflNLydBTI96iBl6xXJtCW-W1ziG4BFhOisA,1999
post_office/migrations/0001_initial.py,sha256=7TaB2WEljVeM1niR6N_cYWz4PwXT5mxirHRlfwIdJeg,4635
post_office/migrations/0002_add_i18n_and_backend_alias.py,sha256=BtWyGrAybC5n7VhUup3GiREZJk6iMgxmSgMAy0IzmeE,5489
post_office/migrations/0003_longer_subject.py,sha256=ZnXopRJEeY_gfvj01t5lthde98HJZa91W3GrmyXp3lg,2749
post_office/migrations/0004_auto_20160607_0901.py,sha256=37BgmxLeGYL0RzYI-5flsCKs3obgqzNQmWEVZyitTLQ,5025
post_office/migrations/0005_auto_20170515_0013.py,sha256=EuA_SPdWF1e-TVBTzrO-z1sFZbiJaPV1ClLhw_GPlvc,456
post_office/migrations/0006_attachment_mimetype.py,sha256=UBDnxzP1KWCaEy1nBYQW6Zuv2EB-36w82jBxVAlkhgY,435
post_office/migrations/0007_auto_20170731_1342.py,sha256=LK8zW5vrxdjG07w5NMr3kisgdx-aBu1PEu2H-LPaNgM,498
post_office/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
post_office/migrations/__pycache__/0001_initial.cpython-36.pyc,,
post_office/migrations/__pycache__/0002_add_i18n_and_backend_alias.cpython-36.pyc,,
post_office/migrations/__pycache__/0003_longer_subject.cpython-36.pyc,,
post_office/migrations/__pycache__/0004_auto_20160607_0901.cpython-36.pyc,,
post_office/migrations/__pycache__/0005_auto_20170515_0013.cpython-36.pyc,,
post_office/migrations/__pycache__/0006_attachment_mimetype.cpython-36.pyc,,
post_office/migrations/__pycache__/0007_auto_20170731_1342.cpython-36.pyc,,
post_office/migrations/__pycache__/__init__.cpython-36.pyc,,
post_office/models.py,sha256=1WOu1px_vWwJzRAtmfoxfIRRx5jPOhjTS2cwltZ6GPM,10942
post_office/settings.py,sha256=PlKALPJvOo9AK326JjSMqI6U5-aO-aZSHqn8cSZUjaI,2584
post_office/test_settings.py,sha256=DkeMMDFiF6s2PCW0FoHv9pXJ8Lu5M5Vo25hdzKV-y3I,2293
post_office/test_urls.py,sha256=jFmkObKRb7-bE2nWqfQ49eSC51F3ayWRCrtymaEr378,123
post_office/tests/__init__.py,sha256=r8KNPnxhYYPrY_1Ko9SSo7Y1DcHlrZL2h6dRtqDePXk,287
post_office/tests/__pycache__/__init__.cpython-36.pyc,,
post_office/tests/__pycache__/test_backends.cpython-36.pyc,,
post_office/tests/__pycache__/test_cache.cpython-36.pyc,,
post_office/tests/__pycache__/test_commands.cpython-36.pyc,,
post_office/tests/__pycache__/test_connections.cpython-36.pyc,,
post_office/tests/__pycache__/test_lockfile.cpython-36.pyc,,
post_office/tests/__pycache__/test_mail.cpython-36.pyc,,
post_office/tests/__pycache__/test_models.cpython-36.pyc,,
post_office/tests/__pycache__/test_utils.cpython-36.pyc,,
post_office/tests/__pycache__/test_views.cpython-36.pyc,,
post_office/tests/test_backends.py,sha256=ZCP30v7ZO_sqsW-HHmIqQYWYE7DsQCQZGG7mxXaZhPc,4636
post_office/tests/test_cache.py,sha256=qKxuSytd8md9JgOFn_R1C0wLEt4ta_akN0c7A6Mwa9E,1437
post_office/tests/test_commands.py,sha256=NXijHn86wXaVrE1hcxiKvJKx-Tn4Ce6YmRPNqyt5U6A,6059
post_office/tests/test_connections.py,sha256=QL_EIy_Pjdv4dCNtLLEF_h2Vso3DrObcG1C4Ds06AIw,459
post_office/tests/test_lockfile.py,sha256=MMLgRhwV5xW72DlWR62yM_KMqQCutCfbDx8iWNs_Sas,1943
post_office/tests/test_mail.py,sha256=fJydVnGF5GRojLg2Oq2l_f_RyJQjVlmxMo5iFXUIREo,16449
post_office/tests/test_models.py,sha256=_1gtj0yL6ur1MQ2o_iq567TXAib1oReD1LB_LilNSvo,14960
post_office/tests/test_utils.py,sha256=_AqeqTZZmsGDQHosVeESOvzMCQ2uD_R0hbMCJrxDIso,8476
post_office/tests/test_views.py,sha256=feYG3bTnQDVRYDKCIsKX_UJerKEaegZXGze20NHrT_Y,1208
post_office/utils.py,sha256=aC8oilDFjQKBtNgZc__GhVX_y22cQrFPU5oKZ-CX-sc,4556
post_office/validators.py,sha256=q8umHXtqM4F93vh0g6m7x-U2eM347w9L6qVXXelF-v4,1409
post_office/views.py,sha256=F42JXgnqFqK0fajXeutyJJxwOszRxoLMNkIhfc4Z7KI,26

+ 6
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/WHEEL View File

Wheel-Version: 1.0
Generator: bdist_wheel (0.30.0)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any


+ 1
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/metadata.json View File

{"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules"], "description_content_type": "UNKNOWN", "extensions": {"python.details": {"contacts": [{"email": "selwin.ong@gmail.com", "name": "Selwin Ong", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/ui/django-post_office"}}}, "extras": ["test"], "generator": "bdist_wheel (0.30.0)", "license": "MIT", "metadata_version": "2.0", "name": "django-post-office", "run_requires": [{"requires": ["django (>=1.8)", "jsonfield"]}, {"extra": "test", "requires": ["tox (>=2.3)"]}], "summary": "A Django app to monitor and send mail asynchronously, complete with template support.", "test_requires": [{"requires": ["tox (>=2.3)"]}], "version": "3.1.0"}

+ 1
- 0
thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/top_level.txt View File

post_office

+ 121
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/DESCRIPTION.rst View File

django-jsonfield
----------------

django-jsonfield is a reusable Django field that allows you to store validated JSON in your model.

It silently takes care of serialization. To use, simply add the field to one of your models.

Python 3 & Django 1.8 through 1.11 supported!

**Use PostgreSQL?** 1.0.0 introduced a breaking change to the underlying data type, so if you were using < 1.0.0 please read https://github.com/dmkoch/django-jsonfield/issues/57 before upgrading. Also, consider switching to Django's native JSONField that was added in Django 1.9.

**Note:** There are a couple of third-party add-on JSONFields for Django. This project is django-jsonfield here on GitHub but is named `jsonfield on PyPI`_. There is another `django-jsonfield on Bitbucket`_, but that one is `django-jsonfield on PyPI`_. I realize this naming conflict is confusing and I am open to merging the two projects.

.. _jsonfield on PyPI: https://pypi.python.org/pypi/jsonfield
.. _django-jsonfield on Bitbucket: https://bitbucket.org/schinckel/django-jsonfield
.. _django-jsonfield on PyPI: https://pypi.python.org/pypi/django-jsonfield

**Note:** Django 1.9 added native PostgreSQL JSON support in `django.contrib.postgres.fields.JSONField`_. This module is still useful if you need to support JSON in databases other than PostgreSQL or are creating a third-party module that needs to be database-agnostic. But if you're an end user using PostgreSQL and want full-featured JSON support, I recommend using the built-in JSONField from Django instead of this module.

.. _django.contrib.postgres.fields.JSONField: https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#jsonfield

**Note:** Semver is followed after the 1.0 release.


Installation
------------

.. code-block:: python

pip install jsonfield


Usage
-----

.. code-block:: python

from django.db import models
from jsonfield import JSONField

class MyModel(models.Model):
json = JSONField()

Advanced Usage
--------------

By default python deserializes json into dict objects. This behavior differs from the standard json behavior because python dicts do not have ordered keys.

To overcome this limitation and keep the sort order of OrderedDict keys the deserialisation can be adjusted on model initialisation:

.. code-block:: python

import collections
class MyModel(models.Model):
json = JSONField(load_kwargs={'object_pairs_hook': collections.OrderedDict})


Other Fields
------------

**jsonfield.JSONCharField**

If you need to use your JSON field in an index or other constraint, you can use **JSONCharField** which subclasses **CharField** instead of **TextField**. You'll also need to specify a **max_length** parameter if you use this field.


Compatibility
--------------

django-jsonfield aims to support the same versions of Django currently maintained by the main Django project. See `Django supported versions`_, currently:

* Django 1.8 (LTS) with Python 2.7, 3.3, 3.4, or 3.5
* Django 1.9 with Python 2.7, 3.4, or 3.5
* Django 1.10 with Python 2.7, 3.4, or 3.5
* Django 1.11 (LTS) with Python 2.7, 3.4, 3.5 or 3.6

.. _Django supported versions: https://www.djangoproject.com/download/#supported-versions


Testing django-jsonfield Locally
--------------------------------

To test against all supported versions of Django:

.. code-block:: shell

$ docker-compose build && docker-compose up

Or just one version (for example Django 1.10 on Python 3.5):

.. code-block:: shell

$ docker-compose build && docker-compose run tox tox -e py35-1.10


Travis CI
---------

.. image:: https://travis-ci.org/dmkoch/django-jsonfield.svg?branch=master
:target: https://travis-ci.org/dmkoch/django-jsonfield

Contact
-------
Web: http://bradjasper.com

Twitter: `@bradjasper`_

Email: `contact@bradjasper.com`_



.. _contact@bradjasper.com: mailto:contact@bradjasper.com
.. _@bradjasper: https://twitter.com/bradjasper

Changes
-------

Take a look at the `changelog`_.

.. _changelog: https://github.com/dmkoch/django-jsonfield/blob/master/CHANGES.rst



+ 1
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/INSTALLER View File

pip

+ 143
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/METADATA View File

Metadata-Version: 2.0
Name: jsonfield
Version: 2.0.2
Summary: A reusable Django field that allows you to store validated JSON in your model.
Home-page: https://github.com/dmkoch/django-jsonfield/
Author: Dan Koch
Author-email: dmkoch@gmail.com
License: MIT
Platform: UNKNOWN
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Framework :: Django
Requires-Dist: Django (>=1.8.0)

django-jsonfield
----------------

django-jsonfield is a reusable Django field that allows you to store validated JSON in your model.

It silently takes care of serialization. To use, simply add the field to one of your models.

Python 3 & Django 1.8 through 1.11 supported!

**Use PostgreSQL?** 1.0.0 introduced a breaking change to the underlying data type, so if you were using < 1.0.0 please read https://github.com/dmkoch/django-jsonfield/issues/57 before upgrading. Also, consider switching to Django's native JSONField that was added in Django 1.9.

**Note:** There are a couple of third-party add-on JSONFields for Django. This project is django-jsonfield here on GitHub but is named `jsonfield on PyPI`_. There is another `django-jsonfield on Bitbucket`_, but that one is `django-jsonfield on PyPI`_. I realize this naming conflict is confusing and I am open to merging the two projects.

.. _jsonfield on PyPI: https://pypi.python.org/pypi/jsonfield
.. _django-jsonfield on Bitbucket: https://bitbucket.org/schinckel/django-jsonfield
.. _django-jsonfield on PyPI: https://pypi.python.org/pypi/django-jsonfield

**Note:** Django 1.9 added native PostgreSQL JSON support in `django.contrib.postgres.fields.JSONField`_. This module is still useful if you need to support JSON in databases other than PostgreSQL or are creating a third-party module that needs to be database-agnostic. But if you're an end user using PostgreSQL and want full-featured JSON support, I recommend using the built-in JSONField from Django instead of this module.

.. _django.contrib.postgres.fields.JSONField: https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#jsonfield

**Note:** Semver is followed after the 1.0 release.


Installation
------------

.. code-block:: python

pip install jsonfield


Usage
-----

.. code-block:: python

from django.db import models
from jsonfield import JSONField

class MyModel(models.Model):
json = JSONField()

Advanced Usage
--------------

By default python deserializes json into dict objects. This behavior differs from the standard json behavior because python dicts do not have ordered keys.

To overcome this limitation and keep the sort order of OrderedDict keys the deserialisation can be adjusted on model initialisation:

.. code-block:: python

import collections
class MyModel(models.Model):
json = JSONField(load_kwargs={'object_pairs_hook': collections.OrderedDict})


Other Fields
------------

**jsonfield.JSONCharField**

If you need to use your JSON field in an index or other constraint, you can use **JSONCharField** which subclasses **CharField** instead of **TextField**. You'll also need to specify a **max_length** parameter if you use this field.


Compatibility
--------------

django-jsonfield aims to support the same versions of Django currently maintained by the main Django project. See `Django supported versions`_, currently:

* Django 1.8 (LTS) with Python 2.7, 3.3, 3.4, or 3.5
* Django 1.9 with Python 2.7, 3.4, or 3.5
* Django 1.10 with Python 2.7, 3.4, or 3.5
* Django 1.11 (LTS) with Python 2.7, 3.4, 3.5 or 3.6

.. _Django supported versions: https://www.djangoproject.com/download/#supported-versions


Testing django-jsonfield Locally
--------------------------------

To test against all supported versions of Django:

.. code-block:: shell

$ docker-compose build && docker-compose up

Or just one version (for example Django 1.10 on Python 3.5):

.. code-block:: shell

$ docker-compose build && docker-compose run tox tox -e py35-1.10


Travis CI
---------

.. image:: https://travis-ci.org/dmkoch/django-jsonfield.svg?branch=master
:target: https://travis-ci.org/dmkoch/django-jsonfield

Contact
-------
Web: http://bradjasper.com

Twitter: `@bradjasper`_

Email: `contact@bradjasper.com`_



.. _contact@bradjasper.com: mailto:contact@bradjasper.com
.. _@bradjasper: https://twitter.com/bradjasper

Changes
-------

Take a look at the `changelog`_.

.. _changelog: https://github.com/dmkoch/django-jsonfield/blob/master/CHANGES.rst



+ 19
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/RECORD View File

jsonfield-2.0.2.dist-info/DESCRIPTION.rst,sha256=ol_8lnYqTVXq5ExDwAmbBhSUylv5skifu1MSqZJUszU,4077
jsonfield-2.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
jsonfield-2.0.2.dist-info/METADATA,sha256=fILysyClwPP4RqmlbgBEyjyhmBkYfqG_laVx_21m19M,4892
jsonfield-2.0.2.dist-info/RECORD,,
jsonfield-2.0.2.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
jsonfield-2.0.2.dist-info/metadata.json,sha256=EkZdQOU_zbNU_Uf6f6l2RPH3n9HoVNU0X8V0DbM5LIY,1010
jsonfield-2.0.2.dist-info/top_level.txt,sha256=vKhrOliM1tJJBXUhSXSoHcWs_90pUa7ogHvn-mzxGKQ,10
jsonfield/__init__.py,sha256=JzGSlVByVSYtIsp9iNf1S8pgxFdZ3ZY6y9Uys8hk2hs,53
jsonfield/__pycache__/__init__.cpython-36.pyc,,
jsonfield/__pycache__/encoder.cpython-36.pyc,,
jsonfield/__pycache__/fields.cpython-36.pyc,,
jsonfield/__pycache__/models.cpython-36.pyc,,
jsonfield/__pycache__/subclassing.cpython-36.pyc,,
jsonfield/__pycache__/tests.cpython-36.pyc,,
jsonfield/encoder.py,sha256=LamzI8S3leLOW0RG80-YUb2oxZxtUhuHXpKogYadL2w,2306
jsonfield/fields.py,sha256=09LBgXPcTxo6-rDwVewn8NhwtZ9yWe1DpoZehGVAcjk,6064
jsonfield/models.py,sha256=yXIA5LSYKowbs8bQWcU1TJ4Yc10MdEP5zWS5QD5P38E,43
jsonfield/subclassing.py,sha256=g1aGtzSlz3OisbQZgRtjDXVvQEqh0jNcMvZvBWHZ_ow,2236
jsonfield/tests.py,sha256=c0E2QV2l2gc3OLTVEOsePHl8Nt3gdaTcU02ShZksAOo,14949

+ 6
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/WHEEL View File

Wheel-Version: 1.0
Generator: bdist_wheel (0.29.0)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any


+ 1
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/metadata.json View File

{"classifiers": ["Environment :: Web Environment", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Framework :: Django"], "extensions": {"python.details": {"contacts": [{"email": "dmkoch@gmail.com", "name": "Dan Koch", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/dmkoch/django-jsonfield/"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "license": "MIT", "metadata_version": "2.0", "name": "jsonfield", "run_requires": [{"requires": ["Django (>=1.8.0)"]}], "summary": "A reusable Django field that allows you to store validated JSON in your model.", "test_requires": [{"requires": ["Django (>=1.8.0)"]}], "version": "2.0.2"}

+ 1
- 0
thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/top_level.txt View File

jsonfield

+ 1
- 0
thesisenv/lib/python3.6/site-packages/jsonfield/__init__.py View File

from .fields import JSONField, JSONCharField # noqa

+ 58
- 0
thesisenv/lib/python3.6/site-packages/jsonfield/encoder.py View File

from django.db.models.query import QuerySet
from django.utils import six, timezone
from django.utils.encoding import force_text
from django.utils.functional import Promise
import datetime
import decimal
import json
import uuid


class JSONEncoder(json.JSONEncoder):
"""
JSONEncoder subclass that knows how to encode date/time/timedelta,
decimal types, generators and other basic python objects.

Taken from https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/utils/encoders.py
"""
def default(self, obj): # noqa
# For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
if isinstance(obj, Promise):
return force_text(obj)
elif isinstance(obj, datetime.datetime):
representation = obj.isoformat()
if obj.microsecond:
representation = representation[:23] + representation[26:]
if representation.endswith('+00:00'):
representation = representation[:-6] + 'Z'
return representation
elif isinstance(obj, datetime.date):
return obj.isoformat()
elif isinstance(obj, datetime.time):
if timezone and timezone.is_aware(obj):
raise ValueError("JSON can't represent timezone-aware times.")
representation = obj.isoformat()
if obj.microsecond:
representation = representation[:12]
return representation
elif isinstance(obj, datetime.timedelta):
return six.text_type(obj.total_seconds())
elif isinstance(obj, decimal.Decimal):
# Serializers will coerce decimals to strings by default.
return float(obj)
elif isinstance(obj, uuid.UUID):
return six.text_type(obj)
elif isinstance(obj, QuerySet):
return tuple(obj)
elif hasattr(obj, 'tolist'):
# Numpy arrays and array scalars.
return obj.tolist()
elif hasattr(obj, '__getitem__'):
try:
return dict(obj)
except:
pass
elif hasattr(obj, '__iter__'):
return tuple(item for item in obj)
return super(JSONEncoder, self).default(obj)

+ 183
- 0
thesisenv/lib/python3.6/site-packages/jsonfield/fields.py View File

import copy
from django.db import models
from django.utils.translation import ugettext_lazy as _
try:
from django.utils import six
except ImportError:
import six

try:
import json
except ImportError:
from django.utils import simplejson as json

from django.forms import fields
try:
from django.forms.utils import ValidationError
except ImportError:
from django.forms.util import ValidationError

from .subclassing import SubfieldBase
from .encoder import JSONEncoder


class JSONFormFieldBase(object):
def __init__(self, *args, **kwargs):
self.load_kwargs = kwargs.pop('load_kwargs', {})
super(JSONFormFieldBase, self).__init__(*args, **kwargs)

def to_python(self, value):
if isinstance(value, six.string_types) and value:
try:
return json.loads(value, **self.load_kwargs)
except ValueError:
raise ValidationError(_("Enter valid JSON"))
return value

def clean(self, value):

if not value and not self.required:
return None

# Trap cleaning errors & bubble them up as JSON errors
try:
return super(JSONFormFieldBase, self).clean(value)
except TypeError:
raise ValidationError(_("Enter valid JSON"))


class JSONFormField(JSONFormFieldBase, fields.CharField):
pass


class JSONCharFormField(JSONFormFieldBase, fields.CharField):
pass


class JSONFieldBase(six.with_metaclass(SubfieldBase, models.Field)):

def __init__(self, *args, **kwargs):
self.dump_kwargs = kwargs.pop('dump_kwargs', {
'cls': JSONEncoder,
'separators': (',', ':')
})
self.load_kwargs = kwargs.pop('load_kwargs', {})

super(JSONFieldBase, self).__init__(*args, **kwargs)

def pre_init(self, value, obj):
"""Convert a string value to JSON only if it needs to be deserialized.

SubfieldBase metaclass has been modified to call this method instead of
to_python so that we can check the obj state and determine if it needs to be
deserialized"""

try:
if obj._state.adding:
# Make sure the primary key actually exists on the object before
# checking if it's empty. This is a special case for South datamigrations
# see: https://github.com/bradjasper/django-jsonfield/issues/52
if getattr(obj, "pk", None) is not None:
if isinstance(value, six.string_types):
try:
return json.loads(value, **self.load_kwargs)
except ValueError:
raise ValidationError(_("Enter valid JSON"))

except AttributeError:
# south fake meta class doesn't create proper attributes
# see this:
# https://github.com/bradjasper/django-jsonfield/issues/52
pass

return value

def to_python(self, value):
"""The SubfieldBase metaclass calls pre_init instead of to_python, however to_python
is still necessary for Django's deserializer"""
return value

def get_prep_value(self, value):
"""Convert JSON object to a string"""
if self.null and value is None:
return None
return json.dumps(value, **self.dump_kwargs)

def _get_val_from_obj(self, obj):
# This function created to replace Django deprecated version
# https://code.djangoproject.com/ticket/24716
if obj is not None:
return getattr(obj, self.attname)
else:
return self.get_default()

def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value, None)

def value_from_object(self, obj):
value = super(JSONFieldBase, self).value_from_object(obj)
if self.null and value is None:
return None
return self.dumps_for_display(value)

def dumps_for_display(self, value):
return json.dumps(value, **self.dump_kwargs)

def formfield(self, **kwargs):

if "form_class" not in kwargs:
kwargs["form_class"] = self.form_class

field = super(JSONFieldBase, self).formfield(**kwargs)

if isinstance(field, JSONFormFieldBase):
field.load_kwargs = self.load_kwargs

if not field.help_text:
field.help_text = "Enter valid JSON"

return field

def get_default(self):
"""
Returns the default value for this field.

The default implementation on models.Field calls force_unicode
on the default, which means you can't set arbitrary Python
objects as the default. To fix this, we just return the value
without calling force_unicode on it. Note that if you set a
callable as a default, the field will still call it. It will
*not* try to pickle and encode it.

"""
if self.has_default():
if callable(self.default):
return self.default()
return copy.deepcopy(self.default)
# If the field doesn't have a default, then we punt to models.Field.
return super(JSONFieldBase, self).get_default()


class JSONField(JSONFieldBase, models.TextField):
"""JSONField is a generic textfield that serializes/deserializes JSON objects"""
form_class = JSONFormField

def dumps_for_display(self, value):
kwargs = {"indent": 2}
kwargs.update(self.dump_kwargs)
return json.dumps(value, **kwargs)


class JSONCharField(JSONFieldBase, models.CharField):
"""JSONCharField is a generic textfield that serializes/deserializes JSON objects,
stored in the database like a CharField, which enables it to be used
e.g. in unique keys"""
form_class = JSONCharFormField


try:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^jsonfield\.fields\.(JSONField|JSONCharField)"])
except ImportError:
pass

+ 1
- 0
thesisenv/lib/python3.6/site-packages/jsonfield/models.py View File

# Django needs this to see it as a project

+ 62
- 0
thesisenv/lib/python3.6/site-packages/jsonfield/subclassing.py View File

# This file was copied from django.db.models.fields.subclassing so that we could
# change the Creator.__set__ behavior. Read the comment below for full details.

"""
Convenience routines for creating non-trivial Field subclasses, as well as
backwards compatibility utilities.

Add SubfieldBase as the __metaclass__ for your Field subclass, implement
to_python() and the other necessary methods and everything will work seamlessly.
"""


class SubfieldBase(type):
"""
A metaclass for custom Field subclasses. This ensures the model's attribute
has the descriptor protocol attached to it.
"""
def __new__(cls, name, bases, attrs):
new_class = super(SubfieldBase, cls).__new__(cls, name, bases, attrs)
new_class.contribute_to_class = make_contrib(
new_class, attrs.get('contribute_to_class')
)
return new_class


class Creator(object):
"""
A placeholder class that provides a way to set the attribute on the model.
"""
def __init__(self, field):
self.field = field

def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__[self.field.name]

def __set__(self, obj, value):
# Usually this would call to_python, but we've changed it to pre_init
# so that we can tell which state we're in. By passing an obj,
# we can definitively tell if a value has already been deserialized
# More: https://github.com/bradjasper/django-jsonfield/issues/33
obj.__dict__[self.field.name] = self.field.pre_init(value, obj)


def make_contrib(superclass, func=None):
"""
Returns a suitable contribute_to_class() method for the Field subclass.

If 'func' is passed in, it is the existing contribute_to_class() method on
the subclass and it is called before anything else. It is assumed in this
case that the existing contribute_to_class() calls all the necessary
superclass methods.
"""
def contribute_to_class(self, cls, name):
if func:
func(self, cls, name)
else:
super(superclass, self).contribute_to_class(cls, name)
setattr(cls, self.name, Creator(self))

return contribute_to_class

+ 392
- 0
thesisenv/lib/python3.6/site-packages/jsonfield/tests.py View File

from decimal import Decimal
import django
from django import forms
from django.core.serializers import deserialize, serialize
from django.core.serializers.base import DeserializationError
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.test import TestCase
try:
import json
except ImportError:
from django.utils import simplejson as json

from .fields import JSONField, JSONCharField
try:
from django.forms.utils import ValidationError
except ImportError:
from django.forms.util import ValidationError

from django.utils.six import string_types

from collections import OrderedDict


class JsonModel(models.Model):
json = JSONField()
default_json = JSONField(default={"check": 12})
complex_default_json = JSONField(default=[{"checkcheck": 1212}])
empty_default = JSONField(default={})


class GenericForeignKeyObj(models.Model):
name = models.CharField('Foreign Obj', max_length=255, null=True)


class JSONModelWithForeignKey(models.Model):
json = JSONField(null=True)
foreign_obj = GenericForeignKey()
object_id = models.PositiveIntegerField(blank=True, null=True, db_index=True)
content_type = models.ForeignKey(ContentType, blank=True, null=True,
on_delete=models.CASCADE)


class JsonCharModel(models.Model):
json = JSONCharField(max_length=100)
default_json = JSONCharField(max_length=100, default={"check": 34})


class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
return {
'__complex__': True,
'real': obj.real,
'imag': obj.imag,
}

return json.JSONEncoder.default(self, obj)


def as_complex(dct):
if '__complex__' in dct:
return complex(dct['real'], dct['imag'])
return dct


class JSONModelCustomEncoders(models.Model):
# A JSON field that can store complex numbers
json = JSONField(
dump_kwargs={'cls': ComplexEncoder, "indent": 4},
load_kwargs={'object_hook': as_complex},
)


class JSONModelWithForeignKeyTestCase(TestCase):
def test_object_create(self):
foreign_obj = GenericForeignKeyObj.objects.create(name='Brain')
JSONModelWithForeignKey.objects.create(foreign_obj=foreign_obj)


class JSONFieldTest(TestCase):
"""JSONField Wrapper Tests"""

json_model = JsonModel

def test_json_field_create(self):
"""Test saving a JSON object in our JSONField"""
json_obj = {
"item_1": "this is a json blah",
"blergh": "hey, hey, hey"}

obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj)

def test_string_in_json_field(self):
"""Test saving an ordinary Python string in our JSONField"""
json_obj = 'blah blah'
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj)

def test_float_in_json_field(self):
"""Test saving a Python float in our JSONField"""
json_obj = 1.23
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj)

def test_int_in_json_field(self):
"""Test saving a Python integer in our JSONField"""
json_obj = 1234567
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj)

def test_decimal_in_json_field(self):
"""Test saving a Python Decimal in our JSONField"""
json_obj = Decimal(12.34)
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

# here we must know to convert the returned string back to Decimal,
# since json does not support that format
self.assertEqual(Decimal(new_obj.json), json_obj)

def test_json_field_modify(self):
"""Test modifying a JSON object in our JSONField"""
json_obj_1 = {'a': 1, 'b': 2}
json_obj_2 = {'a': 3, 'b': 4}

obj = self.json_model.objects.create(json=json_obj_1)
self.assertEqual(obj.json, json_obj_1)
obj.json = json_obj_2

self.assertEqual(obj.json, json_obj_2)
obj.save()
self.assertEqual(obj.json, json_obj_2)

self.assertTrue(obj)

def test_json_field_load(self):
"""Test loading a JSON object from the DB"""
json_obj_1 = {'a': 1, 'b': 2}
obj = self.json_model.objects.create(json=json_obj_1)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj_1)

def test_json_list(self):
"""Test storing a JSON list"""
json_obj = ["my", "list", "of", 1, "objs", {"hello": "there"}]

obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)
self.assertEqual(new_obj.json, json_obj)

def test_empty_objects(self):
"""Test storing empty objects"""
for json_obj in [{}, [], 0, '', False]:
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)
self.assertEqual(json_obj, obj.json)
self.assertEqual(json_obj, new_obj.json)

def test_custom_encoder(self):
"""Test encoder_cls and object_hook"""
value = 1 + 3j # A complex number

obj = JSONModelCustomEncoders.objects.create(json=value)
new_obj = JSONModelCustomEncoders.objects.get(pk=obj.pk)
self.assertEqual(value, new_obj.json)

def test_django_serializers(self):
"""Test serializing/deserializing jsonfield data"""
for json_obj in [{}, [], 0, '', False, {'key': 'value', 'num': 42,
'ary': list(range(5)),
'dict': {'k': 'v'}}]:
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)
self.assert_(new_obj)

queryset = self.json_model.objects.all()
ser = serialize('json', queryset)
for dobj in deserialize('json', ser):
obj = dobj.object
pulled = self.json_model.objects.get(id=obj.pk)
self.assertEqual(obj.json, pulled.json)

def test_default_parameters(self):
"""Test providing a default value to the model"""
model = JsonModel()
model.json = {"check": 12}
self.assertEqual(model.json, {"check": 12})
self.assertEqual(type(model.json), dict)

self.assertEqual(model.default_json, {"check": 12})
self.assertEqual(type(model.default_json), dict)

def test_invalid_json(self):
# invalid json data {] in the json and default_json fields
ser = '[{"pk": 1, "model": "jsonfield.jsoncharmodel", ' \
'"fields": {"json": "{]", "default_json": "{]"}}]'
with self.assertRaises(DeserializationError) as cm:
next(deserialize('json', ser))
# Django 2.0+ uses PEP 3134 exception chaining
if django.VERSION < (2, 0,):
inner = cm.exception.args[0]
else:
inner = cm.exception.__context__
self.assertTrue(isinstance(inner, ValidationError))
self.assertEqual('Enter valid JSON', inner.messages[0])

def test_integer_in_string_in_json_field(self):
"""Test saving the Python string '123' in our JSONField"""
json_obj = '123'
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj)

def test_boolean_in_string_in_json_field(self):
"""Test saving the Python string 'true' in our JSONField"""
json_obj = 'true'
obj = self.json_model.objects.create(json=json_obj)
new_obj = self.json_model.objects.get(id=obj.id)

self.assertEqual(new_obj.json, json_obj)

def test_pass_by_reference_pollution(self):
"""Make sure the default parameter is copied rather than passed by reference"""
model = JsonModel()
model.default_json["check"] = 144
model.complex_default_json[0]["checkcheck"] = 144
self.assertEqual(model.default_json["check"], 144)
self.assertEqual(model.complex_default_json[0]["checkcheck"], 144)

# Make sure when we create a new model, it resets to the default value
# and not to what we just set it to (it would be if it were passed by reference)
model = JsonModel()
self.assertEqual(model.default_json["check"], 12)
self.assertEqual(model.complex_default_json[0]["checkcheck"], 1212)

def test_normal_regex_filter(self):
"""Make sure JSON model can filter regex"""

JsonModel.objects.create(json={"boom": "town"})
JsonModel.objects.create(json={"move": "town"})
JsonModel.objects.create(json={"save": "town"})

self.assertEqual(JsonModel.objects.count(), 3)

self.assertEqual(JsonModel.objects.filter(json__regex=r"boom").count(), 1)
self.assertEqual(JsonModel.objects.filter(json__regex=r"town").count(), 3)

def test_save_blank_object(self):
"""Test that JSON model can save a blank object as none"""

model = JsonModel()
self.assertEqual(model.empty_default, {})

model.save()
self.assertEqual(model.empty_default, {})

model1 = JsonModel(empty_default={"hey": "now"})
self.assertEqual(model1.empty_default, {"hey": "now"})

model1.save()
self.assertEqual(model1.empty_default, {"hey": "now"})


class JSONCharFieldTest(JSONFieldTest):
json_model = JsonCharModel


class OrderedJsonModel(models.Model):
json = JSONField(load_kwargs={'object_pairs_hook': OrderedDict})


class OrderedDictSerializationTest(TestCase):
def setUp(self):
self.ordered_dict = OrderedDict([
('number', [1, 2, 3, 4]),
('notes', True),
('alpha', True),
('romeo', True),
('juliet', True),
('bravo', True),
])
self.expected_key_order = ['number', 'notes', 'alpha', 'romeo', 'juliet', 'bravo']

def test_ordered_dict_differs_from_normal_dict(self):
self.assertEqual(list(self.ordered_dict.keys()), self.expected_key_order)
self.assertNotEqual(dict(self.ordered_dict).keys(), self.expected_key_order)

def test_default_behaviour_loses_sort_order(self):
mod = JsonModel.objects.create(json=self.ordered_dict)
self.assertEqual(list(mod.json.keys()), self.expected_key_order)
mod_from_db = JsonModel.objects.get(id=mod.id)

# mod_from_db lost ordering information during json.loads()
self.assertNotEqual(mod_from_db.json.keys(), self.expected_key_order)

def test_load_kwargs_hook_does_not_lose_sort_order(self):
mod = OrderedJsonModel.objects.create(json=self.ordered_dict)
self.assertEqual(list(mod.json.keys()), self.expected_key_order)
mod_from_db = OrderedJsonModel.objects.get(id=mod.id)
self.assertEqual(list(mod_from_db.json.keys()), self.expected_key_order)


class JsonNotRequiredModel(models.Model):
json = JSONField(blank=True, null=True)


class JsonNotRequiredForm(forms.ModelForm):
class Meta:
model = JsonNotRequiredModel
fields = '__all__'


class JsonModelFormTest(TestCase):
def test_blank_form(self):
form = JsonNotRequiredForm(data={'json': ''})
self.assertFalse(form.has_changed())

def test_form_with_data(self):
form = JsonNotRequiredForm(data={'json': '{}'})
self.assertTrue(form.has_changed())


class TestFieldAPIMethods(TestCase):
def test_get_db_prep_value_method_with_null(self):
json_field_instance = JSONField(null=True)
value = {'a': 1}
prepared_value = json_field_instance.get_db_prep_value(
value, connection=None, prepared=False)
self.assertIsInstance(prepared_value, string_types)
self.assertDictEqual(value, json.loads(prepared_value))
self.assertIs(json_field_instance.get_db_prep_value(
None, connection=None, prepared=True), None)
self.assertIs(json_field_instance.get_db_prep_value(
None, connection=None, prepared=False), None)

def test_get_db_prep_value_method_with_not_null(self):
json_field_instance = JSONField(null=False)
value = {'a': 1}
prepared_value = json_field_instance.get_db_prep_value(
value, connection=None, prepared=False)
self.assertIsInstance(prepared_value, string_types)
self.assertDictEqual(value, json.loads(prepared_value))
self.assertIs(json_field_instance.get_db_prep_value(
None, connection=None, prepared=True), None)
self.assertEqual(json_field_instance.get_db_prep_value(
None, connection=None, prepared=False), 'null')

def test_get_db_prep_value_method_skips_prepared_values(self):
json_field_instance = JSONField(null=False)
value = {'a': 1}
prepared_value = json_field_instance.get_db_prep_value(
value, connection=None, prepared=True)
self.assertIs(prepared_value, value)

def test_get_prep_value_always_json_dumps_if_not_null(self):
json_field_instance = JSONField(null=False)
value = {'a': 1}
prepared_value = json_field_instance.get_prep_value(value)
self.assertIsInstance(prepared_value, string_types)
self.assertDictEqual(value, json.loads(prepared_value))
already_json = json.dumps(value)
double_prepared_value = json_field_instance.get_prep_value(
already_json)
self.assertDictEqual(value,
json.loads(json.loads(double_prepared_value)))
self.assertEqual(json_field_instance.get_prep_value(None), 'null')

def test_get_prep_value_can_return_none_if_null(self):
json_field_instance = JSONField(null=True)
value = {'a': 1}
prepared_value = json_field_instance.get_prep_value(value)
self.assertIsInstance(prepared_value, string_types)
self.assertDictEqual(value, json.loads(prepared_value))
already_json = json.dumps(value)
double_prepared_value = json_field_instance.get_prep_value(
already_json)
self.assertDictEqual(value,
json.loads(json.loads(double_prepared_value)))
self.assertIs(json_field_instance.get_prep_value(None), None)

+ 5
- 0
thesisenv/lib/python3.6/site-packages/post_office/__init__.py View File

VERSION = (3, 1, 0)

from .backends import EmailBackend

default_app_config = 'post_office.apps.PostOfficeConfig'

+ 153
- 0
thesisenv/lib/python3.6/site-packages/post_office/admin.py View File

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
from django.db import models
from django.contrib import admin
from django.conf import settings
from django.forms.widgets import TextInput
from django.utils import six
from django.utils.text import Truncator
from django.utils.translation import ugettext_lazy as _

from .fields import CommaSeparatedEmailField
from .models import Attachment, Log, Email, EmailTemplate, STATUS


def get_message_preview(instance):
return (u'{0}...'.format(instance.message[:25]) if len(instance.message) > 25
else instance.message)

get_message_preview.short_description = 'Message'


class LogInline(admin.StackedInline):
model = Log
extra = 0


class CommaSeparatedEmailWidget(TextInput):

def __init__(self, *args, **kwargs):
super(CommaSeparatedEmailWidget, self).__init__(*args, **kwargs)
self.attrs.update({'class': 'vTextField'})

def _format_value(self, value):
# If the value is a string wrap it in a list so it does not get sliced.
if not value:
return ''
if isinstance(value, six.string_types):
value = [value, ]
return ','.join([item for item in value])


def requeue(modeladmin, request, queryset):
"""An admin action to requeue emails."""
queryset.update(status=STATUS.queued)


requeue.short_description = 'Requeue selected emails'


class EmailAdmin(admin.ModelAdmin):
list_display = ('id', 'to_display', 'subject', 'template',
'status', 'last_updated')
search_fields = ['to', 'subject']
date_hierarchy = 'last_updated'
inlines = [LogInline]
list_filter = ['status']
formfield_overrides = {
CommaSeparatedEmailField: {'widget': CommaSeparatedEmailWidget}
}
actions = [requeue]

def get_queryset(self, request):
return super(EmailAdmin, self).get_queryset(request).select_related('template')

def to_display(self, instance):
return ', '.join(instance.to)

to_display.short_description = 'to'
to_display.admin_order_field = 'to'


class LogAdmin(admin.ModelAdmin):
list_display = ('date', 'email', 'status', get_message_preview)


class SubjectField(TextInput):
def __init__(self, *args, **kwargs):
super(SubjectField, self).__init__(*args, **kwargs)
self.attrs.update({'style': 'width: 610px;'})


class EmailTemplateAdminForm(forms.ModelForm):

language = forms.ChoiceField(choices=settings.LANGUAGES, required=False,
help_text=_("Render template in alternative language"),
label=_("Language"))

class Meta:
model = EmailTemplate
fields = ('name', 'description', 'subject',
'content', 'html_content', 'language', 'default_template')


class EmailTemplateInline(admin.StackedInline):
form = EmailTemplateAdminForm
model = EmailTemplate
extra = 0
fields = ('language', 'subject', 'content', 'html_content',)
formfield_overrides = {
models.CharField: {'widget': SubjectField}
}

def get_max_num(self, request, obj=None, **kwargs):
return len(settings.LANGUAGES)


class EmailTemplateAdmin(admin.ModelAdmin):
form = EmailTemplateAdminForm
list_display = ('name', 'description_shortened', 'subject', 'languages_compact', 'created')
search_fields = ('name', 'description', 'subject')
fieldsets = [
(None, {
'fields': ('name', 'description'),
}),
(_("Default Content"), {
'fields': ('subject', 'content', 'html_content'),
}),
]
inlines = (EmailTemplateInline,) if settings.USE_I18N else ()
formfield_overrides = {
models.CharField: {'widget': SubjectField}
}

def get_queryset(self, request):
return self.model.objects.filter(default_template__isnull=True)

def description_shortened(self, instance):
return Truncator(instance.description.split('\n')[0]).chars(200)
description_shortened.short_description = _("Description")
description_shortened.admin_order_field = 'description'

def languages_compact(self, instance):
languages = [tt.language for tt in instance.translated_templates.order_by('language')]
return ', '.join(languages)
languages_compact.short_description = _("Languages")

def save_model(self, request, obj, form, change):
obj.save()

# if the name got changed, also change the translated templates to match again
if 'name' in form.changed_data:
obj.translated_templates.update(name=obj.name)


class AttachmentAdmin(admin.ModelAdmin):
list_display = ('name', 'file', )


admin.site.register(Email, EmailAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(EmailTemplate, EmailTemplateAdmin)
admin.site.register(Attachment, AttachmentAdmin)

+ 7
- 0
thesisenv/lib/python3.6/site-packages/post_office/apps.py View File

from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _


class PostOfficeConfig(AppConfig):
name = 'post_office'
verbose_name = _("Post Office")

+ 56
- 0
thesisenv/lib/python3.6/site-packages/post_office/backends.py View File

from django.core.files.base import ContentFile
from django.core.mail.backends.base import BaseEmailBackend

from .settings import get_default_priority


class EmailBackend(BaseEmailBackend):

def open(self):
pass

def close(self):
pass

def send_messages(self, email_messages):
"""
Queue one or more EmailMessage objects and returns the number of
email messages sent.
"""
from .mail import create
from .utils import create_attachments

if not email_messages:
return

for email_message in email_messages:
subject = email_message.subject
from_email = email_message.from_email
message = email_message.body
headers = email_message.extra_headers

# Check whether email has 'text/html' alternative
alternatives = getattr(email_message, 'alternatives', ())
for alternative in alternatives:
if alternative[1].startswith('text/html'):
html_message = alternative[0]
break
else:
html_message = ''

attachment_files = dict([(name, ContentFile(content))
for name, content, _ in email_message.attachments])

email = create(sender=from_email,
recipients=email_message.to, cc=email_message.cc,
bcc=email_message.bcc, subject=subject,
message=message, html_message=html_message,
headers=headers)

if attachment_files:
attachments = create_attachments(attachment_files)

email.attachments.add(*attachments)

if get_default_priority() == 'now':
email.dispatch()

+ 26
- 0
thesisenv/lib/python3.6/site-packages/post_office/cache.py View File

from django.template.defaultfilters import slugify

from .settings import get_cache_backend

# Stripped down version of caching functions from django-dbtemplates
# https://github.com/jezdez/django-dbtemplates/blob/develop/dbtemplates/utils/cache.py
cache_backend = get_cache_backend()


def get_cache_key(name):
"""
Prefixes and slugify the key name
"""
return 'post_office:template:%s' % (slugify(name))


def set(name, content):
return cache_backend.set(get_cache_key(name), content)


def get(name):
return cache_backend.get(get_cache_key(name))


def delete(name):
return cache_backend.delete(get_cache_key(name))

+ 46
- 0
thesisenv/lib/python3.6/site-packages/post_office/compat.py View File

try:
import importlib
except ImportError:
from django.utils import importlib

try:
from logging.config import dictConfig # Python >= 2.7
except ImportError:
from django.utils.log import dictConfig # Django <= 1.9

import sys


PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3


if PY3:
string_types = str
text_type = str
else:
string_types = basestring
text_type = unicode


try:
from django.core.cache import caches # Django >= 1.7

def get_cache(name):
return caches[name]
except ImportError:
from django.core.cache import get_cache


try:
from django.utils.encoding import smart_text # For Django >= 1.5
except ImportError:
from django.utils.encoding import smart_unicode as smart_text


# Django 1.4 doesn't have ``import_string`` or ``import_by_path``
def import_attribute(name):
"""Return an attribute from a dotted path name (e.g. "path.to.func")."""
module_name, attribute = name.rsplit('.', 1)
module = importlib.import_module(module_name)
return getattr(module, attribute)

+ 44
- 0
thesisenv/lib/python3.6/site-packages/post_office/connections.py View File

from threading import local

from django.core.mail import get_connection

from .settings import get_backend


# Copied from Django 1.8's django.core.cache.CacheHandler
class ConnectionHandler(object):
"""
A Cache Handler to manage access to Cache instances.

Ensures only one instance of each alias exists per thread.
"""
def __init__(self):
self._connections = local()

def __getitem__(self, alias):
try:
return self._connections.connections[alias]
except AttributeError:
self._connections.connections = {}
except KeyError:
pass

try:
backend = get_backend(alias)
except KeyError:
raise KeyError('%s is not a valid backend alias' % alias)

connection = get_connection(backend)
connection.open()
self._connections.connections[alias] = connection
return connection

def all(self):
return getattr(self._connections, 'connections', {}).values()

def close(self):
for connection in self.all():
connection.close()


connections = ConnectionHandler()

+ 58
- 0
thesisenv/lib/python3.6/site-packages/post_office/fields.py View File

from django.db.models import TextField
from django.utils import six
from django.utils.translation import ugettext_lazy as _

from .validators import validate_comma_separated_emails


class CommaSeparatedEmailField(TextField):
default_validators = [validate_comma_separated_emails]
description = _("Comma-separated emails")

def __init__(self, *args, **kwargs):
kwargs['blank'] = True
super(CommaSeparatedEmailField, self).__init__(*args, **kwargs)

def formfield(self, **kwargs):
defaults = {
'error_messages': {
'invalid': _('Only comma separated emails are allowed.'),
}
}
defaults.update(kwargs)
return super(CommaSeparatedEmailField, self).formfield(**defaults)

def from_db_value(self, value, expression, connection, context):
return self.to_python(value)

def get_prep_value(self, value):
"""
We need to accomodate queries where a single email,
or list of email addresses is supplied as arguments. For example:

- Email.objects.filter(to='mail@example.com')
- Email.objects.filter(to=['one@example.com', 'two@example.com'])
"""
if isinstance(value, six.string_types):
return value
else:
return ', '.join(map(lambda s: s.strip(), value))

def to_python(self, value):
if isinstance(value, six.string_types):
if value == '':
return []
else:
return [s.strip() for s in value.split(',')]
else:
return value

def south_field_triple(self):
"""
Return a suitable description of this field for South.
Taken from smiley chris' easy_thumbnails
"""
from south.modelsinspector import introspector
field_class = 'django.db.models.fields.TextField'
args, kwargs = introspector(self)
return (field_class, args, kwargs)

BIN
thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.mo View File


+ 130
- 0
thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.po View File

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-07-06 07:47+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: admin.py:97
#, fuzzy
msgid "Default Content"
msgstr "Inhalt"

#: admin.py:111
msgid "Description"
msgstr "Beschreibung"

#: admin.py:117
msgid "Languages"
msgstr "Sprachen"

#: fields.py:11
msgid "Comma-separated emails"
msgstr "Durch Kommas getrennte Emails"

#: fields.py:20
msgid "Only comma separated emails are allowed."
msgstr "Nur durch Kommas getrennte Emails sind erlaubt"

#: models.py:38
msgid "low"
msgstr "niedrig"

#: models.py:38
msgid "medium"
msgstr "mittel"

#: models.py:39
msgid "high"
msgstr "hoch"

#: models.py:39
msgid "now"
msgstr "sofort"

#: models.py:40 models.py:169
msgid "sent"
msgstr "gesendet"

#: models.py:40 models.py:169
msgid "failed"
msgstr "fehlgeschlagen"

#: models.py:41
msgid "queued"
msgstr "in der Warteschleife"

#: models.py:44
msgid "Email From"
msgstr "Email Von"

#: models.py:45
msgid "Email To"
msgstr "Email An"

#: models.py:46
msgid "Cc"
msgstr "Kopie"

#: models.py:47
msgid "Bcc"
msgstr "Blinde Kopie"

#: models.py:48 models.py:194
msgid "Subject"
msgstr "Betreff"

#: models.py:49
msgid "Message"
msgstr "Nachricht"

#: models.py:50
msgid "HTML Message"
msgstr "HTML Nachricht"

#: models.py:188
msgid "e.g: 'welcome_email'"
msgstr "z.B. 'welcome_email'"

#: models.py:190
msgid "Description of this template."
msgstr "Beschreibung dieser Vorlage"

#: models.py:196
msgid "Content"
msgstr "Inhalt"

#: models.py:198
msgid "HTML content"
msgstr "HTML Inhalt"

#: models.py:200
msgid "Render template in alternative language"
msgstr "Vorlage in alternativer Sprache rendern"

#: models.py:208
#, fuzzy
msgid "Email Template"
msgstr "Email Vorlage"

#: models.py:209
#, fuzzy
msgid "Email Templates"
msgstr "Email Vorlagen"

#: models.py:236
msgid "The original filename"
msgstr "Ursprünglicher Dateiname"

BIN
thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.mo View File


+ 130
- 0
thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.po View File

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-07-06 07:47+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: admin.py:97
#, fuzzy
msgid "Default Content"
msgstr "Contenuto"

#: admin.py:111
msgid "Description"
msgstr "Descrizione"

#: admin.py:117
msgid "Languages"
msgstr "Lingue"

#: fields.py:11
msgid "Comma-separated emails"
msgstr "Emails separati da virgole"

#: fields.py:20
msgid "Only comma separated emails are allowed."
msgstr "Sono consentiti soltanto emails separati da virgole"

#: models.py:38
msgid "low"
msgstr "bassa"

#: models.py:38
msgid "medium"
msgstr "media"

#: models.py:39
msgid "high"
msgstr "alta"

#: models.py:39
msgid "now"
msgstr "immediata"

#: models.py:40 models.py:169
msgid "sent"
msgstr "inviato"

#: models.py:40 models.py:169
msgid "failed"
msgstr "fallito"

#: models.py:41
msgid "queued"
msgstr "in attesa"

#: models.py:44
msgid "Email From"
msgstr "Email da"

#: models.py:45
msgid "Email To"
msgstr "Email per"

#: models.py:46
msgid "Cc"
msgstr "Copia"

#: models.py:47
msgid "Bcc"
msgstr "Bcc"

#: models.py:48 models.py:194
msgid "Subject"
msgstr "Soggetto"

#: models.py:49
msgid "Message"
msgstr "Messaggio"

#: models.py:50
msgid "HTML Message"
msgstr "HTML Messaggio"

#: models.py:188
msgid "e.g: 'welcome_email'"
msgstr "z.B. 'welcome_email'"

#: models.py:190
msgid "Description of this template."
msgstr "Descrizione di questa template."

#: models.py:196
msgid "Content"
msgstr "Contenuto"

#: models.py:198
msgid "HTML content"
msgstr "Contenuto in HTML"

#: models.py:200
msgid "Render template in alternative language"
msgstr "Rendere template in un altra lingua"

#: models.py:208
#, fuzzy
msgid "Email Template"
msgstr "Email Template"

#: models.py:209
#, fuzzy
msgid "Email Templates"
msgstr "Email Templates"

#: models.py:236
msgid "The original filename"
msgstr "Nome del file originale"

BIN
thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.mo View File


+ 206
- 0
thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.po View File

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-09 13:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"

#: post_office/admin.py:81 post_office/models.py:200
msgid "Render template in alternative language"
msgstr "Wygeneruj szablon w alternatywnym języku"

#: post_office/admin.py:82 post_office/models.py:199
msgid "Language"
msgstr "Język"

#: post_office/admin.py:111
msgid "Default Content"
msgstr "Domyślna zawartość"

#: post_office/admin.py:125 post_office/models.py:188
msgid "Description"
msgstr "Opis"

#: post_office/admin.py:131
msgid "Languages"
msgstr "Języki"

#: post_office/fields.py:11
msgid "Comma-separated emails"
msgstr "Oddzielone przecinkami emaile"

#: post_office/fields.py:20
msgid "Only comma separated emails are allowed."
msgstr "Tylko oddzielone przecinkami emaile są dozwolone."

#: post_office/models.py:36
msgid "low"
msgstr "niski"

#: post_office/models.py:36
msgid "medium"
msgstr "średni"

#: post_office/models.py:37
msgid "high"
msgstr "wysoki"

#: post_office/models.py:37
msgid "now"
msgstr "natychmiastowy"

#: post_office/models.py:38 post_office/models.py:164
msgid "sent"
msgstr "wysłany"

#: post_office/models.py:38 post_office/models.py:164
msgid "failed"
msgstr "odrzucony"

#: post_office/models.py:39
msgid "queued"
msgstr "w kolejce"

#: post_office/models.py:41
msgid "Email From"
msgstr "Email od"

#: post_office/models.py:43
msgid "Email To"
msgstr "Email do"

#: post_office/models.py:44
msgid "Cc"
msgstr "Cc"

#: post_office/models.py:46 post_office/models.py:193
msgid "Subject"
msgstr "Temat"

#: post_office/models.py:47 post_office/models.py:171
msgid "Message"
msgstr "Wiadomość"

#: post_office/models.py:48
msgid "HTML Message"
msgstr "Wiadomość w HTML"

#: post_office/models.py:55 post_office/models.py:169
msgid "Status"
msgstr "Status"

#: post_office/models.py:58
msgid "Priority"
msgstr "Priorytet"

#: post_office/models.py:63
msgid "The scheduled sending time"
msgstr "Zaplanowany czas wysłania"

#: post_office/models.py:65
msgid "Headers"
msgstr "Nagłówki"

#: post_office/models.py:67
msgid "Email template"
msgstr "Szablon emaila"

#: post_office/models.py:68
msgid "Context"
msgstr "Kontekst"

#: post_office/models.py:69
msgid "Backend alias"
msgstr "Backend alias"

#: post_office/models.py:74
msgctxt "Email address"
msgid "Email"
msgstr "Email"

#: post_office/models.py:75
msgctxt "Email addresses"
msgid "Emails"
msgstr "Emaile"

#: post_office/models.py:167
msgid "Email address"
msgstr "Adres email"

#: post_office/models.py:170
msgid "Exception type"
msgstr "Typ wyjątku"

#: post_office/models.py:175
msgid "Log"
msgstr "Log"

#: post_office/models.py:176
msgid "Logs"
msgstr "Logi"

#: post_office/models.py:187 post_office/models.py:241
msgid "Name"
msgstr "Nazwa"

#: post_office/models.py:187
msgid "e.g: 'welcome_email'"
msgstr "np: 'powitalny_email'"

#: post_office/models.py:189
msgid "Description of this template."
msgstr "Opis tego szablonu"

#: post_office/models.py:195
msgid "Content"
msgstr "Zawartość"

#: post_office/models.py:197
msgid "HTML content"
msgstr "Zawartość w HTML"

#: post_office/models.py:203
msgid "Default template"
msgstr "Domyślna zawartość"

#: post_office/models.py:208
msgid "Email Template"
msgstr "Szablon emaila"

#: post_office/models.py:209
msgid "Email Templates"
msgstr "Szablony emaili"

#: post_office/models.py:240
msgid "File"
msgstr "Plik"

#: post_office/models.py:241
msgid "The original filename"
msgstr "Oryginalna nazwa pliku"

#: post_office/models.py:243
msgid "Email addresses"
msgstr "Adresy email"

#: post_office/models.py:247
msgid "Attachment"
msgstr "Załącznik"

#: post_office/models.py:248
msgid "Attachments"
msgstr "Załączniki"

BIN
thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.mo View File


+ 211
- 0
thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.po View File

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-08-07 16:47+0300\n"
"PO-Revision-Date: 2017-08-07 16:49+0300\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 1.8.11\n"

#: post_office/admin.py:83 post_office/models.py:217
msgid "Render template in alternative language"
msgstr "Отправить письмо на другом языке"

#: post_office/admin.py:84 post_office/models.py:216
msgid "Language"
msgstr "Язык"

#: post_office/admin.py:113
msgid "Default Content"
msgstr "Содержимое по умолчанию"

#: post_office/admin.py:127 post_office/models.py:205
msgid "Description"
msgstr "Описание"

#: post_office/admin.py:133
msgid "Languages"
msgstr "Языки"

#: post_office/apps.py:7
msgid "Post Office"
msgstr "Менеджер почты"

#: post_office/fields.py:10
msgid "Comma-separated emails"
msgstr "Список адресов, разделенных запятыми"

#: post_office/fields.py:19
msgid "Only comma separated emails are allowed."
msgstr "Разрешен только разделенный запятыми список адресов."

#: post_office/models.py:34
msgid "low"
msgstr "низкий"

#: post_office/models.py:34
msgid "medium"
msgstr "средний"

#: post_office/models.py:35
msgid "high"
msgstr "высокий"

#: post_office/models.py:35
msgid "now"
msgstr "сейчас"

#: post_office/models.py:36 post_office/models.py:181
msgid "sent"
msgstr "отправлен"

#: post_office/models.py:36 post_office/models.py:181
msgid "failed"
msgstr "ошибка"

#: post_office/models.py:37
msgid "queued"
msgstr "в очереди"

#: post_office/models.py:39
msgid "Email From"
msgstr "Отправитель"

#: post_office/models.py:41
msgid "Email To"
msgstr "Получатель"

#: post_office/models.py:42
msgid "Cc"
msgstr "Копия"

#: post_office/models.py:44 post_office/models.py:210
msgid "Subject"
msgstr "Тема"

#: post_office/models.py:45 post_office/models.py:188
msgid "Message"
msgstr "Сообщение"

#: post_office/models.py:46
msgid "HTML Message"
msgstr "HTML-сообщение"

#: post_office/models.py:53 post_office/models.py:186
msgid "Status"
msgstr "Статус"

#: post_office/models.py:56
msgid "Priority"
msgstr "Приоритет"

#: post_office/models.py:61
msgid "The scheduled sending time"
msgstr "Запланированное время отправки"

#: post_office/models.py:63
msgid "Headers"
msgstr "Заголовки"

#: post_office/models.py:65
msgid "Email template"
msgstr "Шаблон письма"

#: post_office/models.py:67
msgid "Context"
msgstr "Контекст"

#: post_office/models.py:68
msgid "Backend alias"
msgstr "Имя бекенда"

#: post_office/models.py:73
msgctxt "Email address"
msgid "Email"
msgstr "Письмо"

#: post_office/models.py:74
msgctxt "Email addresses"
msgid "Emails"
msgstr "Письма"

#: post_office/models.py:184
msgid "Email address"
msgstr "Email-адрес"

#: post_office/models.py:187
msgid "Exception type"
msgstr "Тип исключения"

#: post_office/models.py:192
msgid "Log"
msgstr "Лог"

#: post_office/models.py:193
msgid "Logs"
msgstr "Логи"

#: post_office/models.py:204 post_office/models.py:258
msgid "Name"
msgstr "Имя"

#: post_office/models.py:204
msgid "e.g: 'welcome_email'"
msgstr "например: 'welcome_email'"

#: post_office/models.py:206
msgid "Description of this template."
msgstr "Описание шаблона."

#: post_office/models.py:212
msgid "Content"
msgstr "Содержимое"

#: post_office/models.py:214
msgid "HTML content"
msgstr "HTML-содержимое"

#: post_office/models.py:220
msgid "Default template"
msgstr "Шаблон по умолчанию"

#: post_office/models.py:225
msgid "Email Template"
msgstr "Шаблон письма"

#: post_office/models.py:226
msgid "Email Templates"
msgstr "Шаблоны писем"

#: post_office/models.py:257
msgid "File"
msgstr "Файл"

#: post_office/models.py:258
msgid "The original filename"
msgstr "Исходное имя файла"

#: post_office/models.py:260
msgid "Email addresses"
msgstr "Email адреса"

#: post_office/models.py:265
msgid "Attachment"
msgstr "Вложение"

#: post_office/models.py:266
msgid "Attachments"
msgstr "Вложения"

+ 148
- 0
thesisenv/lib/python3.6/site-packages/post_office/lockfile.py View File

# This module is taken from https://gist.github.com/ionrock/3015700

# A file lock implementation that tries to avoid platform specific
# issues. It is inspired by a whole bunch of different implementations
# listed below.

# - https://bitbucket.org/jaraco/yg.lockfile/src/6c448dcbf6e5/yg/lockfile/__init__.py
# - http://svn.zope.org/zc.lockfile/trunk/src/zc/lockfile/__init__.py?rev=121133&view=markup
# - http://stackoverflow.com/questions/489861/locking-a-file-in-python
# - http://www.evanfosmark.com/2009/01/cross-platform-file-locking-support-in-python/
# - http://packages.python.org/lockfile/lockfile.html

# There are some tests below and a blog posting conceptually the
# problems I wanted to try and solve. The tests reflect these ideas.

# - http://ionrock.wordpress.com/2012/06/28/file-locking-in-python/

# I'm not advocating using this package. But if you do happen to try it
# out and have suggestions please let me know.

import os
import time


class FileLocked(Exception):
pass


class FileLock(object):

def __init__(self, lock_filename, timeout=None, force=False):
self.lock_filename = '%s.lock' % lock_filename
self.timeout = timeout
self.force = force
self._pid = str(os.getpid())
# Store pid in a file in the same directory as desired lockname
self.pid_filename = os.path.join(
os.path.dirname(self.lock_filename),
self._pid,
) + '.lock'

def get_lock_pid(self):
try:
return int(open(self.lock_filename).read())
except IOError:
# If we can't read symbolic link, there are two possibilities:
# 1. The symbolic link is dead (point to non existing file)
# 2. Symbolic link is not there
# In either case, we can safely release the lock
self.release()

def valid_lock(self):
"""
See if the lock exists and is left over from an old process.
"""

lock_pid = self.get_lock_pid()

# If we're unable to get lock_pid
if lock_pid is None:
return False

# this is our process
if self._pid == lock_pid:
return True

# it is/was another process
# see if it is running
try:
os.kill(lock_pid, 0)
except OSError:
self.release()
return False

# it is running
return True

def is_locked(self, force=False):
# We aren't locked
if not self.valid_lock():
return False

# We are locked, but we want to force it without waiting
if not self.timeout:
if self.force:
self.release()
return False
else:
# We're not waiting or forcing the lock
raise FileLocked()

# Locked, but want to wait for an unlock
interval = .1
intervals = int(self.timeout / interval)

while intervals:
if self.valid_lock():
intervals -= 1
time.sleep(interval)
#print('stopping %s' % intervals)
else:
return True

# check one last time
if self.valid_lock():
if self.force:
self.release()
else:
# still locked :(
raise FileLocked()

def acquire(self):
"""Create a pid filename and create a symlink (the actual lock file)
across platforms that points to it. Symlink is used because it's an
atomic operation across platforms.
"""

pid_file = os.open(self.pid_filename, os.O_CREAT | os.O_EXCL | os.O_RDWR)
os.write(pid_file, str(os.getpid()).encode('utf-8'))
os.close(pid_file)

if hasattr(os, 'symlink'):
os.symlink(self.pid_filename, self.lock_filename)
else:
# Windows platforms doesn't support symlinks, at least not through the os API
self.lock_filename = self.pid_filename


def release(self):
"""Try to delete the lock files. Doesn't matter if we fail"""
if self.lock_filename != self.pid_filename:
try:
os.unlink(self.lock_filename)
except OSError:
pass

try:
os.remove(self.pid_filename)
except OSError:
pass

def __enter__(self):
if not self.is_locked():
self.acquire()
return self

def __exit__(self, type, value, traceback):
self.release()

+ 37
- 0
thesisenv/lib/python3.6/site-packages/post_office/logutils.py View File

import logging

from .compat import dictConfig


# Taken from https://github.com/nvie/rq/blob/master/rq/logutils.py
def setup_loghandlers(level=None):
# Setup logging for post_office if not already configured
logger = logging.getLogger('post_office')
if not logger.handlers:
dictConfig({
"version": 1,
"disable_existing_loggers": False,

"formatters": {
"post_office": {
"format": "[%(levelname)s]%(asctime)s PID %(process)d: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},

"handlers": {
"post_office": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "post_office"
},
},

"loggers": {
"post_office": {
"handlers": ["post_office"],
"level": level or "DEBUG"
}
}
})
return logger

+ 305
- 0
thesisenv/lib/python3.6/site-packages/post_office/mail.py View File

from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import connection as db_connection
from django.db.models import Q
from django.template import Context, Template
from django.utils.timezone import now

from .connections import connections
from .models import Email, EmailTemplate, Log, PRIORITY, STATUS
from .settings import (get_available_backends, get_batch_size,
get_log_level, get_sending_order, get_threads_per_process)
from .utils import (get_email_template, parse_emails, parse_priority,
split_emails, create_attachments)
from .logutils import setup_loghandlers


logger = setup_loghandlers("INFO")


def create(sender, recipients=None, cc=None, bcc=None, subject='', message='',
html_message='', context=None, scheduled_time=None, headers=None,
template=None, priority=None, render_on_delivery=False, commit=True,
backend=''):
"""
Creates an email from supplied keyword arguments. If template is
specified, email subject and content will be rendered during delivery.
"""
priority = parse_priority(priority)
status = None if priority == PRIORITY.now else STATUS.queued

if recipients is None:
recipients = []
if cc is None:
cc = []
if bcc is None:
bcc = []
if context is None:
context = ''

# If email is to be rendered during delivery, save all necessary
# information
if render_on_delivery:
email = Email(
from_email=sender,
to=recipients,
cc=cc,
bcc=bcc,
scheduled_time=scheduled_time,
headers=headers, priority=priority, status=status,
context=context, template=template, backend_alias=backend
)

else:

if template:
subject = template.subject
message = template.content
html_message = template.html_content

_context = Context(context or {})
subject = Template(subject).render(_context)
message = Template(message).render(_context)
html_message = Template(html_message).render(_context)

email = Email(
from_email=sender,
to=recipients,
cc=cc,
bcc=bcc,
subject=subject,
message=message,
html_message=html_message,
scheduled_time=scheduled_time,
headers=headers, priority=priority, status=status,
backend_alias=backend
)

if commit:
email.save()

return email


def send(recipients=None, sender=None, template=None, context=None, subject='',
message='', html_message='', scheduled_time=None, headers=None,
priority=None, attachments=None, render_on_delivery=False,
log_level=None, commit=True, cc=None, bcc=None, language='',
backend=''):

try:
recipients = parse_emails(recipients)
except ValidationError as e:
raise ValidationError('recipients: %s' % e.message)

try:
cc = parse_emails(cc)
except ValidationError as e:
raise ValidationError('c: %s' % e.message)

try:
bcc = parse_emails(bcc)
except ValidationError as e:
raise ValidationError('bcc: %s' % e.message)

if sender is None:
sender = settings.DEFAULT_FROM_EMAIL

priority = parse_priority(priority)

if log_level is None:
log_level = get_log_level()

if not commit:
if priority == PRIORITY.now:
raise ValueError("send_many() can't be used with priority = 'now'")
if attachments:
raise ValueError("Can't add attachments with send_many()")

if template:
if subject:
raise ValueError('You can\'t specify both "template" and "subject" arguments')
if message:
raise ValueError('You can\'t specify both "template" and "message" arguments')
if html_message:
raise ValueError('You can\'t specify both "template" and "html_message" arguments')

# template can be an EmailTemplate instance or name
if isinstance(template, EmailTemplate):
template = template
# If language is specified, ensure template uses the right language
if language:
if template.language != language:
template = template.translated_templates.get(language=language)
else:
template = get_email_template(template, language)

if backend and backend not in get_available_backends().keys():
raise ValueError('%s is not a valid backend alias' % backend)

email = create(sender, recipients, cc, bcc, subject, message, html_message,
context, scheduled_time, headers, template, priority,
render_on_delivery, commit=commit, backend=backend)

if attachments:
attachments = create_attachments(attachments)
email.attachments.add(*attachments)

if priority == PRIORITY.now:
email.dispatch(log_level=log_level)

return email


def send_many(kwargs_list):
"""
Similar to mail.send(), but this function accepts a list of kwargs.
Internally, it uses Django's bulk_create command for efficiency reasons.
Currently send_many() can't be used to send emails with priority = 'now'.
"""
emails = []
for kwargs in kwargs_list:
emails.append(send(commit=False, **kwargs))
Email.objects.bulk_create(emails)


def get_queued():
"""
Returns a list of emails that should be sent:
- Status is queued
- Has scheduled_time lower than the current time or None
"""
return Email.objects.filter(status=STATUS.queued) \
.select_related('template') \
.filter(Q(scheduled_time__lte=now()) | Q(scheduled_time=None)) \
.order_by(*get_sending_order()).prefetch_related('attachments')[:get_batch_size()]


def send_queued(processes=1, log_level=None):
"""
Sends out all queued mails that has scheduled_time less than now or None
"""
queued_emails = get_queued()
total_sent, total_failed = 0, 0
total_email = len(queued_emails)

logger.info('Started sending %s emails with %s processes.' %
(total_email, processes))

if log_level is None:
log_level = get_log_level()

if queued_emails:

# Don't use more processes than number of emails
if total_email < processes:
processes = total_email

if processes == 1:
total_sent, total_failed = _send_bulk(queued_emails,
uses_multiprocessing=False,
log_level=log_level)
else:
email_lists = split_emails(queued_emails, processes)

pool = Pool(processes)
results = pool.map(_send_bulk, email_lists)
pool.terminate()

total_sent = sum([result[0] for result in results])
total_failed = sum([result[1] for result in results])
message = '%s emails attempted, %s sent, %s failed' % (
total_email,
total_sent,
total_failed
)
logger.info(message)
return (total_sent, total_failed)


def _send_bulk(emails, uses_multiprocessing=True, log_level=None):
# Multiprocessing does not play well with database connection
# Fix: Close connections on forking process
# https://groups.google.com/forum/#!topic/django-users/eCAIY9DAfG0
if uses_multiprocessing:
db_connection.close()

if log_level is None:
log_level = get_log_level()

sent_emails = []
failed_emails = [] # This is a list of two tuples (email, exception)
email_count = len(emails)

logger.info('Process started, sending %s emails' % email_count)

def send(email):
try:
email.dispatch(log_level=log_level, commit=False,
disconnect_after_delivery=False)
sent_emails.append(email)
logger.debug('Successfully sent email #%d' % email.id)
except Exception as e:
logger.debug('Failed to send email #%d' % email.id)
failed_emails.append((email, e))

# Prepare emails before we send these to threads for sending
# So we don't need to access the DB from within threads
for email in emails:
# Sometimes this can fail, for example when trying to render
# email from a faulty Django template
try:
email.prepare_email_message()
except Exception as e:
failed_emails.append((email, e))

number_of_threads = min(get_threads_per_process(), email_count)
pool = ThreadPool(number_of_threads)

pool.map(send, emails)
pool.close()
pool.join()

connections.close()

# Update statuses of sent and failed emails
email_ids = [email.id for email in sent_emails]
Email.objects.filter(id__in=email_ids).update(status=STATUS.sent)

email_ids = [email.id for (email, e) in failed_emails]
Email.objects.filter(id__in=email_ids).update(status=STATUS.failed)

# If log level is 0, log nothing, 1 logs only sending failures
# and 2 means log both successes and failures
if log_level >= 1:

logs = []
for (email, exception) in failed_emails:
logs.append(
Log(email=email, status=STATUS.failed,
message=str(exception),
exception_type=type(exception).__name__)
)

if logs:
Log.objects.bulk_create(logs)

if log_level == 2:

logs = []
for email in sent_emails:
logs.append(Log(email=email, status=STATUS.sent))

if logs:
Log.objects.bulk_create(logs)

logger.info(
'Process finished, %s attempted, %s sent, %s failed' % (
email_count, len(sent_emails), len(failed_emails)
)
)

return len(sent_emails), len(failed_emails)

+ 0
- 0
thesisenv/lib/python3.6/site-packages/post_office/management/__init__.py View File


+ 0
- 0
thesisenv/lib/python3.6/site-packages/post_office/management/commands/__init__.py View File


+ 35
- 0
thesisenv/lib/python3.6/site-packages/post_office/management/commands/cleanup_mail.py View File

import datetime

from django.core.management.base import BaseCommand
from django.utils.timezone import now

from ...models import Attachment, Email


class Command(BaseCommand):
help = 'Place deferred messages back in the queue.'

def add_arguments(self, parser):
parser.add_argument('-d', '--days',
type=int, default=90,
help="Cleanup mails older than this many days, defaults to 90.")

parser.add_argument('-da', '--delete-attachments', action='store_true',
help="Delete orphaned attachments.")

def handle(self, verbosity, days, delete_attachments, **options):
# Delete mails and their related logs and queued created before X days

cutoff_date = now() - datetime.timedelta(days)
count = Email.objects.filter(created__lt=cutoff_date).count()
Email.objects.only('id').filter(created__lt=cutoff_date).delete()
print("Deleted {0} mails created before {1} ".format(count, cutoff_date))

if delete_attachments:
attachments = Attachment.objects.filter(emails=None)
attachments_count = len(attachments)
for attachment in attachments:
# Delete the actual file
attachment.file.delete()
attachments.delete()
print("Deleted {0} attachments".format(attachments_count))

+ 60
- 0
thesisenv/lib/python3.6/site-packages/post_office/management/commands/send_queued_mail.py View File

import tempfile
import sys

from django.core.management.base import BaseCommand
from django.db import connection
from django.db.models import Q
from django.utils.timezone import now

from ...lockfile import FileLock, FileLocked
from ...mail import send_queued
from ...models import Email, STATUS
from ...logutils import setup_loghandlers


logger = setup_loghandlers()
default_lockfile = tempfile.gettempdir() + "/post_office"


class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'-p', '--processes',
type=int,
default=1,
help='Number of processes used to send emails',
)
parser.add_argument(
'-L', '--lockfile',
default=default_lockfile,
help='Absolute path of lockfile to acquire',
)
parser.add_argument(
'-l', '--log-level',
type=int,
help='"0" to log nothing, "1" to only log errors',
)

def handle(self, *args, **options):
logger.info('Acquiring lock for sending queued emails at %s.lock' %
options['lockfile'])
try:
with FileLock(options['lockfile']):

while 1:
try:
send_queued(options['processes'],
options.get('log_level'))
except Exception as e:
logger.error(e, exc_info=sys.exc_info(),
extra={'status_code': 500})
raise

# Close DB connection to avoid multiprocessing errors
connection.close()

if not Email.objects.filter(status=STATUS.queued) \
.filter(Q(scheduled_time__lte=now()) | Q(scheduled_time=None)).exists():
break
except FileLocked:
logger.info('Failed to acquire lock, terminating now.')

+ 93
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0001_initial.py View File

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import jsonfield.fields
import post_office.fields
import post_office.validators
import post_office.models


class Migration(migrations.Migration):

dependencies = [
]

operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('file', models.FileField(upload_to=post_office.models.get_upload_path)),
('name', models.CharField(help_text='The original filename', max_length=255)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Email',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('from_email', models.CharField(max_length=254, validators=[post_office.validators.validate_email_with_name])),
('to', post_office.fields.CommaSeparatedEmailField(blank=True)),
('cc', post_office.fields.CommaSeparatedEmailField(blank=True)),
('bcc', post_office.fields.CommaSeparatedEmailField(blank=True)),
('subject', models.CharField(max_length=255, blank=True)),
('message', models.TextField(blank=True)),
('html_message', models.TextField(blank=True)),
('status', models.PositiveSmallIntegerField(blank=True, null=True, db_index=True, choices=[(0, 'sent'), (1, 'failed'), (2, 'queued')])),
('priority', models.PositiveSmallIntegerField(blank=True, null=True, choices=[(0, 'low'), (1, 'medium'), (2, 'high'), (3, 'now')])),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('last_updated', models.DateTimeField(auto_now=True, db_index=True)),
('scheduled_time', models.DateTimeField(db_index=True, null=True, blank=True)),
('headers', jsonfield.fields.JSONField(null=True, blank=True)),
('context', jsonfield.fields.JSONField(null=True, blank=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='EmailTemplate',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(help_text=b"e.g: 'welcome_email'", max_length=255)),
('description', models.TextField(help_text='Description of this template.', blank=True)),
('subject', models.CharField(blank=True, max_length=255, validators=[post_office.validators.validate_template_syntax])),
('content', models.TextField(blank=True, validators=[post_office.validators.validate_template_syntax])),
('html_content', models.TextField(blank=True, validators=[post_office.validators.validate_template_syntax])),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Log',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('date', models.DateTimeField(auto_now_add=True)),
('status', models.PositiveSmallIntegerField(choices=[(0, 'sent'), (1, 'failed')])),
('exception_type', models.CharField(max_length=255, blank=True)),
('message', models.TextField()),
('email', models.ForeignKey(related_name='logs', editable=False, on_delete=models.deletion.CASCADE, to='post_office.Email', )),
],
options={
},
bases=(models.Model,),
),
migrations.AddField(
model_name='email',
name='template',
field=models.ForeignKey(blank=True, on_delete=models.deletion.SET_NULL, to='post_office.EmailTemplate', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='attachment',
name='emails',
field=models.ManyToManyField(related_name='attachments', to='post_office.Email'),
preserve_default=True,
),
]

+ 89
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0002_add_i18n_and_backend_alias.py View File

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import post_office.validators
import post_office.fields


class Migration(migrations.Migration):

dependencies = [
('post_office', '0001_initial'),
]

operations = [
migrations.AlterModelOptions(
name='emailtemplate',
options={'verbose_name': 'Email Template', 'verbose_name_plural': 'Email Templates'},
),
migrations.AddField(
model_name='email',
name='backend_alias',
field=models.CharField(default='', max_length=64, blank=True),
),
migrations.AddField(
model_name='emailtemplate',
name='default_template',
field=models.ForeignKey(related_name='translated_templates', default=None, to='post_office.EmailTemplate', null=True, on_delete=models.deletion.SET_NULL),
),
migrations.AddField(
model_name='emailtemplate',
name='language',
field=models.CharField(default='', help_text='Render template in alternative language', max_length=12, blank=True, choices=[(b'af', b'Afrikaans'), (b'ar', b'Arabic'), (b'ast', b'Asturian'), (b'az', b'Azerbaijani'), (b'bg', b'Bulgarian'), (b'be', b'Belarusian'), (b'bn', b'Bengali'), (b'br', b'Breton'), (b'bs', b'Bosnian'), (b'ca', b'Catalan'), (b'cs', b'Czech'), (b'cy', b'Welsh'), (b'da', b'Danish'), (b'de', b'German'), (b'el', b'Greek'), (b'en', b'English'), (b'en-au', b'Australian English'), (b'en-gb', b'British English'), (b'eo', b'Esperanto'), (b'es', b'Spanish'), (b'es-ar', b'Argentinian Spanish'), (b'es-mx', b'Mexican Spanish'), (b'es-ni', b'Nicaraguan Spanish'), (b'es-ve', b'Venezuelan Spanish'), (b'et', b'Estonian'), (b'eu', b'Basque'), (b'fa', b'Persian'), (b'fi', b'Finnish'), (b'fr', b'French'), (b'fy', b'Frisian'), (b'ga', b'Irish'), (b'gl', b'Galician'), (b'he', b'Hebrew'), (b'hi', b'Hindi'), (b'hr', b'Croatian'), (b'hu', b'Hungarian'), (b'ia', b'Interlingua'), (b'id', b'Indonesian'), (b'io', b'Ido'), (b'is', b'Icelandic'), (b'it', b'Italian'), (b'ja', b'Japanese'), (b'ka', b'Georgian'), (b'kk', b'Kazakh'), (b'km', b'Khmer'), (b'kn', b'Kannada'), (b'ko', b'Korean'), (b'lb', b'Luxembourgish'), (b'lt', b'Lithuanian'), (b'lv', b'Latvian'), (b'mk', b'Macedonian'), (b'ml', b'Malayalam'), (b'mn', b'Mongolian'), (b'mr', b'Marathi'), (b'my', b'Burmese'), (b'nb', b'Norwegian Bokmal'), (b'ne', b'Nepali'), (b'nl', b'Dutch'), (b'nn', b'Norwegian Nynorsk'), (b'os', b'Ossetic'), (b'pa', b'Punjabi'), (b'pl', b'Polish'), (b'pt', b'Portuguese'), (b'pt-br', b'Brazilian Portuguese'), (b'ro', b'Romanian'), (b'ru', b'Russian'), (b'sk', b'Slovak'), (b'sl', b'Slovenian'), (b'sq', b'Albanian'), (b'sr', b'Serbian'), (b'sr-latn', b'Serbian Latin'), (b'sv', b'Swedish'), (b'sw', b'Swahili'), (b'ta', b'Tamil'), (b'te', b'Telugu'), (b'th', b'Thai'), (b'tr', b'Turkish'), (b'tt', b'Tatar'), (b'udm', b'Udmurt'), (b'uk', b'Ukrainian'), (b'ur', b'Urdu'), (b'vi', b'Vietnamese'), (b'zh-cn', b'Simplified Chinese'), (b'zh-hans', b'Simplified Chinese'), (b'zh-hant', b'Traditional Chinese'), (b'zh-tw', b'Traditional Chinese')]),
),
migrations.AlterField(
model_name='email',
name='bcc',
field=post_office.fields.CommaSeparatedEmailField(verbose_name='Bcc', blank=True),
),
migrations.AlterField(
model_name='email',
name='cc',
field=post_office.fields.CommaSeparatedEmailField(verbose_name='Cc', blank=True),
),
migrations.AlterField(
model_name='email',
name='from_email',
field=models.CharField(max_length=254, verbose_name='Email From', validators=[post_office.validators.validate_email_with_name]),
),
migrations.AlterField(
model_name='email',
name='html_message',
field=models.TextField(verbose_name='HTML Message', blank=True),
),
migrations.AlterField(
model_name='email',
name='message',
field=models.TextField(verbose_name='Message', blank=True),
),
migrations.AlterField(
model_name='email',
name='subject',
field=models.CharField(max_length=255, verbose_name='Subject', blank=True),
),
migrations.AlterField(
model_name='email',
name='to',
field=post_office.fields.CommaSeparatedEmailField(verbose_name='Email To', blank=True),
),
migrations.AlterField(
model_name='emailtemplate',
name='content',
field=models.TextField(blank=True, verbose_name='Content', validators=[post_office.validators.validate_template_syntax]),
),
migrations.AlterField(
model_name='emailtemplate',
name='html_content',
field=models.TextField(blank=True, verbose_name='HTML content', validators=[post_office.validators.validate_template_syntax]),
),
migrations.AlterField(
model_name='emailtemplate',
name='subject',
field=models.CharField(blank=True, max_length=255, verbose_name='Subject', validators=[post_office.validators.validate_template_syntax]),
),
migrations.AlterUniqueTogether(
name='emailtemplate',
unique_together=set([('language', 'default_template')]),
),
]

+ 25
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0003_longer_subject.py View File

# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-02-04 08:08
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('post_office', '0002_add_i18n_and_backend_alias'),
]

operations = [
migrations.AlterField(
model_name='email',
name='subject',
field=models.CharField(blank=True, max_length=989, verbose_name='Subject'),
),
migrations.AlterField(
model_name='emailtemplate',
name='language',
field=models.CharField(blank=True, choices=[(b'af', b'Afrikaans'), (b'ar', b'Arabic'), (b'ast', b'Asturian'), (b'az', b'Azerbaijani'), (b'bg', b'Bulgarian'), (b'be', b'Belarusian'), (b'bn', b'Bengali'), (b'br', b'Breton'), (b'bs', b'Bosnian'), (b'ca', b'Catalan'), (b'cs', b'Czech'), (b'cy', b'Welsh'), (b'da', b'Danish'), (b'de', b'German'), (b'el', b'Greek'), (b'en', b'English'), (b'en-au', b'Australian English'), (b'en-gb', b'British English'), (b'eo', b'Esperanto'), (b'es', b'Spanish'), (b'es-ar', b'Argentinian Spanish'), (b'es-co', b'Colombian Spanish'), (b'es-mx', b'Mexican Spanish'), (b'es-ni', b'Nicaraguan Spanish'), (b'es-ve', b'Venezuelan Spanish'), (b'et', b'Estonian'), (b'eu', b'Basque'), (b'fa', b'Persian'), (b'fi', b'Finnish'), (b'fr', b'French'), (b'fy', b'Frisian'), (b'ga', b'Irish'), (b'gd', b'Scottish Gaelic'), (b'gl', b'Galician'), (b'he', b'Hebrew'), (b'hi', b'Hindi'), (b'hr', b'Croatian'), (b'hu', b'Hungarian'), (b'ia', b'Interlingua'), (b'id', b'Indonesian'), (b'io', b'Ido'), (b'is', b'Icelandic'), (b'it', b'Italian'), (b'ja', b'Japanese'), (b'ka', b'Georgian'), (b'kk', b'Kazakh'), (b'km', b'Khmer'), (b'kn', b'Kannada'), (b'ko', b'Korean'), (b'lb', b'Luxembourgish'), (b'lt', b'Lithuanian'), (b'lv', b'Latvian'), (b'mk', b'Macedonian'), (b'ml', b'Malayalam'), (b'mn', b'Mongolian'), (b'mr', b'Marathi'), (b'my', b'Burmese'), (b'nb', b'Norwegian Bokmal'), (b'ne', b'Nepali'), (b'nl', b'Dutch'), (b'nn', b'Norwegian Nynorsk'), (b'os', b'Ossetic'), (b'pa', b'Punjabi'), (b'pl', b'Polish'), (b'pt', b'Portuguese'), (b'pt-br', b'Brazilian Portuguese'), (b'ro', b'Romanian'), (b'ru', b'Russian'), (b'sk', b'Slovak'), (b'sl', b'Slovenian'), (b'sq', b'Albanian'), (b'sr', b'Serbian'), (b'sr-latn', b'Serbian Latin'), (b'sv', b'Swedish'), (b'sw', b'Swahili'), (b'ta', b'Tamil'), (b'te', b'Telugu'), (b'th', b'Thai'), (b'tr', b'Turkish'), (b'tt', b'Tatar'), (b'udm', b'Udmurt'), (b'uk', b'Ukrainian'), (b'ur', b'Urdu'), (b'vi', b'Vietnamese'), (b'zh-hans', b'Simplified Chinese'), (b'zh-hant', b'Traditional Chinese')], default='', help_text='Render template in alternative language', max_length=12),
),
]

+ 120
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0004_auto_20160607_0901.py View File

# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-06-07 07:01
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import jsonfield.fields
import post_office.models


class Migration(migrations.Migration):

dependencies = [
('post_office', '0003_longer_subject'),
]

operations = [
migrations.AlterModelOptions(
name='attachment',
options={'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'},
),
migrations.AlterModelOptions(
name='email',
options={'verbose_name': 'Email', 'verbose_name_plural': 'Emails'},
),
migrations.AlterModelOptions(
name='log',
options={'verbose_name': 'Log', 'verbose_name_plural': 'Logs'},
),
migrations.AlterField(
model_name='attachment',
name='emails',
field=models.ManyToManyField(related_name='attachments', to='post_office.Email', verbose_name='Email addresses'),
),
migrations.AlterField(
model_name='attachment',
name='file',
field=models.FileField(upload_to=post_office.models.get_upload_path, verbose_name='File'),
),
migrations.AlterField(
model_name='attachment',
name='name',
field=models.CharField(help_text='The original filename', max_length=255, verbose_name='Name'),
),
migrations.AlterField(
model_name='email',
name='backend_alias',
field=models.CharField(blank=True, default='', max_length=64, verbose_name='Backend alias'),
),
migrations.AlterField(
model_name='email',
name='context',
field=jsonfield.fields.JSONField(blank=True, null=True, verbose_name='Context'),
),
migrations.AlterField(
model_name='email',
name='headers',
field=jsonfield.fields.JSONField(blank=True, null=True, verbose_name='Headers'),
),
migrations.AlterField(
model_name='email',
name='priority',
field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'low'), (1, 'medium'), (2, 'high'), (3, 'now')], null=True, verbose_name='Priority'),
),
migrations.AlterField(
model_name='email',
name='scheduled_time',
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='The scheduled sending time'),
),
migrations.AlterField(
model_name='email',
name='status',
field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'sent'), (1, 'failed'), (2, 'queued')], db_index=True, null=True, verbose_name='Status'),
),
migrations.AlterField(
model_name='email',
name='template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='post_office.EmailTemplate', verbose_name='Email template'),
),
migrations.AlterField(
model_name='emailtemplate',
name='default_template',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translated_templates', to='post_office.EmailTemplate', verbose_name='Default template'),
),
migrations.AlterField(
model_name='emailtemplate',
name='description',
field=models.TextField(blank=True, help_text='Description of this template.', verbose_name='Description'),
),
migrations.AlterField(
model_name='emailtemplate',
name='language',
field=models.CharField(blank=True, default='', help_text='Render template in alternative language', max_length=12, verbose_name='Language'),
),
migrations.AlterField(
model_name='emailtemplate',
name='name',
field=models.CharField(help_text="e.g: 'welcome_email'", max_length=255, verbose_name='Name'),
),
migrations.AlterField(
model_name='log',
name='email',
field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='post_office.Email', verbose_name='Email address'),
),
migrations.AlterField(
model_name='log',
name='exception_type',
field=models.CharField(blank=True, max_length=255, verbose_name='Exception type'),
),
migrations.AlterField(
model_name='log',
name='message',
field=models.TextField(verbose_name='Message'),
),
migrations.AlterField(
model_name='log',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'sent'), (1, 'failed')], verbose_name='Status'),
),
]

+ 19
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0005_auto_20170515_0013.py View File

# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-15 00:13
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('post_office', '0004_auto_20160607_0901'),
]

operations = [
migrations.AlterUniqueTogether(
name='emailtemplate',
unique_together=set([('name', 'language', 'default_template')]),
),
]

+ 19
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0006_attachment_mimetype.py View File

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('post_office', '0005_auto_20170515_0013'),
]

operations = [
migrations.AddField(
model_name='attachment',
name='mimetype',
field=models.CharField(default='', max_length=255, blank=True),
),
]

+ 19
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/0007_auto_20170731_1342.py View File

# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-31 11:42
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('post_office', '0006_attachment_mimetype'),
]

operations = [
migrations.AlterModelOptions(
name='emailtemplate',
options={'ordering': ['name'], 'verbose_name': 'Email Template', 'verbose_name_plural': 'Email Templates'},
),
]

+ 0
- 0
thesisenv/lib/python3.6/site-packages/post_office/migrations/__init__.py View File


+ 284
- 0
thesisenv/lib/python3.6/site-packages/post_office/models.py View File

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import os

from collections import namedtuple
from uuid import uuid4

from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db import models
from django.template import Context, Template
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from jsonfield import JSONField

from post_office import cache
from post_office.fields import CommaSeparatedEmailField

from .compat import text_type, smart_text
from .connections import connections
from .settings import context_field_class, get_log_level
from .validators import validate_email_with_name, validate_template_syntax


PRIORITY = namedtuple('PRIORITY', 'low medium high now')._make(range(4))
STATUS = namedtuple('STATUS', 'sent failed queued')._make(range(3))


@python_2_unicode_compatible
class Email(models.Model):
"""
A model to hold email information.
"""

PRIORITY_CHOICES = [(PRIORITY.low, _("low")), (PRIORITY.medium, _("medium")),
(PRIORITY.high, _("high")), (PRIORITY.now, _("now"))]
STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed")),
(STATUS.queued, _("queued"))]

from_email = models.CharField(_("Email From"), max_length=254,
validators=[validate_email_with_name])
to = CommaSeparatedEmailField(_("Email To"))
cc = CommaSeparatedEmailField(_("Cc"))
bcc = CommaSeparatedEmailField(_("Bcc"))
subject = models.CharField(_("Subject"), max_length=989, blank=True)
message = models.TextField(_("Message"), blank=True)
html_message = models.TextField(_("HTML Message"), blank=True)
"""
Emails with 'queued' status will get processed by ``send_queued`` command.
Status field will then be set to ``failed`` or ``sent`` depending on
whether it's successfully delivered.
"""
status = models.PositiveSmallIntegerField(
_("Status"),
choices=STATUS_CHOICES, db_index=True,
blank=True, null=True)
priority = models.PositiveSmallIntegerField(_("Priority"),
choices=PRIORITY_CHOICES,
blank=True, null=True)
created = models.DateTimeField(auto_now_add=True, db_index=True)
last_updated = models.DateTimeField(db_index=True, auto_now=True)
scheduled_time = models.DateTimeField(_('The scheduled sending time'),
blank=True, null=True, db_index=True)
headers = JSONField(_('Headers'), blank=True, null=True)
template = models.ForeignKey('post_office.EmailTemplate', blank=True,
null=True, verbose_name=_('Email template'),
on_delete=models.CASCADE)
context = context_field_class(_('Context'), blank=True, null=True)
backend_alias = models.CharField(_('Backend alias'), blank=True, default='',
max_length=64)

class Meta:
app_label = 'post_office'
verbose_name = pgettext_lazy("Email address", "Email")
verbose_name_plural = pgettext_lazy("Email addresses", "Emails")

def __init__(self, *args, **kwargs):
super(Email, self).__init__(*args, **kwargs)
self._cached_email_message = None

def __str__(self):
return u'%s' % self.to

def email_message(self):
"""
Returns Django EmailMessage object for sending.
"""
if self._cached_email_message:
return self._cached_email_message

return self.prepare_email_message()

def prepare_email_message(self):
"""
Returns a django ``EmailMessage`` or ``EmailMultiAlternatives`` object,
depending on whether html_message is empty.
"""
subject = smart_text(self.subject)

if self.template is not None:
_context = Context(self.context)
subject = Template(self.template.subject).render(_context)
message = Template(self.template.content).render(_context)
html_message = Template(self.template.html_content).render(_context)

else:
subject = self.subject
message = self.message
html_message = self.html_message

connection = connections[self.backend_alias or 'default']

if html_message:
msg = EmailMultiAlternatives(
subject=subject, body=message, from_email=self.from_email,
to=self.to, bcc=self.bcc, cc=self.cc,
headers=self.headers, connection=connection)
msg.attach_alternative(html_message, "text/html")
else:
msg = EmailMessage(
subject=subject, body=message, from_email=self.from_email,
to=self.to, bcc=self.bcc, cc=self.cc,
headers=self.headers, connection=connection)

for attachment in self.attachments.all():
msg.attach(attachment.name, attachment.file.read(), mimetype=attachment.mimetype or None)
attachment.file.close()

self._cached_email_message = msg
return msg

def dispatch(self, log_level=None,
disconnect_after_delivery=True, commit=True):
"""
Sends email and log the result.
"""
try:
self.email_message().send()
status = STATUS.sent
message = ''
exception_type = ''
except Exception as e:
status = STATUS.failed
message = str(e)
exception_type = type(e).__name__

# If run in a bulk sending mode, reraise and let the outer
# layer handle the exception
if not commit:
raise

if commit:
self.status = status
self.save(update_fields=['status'])

if log_level is None:
log_level = get_log_level()

# If log level is 0, log nothing, 1 logs only sending failures
# and 2 means log both successes and failures
if log_level == 1:
if status == STATUS.failed:
self.logs.create(status=status, message=message,
exception_type=exception_type)
elif log_level == 2:
self.logs.create(status=status, message=message,
exception_type=exception_type)

return status

def save(self, *args, **kwargs):
self.full_clean()
return super(Email, self).save(*args, **kwargs)


@python_2_unicode_compatible
class Log(models.Model):
"""
A model to record sending email sending activities.
"""

STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed"))]

email = models.ForeignKey(Email, editable=False, related_name='logs',
verbose_name=_('Email address'), on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True)
status = models.PositiveSmallIntegerField(_('Status'), choices=STATUS_CHOICES)
exception_type = models.CharField(_('Exception type'), max_length=255, blank=True)
message = models.TextField(_('Message'))

class Meta:
app_label = 'post_office'
verbose_name = _("Log")
verbose_name_plural = _("Logs")

def __str__(self):
return text_type(self.date)


class EmailTemplateManager(models.Manager):
def get_by_natural_key(self, name, language, default_template):
return self.get(name=name, language=language, default_template=default_template)


@python_2_unicode_compatible
class EmailTemplate(models.Model):
"""
Model to hold template information from db
"""
name = models.CharField(_('Name'), max_length=255, help_text=_("e.g: 'welcome_email'"))
description = models.TextField(_('Description'), blank=True,
help_text=_("Description of this template."))
created = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
subject = models.CharField(max_length=255, blank=True,
verbose_name=_("Subject"), validators=[validate_template_syntax])
content = models.TextField(blank=True,
verbose_name=_("Content"), validators=[validate_template_syntax])
html_content = models.TextField(blank=True,
verbose_name=_("HTML content"), validators=[validate_template_syntax])
language = models.CharField(max_length=12,
verbose_name=_("Language"),
help_text=_("Render template in alternative language"),
default='', blank=True)
default_template = models.ForeignKey('self', related_name='translated_templates',
null=True, default=None, verbose_name=_('Default template'), on_delete=models.CASCADE)

objects = EmailTemplateManager()

class Meta:
app_label = 'post_office'
unique_together = ('name', 'language', 'default_template')
verbose_name = _("Email Template")
verbose_name_plural = _("Email Templates")
ordering = ['name']

def __str__(self):
return u'%s %s' % (self.name, self.language)

def natural_key(self):
return (self.name, self.language, self.default_template)

def save(self, *args, **kwargs):
# If template is a translation, use default template's name
if self.default_template and not self.name:
self.name = self.default_template.name

template = super(EmailTemplate, self).save(*args, **kwargs)
cache.delete(self.name)
return template


def get_upload_path(instance, filename):
"""Overriding to store the original filename"""
if not instance.name:
instance.name = filename # set original filename
date = timezone.now().date()
filename = '{name}.{ext}'.format(name=uuid4().hex,
ext=filename.split('.')[-1])

return os.path.join('post_office_attachments', str(date.year),
str(date.month), str(date.day), filename)


@python_2_unicode_compatible
class Attachment(models.Model):
"""
A model describing an email attachment.
"""
file = models.FileField(_('File'), upload_to=get_upload_path)
name = models.CharField(_('Name'), max_length=255, help_text=_("The original filename"))
emails = models.ManyToManyField(Email, related_name='attachments',
verbose_name=_('Email addresses'))
mimetype = models.CharField(max_length=255, default='', blank=True)

class Meta:
app_label = 'post_office'
verbose_name = _("Attachment")
verbose_name_plural = _("Attachments")

def __str__(self):
return self.name

+ 95
- 0
thesisenv/lib/python3.6/site-packages/post_office/settings.py View File

import warnings

from django.conf import settings
from django.core.cache.backends.base import InvalidCacheBackendError

from .compat import import_attribute, get_cache


def get_backend(alias='default'):
return get_available_backends()[alias]


def get_available_backends():
""" Returns a dictionary of defined backend classes. For example:
{
'default': 'django.core.mail.backends.smtp.EmailBackend',
'locmem': 'django.core.mail.backends.locmem.EmailBackend',
}
"""
backends = get_config().get('BACKENDS', {})

if backends:
return backends

# Try to get backend settings from old style
# POST_OFFICE = {
# 'EMAIL_BACKEND': 'mybackend'
# }
backend = get_config().get('EMAIL_BACKEND')
if backend:
warnings.warn('Please use the new POST_OFFICE["BACKENDS"] settings',
DeprecationWarning)

backends['default'] = backend
return backends

# Fall back to Django's EMAIL_BACKEND definition
backends['default'] = getattr(
settings, 'EMAIL_BACKEND',
'django.core.mail.backends.smtp.EmailBackend')

# If EMAIL_BACKEND is set to use PostOfficeBackend
# and POST_OFFICE_BACKEND is not set, fall back to SMTP
if 'post_office.EmailBackend' in backends['default']:
backends['default'] = 'django.core.mail.backends.smtp.EmailBackend'

return backends


def get_cache_backend():
if hasattr(settings, 'CACHES'):
if "post_office" in settings.CACHES:
return get_cache("post_office")
else:
# Sometimes this raises InvalidCacheBackendError, which is ok too
try:
return get_cache("default")
except InvalidCacheBackendError:
pass
return None


def get_config():
"""
Returns Post Office's configuration in dictionary format. e.g:
POST_OFFICE = {
'BATCH_SIZE': 1000
}
"""
return getattr(settings, 'POST_OFFICE', {})


def get_batch_size():
return get_config().get('BATCH_SIZE', 100)


def get_threads_per_process():
return get_config().get('THREADS_PER_PROCESS', 5)


def get_default_priority():
return get_config().get('DEFAULT_PRIORITY', 'medium')


def get_log_level():
return get_config().get('LOG_LEVEL', 2)


def get_sending_order():
return get_config().get('SENDING_ORDER', ['-priority'])


CONTEXT_FIELD_CLASS = get_config().get('CONTEXT_FIELD_CLASS',
'jsonfield.JSONField')
context_field_class = import_attribute(CONTEXT_FIELD_CLASS)

+ 84
- 0
thesisenv/lib/python3.6/site-packages/post_office/test_settings.py View File

# -*- coding: utf-8 -*-


import django
from distutils.version import StrictVersion

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
},
}

# Default values: True
# POST_OFFICE_CACHE = True
# POST_OFFICE_TEMPLATE_CACHE = True


CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 36000,
'KEY_PREFIX': 'post-office',
},
'post_office': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 36000,
'KEY_PREFIX': 'post-office',
}
}

POST_OFFICE = {
'BACKENDS': {
'default': 'django.core.mail.backends.dummy.EmailBackend',
'locmem': 'django.core.mail.backends.locmem.EmailBackend',
'error': 'post_office.tests.test_backends.ErrorRaisingBackend',
'smtp': 'django.core.mail.backends.smtp.EmailBackend',
'connection_tester': 'post_office.tests.test_mail.ConnectionTestingBackend',
}
}


INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'post_office',
)

SECRET_KEY = 'a'

ROOT_URLCONF = 'post_office.test_urls'

DEFAULT_FROM_EMAIL = 'webmaster@example.com'

if StrictVersion(str(django.get_version())) < '1.10':
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
else:
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
},
},
]

+ 6
- 0
thesisenv/lib/python3.6/site-packages/post_office/test_urls.py View File

from django.conf.urls import url
from django.contrib import admin

urlpatterns = [
url(r'^admin/', admin.site.urls),
]

+ 8
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/__init__.py View File

from .test_backends import BackendTest
from .test_commands import CommandTest
from .test_lockfile import LockTest
from .test_mail import MailTest
from .test_models import ModelTest
from .test_utils import UtilsTest
from .test_cache import CacheTest
from .test_views import AdminViewTest

+ 113
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_backends.py View File

from django.conf import settings
from django.core.mail import EmailMultiAlternatives, send_mail, EmailMessage
from django.core.mail.backends.base import BaseEmailBackend
from django.test import TestCase
from django.test.utils import override_settings

from ..models import Email, STATUS, PRIORITY
from ..settings import get_backend


class ErrorRaisingBackend(BaseEmailBackend):
"""
An EmailBackend that always raises an error during sending
to test if django_mailer handles sending error correctly
"""

def send_messages(self, email_messages):
raise Exception('Fake Error')


class BackendTest(TestCase):

@override_settings(EMAIL_BACKEND='post_office.EmailBackend')
def test_email_backend(self):
"""
Ensure that email backend properly queue email messages.
"""
send_mail('Test', 'Message', 'from@example.com', ['to@example.com'])
email = Email.objects.latest('id')
self.assertEqual(email.subject, 'Test')
self.assertEqual(email.status, STATUS.queued)
self.assertEqual(email.priority, PRIORITY.medium)

def test_email_backend_setting(self):
"""

"""
old_email_backend = getattr(settings, 'EMAIL_BACKEND', None)
old_post_office_backend = getattr(settings, 'POST_OFFICE_BACKEND', None)
if hasattr(settings, 'EMAIL_BACKEND'):
delattr(settings, 'EMAIL_BACKEND')
if hasattr(settings, 'POST_OFFICE_BACKEND'):
delattr(settings, 'POST_OFFICE_BACKEND')

previous_settings = settings.POST_OFFICE
delattr(settings, 'POST_OFFICE')
# If no email backend is set, backend should default to SMTP
self.assertEqual(get_backend(), 'django.core.mail.backends.smtp.EmailBackend')

# If EMAIL_BACKEND is set to PostOfficeBackend, use SMTP to send by default
setattr(settings, 'EMAIL_BACKEND', 'post_office.EmailBackend')
self.assertEqual(get_backend(), 'django.core.mail.backends.smtp.EmailBackend')

# If EMAIL_BACKEND is set on new dictionary-styled settings, use that
setattr(settings, 'POST_OFFICE', {'EMAIL_BACKEND': 'test'})
self.assertEqual(get_backend(), 'test')
delattr(settings, 'POST_OFFICE')

if old_email_backend:
setattr(settings, 'EMAIL_BACKEND', old_email_backend)
else:
delattr(settings, 'EMAIL_BACKEND')
setattr(settings, 'POST_OFFICE', previous_settings)

@override_settings(EMAIL_BACKEND='post_office.EmailBackend')
def test_sending_html_email(self):
"""
"text/html" attachments to Email should be persisted into the database
"""
message = EmailMultiAlternatives('subject', 'body', 'from@example.com',
['recipient@example.com'])
message.attach_alternative('html', "text/html")
message.send()
email = Email.objects.latest('id')
self.assertEqual(email.html_message, 'html')

@override_settings(EMAIL_BACKEND='post_office.EmailBackend')
def test_headers_sent(self):
"""
Test that headers are correctly set on the outgoing emails.
"""
message = EmailMessage('subject', 'body', 'from@example.com',
['recipient@example.com'],
headers={'Reply-To': 'reply@example.com'})
message.send()
email = Email.objects.latest('id')
self.assertEqual(email.headers, {'Reply-To': 'reply@example.com'})

@override_settings(EMAIL_BACKEND='post_office.EmailBackend')
def test_backend_attachments(self):
message = EmailMessage('subject', 'body', 'from@example.com',
['recipient@example.com'])

message.attach('attachment.txt', 'attachment content')
message.send()

email = Email.objects.latest('id')
self.assertEqual(email.attachments.count(), 1)
self.assertEqual(email.attachments.all()[0].name, 'attachment.txt')
self.assertEqual(email.attachments.all()[0].file.read(), b'attachment content')

@override_settings(
EMAIL_BACKEND='post_office.EmailBackend',
POST_OFFICE={
'DEFAULT_PRIORITY': 'now',
'BACKENDS': {'default': 'django.core.mail.backends.dummy.EmailBackend'}
}
)
def test_default_priority_now(self):
# If DEFAULT_PRIORITY is "now", mails should be sent right away
send_mail('Test', 'Message', 'from1@example.com', ['to@example.com'])
email = Email.objects.latest('id')
self.assertEqual(email.status, STATUS.sent)

+ 41
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_cache.py View File

from django.conf import settings
from django.test import TestCase

from post_office import cache
from ..settings import get_cache_backend


class CacheTest(TestCase):

def test_get_backend_settings(self):
"""Test basic get backend function and its settings"""
# Sanity check
self.assertTrue('post_office' in settings.CACHES)
self.assertTrue(get_cache_backend())

# If no post office key is defined, it should return default
del(settings.CACHES['post_office'])
self.assertTrue(get_cache_backend())

# If no caches key in settings, it should return None
delattr(settings, 'CACHES')
self.assertEqual(None, get_cache_backend())

def test_get_cache_key(self):
"""
Test for converting names to cache key
"""
self.assertEqual('post_office:template:test', cache.get_cache_key('test'))
self.assertEqual('post_office:template:test-slugify', cache.get_cache_key('test slugify'))

def test_basic_cache_operations(self):
"""
Test basic cache operations
"""
# clean test cache
cache.cache_backend.clear()
self.assertEqual(None, cache.get('test-cache'))
cache.set('test-cache', 'awesome content')
self.assertTrue('awesome content', cache.get('test-cache'))
cache.delete('test-cache')
self.assertEqual(None, cache.get('test-cache'))

+ 150
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_commands.py View File

import datetime
import os

from django.core.files.base import ContentFile
from django.core.management import call_command
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.timezone import now

from ..models import Attachment, Email, STATUS


class CommandTest(TestCase):

def test_cleanup_mail_with_orphaned_attachments(self):
self.assertEqual(Email.objects.count(), 0)
email = Email.objects.create(to=['to@example.com'],
from_email='from@example.com',
subject='Subject')

email.created = now() - datetime.timedelta(31)
email.save()

attachment = Attachment()
attachment.file.save(
'test.txt', content=ContentFile('test file content'), save=True
)
email.attachments.add(attachment)
attachment_path = attachment.file.name

# We have orphaned attachment now
call_command('cleanup_mail', days=30)
self.assertEqual(Email.objects.count(), 0)
self.assertEqual(Attachment.objects.count(), 1)

# Actually cleanup orphaned attachments
call_command('cleanup_mail', '-da', days=30)
self.assertEqual(Email.objects.count(), 0)
self.assertEqual(Attachment.objects.count(), 0)

# Check that the actual file has been deleted as well
self.assertFalse(os.path.exists(attachment_path))

# Check if the email attachment's actual file have been deleted
Email.objects.all().delete()
email = Email.objects.create(to=['to@example.com'],
from_email='from@example.com',
subject='Subject')
email.created = now() - datetime.timedelta(31)
email.save()

attachment = Attachment()
attachment.file.save(
'test.txt', content=ContentFile('test file content'), save=True
)
email.attachments.add(attachment)
attachment_path = attachment.file.name

# Simulate that the files have been deleted by accidents
os.remove(attachment_path)

# No exceptions should break the cleanup
call_command('cleanup_mail', '-da', days=30)
self.assertEqual(Email.objects.count(), 0)
self.assertEqual(Attachment.objects.count(), 0)


def test_cleanup_mail(self):
"""
The ``cleanup_mail`` command deletes mails older than a specified
amount of days
"""
self.assertEqual(Email.objects.count(), 0)

# The command shouldn't delete today's email
email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'])
call_command('cleanup_mail', days=30)
self.assertEqual(Email.objects.count(), 1)

# Email older than 30 days should be deleted
email.created = now() - datetime.timedelta(31)
email.save()
call_command('cleanup_mail', days=30)
self.assertEqual(Email.objects.count(), 0)

TEST_SETTINGS = {
'BACKENDS': {
'default': 'django.core.mail.backends.dummy.EmailBackend',
},
'BATCH_SIZE': 1
}

@override_settings(POST_OFFICE=TEST_SETTINGS)
def test_send_queued_mail(self):
"""
Ensure that ``send_queued_mail`` behaves properly and sends all queued
emails in two batches.
"""
# Make sure that send_queued_mail with empty queue does not raise error
call_command('send_queued_mail', processes=1)

Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued)
Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued)
call_command('send_queued_mail', processes=1)
self.assertEqual(Email.objects.filter(status=STATUS.sent).count(), 2)
self.assertEqual(Email.objects.filter(status=STATUS.queued).count(), 0)

def test_successful_deliveries_logging(self):
"""
Successful deliveries are only logged when log_level is 2.
"""
email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued)
call_command('send_queued_mail', log_level=0)
self.assertEqual(email.logs.count(), 0)

email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued)
call_command('send_queued_mail', log_level=1)
self.assertEqual(email.logs.count(), 0)

email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued)
call_command('send_queued_mail', log_level=2)
self.assertEqual(email.logs.count(), 1)

def test_failed_deliveries_logging(self):
"""
Failed deliveries are logged when log_level is 1 and 2.
"""
email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued,
backend_alias='error')
call_command('send_queued_mail', log_level=0)
self.assertEqual(email.logs.count(), 0)

email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued,
backend_alias='error')
call_command('send_queued_mail', log_level=1)
self.assertEqual(email.logs.count(), 1)

email = Email.objects.create(from_email='from@example.com',
to=['to@example.com'], status=STATUS.queued,
backend_alias='error')
call_command('send_queued_mail', log_level=2)
self.assertEqual(email.logs.count(), 1)

+ 13
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_connections.py View File

from django.core.mail import backends
from django.test import TestCase

from .test_backends import ErrorRaisingBackend
from ..connections import connections


class ConnectionTest(TestCase):

def test_get_connection(self):
# Ensure ConnectionHandler returns the right connection
self.assertTrue(isinstance(connections['error'], ErrorRaisingBackend))
self.assertTrue(isinstance(connections['locmem'], backends.locmem.EmailBackend))

+ 75
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_lockfile.py View File

import time
import os

from django.test import TestCase

from ..lockfile import FileLock, FileLocked


def setup_fake_lock(lock_file_name):
pid = os.getpid()
lockfile = '%s.lock' % pid
try:
os.remove(lock_file_name)
except OSError:
pass
os.symlink(lockfile, lock_file_name)


class LockTest(TestCase):

def test_process_killed_force_unlock(self):
pid = os.getpid()
lockfile = '%s.lock' % pid
setup_fake_lock('test.lock')

with open(lockfile, 'w+') as f:
f.write('9999999')
assert os.path.exists(lockfile)
with FileLock('test'):
assert True

def test_force_unlock_in_same_process(self):
pid = os.getpid()
lockfile = '%s.lock' % pid
os.symlink(lockfile, 'test.lock')

with open(lockfile, 'w+') as f:
f.write(str(os.getpid()))

with FileLock('test', force=True):
assert True

def test_exception_after_timeout(self):
pid = os.getpid()
lockfile = '%s.lock' % pid
setup_fake_lock('test.lock')

with open(lockfile, 'w+') as f:
f.write(str(os.getpid()))

try:
with FileLock('test', timeout=1):
assert False
except FileLocked:
assert True

def test_force_after_timeout(self):
pid = os.getpid()
lockfile = '%s.lock' % pid
setup_fake_lock('test.lock')

with open(lockfile, 'w+') as f:
f.write(str(os.getpid()))

timeout = 1
start = time.time()
with FileLock('test', timeout=timeout, force=True):
assert True
end = time.time()
assert end - start > timeout

def test_get_lock_pid(self):
"""Ensure get_lock_pid() works properly"""
with FileLock('test', timeout=1, force=True) as lock:
self.assertEqual(lock.get_lock_pid(), int(os.getpid()))

+ 400
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_mail.py View File

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import date, datetime

from django.core import mail
from django.core.files.base import ContentFile
from django.conf import settings

from django.test import TestCase
from django.test.utils import override_settings

from ..settings import get_batch_size, get_log_level, get_threads_per_process
from ..models import Email, EmailTemplate, Attachment, PRIORITY, STATUS
from ..mail import (create, get_queued,
send, send_many, send_queued, _send_bulk)


connection_counter = 0


class ConnectionTestingBackend(mail.backends.base.BaseEmailBackend):
'''
An EmailBackend that increments a global counter when connection is opened
'''

def open(self):
global connection_counter
connection_counter += 1

def send_messages(self, email_messages):
pass


class MailTest(TestCase):

@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
def test_send_queued_mail(self):
"""
Check that only queued messages are sent.
"""
kwargs = {
'to': ['to@example.com'],
'from_email': 'bob@example.com',
'subject': 'Test',
'message': 'Message',
}
failed_mail = Email.objects.create(status=STATUS.failed, **kwargs)
none_mail = Email.objects.create(status=None, **kwargs)

# This should be the only email that gets sent
queued_mail = Email.objects.create(status=STATUS.queued, **kwargs)
send_queued()
self.assertNotEqual(Email.objects.get(id=failed_mail.id).status, STATUS.sent)
self.assertNotEqual(Email.objects.get(id=none_mail.id).status, STATUS.sent)
self.assertEqual(Email.objects.get(id=queued_mail.id).status, STATUS.sent)

@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
def test_send_queued_mail_multi_processes(self):
"""
Check that send_queued works well with multiple processes
"""
kwargs = {
'to': ['to@example.com'],
'from_email': 'bob@example.com',
'subject': 'Test',
'message': 'Message',
'status': STATUS.queued
}

# All three emails should be sent
self.assertEqual(Email.objects.filter(status=STATUS.sent).count(), 0)
for i in range(3):
Email.objects.create(**kwargs)
total_sent, total_failed = send_queued(processes=2)
self.assertEqual(total_sent, 3)

def test_send_bulk(self):
"""
Ensure _send_bulk() properly sends out emails.
"""
email = Email.objects.create(
to=['to@example.com'], from_email='bob@example.com',
subject='send bulk', message='Message', status=STATUS.queued,
backend_alias='locmem')
_send_bulk([email], uses_multiprocessing=False)
self.assertEqual(Email.objects.get(id=email.id).status, STATUS.sent)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'send bulk')

@override_settings(EMAIL_BACKEND='post_office.tests.test_mail.ConnectionTestingBackend')
def test_send_bulk_reuses_open_connection(self):
"""
Ensure _send_bulk() only opens connection once to send multiple emails.
"""
global connection_counter
self.assertEqual(connection_counter, 0)
email = Email.objects.create(to=['to@example.com'],
from_email='bob@example.com', subject='',
message='', status=STATUS.queued, backend_alias='connection_tester')
email_2 = Email.objects.create(to=['to@example.com'],
from_email='bob@example.com', subject='',
message='', status=STATUS.queued,
backend_alias='connection_tester')
_send_bulk([email, email_2])
self.assertEqual(connection_counter, 1)

def test_get_queued(self):
"""
Ensure get_queued returns only emails that should be sent
"""
kwargs = {
'to': 'to@example.com',
'from_email': 'bob@example.com',
'subject': 'Test',
'message': 'Message',
}
self.assertEqual(list(get_queued()), [])

# Emails with statuses failed, sent or None shouldn't be returned
Email.objects.create(status=STATUS.failed, **kwargs)
Email.objects.create(status=None, **kwargs)
Email.objects.create(status=STATUS.sent, **kwargs)
self.assertEqual(list(get_queued()), [])

# Email with queued status and None as scheduled_time should be included
queued_email = Email.objects.create(status=STATUS.queued,
scheduled_time=None, **kwargs)
self.assertEqual(list(get_queued()), [queued_email])

# Email scheduled for the future should not be included
Email.objects.create(status=STATUS.queued,
scheduled_time=date(2020, 12, 13), **kwargs)
self.assertEqual(list(get_queued()), [queued_email])

# Email scheduled in the past should be included
past_email = Email.objects.create(status=STATUS.queued,
scheduled_time=date(2010, 12, 13), **kwargs)
self.assertEqual(list(get_queued()), [queued_email, past_email])

def test_get_batch_size(self):
"""
Ensure BATCH_SIZE setting is read correctly.
"""
previous_settings = settings.POST_OFFICE
self.assertEqual(get_batch_size(), 100)
setattr(settings, 'POST_OFFICE', {'BATCH_SIZE': 10})
self.assertEqual(get_batch_size(), 10)
settings.POST_OFFICE = previous_settings

def test_get_threads_per_process(self):
"""
Ensure THREADS_PER_PROCESS setting is read correctly.
"""
previous_settings = settings.POST_OFFICE
self.assertEqual(get_threads_per_process(), 5)
setattr(settings, 'POST_OFFICE', {'THREADS_PER_PROCESS': 10})
self.assertEqual(get_threads_per_process(), 10)
settings.POST_OFFICE = previous_settings

def test_get_log_level(self):
"""
Ensure LOG_LEVEL setting is read correctly.
"""
previous_settings = settings.POST_OFFICE
self.assertEqual(get_log_level(), 2)
setattr(settings, 'POST_OFFICE', {'LOG_LEVEL': 1})
self.assertEqual(get_log_level(), 1)
# Restore ``LOG_LEVEL``
setattr(settings, 'POST_OFFICE', {'LOG_LEVEL': 2})
settings.POST_OFFICE = previous_settings

def test_create(self):
"""
Test basic email creation
"""

# Test that email is persisted only when commit=True
email = create(
sender='from@example.com', recipients=['to@example.com'],
commit=False
)
self.assertEqual(email.id, None)
email = create(
sender='from@example.com', recipients=['to@example.com'],
commit=True
)
self.assertNotEqual(email.id, None)

# Test that email is created with the right status
email = create(
sender='from@example.com', recipients=['to@example.com'],
priority=PRIORITY.now
)
self.assertEqual(email.status, None)
email = create(
sender='from@example.com', recipients=['to@example.com'],
priority=PRIORITY.high
)
self.assertEqual(email.status, STATUS.queued)

# Test that email is created with the right content
context = {
'subject': 'My subject',
'message': 'My message',
'html': 'My html',
}
now = datetime.now()
email = create(
sender='from@example.com', recipients=['to@example.com'],
subject='Test {{ subject }}', message='Test {{ message }}',
html_message='Test {{ html }}', context=context,
scheduled_time=now, headers={'header': 'Test header'},
)
self.assertEqual(email.from_email, 'from@example.com')
self.assertEqual(email.to, ['to@example.com'])
self.assertEqual(email.subject, 'Test My subject')
self.assertEqual(email.message, 'Test My message')
self.assertEqual(email.html_message, 'Test My html')
self.assertEqual(email.scheduled_time, now)
self.assertEqual(email.headers, {'header': 'Test header'})

def test_send_many(self):
"""Test send_many creates the right emails """
kwargs_list = [
{'sender': 'from@example.com', 'recipients': ['a@example.com']},
{'sender': 'from@example.com', 'recipients': ['b@example.com']},
]
send_many(kwargs_list)
self.assertEqual(Email.objects.filter(to=['a@example.com']).count(), 1)

def test_send_with_attachments(self):
attachments = {
'attachment_file1.txt': ContentFile('content'),
'attachment_file2.txt': ContentFile('content'),
}
email = send(recipients=['a@example.com', 'b@example.com'],
sender='from@example.com', message='message',
subject='subject', attachments=attachments)

self.assertTrue(email.pk)
self.assertEqual(email.attachments.count(), 2)

def test_send_with_render_on_delivery(self):
"""
Ensure that mail.send() create email instances with appropriate
fields being saved
"""
template = EmailTemplate.objects.create(
subject='Subject {{ name }}',
content='Content {{ name }}',
html_content='HTML {{ name }}'
)
context = {'name': 'test'}
email = send(recipients=['a@example.com', 'b@example.com'],
template=template, context=context,
render_on_delivery=True)
self.assertEqual(email.subject, '')
self.assertEqual(email.message, '')
self.assertEqual(email.html_message, '')
self.assertEqual(email.template, template)

# context shouldn't be persisted when render_on_delivery = False
email = send(recipients=['a@example.com'],
template=template, context=context,
render_on_delivery=False)
self.assertEqual(email.context, None)

def test_send_with_attachments_multiple_recipients(self):
"""Test reusing the same attachment objects for several email objects"""
attachments = {
'attachment_file1.txt': ContentFile('content'),
'attachment_file2.txt': ContentFile('content'),
}
email = send(recipients=['a@example.com', 'b@example.com'],
sender='from@example.com', message='message',
subject='subject', attachments=attachments)

self.assertEqual(email.attachments.count(), 2)
self.assertEqual(Attachment.objects.count(), 2)

def test_create_with_template(self):
"""If render_on_delivery is True, subject and content
won't be rendered, context also won't be saved."""

template = EmailTemplate.objects.create(
subject='Subject {{ name }}',
content='Content {{ name }}',
html_content='HTML {{ name }}'
)
context = {'name': 'test'}
email = create(
sender='from@example.com', recipients=['to@example.com'],
template=template, context=context, render_on_delivery=True
)
self.assertEqual(email.subject, '')
self.assertEqual(email.message, '')
self.assertEqual(email.html_message, '')
self.assertEqual(email.context, context)
self.assertEqual(email.template, template)

def test_create_with_template_and_empty_context(self):
"""If render_on_delivery is False, subject and content
will be rendered, context won't be saved."""

template = EmailTemplate.objects.create(
subject='Subject {% now "Y" %}',
content='Content {% now "Y" %}',
html_content='HTML {% now "Y" %}'
)
context = None
email = create(
sender='from@example.com', recipients=['to@example.com'],
template=template, context=context
)
today = date.today()
current_year = today.year
self.assertEqual(email.subject, 'Subject %d' % current_year)
self.assertEqual(email.message, 'Content %d' % current_year)
self.assertEqual(email.html_message, 'HTML %d' % current_year)
self.assertEqual(email.context, None)
self.assertEqual(email.template, None)

def test_backend_alias(self):
"""Test backend_alias field is properly set."""

email = send(recipients=['a@example.com'],
sender='from@example.com', message='message',
subject='subject')
self.assertEqual(email.backend_alias, '')

email = send(recipients=['a@example.com'],
sender='from@example.com', message='message',
subject='subject', backend='locmem')
self.assertEqual(email.backend_alias, 'locmem')

with self.assertRaises(ValueError):
send(recipients=['a@example.com'], sender='from@example.com',
message='message', subject='subject', backend='foo')

@override_settings(LANGUAGES=(('en', 'English'), ('ru', 'Russian')))
def test_send_with_template(self):
"""If render_on_delivery is False, subject and content
will be rendered, context won't be saved."""

template = EmailTemplate.objects.create(
subject='Subject {{ name }}',
content='Content {{ name }}',
html_content='HTML {{ name }}'
)
russian_template = EmailTemplate(
default_template=template,
language='ru',
subject='предмет {{ name }}',
content='содержание {{ name }}',
html_content='HTML {{ name }}'
)
russian_template.save()

context = {'name': 'test'}
email = send(recipients=['to@example.com'], sender='from@example.com',
template=template, context=context)
email = Email.objects.get(id=email.id)
self.assertEqual(email.subject, 'Subject test')
self.assertEqual(email.message, 'Content test')
self.assertEqual(email.html_message, 'HTML test')
self.assertEqual(email.context, None)
self.assertEqual(email.template, None)

# check, if we use the Russian version
email = send(recipients=['to@example.com'], sender='from@example.com',
template=russian_template, context=context)
email = Email.objects.get(id=email.id)
self.assertEqual(email.subject, 'предмет test')
self.assertEqual(email.message, 'содержание test')
self.assertEqual(email.html_message, 'HTML test')
self.assertEqual(email.context, None)
self.assertEqual(email.template, None)

# Check that send picks template with the right language
email = send(recipients=['to@example.com'], sender='from@example.com',
template=template, context=context, language='ru')
email = Email.objects.get(id=email.id)
self.assertEqual(email.subject, 'предмет test')

email = send(recipients=['to@example.com'], sender='from@example.com',
template=template, context=context, language='ru',
render_on_delivery=True)
self.assertEqual(email.template.language, 'ru')

def test_send_bulk_with_faulty_template(self):
template = EmailTemplate.objects.create(
subject='{% if foo %}Subject {{ name }}',
content='Content {{ name }}',
html_content='HTML {{ name }}'
)
email = Email.objects.create(to='to@example.com', from_email='from@example.com',
template=template, status=STATUS.queued)
_send_bulk([email], uses_multiprocessing=False)
email = Email.objects.get(id=email.id)
self.assertEqual(email.status, STATUS.failed)

+ 332
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_models.py View File

import django
import json
import os

from datetime import datetime, timedelta

from django.conf import settings as django_settings
from django.core import mail
from django.core import serializers
from django.core.files.base import ContentFile
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.forms.models import modelform_factory
from django.test import TestCase
from django.utils import timezone

from ..models import Email, Log, PRIORITY, STATUS, EmailTemplate, Attachment
from ..mail import send


class ModelTest(TestCase):

def test_email_message(self):
"""
Test to make sure that model's "email_message" method
returns proper email classes.
"""

# If ``html_message`` is set, ``EmailMultiAlternatives`` is expected
email = Email.objects.create(to=['to@example.com'],
from_email='from@example.com', subject='Subject',
message='Message', html_message='<p>HTML</p>')
message = email.email_message()
self.assertEqual(type(message), EmailMultiAlternatives)
self.assertEqual(message.from_email, 'from@example.com')
self.assertEqual(message.to, ['to@example.com'])
self.assertEqual(message.subject, 'Subject')
self.assertEqual(message.body, 'Message')
self.assertEqual(message.alternatives, [('<p>HTML</p>', 'text/html')])

# Without ``html_message``, ``EmailMessage`` class is expected
email = Email.objects.create(to=['to@example.com'],
from_email='from@example.com', subject='Subject',
message='Message')
message = email.email_message()
self.assertEqual(type(message), EmailMessage)
self.assertEqual(message.from_email, 'from@example.com')
self.assertEqual(message.to, ['to@example.com'])
self.assertEqual(message.subject, 'Subject')
self.assertEqual(message.body, 'Message')

def test_email_message_render(self):
"""
Ensure Email instance with template is properly rendered.
"""
template = EmailTemplate.objects.create(
subject='Subject {{ name }}',
content='Content {{ name }}',
html_content='HTML {{ name }}'
)
context = {'name': 'test'}
email = Email.objects.create(to=['to@example.com'], template=template,
from_email='from@e.com', context=context)
message = email.email_message()
self.assertEqual(message.subject, 'Subject test')
self.assertEqual(message.body, 'Content test')
self.assertEqual(message.alternatives[0][0], 'HTML test')

def test_dispatch(self):
"""
Ensure that email.dispatch() actually sends out the email
"""
email = Email.objects.create(to=['to@example.com'], from_email='from@example.com',
subject='Test dispatch', message='Message', backend_alias='locmem')
email.dispatch()
self.assertEqual(mail.outbox[0].subject, 'Test dispatch')

def test_status_and_log(self):
"""
Ensure that status and log are set properly on successful sending
"""
email = Email.objects.create(to=['to@example.com'], from_email='from@example.com',
subject='Test', message='Message', backend_alias='locmem', id=333)
# Ensure that after dispatch status and logs are correctly set
email.dispatch()
log = Log.objects.latest('id')
self.assertEqual(email.status, STATUS.sent)
self.assertEqual(log.email, email)

def test_status_and_log_on_error(self):
"""
Ensure that status and log are set properly on sending failure
"""
email = Email.objects.create(to=['to@example.com'], from_email='from@example.com',
subject='Test', message='Message',
backend_alias='error')
# Ensure that after dispatch status and logs are correctly set
email.dispatch()
log = Log.objects.latest('id')
self.assertEqual(email.status, STATUS.failed)
self.assertEqual(log.email, email)
self.assertEqual(log.status, STATUS.failed)
self.assertEqual(log.message, 'Fake Error')
self.assertEqual(log.exception_type, 'Exception')

def test_errors_while_getting_connection_are_logged(self):
"""
Ensure that status and log are set properly on sending failure
"""
email = Email.objects.create(to=['to@example.com'], subject='Test',
from_email='from@example.com',
message='Message', backend_alias='random')
# Ensure that after dispatch status and logs are correctly set
email.dispatch()
log = Log.objects.latest('id')
self.assertEqual(email.status, STATUS.failed)
self.assertEqual(log.email, email)
self.assertEqual(log.status, STATUS.failed)
self.assertIn('is not a valid', log.message)

def test_default_sender(self):
email = send(['to@example.com'], subject='foo')
self.assertEqual(email.from_email,
django_settings.DEFAULT_FROM_EMAIL)

def test_send_argument_checking(self):
"""
mail.send() should raise an Exception if:
- "template" is used with "subject", "message" or "html_message"
- recipients is not in tuple or list format
"""
self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com',
template='foo', subject='bar')
self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com',
template='foo', message='bar')
self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com',
template='foo', html_message='bar')
self.assertRaises(ValueError, send, 'to@example.com', 'from@a.com',
template='foo', html_message='bar')
self.assertRaises(ValueError, send, cc='cc@example.com', sender='from@a.com',
template='foo', html_message='bar')
self.assertRaises(ValueError, send, bcc='bcc@example.com', sender='from@a.com',
template='foo', html_message='bar')

def test_send_with_template(self):
"""
Ensure mail.send correctly creates templated emails to recipients
"""
Email.objects.all().delete()
headers = {'Reply-to': 'reply@email.com'}
email_template = EmailTemplate.objects.create(name='foo', subject='bar',
content='baz')
scheduled_time = datetime.now() + timedelta(days=1)
email = send(recipients=['to1@example.com', 'to2@example.com'], sender='from@a.com',
headers=headers, template=email_template,
scheduled_time=scheduled_time)
self.assertEqual(email.to, ['to1@example.com', 'to2@example.com'])
self.assertEqual(email.headers, headers)
self.assertEqual(email.scheduled_time, scheduled_time)

# Test without header
Email.objects.all().delete()
email = send(recipients=['to1@example.com', 'to2@example.com'], sender='from@a.com',
template=email_template)
self.assertEqual(email.to, ['to1@example.com', 'to2@example.com'])
self.assertEqual(email.headers, None)

def test_send_without_template(self):
headers = {'Reply-to': 'reply@email.com'}
scheduled_time = datetime.now() + timedelta(days=1)
email = send(sender='from@a.com',
recipients=['to1@example.com', 'to2@example.com'],
cc=['cc1@example.com', 'cc2@example.com'],
bcc=['bcc1@example.com', 'bcc2@example.com'],
subject='foo', message='bar', html_message='baz',
context={'name': 'Alice'}, headers=headers,
scheduled_time=scheduled_time, priority=PRIORITY.low)

self.assertEqual(email.to, ['to1@example.com', 'to2@example.com'])
self.assertEqual(email.cc, ['cc1@example.com', 'cc2@example.com'])
self.assertEqual(email.bcc, ['bcc1@example.com', 'bcc2@example.com'])
self.assertEqual(email.subject, 'foo')
self.assertEqual(email.message, 'bar')
self.assertEqual(email.html_message, 'baz')
self.assertEqual(email.headers, headers)
self.assertEqual(email.priority, PRIORITY.low)
self.assertEqual(email.scheduled_time, scheduled_time)

# Same thing, but now with context
email = send(['to1@example.com'], 'from@a.com',
subject='Hi {{ name }}', message='Message {{ name }}',
html_message='<b>{{ name }}</b>',
context={'name': 'Bob'}, headers=headers)
self.assertEqual(email.to, ['to1@example.com'])
self.assertEqual(email.subject, 'Hi Bob')
self.assertEqual(email.message, 'Message Bob')
self.assertEqual(email.html_message, '<b>Bob</b>')
self.assertEqual(email.headers, headers)

def test_invalid_syntax(self):
"""
Ensures that invalid template syntax will result in validation errors
when saving a ModelForm of an EmailTemplate.
"""
data = dict(
name='cost',
subject='Hi there!{{ }}',
content='Welcome {{ name|titl }} to the site.',
html_content='{% block content %}<h1>Welcome to the site</h1>'
)

EmailTemplateForm = modelform_factory(EmailTemplate,
exclude=['template'])
form = EmailTemplateForm(data)

self.assertFalse(form.is_valid())

self.assertEqual(form.errors['default_template'], [u'This field is required.'])
self.assertEqual(form.errors['content'], [u"Invalid filter: 'titl'"])
self.assertIn(form.errors['html_content'],
[[u'Unclosed tags: endblock '],
[u"Unclosed tag on line 1: 'block'. Looking for one of: endblock."]])
self.assertIn(form.errors['subject'],
[[u'Empty variable tag'], [u'Empty variable tag on line 1']])

def test_string_priority(self):
"""
Regression test for:
https://github.com/ui/django-post_office/issues/23
"""
email = send(['to1@example.com'], 'from@a.com', priority='low')
self.assertEqual(email.priority, PRIORITY.low)

def test_default_priority(self):
email = send(recipients=['to1@example.com'], sender='from@a.com')
self.assertEqual(email.priority, PRIORITY.medium)

def test_string_priority_exception(self):
invalid_priority_send = lambda: send(['to1@example.com'], 'from@a.com', priority='hgh')

with self.assertRaises(ValueError) as context:
invalid_priority_send()

self.assertEqual(
str(context.exception),
'Invalid priority, must be one of: low, medium, high, now'
)

def test_send_recipient_display_name(self):
"""
Regression test for:
https://github.com/ui/django-post_office/issues/73
"""
email = send(recipients=['Alice Bob <email@example.com>'], sender='from@a.com')
self.assertTrue(email.to)

def test_attachment_filename(self):
attachment = Attachment()

attachment.file.save(
'test.txt',
content=ContentFile('test file content'),
save=True
)
self.assertEqual(attachment.name, 'test.txt')

# Test that it is saved to the correct subdirectory
date = timezone.now().date()
expected_path = os.path.join('post_office_attachments', str(date.year),
str(date.month), str(date.day))
self.assertTrue(expected_path in attachment.file.name)

def test_attachments_email_message(self):
email = Email.objects.create(to=['to@example.com'],
from_email='from@example.com',
subject='Subject')

attachment = Attachment()
attachment.file.save(
'test.txt', content=ContentFile('test file content'), save=True
)
email.attachments.add(attachment)
message = email.email_message()

# https://docs.djangoproject.com/en/1.11/releases/1.11/#email
if django.VERSION >= (1, 11,):
self.assertEqual(message.attachments,
[('test.txt', 'test file content', 'text/plain')])
else:
self.assertEqual(message.attachments,
[('test.txt', b'test file content', None)])

def test_attachments_email_message_with_mimetype(self):
email = Email.objects.create(to=['to@example.com'],
from_email='from@example.com',
subject='Subject')

attachment = Attachment()
attachment.file.save(
'test.txt', content=ContentFile('test file content'), save=True
)
attachment.mimetype = 'text/plain'
attachment.save()
email.attachments.add(attachment)
message = email.email_message()

if django.VERSION >= (1, 11,):
self.assertEqual(message.attachments,
[('test.txt', 'test file content', 'text/plain')])
else:
self.assertEqual(message.attachments,
[('test.txt', b'test file content', 'text/plain')])

def test_translated_template_uses_default_templates_name(self):
template = EmailTemplate.objects.create(name='name')
id_template = template.translated_templates.create(language='id')
self.assertEqual(id_template.name, template.name)

def test_models_repr(self):
self.assertEqual(repr(EmailTemplate(name='test', language='en')),
'<EmailTemplate: test en>')
self.assertEqual(repr(Email(to=['test@example.com'])),
"<Email: ['test@example.com']>")

def test_natural_key(self):
template = EmailTemplate.objects.create(name='name')
self.assertEqual(template, EmailTemplate.objects.get_by_natural_key(*template.natural_key()))

data = serializers.serialize('json', [template], use_natural_primary_keys=True)
self.assertNotIn('pk', json.loads(data)[0])
deserialized_objects = serializers.deserialize('json', data, use_natural_primary_keys=True)
list(deserialized_objects)[0].save()
self.assertEqual(EmailTemplate.objects.count(), 1)

+ 203
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_utils.py View File

from django.core.files.base import ContentFile
from django.core.exceptions import ValidationError

from django.test import TestCase
from django.test.utils import override_settings

from ..models import Email, STATUS, PRIORITY, EmailTemplate, Attachment
from ..utils import (create_attachments, get_email_template, parse_emails,
parse_priority, send_mail, split_emails)
from ..validators import validate_email_with_name, validate_comma_separated_emails


@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
class UtilsTest(TestCase):

def test_mail_status(self):
"""
Check that send_mail assigns the right status field to Email instances
"""
send_mail('subject', 'message', 'from@example.com', ['to@example.com'],
priority=PRIORITY.medium)
email = Email.objects.latest('id')
self.assertEqual(email.status, STATUS.queued)

# Emails sent with "now" priority is sent right away
send_mail('subject', 'message', 'from@example.com', ['to@example.com'],
priority=PRIORITY.now)
email = Email.objects.latest('id')
self.assertEqual(email.status, STATUS.sent)

def test_email_validator(self):
# These should validate
validate_email_with_name('email@example.com')
validate_email_with_name('Alice Bob <email@example.com>')
Email.objects.create(to=['to@example.com'], from_email='Alice <from@example.com>',
subject='Test', message='Message', status=STATUS.sent)

# Should also support international domains
validate_email_with_name('Alice Bob <email@example.co.id>')

# These should raise ValidationError
self.assertRaises(ValidationError, validate_email_with_name, 'invalid')
self.assertRaises(ValidationError, validate_email_with_name, 'Al <ab>')

def test_comma_separated_email_list_validator(self):
# These should validate
validate_comma_separated_emails(['email@example.com'])
validate_comma_separated_emails(
['email@example.com', 'email2@example.com', 'email3@example.com']
)
validate_comma_separated_emails(['Alice Bob <email@example.com>'])

# Should also support international domains
validate_comma_separated_emails(['email@example.co.id'])

# These should raise ValidationError
self.assertRaises(ValidationError, validate_comma_separated_emails,
['email@example.com', 'invalid_mail', 'email@example.com'])

def test_get_template_email(self):
# Sanity Check
name = 'customer/happy-holidays'
self.assertRaises(EmailTemplate.DoesNotExist, get_email_template, name)
template = EmailTemplate.objects.create(name=name, content='test')

# First query should hit database
self.assertNumQueries(1, lambda: get_email_template(name))
# Second query should hit cache instead
self.assertNumQueries(0, lambda: get_email_template(name))

# It should return the correct template
self.assertEqual(template, get_email_template(name))

# Repeat with language support
template = EmailTemplate.objects.create(name=name, content='test',
language='en')
# First query should hit database
self.assertNumQueries(1, lambda: get_email_template(name, 'en'))
# Second query should hit cache instead
self.assertNumQueries(0, lambda: get_email_template(name, 'en'))

# It should return the correct template
self.assertEqual(template, get_email_template(name, 'en'))

def test_template_caching_settings(self):
"""Check if POST_OFFICE_CACHE and POST_OFFICE_TEMPLATE_CACHE understood
correctly
"""
def is_cache_used(suffix='', desired_cache=False):
"""Raise exception if real cache usage not equal to desired_cache value
"""
# to avoid cache cleaning - just create new template
name = 'can_i/suport_cache_settings%s' % suffix
self.assertRaises(
EmailTemplate.DoesNotExist, get_email_template, name
)
EmailTemplate.objects.create(name=name, content='test')

# First query should hit database anyway
self.assertNumQueries(1, lambda: get_email_template(name))
# Second query should hit cache instead only if we want it
self.assertNumQueries(
0 if desired_cache else 1,
lambda: get_email_template(name)
)
return

# default - use cache
is_cache_used(suffix='with_default_cache', desired_cache=True)

# disable cache
with self.settings(POST_OFFICE_CACHE=False):
is_cache_used(suffix='cache_disabled_global', desired_cache=False)
with self.settings(POST_OFFICE_TEMPLATE_CACHE=False):
is_cache_used(
suffix='cache_disabled_for_templates', desired_cache=False
)
with self.settings(POST_OFFICE_CACHE=True, POST_OFFICE_TEMPLATE_CACHE=False):
is_cache_used(
suffix='cache_disabled_for_templates_but_enabled_global',
desired_cache=False
)
return

def test_split_emails(self):
"""
Check that split emails correctly divide email lists for multiprocessing
"""
for i in range(225):
Email.objects.create(from_email='from@example.com', to=['to@example.com'])
expected_size = [57, 56, 56, 56]
email_list = split_emails(Email.objects.all(), 4)
self.assertEqual(expected_size, [len(emails) for emails in email_list])

def test_create_attachments(self):
attachments = create_attachments({
'attachment_file1.txt': ContentFile('content'),
'attachment_file2.txt': ContentFile('content'),
})

self.assertEqual(len(attachments), 2)
self.assertIsInstance(attachments[0], Attachment)
self.assertTrue(attachments[0].pk)
self.assertEqual(attachments[0].file.read(), b'content')
self.assertTrue(attachments[0].name.startswith('attachment_file'))
self.assertEquals(attachments[0].mimetype, u'')

def test_create_attachments_with_mimetype(self):
attachments = create_attachments({
'attachment_file1.txt': {
'file': ContentFile('content'),
'mimetype': 'text/plain'
},
'attachment_file2.jpg': {
'file': ContentFile('content'),
'mimetype': 'text/plain'
}
})

self.assertEqual(len(attachments), 2)
self.assertIsInstance(attachments[0], Attachment)
self.assertTrue(attachments[0].pk)
self.assertEquals(attachments[0].file.read(), b'content')
self.assertTrue(attachments[0].name.startswith('attachment_file'))
self.assertEquals(attachments[0].mimetype, 'text/plain')

def test_create_attachments_open_file(self):
attachments = create_attachments({
'attachment_file.py': __file__,
})

self.assertEqual(len(attachments), 1)
self.assertIsInstance(attachments[0], Attachment)
self.assertTrue(attachments[0].pk)
self.assertTrue(attachments[0].file.read())
self.assertEquals(attachments[0].name, 'attachment_file.py')
self.assertEquals(attachments[0].mimetype, u'')

def test_parse_priority(self):
self.assertEqual(parse_priority('now'), PRIORITY.now)
self.assertEqual(parse_priority('high'), PRIORITY.high)
self.assertEqual(parse_priority('medium'), PRIORITY.medium)
self.assertEqual(parse_priority('low'), PRIORITY.low)

def test_parse_emails(self):
# Converts a single email to list of email
self.assertEqual(
parse_emails('test@example.com'),
['test@example.com']
)

# None is converted into an empty list
self.assertEqual(parse_emails(None), [])

# Raises ValidationError if email is invalid
self.assertRaises(
ValidationError,
parse_emails, 'invalid_email'
)
self.assertRaises(
ValidationError,
parse_emails, ['invalid_email', 'test@example.com']
)

+ 35
- 0
thesisenv/lib/python3.6/site-packages/post_office/tests/test_views.py View File

from django.contrib.auth.models import User
from django.test.client import Client
from django.test import TestCase

try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse

from post_office import mail
from post_office.models import Email


admin_username = 'real_test_admin'
admin_email = 'read@admin.com'
admin_pass = 'admin_pass'


class AdminViewTest(TestCase):
def setUp(self):
user = User.objects.create_superuser(admin_username, admin_email, admin_pass)
self.client = Client()
self.client.login(username=user.username, password=admin_pass)

# Small test to make sure the admin interface is loaded
def test_admin_interface(self):
response = self.client.get(reverse('admin:index'))
self.assertEqual(response.status_code, 200)

def test_admin_change_page(self):
"""Ensure that changing an email object in admin works."""
mail.send(recipients=['test@example.com'], headers={'foo': 'bar'})
email = Email.objects.latest('id')
response = self.client.get(reverse('admin:post_office_email_change', args=[email.id]))
self.assertEqual(response.status_code, 200)

+ 138
- 0
thesisenv/lib/python3.6/site-packages/post_office/utils.py View File

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files import File
from django.utils.encoding import force_text

from post_office import cache
from .compat import string_types
from .models import Email, PRIORITY, STATUS, EmailTemplate, Attachment
from .settings import get_default_priority
from .validators import validate_email_with_name


def send_mail(subject, message, from_email, recipient_list, html_message='',
scheduled_time=None, headers=None, priority=PRIORITY.medium):
"""
Add a new message to the mail queue. This is a replacement for Django's
``send_mail`` core email method.
"""

subject = force_text(subject)
status = None if priority == PRIORITY.now else STATUS.queued
emails = []
for address in recipient_list:
emails.append(
Email.objects.create(
from_email=from_email, to=address, subject=subject,
message=message, html_message=html_message, status=status,
headers=headers, priority=priority, scheduled_time=scheduled_time
)
)
if priority == PRIORITY.now:
for email in emails:
email.dispatch()
return emails


def get_email_template(name, language=''):
"""
Function that returns an email template instance, from cache or DB.
"""
use_cache = getattr(settings, 'POST_OFFICE_CACHE', True)
if use_cache:
use_cache = getattr(settings, 'POST_OFFICE_TEMPLATE_CACHE', True)
if not use_cache:
return EmailTemplate.objects.get(name=name, language=language)
else:
composite_name = '%s:%s' % (name, language)
email_template = cache.get(composite_name)
if email_template is not None:
return email_template
else:
email_template = EmailTemplate.objects.get(name=name,
language=language)
cache.set(composite_name, email_template)
return email_template


def split_emails(emails, split_count=1):
# Group emails into X sublists
# taken from http://www.garyrobinson.net/2008/04/splitting-a-pyt.html
# Strange bug, only return 100 email if we do not evaluate the list
if list(emails):
return [emails[i::split_count] for i in range(split_count)]


def create_attachments(attachment_files):
"""
Create Attachment instances from files

attachment_files is a dict of:
* Key - the filename to be used for the attachment.
* Value - file-like object, or a filename to open OR a dict of {'file': file-like-object, 'mimetype': string}

Returns a list of Attachment objects
"""
attachments = []
for filename, filedata in attachment_files.items():

if isinstance(filedata, dict):
content = filedata.get('file', None)
mimetype = filedata.get('mimetype', None)
else:
content = filedata
mimetype = None

opened_file = None

if isinstance(content, string_types):
# `content` is a filename - try to open the file
opened_file = open(content, 'rb')
content = File(opened_file)

attachment = Attachment()
if mimetype:
attachment.mimetype = mimetype
attachment.file.save(filename, content=content, save=True)

attachments.append(attachment)

if opened_file is not None:
opened_file.close()

return attachments


def parse_priority(priority):
if priority is None:
priority = get_default_priority()
# If priority is given as a string, returns the enum representation
if isinstance(priority, string_types):
priority = getattr(PRIORITY, priority, None)

if priority is None:
raise ValueError('Invalid priority, must be one of: %s' %
', '.join(PRIORITY._fields))
return priority


def parse_emails(emails):
"""
A function that returns a list of valid email addresses.
This function will also convert a single email address into
a list of email addresses.
None value is also converted into an empty list.
"""

if isinstance(emails, string_types):
emails = [emails]
elif emails is None:
emails = []

for email in emails:
try:
validate_email_with_name(email)
except ValidationError:
raise ValidationError('%s is not a valid email address' % email)

return emails

+ 50
- 0
thesisenv/lib/python3.6/site-packages/post_office/validators.py View File

from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.template import Template, TemplateSyntaxError, TemplateDoesNotExist
from django.utils.encoding import force_text

from .compat import text_type


def validate_email_with_name(value):
"""
Validate email address.

Both "Recipient Name <email@example.com>" and "email@example.com" are valid.
"""
value = force_text(value)

if '<' and '>' in value:
start = value.find('<') + 1
end = value.find('>')
if start < end:
recipient = value[start:end]
else:
recipient = value

validate_email(recipient)


def validate_comma_separated_emails(value):
"""
Validate every email address in a comma separated list of emails.
"""
if not isinstance(value, (tuple, list)):
raise ValidationError('Email list must be a list/tuple.')

for email in value:
try:
validate_email_with_name(email)
except ValidationError:
raise ValidationError('Invalid email: %s' % email, code='invalid')


def validate_template_syntax(source):
"""
Basic Django Template syntax validation. This allows for robuster template
authoring.
"""
try:
Template(source)
except (TemplateSyntaxError, TemplateDoesNotExist) as err:
raise ValidationError(text_type(err))

+ 1
- 0
thesisenv/lib/python3.6/site-packages/post_office/views.py View File

# Create your views here.

Loading…
Cancel
Save