+
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/app/__init__.py b/thesisenv/lib/python3.6/site-packages/django_common/templatetags/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/app/__init__.py
rename to thesisenv/lib/python3.6/site-packages/django_common/templatetags/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/django_common/templatetags/custom_tags.py b/thesisenv/lib/python3.6/site-packages/django_common/templatetags/custom_tags.py
new file mode 100644
index 0000000..8e06ec9
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common/templatetags/custom_tags.py
@@ -0,0 +1,95 @@
+from __future__ import print_function, unicode_literals, with_statement, division
+
+from django import template
+from django.forms import widgets
+from django.template.loader import get_template
+
+register = template.Library()
+
+
+class FormFieldNode(template.Node):
+ """
+ Helper class for the render_form_field below
+ """
+ def __init__(self, form_field, help_text=None, css_classes=None):
+ self.form_field = template.Variable(form_field)
+ self.help_text = help_text[1:-1] if help_text else help_text
+ self.css_classes = css_classes[1:-1] if css_classes else css_classes
+
+ def render(self, context):
+
+ try:
+ form_field = self.form_field.resolve(context)
+ except template.VariableDoesNotExist:
+ return ''
+
+ widget = form_field.field.widget
+
+ if isinstance(widget, widgets.HiddenInput):
+ return form_field
+ elif isinstance(widget, widgets.RadioSelect):
+ t = get_template('common/fragments/radio_field.html')
+ elif isinstance(widget, widgets.CheckboxInput):
+ t = get_template('common/fragments/checkbox_field.html')
+ elif isinstance(widget, widgets.CheckboxSelectMultiple):
+ t = get_template('common/fragments/multi_checkbox_field.html')
+ else:
+ t = get_template('common/fragments/form_field.html')
+
+ help_text = self.help_text
+ if help_text is None:
+ help_text = form_field.help_text
+
+ return t.render({
+ 'form_field': form_field,
+ 'help_text': help_text,
+ 'css_classes': self.css_classes
+ })
+
+
+@register.tag
+def render_form_field(parser, token):
+ """
+ Usage is {% render_form_field form.field_name optional_help_text optional_css_classes %}
+
+ - optional_help_text and optional_css_classes are strings
+ - if optional_help_text is not given, then it is taken from form field object
+ """
+ try:
+ help_text = None
+ css_classes = None
+
+ token_split = token.split_contents()
+ if len(token_split) == 4:
+ tag_name, form_field, help_text, css_classes = token.split_contents()
+ elif len(token_split) == 3:
+ tag_name, form_field, help_text = token.split_contents()
+ else:
+ tag_name, form_field = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError(
+ "Unable to parse arguments for {0}".format(repr(token.contents.split()[0])))
+
+ return FormFieldNode(form_field, help_text=help_text, css_classes=css_classes)
+
+
+@register.simple_tag
+def active(request, pattern):
+ """
+ Returns the string 'active' if pattern matches.
+ Used to assign a css class in navigation bars to active tab/section.
+ """
+ if request.path == pattern:
+ return 'active'
+ return ''
+
+
+@register.simple_tag
+def active_starts(request, pattern):
+ """
+ Returns the string 'active' if request url starts with pattern.
+ Used to assign a css class in navigation bars to active tab/section.
+ """
+ if request.path.startswith(pattern):
+ return 'active'
+ return ''
diff --git a/thesisenv/lib/python3.6/site-packages/django_common/tests.py b/thesisenv/lib/python3.6/site-packages/django_common/tests.py
new file mode 100644
index 0000000..71a0c50
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common/tests.py
@@ -0,0 +1,38 @@
+from __future__ import print_function, unicode_literals, with_statement, division
+
+from django.utils import unittest
+from django.core.management import call_command
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+import sys
+import random
+
+
+class SimpleTestCase(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_generate_secret_key(self):
+ """ Test generation of a secret key """
+ out = StringIO()
+ sys.stdout = out
+
+ for i in range(10):
+ random_number = random.randrange(10, 100)
+ call_command('generate_secret_key', length=random_number)
+ secret_key = self._get_secret_key(out.getvalue())
+
+ out.truncate(0)
+ out.seek(0)
+
+ assert len(secret_key) == random_number
+
+ def _get_secret_key(self, result):
+ """ Get only the value of a SECRET_KEY """
+ for index, key in enumerate(result):
+ if key == ':':
+ return str(result[index + 1:]).strip()
diff --git a/thesisenv/lib/python3.6/site-packages/django_common/tzinfo.py b/thesisenv/lib/python3.6/site-packages/django_common/tzinfo.py
new file mode 100644
index 0000000..e224ad7
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common/tzinfo.py
@@ -0,0 +1,177 @@
+from __future__ import print_function, unicode_literals, with_statement, division
+
+# From the python documentation
+# http://docs.python.org/library/datetime.html
+from datetime import tzinfo, timedelta, datetime
+
+ZERO = timedelta(0)
+HOUR = timedelta(hours=1)
+
+# A UTC class.
+
+
+class UTC(tzinfo):
+ """
+ UTC
+ """
+ def utcoffset(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return ZERO
+
+utc = UTC()
+
+# A class building tzinfo objects for fixed-offset time zones.
+# Note that FixedOffset(0, "UTC") is a different way to build a
+# UTC tzinfo object.
+
+
+class FixedOffset(tzinfo):
+ """
+ Fixed offset in minutes east from UTC.
+ """
+ def __init__(self, offset, name):
+ self.__offset = timedelta(minutes=offset)
+ self.__name = name
+
+ def utcoffset(self, dt):
+ return self.__offset
+
+ def tzname(self, dt):
+ return self.__name
+
+ def dst(self, dt):
+ return ZERO
+
+# A class capturing the platform's idea of local time.
+
+import time as _time
+
+STDOFFSET = timedelta(seconds=-_time.timezone)
+if _time.daylight:
+ DSTOFFSET = timedelta(seconds=-_time.altzone)
+else:
+ DSTOFFSET = STDOFFSET
+
+DSTDIFF = DSTOFFSET - STDOFFSET
+
+
+class LocalTimezone(tzinfo):
+ def utcoffset(self, dt):
+ if self._isdst(dt):
+ return DSTOFFSET
+ else:
+ return STDOFFSET
+
+ def dst(self, dt):
+ if self._isdst(dt):
+ return DSTDIFF
+ else:
+ return ZERO
+
+ def tzname(self, dt):
+ return _time.tzname[self._isdst(dt)]
+
+ def _isdst(self, dt):
+ tt = (dt.year, dt.month, dt.day,
+ dt.hour, dt.minute, dt.second,
+ dt.weekday(), 0, -1)
+ stamp = _time.mktime(tt)
+ tt = _time.localtime(stamp)
+ return tt.tm_isdst > 0
+
+Local = LocalTimezone()
+
+
+# A complete implementation of current DST rules for major US time zones.
+
+def first_sunday_on_or_after(dt):
+ days_to_go = 6 - dt.weekday()
+ if days_to_go:
+ dt += timedelta(days_to_go)
+ return dt
+
+
+# US DST Rules
+#
+# This is a simplified (i.e., wrong for a few cases) set of rules for US
+# DST start and end times. For a complete and up-to-date set of DST rules
+# and timezone definitions, visit the Olson Database (or try pytz):
+# http://www.twinsun.com/tz/tz-link.htm
+# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
+#
+# In the US, since 2007, DST starts at 2am (standard time) on the second
+# Sunday in March, which is the first Sunday on or after Mar 8.
+DSTSTART_2007 = datetime(1, 3, 8, 2)
+# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
+DSTEND_2007 = datetime(1, 11, 1, 1)
+# From 1987 to 2006, DST used to start at 2am (standard time) on the first
+# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
+# Sunday of October, which is the first Sunday on or after Oct 25.
+DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
+DSTEND_1987_2006 = datetime(1, 10, 25, 1)
+# From 1967 to 1986, DST used to start at 2am (standard time) on the last
+# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
+# 1am standard time) on the last Sunday of October, which is the first Sunday
+# on or after Oct 25.
+DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
+DSTEND_1967_1986 = DSTEND_1987_2006
+
+
+class USTimeZone(tzinfo):
+ def __init__(self, hours, reprname, stdname, dstname):
+ self.stdoffset = timedelta(hours=hours)
+ self.reprname = reprname
+ self.stdname = stdname
+ self.dstname = dstname
+
+ def __repr__(self):
+ return self.reprname
+
+ def tzname(self, dt):
+ if self.dst(dt):
+ return self.dstname
+ else:
+ return self.stdname
+
+ def utcoffset(self, dt):
+ return self.stdoffset + self.dst(dt)
+
+ def dst(self, dt):
+ if dt is None or dt.tzinfo is None:
+ # An exception may be sensible here, in one or both cases.
+ # It depends on how you want to treat them. The default
+ # fromutc() implementation (called by the default astimezone()
+ # implementation) passes a datetime with dt.tzinfo is self.
+ return ZERO
+ assert dt.tzinfo is self
+
+ # Find start and end times for US DST. For years before 1967, return
+ # ZERO for no DST.
+ if 2006 < dt.year:
+ dststart, dstend = DSTSTART_2007, DSTEND_2007
+ elif 1986 < dt.year < 2007:
+ dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
+ elif 1966 < dt.year < 1987:
+ dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
+ else:
+ return ZERO
+
+ start = first_sunday_on_or_after(dststart.replace(year=dt.year))
+ end = first_sunday_on_or_after(dstend.replace(year=dt.year))
+
+ # Can't compare naive to aware objects, so strip the timezone from
+ # dt first.
+ if start <= dt.replace(tzinfo=None) < end:
+ return HOUR
+ else:
+ return ZERO
+
+Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
+Central = USTimeZone(-6, "Central", "CST", "CDT")
+Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
+Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/PKG-INFO b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/PKG-INFO
new file mode 100644
index 0000000..a5af952
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/PKG-INFO
@@ -0,0 +1,327 @@
+Metadata-Version: 1.1
+Name: django-common-helpers
+Version: 0.9.2
+Summary: Common things every Django app needs!
+Home-page: http://github.com/tivix/django-common
+Author: Tivix
+Author-email: dev@tivix.com
+License: UNKNOWN
+Description: =====================
+ django-common-helpers
+ =====================
+
+
+ Overview
+ ---------
+
+ Django-common consists of the following things:
+
+ - A middleware that makes sure your web-app runs either on or without 'www' in the domain.
+
+ - A ``SessionManagerBase`` base class, that helps in keeping your session related code object-oriented and clean! See session.py for usage details.
+
+ - An ``EmailBackend`` for authenticating users based on their email, apart from username.
+
+ - Some custom db fields that you can use in your models including a ``UniqueHashField`` and ``RandomHashField``.
+
+ - Bunch of helpful functions in helper.py
+
+ - A ``render_form_field`` template tag that makes rendering form fields easy and DRY.
+
+ - A couple of dry response classes: ``JsonResponse`` and ``XMLResponse`` in the django_common.http that can be used in views that give json/xml responses.
+
+
+ Installation
+ -------------
+
+ - Install django_common (ideally in your virtualenv!) using pip or simply getting a copy of the code and putting it in a directory in your codebase.
+
+ - Add ``django_common`` to your Django settings ``INSTALLED_APPS``::
+
+ INSTALLED_APPS = [
+ # ...
+ "django_common",
+ ]
+
+ - Add the following to your settings.py with appropriate values:
+
+ - IS_DEV
+ - IS_PROD
+ - DOMAIN_NAME
+ - WWW_ROOT
+
+ - Add ``common_settings`` to your Django settings ``TEMPLATE_CONTEXT_PROCESSORS``::
+
+ TEMPLATE_CONTEXT_PROCESSORS = [
+ # ...
+ 'django_common.context_processors.common_settings',
+ ]
+
+ - Add ``EmailBackend`` to the Django settings ``AUTHENTICATION_BACKENDS``::
+
+ AUTHENTICATION_BACKENDS = (
+ 'django_common.auth_backends.EmailBackend',
+ 'django.contrib.auth.backends.ModelBackend'
+ )
+
+ - Add ``WWWRedirectMiddleware`` if required to the list of middlewares::
+
+ MIDDLEWARE_CLASSES = [
+ # ...
+ "WWWRedirectMiddleware",
+ ]
+
+ - Scaffolds / ajax_form.js (ajax forms) etc. require jQuery
+
+
+ Scaffolding feature
+ -------------------
+
+ 1. Installing
+
+ To get scaffold just download ``scaffold`` branch of django-common, add it to ``INSTALLED_APPS`` and set up ``SCAFFOLD_APPS_DIR`` in settings.
+
+ Default is set to main app directory. However if you use django_base_project you must set up this to ``SCAFFOLD_APPS_DIR = 'apps/'``.
+
+ 2. Run
+
+ To run scaffold type::
+
+ python manage.py scaffold APPNAME --model MODELNAME [fields]
+
+ APPNAME is app name. If app does not exists it will be created.
+ MODELNAME is model name. Just enter model name that you want to create (for example: Blog, Topic, Post etc). It must be alphanumerical. Only one model per run is allowed!
+
+ [fields] - list of the model fields.
+
+ 3. Field types
+
+ Available fields::
+
+ char - CharField
+ text - TextField
+ int - IntegerFIeld
+ decimal -DecimalField
+ datetime - DateTimeField
+ foreign - ForeignKey
+
+ All fields requires name that is provided after ``:`` sign, for example::
+
+ char:title text:body int:posts datetime:create_date
+
+ Two fields ``foreign`` and ``decimal`` requires additional parameters:
+
+ - "foreign" as third argument takes foreignkey model, example::
+
+ foreign:blog:Blog, foreign:post:Post, foreign:added_by:User
+
+ NOTICE: All foreign key models must alread exist in project. User and Group model are imported automatically.
+
+ - decimal field requires two more arguments ``max_digits`` and ``decimal_places``, example::
+
+ decimal:total_cost:10:2
+
+ NOTICE: To all models scaffold automatically adds two fields: update_date and create_date.
+
+ 4. How it works?
+
+ Scaffold creates models, views (CRUD), forms, templates, admin, urls and basic tests (CRUD). Scaffold templates are using two blocks extending from base.html::
+
+ {% extends "base.html" %}
+ {% block page-title %} {% endblock %}
+ {% block conent %} {% endblock %}
+
+ So be sure you have your base.html set up properly.
+
+ Scaffolding example usage
+ -------------------------
+
+ Let's create very simple ``forum`` app. We need ``Forum``, ``Topic`` and ``Post`` model.
+
+ - Forum model
+
+ Forum model needs just one field ``name``::
+
+ python manage.py scaffold forum --model Forum char:name
+
+ - Topic model
+
+ Topics are created by site users so we need: ``created_by``, ``title`` and ``Forum`` foreign key (``update_date`` and ``create_date`` are always added to models)::
+
+ python manage.py scaffold forum --model Topic foreign:created_by:User char:title foreign:forum:Forum
+
+ - Post model
+
+ Last one are Posts. Posts are related to Topics. Here we need: ``title``, ``body``, ``created_by`` and foreign key to ``Topic``::
+
+ python manage.py scaffold forum --model Post char:title text:body foreign:created_by:User foreign:topic:Topic
+
+ All data should be in place!
+
+ Now you must add ``forum`` app to ``INSTALLED_APPS`` and include app in ``urls.py`` file by adding into urlpatterns::
+
+ urlpatterns = [
+ ...
+ url(r'^', include('forum.urls')),
+ ]
+
+ Now syncdb new app and you are ready to go::
+
+ python manage.py syncdb
+
+ Run your server::
+
+ python manage.py runserver
+
+ And go to forum main page::
+
+ http://localhost:8000/forum/
+
+ All structure are in place. Now you can personalize models, templates and urls.
+
+ At the end you can test new app by runing test::
+
+ python manage.py test forum
+
+ Creating test database for alias 'default'...
+ .......
+ ----------------------------------------------------------------------
+ Ran 7 tests in 0.884s
+
+ OK
+
+ Happy scaffolding!
+
+ Generation of SECRET_KEY
+ ------------------------
+
+ Sometimes you need to generate a new ``SECRET_KEY`` so now you can generate it using this command:
+
+ $ python manage.py generate_secret_key
+
+ Sample output:
+
+ $ python manage.py generate_secret_key
+
+ SECRET_KEY: 7,=_3t?n@'wV=p`ITIA6"CUgJReZf?s:`f~Jtl#2i=i^z%rCp-
+
+ Optional arguments
+
+ 1. ``--length`` - is the length of the key ``default=50``
+ 2. ``--alphabet`` - is the alphabet to use to generate the key ``default=ascii letters + punctuation symbols``
+
+ Django settings keys
+ --------------------
+
+ - DOMAIN_NAME - Domain name, ``"www.example.com"``
+ - WWW_ROOT - Root website url, ``"https://www.example.com/"``
+ - IS_DEV - Current environment is development environment
+ - IS_PROD - Current environment is production environment
+
+
+ This open-source app is brought to you by Tivix, Inc. ( http://tivix.com/ )
+
+
+ Changelog
+ =========
+
+ 0.9.2
+ -----
+ - Change for Django 2.X
+
+ 0.9.1
+ -----
+ - Change for Django 1.10 - render() must be called with a dict, not a Context
+
+ 0.9.0
+ -----
+ - Django 1.10 support
+ - README.txt invalid characters fix
+ - Add support for custom user model in EmailBackend
+ - Fixes for DB fields and management commands
+
+ 0.8.0
+ -----
+ - compatability code moved to compat.py
+ - ``generate_secret_key`` management command.
+ - Fix relating to https://code.djangoproject.com/ticket/17627, package name change.
+ - Pass form fields with HiddenInput widget through render_form_field
+ - string.format usage / other refactoring / more support for Python 3
+
+
+ 0.7.0
+ -----
+ - PEP8 codebase cleanup.
+ - Improved python3 support.
+ - Django 1.8 support.
+
+ 0.6.4
+ -----
+ - Added python3 support.
+
+ 0.6.3
+ -----
+ - Changed mimetype to content_type in class JsonReponse to reflect Django 1.7 deprecation.
+
+ 0.6.2
+ -----
+ - Django 1.7 compatability using simplejson as fallback
+
+
+ 0.6.1
+ -----
+ - Added support for attaching content to emails manually (without providing path to file).
+
+ - Added LoginRequiredMixin
+
+
+ 0.6
+ ---
+ - Added support for Django 1.5
+
+ - Added fixes in nested inlines
+
+ - Added support for a multi-select checkbox field template and radio button in render_form_field
+
+ - Added Test Email Backend for overwrite TO, CC and BCC fields in all outgoing emails
+
+ - Added Custom File Email Backend to save emails as file with custom extension
+
+ - Rewrote fragments to be Bootstrap-compatible
+
+
+ 0.5.1
+ -----
+
+ - root_path deprecated in Django 1.4+
+
+
+ 0.5
+ ---
+
+ - Added self.get_inline_instances() usages instead of self.inline_instances
+
+ - Changed minimum requirement to Django 1.4+ because of the above.
+
+
+ 0.4
+ ---
+
+ - Added nested inline templates, js and full ajax support. Now we can add/remove nested fields dynamically.
+
+ - JsonpResponse object for padded JSON
+
+ - User time tracking feature - how long the user has been on site, associated middleware etc.
+
+ - @anonymous_required decorator: for views that should not be accessed by a logged-in user.
+
+ - Added EncryptedTextField and EncryptedCharField
+
+ - Misc. bug fixes
+Keywords: django
+Platform: UNKNOWN
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/SOURCES.txt b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/SOURCES.txt
new file mode 100644
index 0000000..2dcbfef
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/SOURCES.txt
@@ -0,0 +1,44 @@
+AUTHORS
+LICENSE
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+django_common/__init__.py
+django_common/admin.py
+django_common/auth_backends.py
+django_common/classmaker.py
+django_common/compat.py
+django_common/context_processors.py
+django_common/db_fields.py
+django_common/decorators.py
+django_common/email_backends.py
+django_common/helper.py
+django_common/http.py
+django_common/middleware.py
+django_common/mixin.py
+django_common/scaffold.py
+django_common/session.py
+django_common/settings.py
+django_common/tests.py
+django_common/tzinfo.py
+django_common/management/__init__.py
+django_common/management/commands/__init__.py
+django_common/management/commands/generate_secret_key.py
+django_common/management/commands/scaffold.py
+django_common/static/django_common/js/ajax_form.js
+django_common/static/django_common/js/common.js
+django_common/templates/common/admin/nested.html
+django_common/templates/common/admin/nested_tabular.html
+django_common/templates/common/fragments/checkbox_field.html
+django_common/templates/common/fragments/form_field.html
+django_common/templates/common/fragments/multi_checkbox_field.html
+django_common/templates/common/fragments/radio_field.html
+django_common/templatetags/__init__.py
+django_common/templatetags/custom_tags.py
+django_common_helpers.egg-info/PKG-INFO
+django_common_helpers.egg-info/SOURCES.txt
+django_common_helpers.egg-info/dependency_links.txt
+django_common_helpers.egg-info/not-zip-safe
+django_common_helpers.egg-info/requires.txt
+django_common_helpers.egg-info/top_level.txt
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/dependency_links.txt b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/installed-files.txt b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/installed-files.txt
new file mode 100644
index 0000000..f3a85d9
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/installed-files.txt
@@ -0,0 +1,62 @@
+../django_common/__init__.py
+../django_common/__pycache__/__init__.cpython-36.pyc
+../django_common/__pycache__/admin.cpython-36.pyc
+../django_common/__pycache__/auth_backends.cpython-36.pyc
+../django_common/__pycache__/classmaker.cpython-36.pyc
+../django_common/__pycache__/compat.cpython-36.pyc
+../django_common/__pycache__/context_processors.cpython-36.pyc
+../django_common/__pycache__/db_fields.cpython-36.pyc
+../django_common/__pycache__/decorators.cpython-36.pyc
+../django_common/__pycache__/email_backends.cpython-36.pyc
+../django_common/__pycache__/helper.cpython-36.pyc
+../django_common/__pycache__/http.cpython-36.pyc
+../django_common/__pycache__/middleware.cpython-36.pyc
+../django_common/__pycache__/mixin.cpython-36.pyc
+../django_common/__pycache__/scaffold.cpython-36.pyc
+../django_common/__pycache__/session.cpython-36.pyc
+../django_common/__pycache__/settings.cpython-36.pyc
+../django_common/__pycache__/tests.cpython-36.pyc
+../django_common/__pycache__/tzinfo.cpython-36.pyc
+../django_common/admin.py
+../django_common/auth_backends.py
+../django_common/classmaker.py
+../django_common/compat.py
+../django_common/context_processors.py
+../django_common/db_fields.py
+../django_common/decorators.py
+../django_common/email_backends.py
+../django_common/helper.py
+../django_common/http.py
+../django_common/management/__init__.py
+../django_common/management/__pycache__/__init__.cpython-36.pyc
+../django_common/management/commands/__init__.py
+../django_common/management/commands/__pycache__/__init__.cpython-36.pyc
+../django_common/management/commands/__pycache__/generate_secret_key.cpython-36.pyc
+../django_common/management/commands/__pycache__/scaffold.cpython-36.pyc
+../django_common/management/commands/generate_secret_key.py
+../django_common/management/commands/scaffold.py
+../django_common/middleware.py
+../django_common/mixin.py
+../django_common/scaffold.py
+../django_common/session.py
+../django_common/settings.py
+../django_common/static/django_common/js/ajax_form.js
+../django_common/static/django_common/js/common.js
+../django_common/templates/common/admin/nested.html
+../django_common/templates/common/admin/nested_tabular.html
+../django_common/templates/common/fragments/checkbox_field.html
+../django_common/templates/common/fragments/form_field.html
+../django_common/templates/common/fragments/multi_checkbox_field.html
+../django_common/templates/common/fragments/radio_field.html
+../django_common/templatetags/__init__.py
+../django_common/templatetags/__pycache__/__init__.cpython-36.pyc
+../django_common/templatetags/__pycache__/custom_tags.cpython-36.pyc
+../django_common/templatetags/custom_tags.py
+../django_common/tests.py
+../django_common/tzinfo.py
+PKG-INFO
+SOURCES.txt
+dependency_links.txt
+not-zip-safe
+requires.txt
+top_level.txt
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/not-zip-safe b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/requires.txt b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/requires.txt
new file mode 100644
index 0000000..f97e3c8
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/requires.txt
@@ -0,0 +1 @@
+Django>=1.8.0
diff --git a/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/top_level.txt b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/top_level.txt
new file mode 100644
index 0000000..8951166
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_common_helpers-0.9.2-py3.6.egg-info/top_level.txt
@@ -0,0 +1 @@
+django_common
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/PKG-INFO b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/PKG-INFO
new file mode 100644
index 0000000..db3973d
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/PKG-INFO
@@ -0,0 +1,40 @@
+Metadata-Version: 1.1
+Name: django-cron
+Version: 0.5.1
+Summary: Running python crons in a Django project
+Home-page: http://github.com/tivix/django-cron
+Author: Sumit Chachra
+Author-email: chachra@tivix.com
+License: UNKNOWN
+Description: ===========
+ django-cron
+ ===========
+
+ .. image:: https://travis-ci.org/Tivix/django-cron.png
+ :target: https://travis-ci.org/Tivix/django-cron
+
+
+ .. image:: https://coveralls.io/repos/Tivix/django-cron/badge.png
+ :target: https://coveralls.io/r/Tivix/django-cron?branch=master
+
+
+ .. image:: https://readthedocs.org/projects/django-cron/badge/?version=latest
+ :target: https://readthedocs.org/projects/django-cron/?badge=latest
+
+ Django-cron lets you run Django/Python code on a recurring basis providing basic plumbing to track and execute tasks. The 2 most common ways in which most people go about this is either writing custom python scripts or a management command per cron (leads to too many management commands!). Along with that some mechanism to track success, failure etc. is also usually necesary.
+
+ This app solves both issues to a reasonable extent. This is by no means a replacement for queues like Celery ( http://celeryproject.org/ ) etc.
+
+
+ Documentation
+ =============
+ http://django-cron.readthedocs.org/en/latest/
+
+ This open-source app is brought to you by Tivix, Inc. ( http://tivix.com/ )
+Keywords: django cron
+Platform: UNKNOWN
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/SOURCES.txt b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/SOURCES.txt
new file mode 100644
index 0000000..e0dccf3
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/SOURCES.txt
@@ -0,0 +1,29 @@
+AUTHORS
+LICENSE
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+django_cron/__init__.py
+django_cron/admin.py
+django_cron/cron.py
+django_cron/helpers.py
+django_cron/models.py
+django_cron/tests.py
+django_cron.egg-info/PKG-INFO
+django_cron.egg-info/SOURCES.txt
+django_cron.egg-info/dependency_links.txt
+django_cron.egg-info/not-zip-safe
+django_cron.egg-info/requires.txt
+django_cron.egg-info/top_level.txt
+django_cron/backends/__init__.py
+django_cron/backends/lock/__init__.py
+django_cron/backends/lock/base.py
+django_cron/backends/lock/cache.py
+django_cron/backends/lock/file.py
+django_cron/management/__init__.py
+django_cron/management/commands/__init__.py
+django_cron/management/commands/runcrons.py
+django_cron/migrations/0001_initial.py
+django_cron/migrations/0002_remove_max_length_from_CronJobLog_message.py
+django_cron/migrations/__init__.py
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/dependency_links.txt b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/installed-files.txt b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/installed-files.txt
new file mode 100644
index 0000000..bba35da
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/installed-files.txt
@@ -0,0 +1,40 @@
+../django_cron/__init__.py
+../django_cron/__pycache__/__init__.cpython-36.pyc
+../django_cron/__pycache__/admin.cpython-36.pyc
+../django_cron/__pycache__/cron.cpython-36.pyc
+../django_cron/__pycache__/helpers.cpython-36.pyc
+../django_cron/__pycache__/models.cpython-36.pyc
+../django_cron/__pycache__/tests.cpython-36.pyc
+../django_cron/admin.py
+../django_cron/backends/__init__.py
+../django_cron/backends/__pycache__/__init__.cpython-36.pyc
+../django_cron/backends/lock/__init__.py
+../django_cron/backends/lock/__pycache__/__init__.cpython-36.pyc
+../django_cron/backends/lock/__pycache__/base.cpython-36.pyc
+../django_cron/backends/lock/__pycache__/cache.cpython-36.pyc
+../django_cron/backends/lock/__pycache__/file.cpython-36.pyc
+../django_cron/backends/lock/base.py
+../django_cron/backends/lock/cache.py
+../django_cron/backends/lock/file.py
+../django_cron/cron.py
+../django_cron/helpers.py
+../django_cron/management/__init__.py
+../django_cron/management/__pycache__/__init__.cpython-36.pyc
+../django_cron/management/commands/__init__.py
+../django_cron/management/commands/__pycache__/__init__.cpython-36.pyc
+../django_cron/management/commands/__pycache__/runcrons.cpython-36.pyc
+../django_cron/management/commands/runcrons.py
+../django_cron/migrations/0001_initial.py
+../django_cron/migrations/0002_remove_max_length_from_CronJobLog_message.py
+../django_cron/migrations/__init__.py
+../django_cron/migrations/__pycache__/0001_initial.cpython-36.pyc
+../django_cron/migrations/__pycache__/0002_remove_max_length_from_CronJobLog_message.cpython-36.pyc
+../django_cron/migrations/__pycache__/__init__.cpython-36.pyc
+../django_cron/models.py
+../django_cron/tests.py
+PKG-INFO
+SOURCES.txt
+dependency_links.txt
+not-zip-safe
+requires.txt
+top_level.txt
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/not-zip-safe b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/requires.txt b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/requires.txt
new file mode 100644
index 0000000..4c78d87
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/requires.txt
@@ -0,0 +1,2 @@
+Django>=1.8.0
+django-common-helpers>=0.6.4
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/top_level.txt b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/top_level.txt
new file mode 100644
index 0000000..235320b
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron-0.5.1-py3.6.egg-info/top_level.txt
@@ -0,0 +1 @@
+django_cron
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/__init__.py b/thesisenv/lib/python3.6/site-packages/django_cron/__init__.py
new file mode 100644
index 0000000..95be4c0
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/__init__.py
@@ -0,0 +1,234 @@
+import logging
+from datetime import timedelta
+import traceback
+import time
+
+from django.conf import settings
+from django.utils.timezone import now as utc_now, localtime, is_naive
+from django.db.models import Q
+
+
+DEFAULT_LOCK_BACKEND = 'django_cron.backends.lock.cache.CacheLock'
+logger = logging.getLogger('django_cron')
+
+
+def get_class(kls):
+ """
+ TODO: move to django-common app.
+ Converts a string to a class.
+ Courtesy: http://stackoverflow.com/questions/452969/does-python-have-an-equivalent-to-java-class-forname/452981#452981
+ """
+ parts = kls.split('.')
+ module = ".".join(parts[:-1])
+ m = __import__(module)
+ for comp in parts[1:]:
+ m = getattr(m, comp)
+ return m
+
+
+def get_current_time():
+ now = utc_now()
+ return now if is_naive(now) else localtime(now)
+
+
+class Schedule(object):
+ def __init__(self, run_every_mins=None, run_at_times=None, retry_after_failure_mins=None):
+ if run_at_times is None:
+ run_at_times = []
+ self.run_every_mins = run_every_mins
+ self.run_at_times = run_at_times
+ self.retry_after_failure_mins = retry_after_failure_mins
+
+
+class CronJobBase(object):
+ """
+ Sub-classes should have the following properties:
+ + code - This should be a code specific to the cron being run. Eg. 'general.stats' etc.
+ + schedule
+
+ Following functions:
+ + do - This is the actual business logic to be run at the given schedule
+ """
+ def __init__(self):
+ self.prev_success_cron = None
+
+ def set_prev_success_cron(self, prev_success_cron):
+ self.prev_success_cron = prev_success_cron
+
+ def get_prev_success_cron(self):
+ return self.prev_success_cron
+
+ @classmethod
+ def get_time_until_run(cls):
+ from django_cron.models import CronJobLog
+ try:
+ last_job = CronJobLog.objects.filter(
+ code=cls.code).latest('start_time')
+ except CronJobLog.DoesNotExist:
+ return timedelta()
+ return (last_job.start_time +
+ timedelta(minutes=cls.schedule.run_every_mins) - utc_now())
+
+
+class CronJobManager(object):
+ """
+ A manager instance should be created per cron job to be run.
+ Does all the logger tracking etc. for it.
+ Used as a context manager via 'with' statement to ensure
+ proper logger in cases of job failure.
+ """
+
+ def __init__(self, cron_job_class, silent=False, *args, **kwargs):
+ super(CronJobManager, self).__init__(*args, **kwargs)
+
+ self.cron_job_class = cron_job_class
+ self.silent = silent
+ self.lock_class = self.get_lock_class()
+ self.previously_ran_successful_cron = None
+
+ def should_run_now(self, force=False):
+ from django_cron.models import CronJobLog
+ cron_job = self.cron_job
+ """
+ Returns a boolean determining whether this cron should run now or not!
+ """
+
+ self.user_time = None
+ self.previously_ran_successful_cron = None
+
+ # If we pass --force options, we force cron run
+ if force:
+ return True
+ if cron_job.schedule.run_every_mins is not None:
+
+ # We check last job - success or not
+ last_job = None
+ try:
+ last_job = CronJobLog.objects.filter(code=cron_job.code).latest('start_time')
+ except CronJobLog.DoesNotExist:
+ pass
+ if last_job:
+ if not last_job.is_success and cron_job.schedule.retry_after_failure_mins:
+ if get_current_time() > last_job.start_time + timedelta(minutes=cron_job.schedule.retry_after_failure_mins):
+ return True
+ else:
+ return False
+
+ try:
+ self.previously_ran_successful_cron = CronJobLog.objects.filter(
+ code=cron_job.code,
+ is_success=True,
+ ran_at_time__isnull=True
+ ).latest('start_time')
+ except CronJobLog.DoesNotExist:
+ pass
+
+ if self.previously_ran_successful_cron:
+ if get_current_time() > self.previously_ran_successful_cron.start_time + timedelta(minutes=cron_job.schedule.run_every_mins):
+ return True
+ else:
+ return True
+
+ if cron_job.schedule.run_at_times:
+ for time_data in cron_job.schedule.run_at_times:
+ user_time = time.strptime(time_data, "%H:%M")
+ now = get_current_time()
+ actual_time = time.strptime("%s:%s" % (now.hour, now.minute), "%H:%M")
+ if actual_time >= user_time:
+ qset = CronJobLog.objects.filter(
+ code=cron_job.code,
+ ran_at_time=time_data,
+ is_success=True
+ ).filter(
+ Q(start_time__gt=now) | Q(end_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
+ )
+ if not qset:
+ self.user_time = time_data
+ return True
+
+ return False
+
+ def make_log(self, *messages, **kwargs):
+ cron_log = self.cron_log
+
+ cron_job = getattr(self, 'cron_job', self.cron_job_class)
+ cron_log.code = cron_job.code
+
+ cron_log.is_success = kwargs.get('success', True)
+ cron_log.message = self.make_log_msg(*messages)
+ cron_log.ran_at_time = getattr(self, 'user_time', None)
+ cron_log.end_time = get_current_time()
+ cron_log.save()
+
+ def make_log_msg(self, msg, *other_messages):
+ MAX_MESSAGE_LENGTH = 1000
+ if not other_messages:
+ # assume that msg is a single string
+ return msg[-MAX_MESSAGE_LENGTH:]
+ else:
+ if len(msg):
+ msg += "\n...\n"
+ NEXT_MESSAGE_OFFSET = MAX_MESSAGE_LENGTH - len(msg)
+ else:
+ NEXT_MESSAGE_OFFSET = MAX_MESSAGE_LENGTH
+
+ if NEXT_MESSAGE_OFFSET > 0:
+ msg += other_messages[0][-NEXT_MESSAGE_OFFSET:]
+ return self.make_log_msg(msg, *other_messages[1:])
+ else:
+ return self.make_log_msg(msg)
+
+ def __enter__(self):
+ from django_cron.models import CronJobLog
+ self.cron_log = CronJobLog(start_time=get_current_time())
+
+ return self
+
+ def __exit__(self, ex_type, ex_value, ex_traceback):
+ if ex_type == self.lock_class.LockFailedException:
+ if not self.silent:
+ logger.info(ex_value)
+
+ elif ex_type is not None:
+ try:
+ trace = "".join(traceback.format_exception(ex_type, ex_value, ex_traceback))
+ self.make_log(self.msg, trace, success=False)
+ except Exception as e:
+ err_msg = "Error saving cronjob log message: %s" % e
+ logger.error(err_msg)
+
+ return True # prevent exception propagation
+
+ def run(self, force=False):
+ """
+ apply the logic of the schedule and call do() on the CronJobBase class
+ """
+ cron_job_class = self.cron_job_class
+ if not issubclass(cron_job_class, CronJobBase):
+ raise Exception('The cron_job to be run must be a subclass of %s' % CronJobBase.__name__)
+
+ with self.lock_class(cron_job_class, self.silent):
+ self.cron_job = cron_job_class()
+
+ if self.should_run_now(force):
+ logger.debug("Running cron: %s code %s", cron_job_class.__name__, self.cron_job.code)
+ self.msg = self.cron_job.do()
+ self.make_log(self.msg, success=True)
+ self.cron_job.set_prev_success_cron(self.previously_ran_successful_cron)
+
+ def get_lock_class(self):
+ name = getattr(settings, 'DJANGO_CRON_LOCK_BACKEND', DEFAULT_LOCK_BACKEND)
+ try:
+ return get_class(name)
+ except Exception as err:
+ raise Exception("invalid lock module %s. Can't use it: %s." % (name, err))
+
+ @property
+ def msg(self):
+ return getattr(self, '_msg', '')
+
+ @msg.setter
+ def msg(self, msg):
+ if msg is None:
+ msg = ''
+ self._msg = msg
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/admin.py b/thesisenv/lib/python3.6/site-packages/django_cron/admin.py
new file mode 100644
index 0000000..af70b5a
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/admin.py
@@ -0,0 +1,56 @@
+from datetime import timedelta
+
+from django.contrib import admin
+from django.db.models import F
+from django.utils.translation import ugettext_lazy as _
+
+from django_cron.models import CronJobLog
+from django_cron.helpers import humanize_duration
+
+
+class DurationFilter(admin.SimpleListFilter):
+ title = _('duration')
+ parameter_name = 'duration'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('lte_minute', _('<= 1 minute')),
+ ('gt_minute', _('> 1 minute')),
+ ('gt_hour', _('> 1 hour')),
+ ('gt_day', _('> 1 day')),
+ )
+
+ def queryset(self, request, queryset):
+ if self.value() == 'lte_minute':
+ return queryset.filter(end_time__lte=F('start_time') + timedelta(minutes=1))
+ if self.value() == 'gt_minute':
+ return queryset.filter(end_time__gt=F('start_time') + timedelta(minutes=1))
+ if self.value() == 'gt_hour':
+ return queryset.filter(end_time__gt=F('start_time') + timedelta(hours=1))
+ if self.value() == 'gt_day':
+ return queryset.filter(end_time__gt=F('start_time') + timedelta(days=1))
+
+
+class CronJobLogAdmin(admin.ModelAdmin):
+ class Meta:
+ model = CronJobLog
+
+ search_fields = ('code', 'message')
+ ordering = ('-start_time',)
+ list_display = ('code', 'start_time', 'end_time', 'humanize_duration', 'is_success')
+ list_filter = ('code', 'start_time', 'is_success', DurationFilter)
+
+ def get_readonly_fields(self, request, obj=None):
+ if not request.user.is_superuser and obj is not None:
+ names = [f.name for f in CronJobLog._meta.fields if f.name != 'id']
+ return self.readonly_fields + tuple(names)
+ return self.readonly_fields
+
+ def humanize_duration(self, obj):
+ return humanize_duration(obj.end_time - obj.start_time)
+
+ humanize_duration.short_description = _("Duration")
+ humanize_duration.admin_order_field = 'duration'
+
+
+admin.site.register(CronJobLog, CronJobLogAdmin)
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/backends/__init__.py b/thesisenv/lib/python3.6/site-packages/django_cron/backends/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/backends/__init__.py
rename to thesisenv/lib/python3.6/site-packages/django_cron/backends/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/bin/__init__.py b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/bin/__init__.py
rename to thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/base.py b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/base.py
new file mode 100644
index 0000000..f34f732
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/base.py
@@ -0,0 +1,68 @@
+class DjangoCronJobLock(object):
+ """
+ The lock class to use in runcrons management command.
+ Intendent usage is
+ with CacheLock(cron_class, silent):
+ do work
+ or inside try - except:
+ try:
+ with CacheLock(cron_class, silent):
+ do work
+ except DjangoCronJobLock.LockFailedException:
+ pass
+ """
+ class LockFailedException(Exception):
+ pass
+
+ def __init__(self, cron_class, silent, *args, **kwargs):
+ """
+ This method inits the class.
+ You should take care of getting all
+ nessesary thing from input parameters here
+ Base class processes
+ * self.job_name
+ * self.job_code
+ * self.parallel
+ * self.silent
+ for you. The rest is backend-specific.
+ """
+ self.job_name = cron_class.__name__
+ self.job_code = cron_class.code
+ self.parallel = getattr(cron_class, 'ALLOW_PARALLEL_RUNS', False)
+ self.silent = silent
+
+ def lock(self):
+ """
+ This method called to acquire lock. Typically. it will
+ be called from __enter__ method.
+ Return True is success,
+ False if fail.
+ Here you can optionally call self.notice_lock_failed().
+ """
+ raise NotImplementedError(
+ 'You have to implement lock(self) method for your class'
+ )
+
+ def release(self):
+ """
+ This method called to release lock.
+ Tipically called from __exit__ method.
+ No need to return anything currently.
+ """
+ raise NotImplementedError(
+ 'You have to implement release(self) method for your class'
+ )
+
+ def lock_failed_message(self):
+ return "%s: lock found. Will try later." % self.job_name
+
+ def __enter__(self):
+ if self.parallel:
+ return
+ else:
+ if not self.lock():
+ raise self.LockFailedException(self.lock_failed_message())
+
+ def __exit__(self, type, value, traceback):
+ if not self.parallel:
+ self.release()
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/cache.py b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/cache.py
new file mode 100644
index 0000000..4e92859
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/cache.py
@@ -0,0 +1,73 @@
+from django.conf import settings
+from django.core.cache import caches
+from django.utils import timezone
+
+from django_cron.backends.lock.base import DjangoCronJobLock
+
+
+class CacheLock(DjangoCronJobLock):
+ """
+ One of simplest lock backends, uses django cache to
+ prevent parallel runs of commands.
+ """
+ DEFAULT_LOCK_TIME = 24 * 60 * 60 # 24 hours
+
+ def __init__(self, cron_class, *args, **kwargs):
+ super(CacheLock, self).__init__(cron_class, *args, **kwargs)
+
+ self.cache = self.get_cache_by_name()
+ self.lock_name = self.get_lock_name()
+ self.timeout = self.get_cache_timeout(cron_class)
+
+ def lock(self):
+ """
+ This method sets a cache variable to mark current job as "already running".
+ """
+ if self.cache.get(self.lock_name):
+ return False
+ else:
+ self.cache.set(self.lock_name, timezone.now(), self.timeout)
+ return True
+
+ def release(self):
+ self.cache.delete(self.lock_name)
+
+ def lock_failed_message(self):
+ started = self.get_running_lock_date()
+ msgs = [
+ "%s: lock has been found. Other cron started at %s" % (
+ self.job_name, started
+ ),
+ "Current timeout for job %s is %s seconds (cache key name is '%s')." % (
+ self.job_name, self.timeout, self.lock_name
+ )
+ ]
+ return msgs
+
+ def get_cache_by_name(self):
+ """
+ Gets a specified cache (or the `default` cache if CRON_CACHE is not set)
+ """
+ cache_name = getattr(settings, 'DJANGO_CRON_CACHE', 'default')
+
+ # Allow the possible InvalidCacheBackendError to happen here
+ # instead of allowing unexpected parallel runs of cron jobs
+ return caches[cache_name]
+
+ def get_lock_name(self):
+ return self.job_name
+
+ def get_cache_timeout(self, cron_class):
+ timeout = self.DEFAULT_LOCK_TIME
+ try:
+ timeout = getattr(cron_class, 'DJANGO_CRON_LOCK_TIME', settings.DJANGO_CRON_LOCK_TIME)
+ except:
+ pass
+ return timeout
+
+ def get_running_lock_date(self):
+ date = self.cache.get(self.lock_name)
+ if date and not timezone.is_aware(date):
+ tz = timezone.get_current_timezone()
+ date = timezone.make_aware(date, tz)
+ return date
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/file.py b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/file.py
new file mode 100644
index 0000000..7c54bef
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/backends/lock/file.py
@@ -0,0 +1,68 @@
+import os
+import sys
+import errno
+
+from django.conf import settings
+from django.core.files import locks
+
+from django_cron.backends.lock.base import DjangoCronJobLock
+
+
+class FileLock(DjangoCronJobLock):
+ """
+ Quite a simple lock backend that uses some kind of pid file.
+ """
+ def lock(self):
+ try:
+ lock_name = self.get_lock_name()
+ # need loop to avoid races on file unlinking
+ while True:
+ f = open(lock_name, 'wb+', 0)
+ locks.lock(f, locks.LOCK_EX | locks.LOCK_NB)
+ # Here is the Race:
+ # Previous process "A" is still running. Process "B" opens
+ # the file and then the process "A" finishes and deletes it.
+ # "B" locks the deleted file (by fd it already have) and runs,
+ # then the next process "C" creates _new_ file and locks it
+ # successfully while "B" is still running.
+ # We just need to check that "B" didn't lock a deleted file
+ # to avoid any problems. If process "C" have locked
+ # a new file wile "B" stats it then ok, let "B" quit and "C"
+ # run. We can still meet an attacker that permanently
+ # creates and deletes our file but we can't avoid problems
+ # in that case.
+ if os.path.isfile(lock_name):
+ st1 = os.fstat(f.fileno())
+ st2 = os.stat(lock_name)
+ if st1.st_ino == st2.st_ino:
+ f.write(bytes(str(os.getpid()).encode('utf-8')))
+ self.lockfile = f
+ return True
+ # else:
+ # retry. Don't unlink, next process might already use it.
+ f.close()
+
+ except IOError as e:
+ if e.errno in (errno.EACCES, errno.EAGAIN):
+ return False
+ else:
+ e = sys.exc_info()[1]
+ raise e
+ # TODO: perhaps on windows I need to catch different exception type
+
+ def release(self):
+ f = self.lockfile
+ # unlink before release lock to avoid race
+ # see comment in self.lock for description
+ os.unlink(f.name)
+ f.close()
+
+ def get_lock_name(self):
+ default_path = '/tmp'
+ path = getattr(settings, 'DJANGO_CRON_LOCKFILE_PATH', default_path)
+ if not os.path.isdir(path):
+ # let it die if failed, can't run further anyway
+ os.makedirs(path)
+
+ filename = self.job_name + '.lock'
+ return os.path.join(path, filename)
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/cron.py b/thesisenv/lib/python3.6/site-packages/django_cron/cron.py
new file mode 100644
index 0000000..944d1b6
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/cron.py
@@ -0,0 +1,46 @@
+from django.conf import settings
+
+from django_common.helper import send_mail
+
+from django_cron import CronJobBase, Schedule, get_class
+from django_cron.models import CronJobLog
+
+
+class FailedRunsNotificationCronJob(CronJobBase):
+ """
+ Send email if cron failed to run X times in a row
+ """
+ RUN_EVERY_MINS = 30
+
+ schedule = Schedule(run_every_mins=RUN_EVERY_MINS)
+ code = 'django_cron.FailedRunsNotificationCronJob'
+
+ def do(self):
+
+ crons_to_check = [get_class(x) for x in settings.CRON_CLASSES]
+ emails = [admin[1] for admin in settings.ADMINS]
+
+ failed_runs_cronjob_email_prefix = getattr(settings, 'FAILED_RUNS_CRONJOB_EMAIL_PREFIX', '')
+
+ for cron in crons_to_check:
+
+ min_failures = getattr(cron, 'MIN_NUM_FAILURES', 10)
+ jobs = CronJobLog.objects.filter(code=cron.code).order_by('-end_time')[:min_failures]
+ failures = 0
+ message = ''
+
+ for job in jobs:
+ if not job.is_success:
+ failures += 1
+ message += 'Job ran at %s : \n\n %s \n\n' % (job.start_time, job.message)
+
+ if failures >= min_failures:
+ send_mail(
+ '%s%s failed %s times in a row!' % (
+ failed_runs_cronjob_email_prefix,
+ cron.code,
+ min_failures,
+ ),
+ message,
+ settings.DEFAULT_FROM_EMAIL, emails
+ )
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/helpers.py b/thesisenv/lib/python3.6/site-packages/django_cron/helpers.py
new file mode 100644
index 0000000..5b40ac4
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/helpers.py
@@ -0,0 +1,29 @@
+from django.utils.translation import ugettext as _
+from django.template.defaultfilters import pluralize
+
+
+def humanize_duration(duration):
+ """
+ Returns a humanized string representing time difference
+
+ For example: 2 days 1 hour 25 minutes 10 seconds
+ """
+ days = duration.days
+ hours = int(duration.seconds / 3600)
+ minutes = int(duration.seconds % 3600 / 60)
+ seconds = int(duration.seconds % 3600 % 60)
+
+ parts = []
+ if days > 0:
+ parts.append(u'%s %s' % (days, pluralize(days, _('day,days'))))
+
+ if hours > 0:
+ parts.append(u'%s %s' % (hours, pluralize(hours, _('hour,hours'))))
+
+ if minutes > 0:
+ parts.append(u'%s %s' % (minutes, pluralize(minutes, _('minute,minutes'))))
+
+ if seconds > 0:
+ parts.append(u'%s %s' % (seconds, pluralize(seconds, _('second,seconds'))))
+
+ return ', '.join(parts) if len(parts) != 0 else _('< 1 second')
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/compat_modules/__init__.py b/thesisenv/lib/python3.6/site-packages/django_cron/management/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/compat_modules/__init__.py
rename to thesisenv/lib/python3.6/site-packages/django_cron/management/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/concurrency/__init__.py b/thesisenv/lib/python3.6/site-packages/django_cron/management/commands/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/concurrency/__init__.py
rename to thesisenv/lib/python3.6/site-packages/django_cron/management/commands/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/management/commands/runcrons.py b/thesisenv/lib/python3.6/site-packages/django_cron/management/commands/runcrons.py
new file mode 100644
index 0000000..2e506ba
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/management/commands/runcrons.py
@@ -0,0 +1,80 @@
+import traceback
+from datetime import timedelta
+
+from django.core.management.base import BaseCommand
+from django.conf import settings
+from django.db import close_old_connections
+
+from django_cron import CronJobManager, get_class, get_current_time
+from django_cron.models import CronJobLog
+
+
+DEFAULT_LOCK_TIME = 24 * 60 * 60 # 24 hours
+
+
+class Command(BaseCommand):
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'cron_classes',
+ nargs='*'
+ )
+ parser.add_argument(
+ '--force',
+ action='store_true',
+ help='Force cron runs'
+ )
+ parser.add_argument(
+ '--silent',
+ action='store_true',
+ help='Do not push any message on console'
+ )
+
+ def handle(self, *args, **options):
+ """
+ Iterates over all the CRON_CLASSES (or if passed in as a commandline argument)
+ and runs them.
+ """
+ cron_classes = options['cron_classes']
+ if cron_classes:
+ cron_class_names = cron_classes
+ else:
+ cron_class_names = getattr(settings, 'CRON_CLASSES', [])
+
+ try:
+ crons_to_run = [get_class(x) for x in cron_class_names]
+ except Exception:
+ error = traceback.format_exc()
+ self.stdout.write('Make sure these are valid cron class names: %s\n%s' % (cron_class_names, error))
+ return
+
+ for cron_class in crons_to_run:
+ run_cron_with_cache_check(
+ cron_class,
+ force=options['force'],
+ silent=options['silent']
+ )
+
+ clear_old_log_entries()
+ close_old_connections()
+
+
+def run_cron_with_cache_check(cron_class, force=False, silent=False):
+ """
+ Checks the cache and runs the cron or not.
+
+ @cron_class - cron class to run.
+ @force - run job even if not scheduled
+ @silent - suppress notifications
+ """
+
+ with CronJobManager(cron_class, silent) as manager:
+ manager.run(force)
+
+
+def clear_old_log_entries():
+ """
+ Removes older log entries, if the appropriate setting has been set
+ """
+ if hasattr(settings, 'DJANGO_CRON_DELETE_LOGS_OLDER_THAN'):
+ delta = timedelta(days=settings.DJANGO_CRON_DELETE_LOGS_OLDER_THAN)
+ CronJobLog.objects.filter(end_time__lt=get_current_time() - delta).delete()
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/migrations/0001_initial.py b/thesisenv/lib/python3.6/site-packages/django_cron/migrations/0001_initial.py
new file mode 100644
index 0000000..4f10227
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/migrations/0001_initial.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CronJobLog',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(max_length=64, db_index=True)),
+ ('start_time', models.DateTimeField(db_index=True)),
+ ('end_time', models.DateTimeField(db_index=True)),
+ ('is_success', models.BooleanField(default=False)),
+ ('message', models.TextField(max_length=1000, blank=True)),
+ ('ran_at_time', models.TimeField(db_index=True, null=True, editable=False, blank=True)),
+ ],
+ ),
+ migrations.AlterIndexTogether(
+ name='cronjoblog',
+ index_together=set([('code', 'is_success', 'ran_at_time'), ('code', 'start_time', 'ran_at_time'), ('code', 'start_time')]),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/migrations/0002_remove_max_length_from_CronJobLog_message.py b/thesisenv/lib/python3.6/site-packages/django_cron/migrations/0002_remove_max_length_from_CronJobLog_message.py
new file mode 100644
index 0000000..0e16622
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/migrations/0002_remove_max_length_from_CronJobLog_message.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_cron', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='cronjoblog',
+ name='message',
+ field=models.TextField(default='', blank=True),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/contrib/__init__.py b/thesisenv/lib/python3.6/site-packages/django_cron/migrations/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/contrib/__init__.py
rename to thesisenv/lib/python3.6/site-packages/django_cron/migrations/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/models.py b/thesisenv/lib/python3.6/site-packages/django_cron/models.py
new file mode 100644
index 0000000..73e093a
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/models.py
@@ -0,0 +1,28 @@
+from django.db import models
+
+
+class CronJobLog(models.Model):
+ """
+ Keeps track of the cron jobs that ran etc. and any error
+ messages if they failed.
+ """
+ code = models.CharField(max_length=64, db_index=True)
+ start_time = models.DateTimeField(db_index=True)
+ end_time = models.DateTimeField(db_index=True)
+ is_success = models.BooleanField(default=False)
+ message = models.TextField(default='', blank=True) # TODO: db_index=True
+
+ # This field is used to mark jobs executed in exact time.
+ # Jobs that run every X minutes, have this field empty.
+ ran_at_time = models.TimeField(null=True, blank=True, db_index=True, editable=False)
+
+ def __unicode__(self):
+ return '%s (%s)' % (self.code, 'Success' if self.is_success else 'Fail')
+
+ class Meta:
+ index_together = [
+ ('code', 'is_success', 'ran_at_time'),
+ ('code', 'start_time', 'ran_at_time'),
+ ('code', 'start_time') # useful when finding latest run (order by start_time) of cron
+ ]
+ app_label = 'django_cron'
diff --git a/thesisenv/lib/python3.6/site-packages/django_cron/tests.py b/thesisenv/lib/python3.6/site-packages/django_cron/tests.py
new file mode 100644
index 0000000..1ec872c
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_cron/tests.py
@@ -0,0 +1,181 @@
+import threading
+from time import sleep
+from datetime import timedelta
+
+from django import db
+from django.test import TransactionTestCase
+from django.core.management import call_command
+from django.test.utils import override_settings
+from django.test.client import Client
+from django.urls import reverse
+from django.contrib.auth.models import User
+
+from freezegun import freeze_time
+
+from django_cron.helpers import humanize_duration
+from django_cron.models import CronJobLog
+
+
+class OutBuffer(object):
+ content = []
+ modified = False
+ _str_cache = ''
+
+ def write(self, *args):
+ self.content.extend(args)
+ self.modified = True
+
+ def str_content(self):
+ if self.modified:
+ self._str_cache = ''.join((str(x) for x in self.content))
+ self.modified = False
+
+ return self._str_cache
+
+
+class TestCase(TransactionTestCase):
+
+ success_cron = 'test_crons.TestSucessCronJob'
+ error_cron = 'test_crons.TestErrorCronJob'
+ five_mins_cron = 'test_crons.Test5minsCronJob'
+ run_at_times_cron = 'test_crons.TestRunAtTimesCronJob'
+ wait_3sec_cron = 'test_crons.Wait3secCronJob'
+ does_not_exist_cron = 'ThisCronObviouslyDoesntExist'
+ test_failed_runs_notification_cron = 'django_cron.cron.FailedRunsNotificationCronJob'
+
+ def setUp(self):
+ CronJobLog.objects.all().delete()
+
+ def test_success_cron(self):
+ logs_count = CronJobLog.objects.all().count()
+ call_command('runcrons', self.success_cron, force=True)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ def test_failed_cron(self):
+ logs_count = CronJobLog.objects.all().count()
+ call_command('runcrons', self.error_cron, force=True)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ def test_not_exists_cron(self):
+ logs_count = CronJobLog.objects.all().count()
+ out_buffer = OutBuffer()
+ call_command('runcrons', self.does_not_exist_cron, force=True, stdout=out_buffer)
+
+ self.assertIn('Make sure these are valid cron class names', out_buffer.str_content())
+ self.assertIn(self.does_not_exist_cron, out_buffer.str_content())
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count)
+
+ @override_settings(DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.file.FileLock')
+ def test_file_locking_backend(self):
+ logs_count = CronJobLog.objects.all().count()
+ call_command('runcrons', self.success_cron, force=True)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ def test_runs_every_mins(self):
+ logs_count = CronJobLog.objects.all().count()
+
+ with freeze_time("2014-01-01 00:00:00"):
+ call_command('runcrons', self.five_mins_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ with freeze_time("2014-01-01 00:04:59"):
+ call_command('runcrons', self.five_mins_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ with freeze_time("2014-01-01 00:05:01"):
+ call_command('runcrons', self.five_mins_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 2)
+
+ def test_runs_at_time(self):
+ logs_count = CronJobLog.objects.all().count()
+ with freeze_time("2014-01-01 00:00:01"):
+ call_command('runcrons', self.run_at_times_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ with freeze_time("2014-01-01 00:04:50"):
+ call_command('runcrons', self.run_at_times_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ with freeze_time("2014-01-01 00:05:01"):
+ call_command('runcrons', self.run_at_times_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 2)
+
+ def test_admin(self):
+ password = 'test'
+ user = User.objects.create_superuser(
+ 'test',
+ 'test@tivix.com',
+ password
+ )
+ self.client = Client()
+ self.client.login(username=user.username, password=password)
+
+ # edit CronJobLog object
+ call_command('runcrons', self.success_cron, force=True)
+ log = CronJobLog.objects.all()[0]
+ url = reverse('admin:django_cron_cronjoblog_change', args=(log.id,))
+ response = self.client.get(url)
+ self.assertIn('Cron job logs', str(response.content))
+
+ def run_cronjob_in_thread(self, logs_count):
+ call_command('runcrons', self.wait_3sec_cron)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+ db.close_old_connections()
+
+ def test_cache_locking_backend(self):
+ """
+ with cache locking backend
+ """
+ logs_count = CronJobLog.objects.all().count()
+ t = threading.Thread(target=self.run_cronjob_in_thread, args=(logs_count,))
+ t.daemon = True
+ t.start()
+ # this shouldn't get running
+ sleep(0.1) # to avoid race condition
+ call_command('runcrons', self.wait_3sec_cron)
+ t.join(10)
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ # TODO: this test doesn't pass - seems that second cronjob is locking file
+ # however it should throw an exception that file is locked by other cronjob
+ # @override_settings(
+ # DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.file.FileLock',
+ # DJANGO_CRON_LOCKFILE_PATH=os.path.join(os.getcwd())
+ # )
+ # def test_file_locking_backend_in_thread(self):
+ # """
+ # with file locking backend
+ # """
+ # logs_count = CronJobLog.objects.all().count()
+ # t = threading.Thread(target=self.run_cronjob_in_thread, args=(logs_count,))
+ # t.daemon = True
+ # t.start()
+ # # this shouldn't get running
+ # sleep(1) # to avoid race condition
+ # call_command('runcrons', self.wait_3sec_cron)
+ # t.join(10)
+ # self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
+
+ def test_failed_runs_notification(self):
+ CronJobLog.objects.all().delete()
+ logs_count = CronJobLog.objects.all().count()
+
+ for i in range(10):
+ call_command('runcrons', self.error_cron, force=True)
+ call_command('runcrons', self.test_failed_runs_notification_cron)
+
+ self.assertEqual(CronJobLog.objects.all().count(), logs_count + 11)
+
+ def test_humanize_duration(self):
+ test_subjects = (
+ (timedelta(days=1, hours=1, minutes=1, seconds=1), '1 day, 1 hour, 1 minute, 1 second'),
+ (timedelta(days=2), '2 days'),
+ (timedelta(days=15, minutes=4), '15 days, 4 minutes'),
+ (timedelta(), '< 1 second'),
+ )
+
+ for duration, humanized in test_subjects:
+ self.assertEqual(
+ humanize_duration(duration),
+ humanized
+ )
diff --git a/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/DESCRIPTION.rst b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/DESCRIPTION.rst
new file mode 100644
index 0000000..fc7832d
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/DESCRIPTION.rst
@@ -0,0 +1,654 @@
+==================
+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
`_ or `Celery `_
+* Uses multiprocessing (and threading) to send a large number of emails in parallel
+* Supports multilingual email templates (i18n)
+
+
+Dependencies
+============
+
+* `django >= 1.8 `_
+* `django-jsonfield `_
+
+
+Installation
+============
+
+|Build Status|
+
+
+* Install from PyPI (or you `manually download from PyPI `_)::
+
+ 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 there!',
+ )
+
+
+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 ``) |
++--------------------+----------+--------------------------------------------------+
+| 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, {{ name }}!',
+ 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 `_
+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 {{ name }}, 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 alice, 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 `_::
+
+ 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 `_:
+
+.. 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 `_.
+
+
+Created and maintained by the cool guys at `Stamps `_,
+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/
+
+
diff --git a/thesisenv/lib/python3.6/site-packages/celery-3.1.26.post2.dist-info/INSTALLER b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/INSTALLER
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery-3.1.26.post2.dist-info/INSTALLER
rename to thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/INSTALLER
diff --git a/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/METADATA b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/METADATA
new file mode 100644
index 0000000..2bdcf2f
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/METADATA
@@ -0,0 +1,685 @@
+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 `_ or `Celery `_
+* Uses multiprocessing (and threading) to send a large number of emails in parallel
+* Supports multilingual email templates (i18n)
+
+
+Dependencies
+============
+
+* `django >= 1.8 `_
+* `django-jsonfield `_
+
+
+Installation
+============
+
+|Build Status|
+
+
+* Install from PyPI (or you `manually download from PyPI `_)::
+
+ 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 there!',
+ )
+
+
+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 ``) |
++--------------------+----------+--------------------------------------------------+
+| 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, {{ name }}!',
+ 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 `_
+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 {{ name }}, 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 alice, 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 `_::
+
+ 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 `_:
+
+.. 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 `_.
+
+
+Created and maintained by the cool guys at `Stamps `_,
+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/
+
+
diff --git a/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/RECORD b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/RECORD
new file mode 100644
index 0000000..b22f417
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/RECORD
@@ -0,0 +1,95 @@
+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
diff --git a/thesisenv/lib/python3.6/site-packages/celery-3.1.26.post2.dist-info/WHEEL b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/WHEEL
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery-3.1.26.post2.dist-info/WHEEL
rename to thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/WHEEL
diff --git a/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/metadata.json b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/metadata.json
new file mode 100644
index 0000000..6cf5154
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/metadata.json
@@ -0,0 +1 @@
+{"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"}
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/top_level.txt b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/top_level.txt
new file mode 100644
index 0000000..ba90aa3
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/django_post_office-3.1.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+post_office
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/DESCRIPTION.rst b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/DESCRIPTION.rst
new file mode 100644
index 0000000..caa9f30
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/DESCRIPTION.rst
@@ -0,0 +1,121 @@
+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
+
+
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/INSTALLER b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/METADATA b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/METADATA
new file mode 100644
index 0000000..2407db7
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/METADATA
@@ -0,0 +1,143 @@
+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
+
+
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/RECORD b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/RECORD
new file mode 100644
index 0000000..aff8e2d
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/RECORD
@@ -0,0 +1,19 @@
+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
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/WHEEL b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/WHEEL
new file mode 100644
index 0000000..8b6dd1b
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/WHEEL
@@ -0,0 +1,6 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.29.0)
+Root-Is-Purelib: true
+Tag: py2-none-any
+Tag: py3-none-any
+
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/metadata.json b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/metadata.json
new file mode 100644
index 0000000..ee61bdf
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/metadata.json
@@ -0,0 +1 @@
+{"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"}
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/top_level.txt b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/top_level.txt
new file mode 100644
index 0000000..4fdcfcf
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield-2.0.2.dist-info/top_level.txt
@@ -0,0 +1 @@
+jsonfield
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield/__init__.py b/thesisenv/lib/python3.6/site-packages/jsonfield/__init__.py
new file mode 100644
index 0000000..54360e2
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield/__init__.py
@@ -0,0 +1 @@
+from .fields import JSONField, JSONCharField # noqa
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield/encoder.py b/thesisenv/lib/python3.6/site-packages/jsonfield/encoder.py
new file mode 100644
index 0000000..4923a90
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield/encoder.py
@@ -0,0 +1,58 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield/fields.py b/thesisenv/lib/python3.6/site-packages/jsonfield/fields.py
new file mode 100644
index 0000000..21a6e23
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield/fields.py
@@ -0,0 +1,183 @@
+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
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield/models.py b/thesisenv/lib/python3.6/site-packages/jsonfield/models.py
new file mode 100644
index 0000000..e5faf1b
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield/models.py
@@ -0,0 +1 @@
+# Django needs this to see it as a project
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield/subclassing.py b/thesisenv/lib/python3.6/site-packages/jsonfield/subclassing.py
new file mode 100644
index 0000000..49e30e1
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield/subclassing.py
@@ -0,0 +1,62 @@
+# 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
diff --git a/thesisenv/lib/python3.6/site-packages/jsonfield/tests.py b/thesisenv/lib/python3.6/site-packages/jsonfield/tests.py
new file mode 100644
index 0000000..2d0b980
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/jsonfield/tests.py
@@ -0,0 +1,392 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/__init__.py b/thesisenv/lib/python3.6/site-packages/post_office/__init__.py
new file mode 100644
index 0000000..2a93401
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/__init__.py
@@ -0,0 +1,5 @@
+VERSION = (3, 1, 0)
+
+from .backends import EmailBackend
+
+default_app_config = 'post_office.apps.PostOfficeConfig'
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/admin.py b/thesisenv/lib/python3.6/site-packages/post_office/admin.py
new file mode 100644
index 0000000..dc6c277
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/admin.py
@@ -0,0 +1,153 @@
+# -*- 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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/apps.py b/thesisenv/lib/python3.6/site-packages/post_office/apps.py
new file mode 100644
index 0000000..62cbb31
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class PostOfficeConfig(AppConfig):
+ name = 'post_office'
+ verbose_name = _("Post Office")
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/backends.py b/thesisenv/lib/python3.6/site-packages/post_office/backends.py
new file mode 100644
index 0000000..8aa1a9b
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/backends.py
@@ -0,0 +1,56 @@
+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()
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/cache.py b/thesisenv/lib/python3.6/site-packages/post_office/cache.py
new file mode 100644
index 0000000..232b0bf
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/cache.py
@@ -0,0 +1,26 @@
+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))
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/compat.py b/thesisenv/lib/python3.6/site-packages/post_office/compat.py
new file mode 100644
index 0000000..a7e6dca
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/compat.py
@@ -0,0 +1,46 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/connections.py b/thesisenv/lib/python3.6/site-packages/post_office/connections.py
new file mode 100644
index 0000000..088ef45
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/connections.py
@@ -0,0 +1,44 @@
+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()
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/fields.py b/thesisenv/lib/python3.6/site-packages/post_office/fields.py
new file mode 100644
index 0000000..16b42ed
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/fields.py
@@ -0,0 +1,58 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.mo b/thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..f397659
Binary files /dev/null and b/thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.mo differ
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.po b/thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..bece62a
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,130 @@
+# 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 , 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 \n"
+"Language-Team: LANGUAGE \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"
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.mo b/thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..2bac639
Binary files /dev/null and b/thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.mo differ
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.po b/thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 0000000..9d43d7a
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,130 @@
+# 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 , 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 \n"
+"Language-Team: LANGUAGE \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"
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.mo b/thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6a2b569
Binary files /dev/null and b/thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.po b/thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..b280767
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,206 @@
+# 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 , 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 \n"
+"Language-Team: LANGUAGE \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"
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.mo b/thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..359b303
Binary files /dev/null and b/thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.mo differ
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.po b/thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.po
new file mode 100644
index 0000000..422a178
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/locale/ru_RU/LC_MESSAGES/django.po
@@ -0,0 +1,211 @@
+# 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 , 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 "Вложения"
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/lockfile.py b/thesisenv/lib/python3.6/site-packages/post_office/lockfile.py
new file mode 100644
index 0000000..a9a2f81
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/lockfile.py
@@ -0,0 +1,148 @@
+# 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()
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/logutils.py b/thesisenv/lib/python3.6/site-packages/post_office/logutils.py
new file mode 100644
index 0000000..101ed3c
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/logutils.py
@@ -0,0 +1,37 @@
+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
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/mail.py b/thesisenv/lib/python3.6/site-packages/post_office/mail.py
new file mode 100644
index 0000000..d70355c
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/mail.py
@@ -0,0 +1,305 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/events/__init__.py b/thesisenv/lib/python3.6/site-packages/post_office/management/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/events/__init__.py
rename to thesisenv/lib/python3.6/site-packages/post_office/management/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/fixups/__init__.py b/thesisenv/lib/python3.6/site-packages/post_office/management/commands/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/fixups/__init__.py
rename to thesisenv/lib/python3.6/site-packages/post_office/management/commands/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/management/commands/cleanup_mail.py b/thesisenv/lib/python3.6/site-packages/post_office/management/commands/cleanup_mail.py
new file mode 100644
index 0000000..5d10db9
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/management/commands/cleanup_mail.py
@@ -0,0 +1,35 @@
+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))
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/management/commands/send_queued_mail.py b/thesisenv/lib/python3.6/site-packages/post_office/management/commands/send_queued_mail.py
new file mode 100644
index 0000000..ab739e9
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/management/commands/send_queued_mail.py
@@ -0,0 +1,60 @@
+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.')
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0001_initial.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0001_initial.py
new file mode 100644
index 0000000..a9c6270
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0001_initial.py
@@ -0,0 +1,93 @@
+# -*- 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,
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0002_add_i18n_and_backend_alias.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0002_add_i18n_and_backend_alias.py
new file mode 100644
index 0000000..c98f9bc
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0002_add_i18n_and_backend_alias.py
@@ -0,0 +1,89 @@
+# -*- 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')]),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0003_longer_subject.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0003_longer_subject.py
new file mode 100644
index 0000000..8bc645f
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0003_longer_subject.py
@@ -0,0 +1,25 @@
+# -*- 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),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0004_auto_20160607_0901.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0004_auto_20160607_0901.py
new file mode 100644
index 0000000..b749054
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0004_auto_20160607_0901.py
@@ -0,0 +1,120 @@
+# -*- 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'),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0005_auto_20170515_0013.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0005_auto_20170515_0013.py
new file mode 100644
index 0000000..842b2e0
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0005_auto_20170515_0013.py
@@ -0,0 +1,19 @@
+# -*- 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')]),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0006_attachment_mimetype.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0006_attachment_mimetype.py
new file mode 100644
index 0000000..189f08f
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0006_attachment_mimetype.py
@@ -0,0 +1,19 @@
+# -*- 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),
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/migrations/0007_auto_20170731_1342.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0007_auto_20170731_1342.py
new file mode 100644
index 0000000..2745d99
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/migrations/0007_auto_20170731_1342.py
@@ -0,0 +1,19 @@
+# -*- 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'},
+ ),
+ ]
diff --git a/thesisenv/lib/python3.6/site-packages/celery/tests/functional/__init__.py b/thesisenv/lib/python3.6/site-packages/post_office/migrations/__init__.py
similarity index 100%
rename from thesisenv/lib/python3.6/site-packages/celery/tests/functional/__init__.py
rename to thesisenv/lib/python3.6/site-packages/post_office/migrations/__init__.py
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/models.py b/thesisenv/lib/python3.6/site-packages/post_office/models.py
new file mode 100644
index 0000000..b074343
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/models.py
@@ -0,0 +1,284 @@
+# -*- 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
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/settings.py b/thesisenv/lib/python3.6/site-packages/post_office/settings.py
new file mode 100644
index 0000000..5d61ac0
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/settings.py
@@ -0,0 +1,95 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/test_settings.py b/thesisenv/lib/python3.6/site-packages/post_office/test_settings.py
new file mode 100644
index 0000000..63c28f7
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/test_settings.py
@@ -0,0 +1,84 @@
+# -*- 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',
+ ],
+ },
+ },
+]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/test_urls.py b/thesisenv/lib/python3.6/site-packages/post_office/test_urls.py
new file mode 100644
index 0000000..ede2ec9
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/test_urls.py
@@ -0,0 +1,6 @@
+from django.conf.urls import url
+from django.contrib import admin
+
+urlpatterns = [
+ url(r'^admin/', admin.site.urls),
+]
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/__init__.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/__init__.py
new file mode 100644
index 0000000..f682b8b
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/__init__.py
@@ -0,0 +1,8 @@
+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
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_backends.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_backends.py
new file mode 100644
index 0000000..3b122d5
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_backends.py
@@ -0,0 +1,113 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_cache.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_cache.py
new file mode 100644
index 0000000..bde6dbd
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_cache.py
@@ -0,0 +1,41 @@
+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'))
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_commands.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_commands.py
new file mode 100644
index 0000000..4948606
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_commands.py
@@ -0,0 +1,150 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_connections.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_connections.py
new file mode 100644
index 0000000..aecae75
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_connections.py
@@ -0,0 +1,13 @@
+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))
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_lockfile.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_lockfile.py
new file mode 100644
index 0000000..852c236
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_lockfile.py
@@ -0,0 +1,75 @@
+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()))
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_mail.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_mail.py
new file mode 100644
index 0000000..df5b66d
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_mail.py
@@ -0,0 +1,400 @@
+# -*- 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)
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_models.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_models.py
new file mode 100644
index 0000000..1d2245d
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_models.py
@@ -0,0 +1,332 @@
+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='HTML
')
+ 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, [('HTML
', '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='{{ name }}',
+ 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, 'Bob')
+ 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 %}Welcome to the site
'
+ )
+
+ 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 '], 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')),
+ '')
+ self.assertEqual(repr(Email(to=['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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_utils.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_utils.py
new file mode 100644
index 0000000..dbdf5bd
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_utils.py
@@ -0,0 +1,203 @@
+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.objects.create(to=['to@example.com'], from_email='Alice ',
+ subject='Test', message='Message', status=STATUS.sent)
+
+ # Should also support international domains
+ validate_email_with_name('Alice Bob ')
+
+ # These should raise ValidationError
+ self.assertRaises(ValidationError, validate_email_with_name, 'invalid')
+ self.assertRaises(ValidationError, validate_email_with_name, 'Al ')
+
+ 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 '])
+
+ # 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']
+ )
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/tests/test_views.py b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_views.py
new file mode 100644
index 0000000..8283666
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/tests/test_views.py
@@ -0,0 +1,35 @@
+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)
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/utils.py b/thesisenv/lib/python3.6/site-packages/post_office/utils.py
new file mode 100644
index 0000000..11c4b39
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/utils.py
@@ -0,0 +1,138 @@
+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
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/validators.py b/thesisenv/lib/python3.6/site-packages/post_office/validators.py
new file mode 100644
index 0000000..a1be485
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/validators.py
@@ -0,0 +1,50 @@
+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 " 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))
diff --git a/thesisenv/lib/python3.6/site-packages/post_office/views.py b/thesisenv/lib/python3.6/site-packages/post_office/views.py
new file mode 100644
index 0000000..60f00ef
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/post_office/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/PKG-INFO b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/PKG-INFO
new file mode 100644
index 0000000..46806cc
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/PKG-INFO
@@ -0,0 +1,20 @@
+Metadata-Version: 1.1
+Name: uWSGI
+Version: 2.0.17.1
+Summary: The uWSGI server
+Home-page: https://uwsgi-docs.readthedocs.io/en/latest/
+Author: Unbit
+Author-email: info@unbit.it
+License: GPLv2+
+Description: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
diff --git a/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/SOURCES.txt b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/SOURCES.txt
new file mode 100644
index 0000000..3d6004d
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/SOURCES.txt
@@ -0,0 +1,6 @@
+README
+uwsgidecorators.py
+uWSGI.egg-info/PKG-INFO
+uWSGI.egg-info/SOURCES.txt
+uWSGI.egg-info/dependency_links.txt
+uWSGI.egg-info/top_level.txt
\ No newline at end of file
diff --git a/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/dependency_links.txt b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/installed-files.txt b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/installed-files.txt
new file mode 100644
index 0000000..ac3e2fb
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/installed-files.txt
@@ -0,0 +1,6 @@
+../__pycache__/uwsgidecorators.cpython-36.pyc
+../uwsgidecorators.py
+PKG-INFO
+SOURCES.txt
+dependency_links.txt
+top_level.txt
diff --git a/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/top_level.txt b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/top_level.txt
new file mode 100644
index 0000000..474f53a
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/uWSGI-2.0.17.1-py3.6.egg-info/top_level.txt
@@ -0,0 +1 @@
+uwsgidecorators
diff --git a/thesisenv/lib/python3.6/site-packages/uwsgidecorators.py b/thesisenv/lib/python3.6/site-packages/uwsgidecorators.py
new file mode 100644
index 0000000..dd8e880
--- /dev/null
+++ b/thesisenv/lib/python3.6/site-packages/uwsgidecorators.py
@@ -0,0 +1,419 @@
+from functools import partial
+import sys
+from threading import Thread
+
+try:
+ import cPickle as pickle
+except:
+ import pickle
+
+import uwsgi
+
+if uwsgi.masterpid() == 0:
+ raise Exception(
+ "you have to enable the uWSGI master process to use this module")
+
+spooler_functions = {}
+mule_functions = {}
+postfork_chain = []
+
+
+# Python3 compatibility
+def _encode1(val):
+ if sys.version_info >= (3, 0) and isinstance(val, str):
+ return val.encode('utf-8')
+ else:
+ return val
+
+
+def _decode1(val):
+ if sys.version_info >= (3, 0) and isinstance(val, bytes):
+ return val.decode('utf-8')
+ else:
+ return val
+
+
+def _encode_to_spooler(vars):
+ return dict((_encode1(K), _encode1(V)) for (K, V) in vars.items())
+
+
+def _decode_from_spooler(vars):
+ return dict((_decode1(K), _decode1(V)) for (K, V) in vars.items())
+
+
+def get_free_signal():
+ for signum in range(0, 256):
+ if not uwsgi.signal_registered(signum):
+ return signum
+
+ raise Exception("No free uwsgi signal available")
+
+
+def manage_spool_request(vars):
+ # To check whether 'args' is in vals or not - decode the keys first,
+ # because in python3 all keys in 'vals' are have 'byte' types
+ vars = dict((_decode1(K), V) for (K, V) in vars.items())
+ if 'args' in vars:
+ for k in ('args', 'kwargs'):
+ vars[k] = pickle.loads(vars.pop(k))
+
+ vars = _decode_from_spooler(vars)
+ f = spooler_functions[vars['ud_spool_func']]
+
+ if 'args' in vars:
+ ret = f(*vars['args'], **vars['kwargs'])
+ else:
+ ret = f(vars)
+
+ return int(vars.get('ud_spool_ret', ret))
+
+
+def postfork_chain_hook():
+ for f in postfork_chain:
+ f()
+
+uwsgi.spooler = manage_spool_request
+uwsgi.post_fork_hook = postfork_chain_hook
+
+
+class postfork(object):
+ def __init__(self, f):
+ if callable(f):
+ self.wid = 0
+ self.f = f
+ else:
+ self.f = None
+ self.wid = f
+ postfork_chain.append(self)
+ def __call__(self, *args, **kwargs):
+ if self.f:
+ if self.wid > 0 and self.wid != uwsgi.worker_id():
+ return
+ return self.f()
+ self.f = args[0]
+
+
+class _spoolraw(object):
+
+ def __call__(self, *args, **kwargs):
+ arguments = self.base_dict.copy()
+ if not self.pass_arguments:
+ if len(args) > 0:
+ arguments.update(args[0])
+ if kwargs:
+ arguments.update(kwargs)
+ else:
+ spooler_args = {}
+ for key in ('message_dict', 'spooler', 'priority', 'at', 'body'):
+ if key in kwargs:
+ spooler_args.update({key: kwargs.pop(key)})
+ arguments.update(spooler_args)
+ arguments.update(
+ {'args': pickle.dumps(args), 'kwargs': pickle.dumps(kwargs)})
+ return uwsgi.spool(_encode_to_spooler(arguments))
+
+ # For backward compatibility (uWSGI < 1.9.13)
+ def spool(self, *args, **kwargs):
+ return self.__class__.__call__(self, *args, **kwargs)
+
+ def __init__(self, f, pass_arguments):
+ if not 'spooler' in uwsgi.opt:
+ raise Exception(
+ "you have to enable the uWSGI spooler to use @%s decorator" % self.__class__.__name__)
+ self.f = f
+ spooler_functions[self.f.__name__] = self.f
+ # For backward compatibility (uWSGI < 1.9.13)
+ self.f.spool = self.__call__
+ self.pass_arguments = pass_arguments
+ self.base_dict = {'ud_spool_func': self.f.__name__}
+
+
+class _spool(_spoolraw):
+
+ def __call__(self, *args, **kwargs):
+ self.base_dict['ud_spool_ret'] = str(uwsgi.SPOOL_OK)
+ return _spoolraw.__call__(self, *args, **kwargs)
+
+
+class _spoolforever(_spoolraw):
+
+ def __call__(self, *args, **kwargs):
+ self.base_dict['ud_spool_ret'] = str(uwsgi.SPOOL_RETRY)
+ return _spoolraw.__call__(self, *args, **kwargs)
+
+
+def spool_decorate(f=None, pass_arguments=False, _class=_spoolraw):
+ if not f:
+ return partial(_class, pass_arguments=pass_arguments)
+ return _class(f, pass_arguments)
+
+
+def spoolraw(f=None, pass_arguments=False):
+ return spool_decorate(f, pass_arguments)
+
+
+def spool(f=None, pass_arguments=False):
+ return spool_decorate(f, pass_arguments, _spool)
+
+
+def spoolforever(f=None, pass_arguments=False):
+ return spool_decorate(f, pass_arguments, _spoolforever)
+
+
+class mulefunc(object):
+
+ def __init__(self, f):
+ if callable(f):
+ self.fname = f.__name__
+ self.mule = 0
+ mule_functions[f.__name__] = f
+ else:
+ self.mule = f
+ self.fname = None
+
+ def real_call(self, *args, **kwargs):
+ uwsgi.mule_msg(pickle.dumps(
+ {
+ 'service': 'uwsgi_mulefunc',
+ 'func': self.fname,
+ 'args': args,
+ 'kwargs': kwargs
+ }
+ ), self.mule)
+
+ def __call__(self, *args, **kwargs):
+ if not self.fname:
+ self.fname = args[0].__name__
+ mule_functions[self.fname] = args[0]
+ return self.real_call
+
+ return self.real_call(*args, **kwargs)
+
+
+def mule_msg_dispatcher(message):
+ msg = pickle.loads(message)
+ if msg['service'] == 'uwsgi_mulefunc':
+ return mule_functions[msg['func']](*msg['args'], **msg['kwargs'])
+
+uwsgi.mule_msg_hook = mule_msg_dispatcher
+
+
+class rpc(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self, f):
+ uwsgi.register_rpc(self.name, f)
+ return f
+
+
+class farm_loop(object):
+
+ def __init__(self, f, farm):
+ self.f = f
+ self.farm = farm
+
+ def __call__(self):
+ if uwsgi.mule_id() == 0:
+ return
+ if not uwsgi.in_farm(self.farm):
+ return
+ while True:
+ message = uwsgi.farm_get_msg()
+ if message:
+ self.f(message)
+
+
+class farm(object):
+
+ def __init__(self, name=None, **kwargs):
+ self.name = name
+
+ def __call__(self, f):
+ postfork_chain.append(farm_loop(f, self.name))
+
+
+class mule_brain(object):
+
+ def __init__(self, f, num):
+ self.f = f
+ self.num = num
+
+ def __call__(self):
+ if uwsgi.mule_id() == self.num:
+ try:
+ self.f()
+ except:
+ exc = sys.exc_info()
+ sys.excepthook(exc[0], exc[1], exc[2])
+ sys.exit(1)
+
+
+class mule_brainloop(mule_brain):
+
+ def __call__(self):
+ if uwsgi.mule_id() == self.num:
+ while True:
+ try:
+ self.f()
+ except:
+ exc = sys.exc_info()
+ sys.excepthook(exc[0], exc[1], exc[2])
+ sys.exit(1)
+
+
+class mule(object):
+ def __init__(self, num):
+ self.num = num
+
+ def __call__(self, f):
+ postfork_chain.append(mule_brain(f, self.num))
+
+
+class muleloop(mule):
+ def __call__(self, f):
+ postfork_chain.append(mule_brainloop(f, self.num))
+
+
+class mulemsg_loop(object):
+
+ def __init__(self, f, num):
+ self.f = f
+ self.num = num
+
+ def __call__(self):
+ if uwsgi.mule_id() == self.num:
+ while True:
+ message = uwsgi.mule_get_msg()
+ if message:
+ self.f(message)
+
+
+class mulemsg(object):
+ def __init__(self, num):
+ self.num = num
+
+ def __call__(self, f):
+ postfork_chain.append(mulemsg_loop(f, self.num))
+
+
+class signal(object):
+
+ def __init__(self, num, **kwargs):
+ self.num = num
+ self.target = kwargs.get('target', '')
+
+ def __call__(self, f):
+ uwsgi.register_signal(self.num, self.target, f)
+ return f
+
+
+class timer(object):
+
+ def __init__(self, secs, **kwargs):
+ self.num = kwargs.get('signum', get_free_signal())
+ self.secs = secs
+ self.target = kwargs.get('target', '')
+
+ def __call__(self, f):
+ uwsgi.register_signal(self.num, self.target, f)
+ uwsgi.add_timer(self.num, self.secs)
+ return f
+
+
+class cron(object):
+
+ def __init__(self, minute, hour, day, month, dayweek, **kwargs):
+ self.num = kwargs.get('signum', get_free_signal())
+ self.minute = minute
+ self.hour = hour
+ self.day = day
+ self.month = month
+ self.dayweek = dayweek
+ self.target = kwargs.get('target', '')
+
+ def __call__(self, f):
+ uwsgi.register_signal(self.num, self.target, f)
+ uwsgi.add_cron(self.num, self.minute, self.hour,
+ self.day, self.month, self.dayweek)
+ return f
+
+
+class rbtimer(object):
+
+ def __init__(self, secs, **kwargs):
+ self.num = kwargs.get('signum', get_free_signal())
+ self.secs = secs
+ self.target = kwargs.get('target', '')
+
+ def __call__(self, f):
+ uwsgi.register_signal(self.num, self.target, f)
+ uwsgi.add_rb_timer(self.num, self.secs)
+ return f
+
+
+class filemon(object):
+
+ def __init__(self, fsobj, **kwargs):
+ self.num = kwargs.get('signum', get_free_signal())
+ self.fsobj = fsobj
+ self.target = kwargs.get('target', '')
+
+ def __call__(self, f):
+ uwsgi.register_signal(self.num, self.target, f)
+ uwsgi.add_file_monitor(self.num, self.fsobj)
+ return f
+
+
+class erlang(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self, f):
+ uwsgi.erlang_register_process(self.name, f)
+ return f
+
+
+class lock(object):
+ def __init__(self, f):
+ self.f = f
+
+ def __call__(self, *args, **kwargs):
+ # ensure the spooler will not call it
+ if uwsgi.i_am_the_spooler():
+ return
+ uwsgi.lock()
+ try:
+ return self.f(*args, **kwargs)
+ finally:
+ uwsgi.unlock()
+
+
+class thread(object):
+
+ def __init__(self, f):
+ self.f = f
+
+ def __call__(self, *args):
+ t = Thread(target=self.f, args=args)
+ t.daemon = True
+ t.start()
+ return self.f
+
+
+class harakiri(object):
+
+ def __init__(self, seconds):
+ self.s = seconds
+
+ def real_call(self, *args, **kwargs):
+ uwsgi.set_user_harakiri(self.s)
+ r = self.f(*args, **kwargs)
+ uwsgi.set_user_harakiri(0)
+ return r
+
+ def __call__(self, f):
+ self.f = f
+ return self.real_call