django-notification-1.2.0/0000755000076500000240000000000012354526115016223 5ustar paltmanstaff00000000000000django-notification-1.2.0/AUTHORS0000644000076500000240000000050212134247112017261 0ustar paltmanstaff00000000000000The PRIMARY AUTHORS are: * James Tauber * Brian Rosner * Jannis Leidel * Patrick Altman ADDITIONAL CONTRIBUTORS include: * Eduardo Padoan * Fabian Neumann * Juanjo Conti * Michael Trier * Nolan Brubaker * Mathieu Pillard * Matt Croydon * Daniel Greenfeld * Sławek Ehlert * neelesh * Gaël Le Mignot * Eduardo O. Padoandjango-notification-1.2.0/django_notification.egg-info/0000755000076500000240000000000012354526115023725 5ustar paltmanstaff00000000000000django-notification-1.2.0/django_notification.egg-info/dependency_links.txt0000644000076500000240000000000112354526115027773 0ustar paltmanstaff00000000000000 django-notification-1.2.0/django_notification.egg-info/not-zip-safe0000644000076500000240000000000112075020011026133 0ustar paltmanstaff00000000000000 django-notification-1.2.0/django_notification.egg-info/PKG-INFO0000644000076500000240000001744612354526115025036 0ustar paltmanstaff00000000000000Metadata-Version: 1.1 Name: django-notification Version: 1.2.0 Summary: User notification management for the Django web framework Home-page: https://github.com/pinax/django-notification Author: James Tauber Author-email: jtauber@jtauber.com License: UNKNOWN Description: .. _usage: Usage ===== Integrating notification support into your app is a simple three-step process. * create your notice types * create your notice templates * send notifications Creating Notice Types --------------------- You need to call ``create_notice_type(label, display, description)`` once to create the notice types for your application in the database. ``label`` is just the internal shortname that will be used for the type, ``display`` is what the user will see as the name of the notification type and `description` is a short description. For example:: notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation") One good way to automatically do this notice type creation is in a ``management.py`` file for your app, attached to the syncdb signal. Here is an example:: from django.conf import settings from django.db.models import signals from django.utils.translation import ugettext_noop as _ if "notification" in settings.INSTALLED_APPS: from notification import models as notification def create_notice_types(app, created_models, verbosity, **kwargs): notification.create_notice_type("friends_invite", _("Invitation Received"), _("you have received an invitation")) notification.create_notice_type("friends_accept", _("Acceptance Received"), _("an invitation you sent has been accepted")) signals.post_syncdb.connect(create_notice_types, sender=notification) else: print "Skipping creation of NoticeTypes as notification app not found" Notice that the code is wrapped in a conditional clause so if django-notification is not installed, your app will proceed anyway. Note that the display and description arguments are marked for translation by using ugettext_noop. That will enable you to use Django's makemessages management command and use django-notification's i18n capabilities. Notification templates ---------------------- There are four different templates that can be written to for the actual content of the notices: * ``short.txt`` is a very short, text-only version of the notice (suitable for things like email subjects) * ``full.txt`` is a longer, text-only version of the notice (suitable for things like email bodies) * ``notice.html`` is a short, html version of the notice, displayed in a user's notice list on the website * ``full.html`` is a long, html version of the notice (not currently used for anything) Each of these should be put in a directory on the template path called ``notification//``. If any of these are missing, a default would be used. In practice, ``notice.html`` and ``full.txt`` should be provided at a minimum. For example, ``notification/friends_invite/notice.html`` might contain:: {% load i18n %}{% url invitations as invitation_page %}{% url profile_detail username=invitation.from_user.username as user_url %} {% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend (see invitations){% endblocktrans %} and ``notification/friends/full.txt`` might contain:: {% load i18n %}{% url invitations as invitation_page %}{% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend. You can accept their invitation at: http://{{ current_site }}{{ invitation_page }} {% endblocktrans %} The context variables are provided when sending the notification. Sending Notification ==================== There are two different ways of sending out notifications. We have support for blocking and non-blocking methods of sending notifications. The most simple way to send out a notification, for example:: notification.send([to_user], "friends_invite", {"from_user": from_user}) One thing to note is that ``send`` is a proxy around either ``send_now`` or ``queue``. They all have the same signature:: send(users, label, extra_context) The parameters are: * ``users`` is an iterable of ``User`` objects to send the notification to. * ``label`` is the label you used in the previous step to identify the notice type. * ``extra_content`` is a dictionary to add custom context entries to the template used to render to notification. This is optional. ``send_now`` vs. ``queue`` vs. ``send`` --------------------------------------- Lets first break down what each does. ``send_now`` ~~~~~~~~~~~~ This is a blocking call that will check each user for elgibility of the notice and actually peform the send. ``queue`` ~~~~~~~~~ This is a non-blocking call that will queue the call to ``send_now`` to be executed at a later time. To later execute the call you need to use the ``emit_notices`` management command. ``send`` ~~~~~~~~ A proxy around ``send_now`` and ``queue``. It gets its behavior from a global setting named ``NOTIFICATION_QUEUE_ALL``. By default it is ``False``. This setting is meant to help control whether you want to queue any call to ``send``. ``send`` also accepts ``now`` and ``queue`` keyword arguments. By default each option is set to ``False`` to honor the global setting which is ``False``. This enables you to override on a per call basis whether it should call ``send_now`` or ``queue``. Optional notification support ----------------------------- In case you want to use django-notification in your reusable app, you can wrap the import of django-notification in a conditional clause that tests if it's installed before sending a notice. As a result your app or project still functions without notification. For example:: from django.conf import settings if "notification" in settings.INSTALLED_APPS: from notification import models as notification else: notification = None and then, later:: if notification: notification.send([to_user], "friends_invite", {"from_user": from_user}) Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Framework :: Django django-notification-1.2.0/django_notification.egg-info/requires.txt0000644000076500000240000000001312354526115026317 0ustar paltmanstaff00000000000000django>=1.4django-notification-1.2.0/django_notification.egg-info/SOURCES.txt0000644000076500000240000000257012354526115025615 0ustar paltmanstaff00000000000000AUTHORS LICENSE MANIFEST.in README.rst setup.py django_notification.egg-info/PKG-INFO django_notification.egg-info/SOURCES.txt django_notification.egg-info/dependency_links.txt django_notification.egg-info/not-zip-safe django_notification.egg-info/requires.txt django_notification.egg-info/top_level.txt docs/Makefile docs/changelog.rst docs/conf.py docs/index.rst docs/make.bat docs/settings.rst docs/usage.rst notification/__init__.py notification/admin.py notification/compat.py notification/engine.py notification/lockfile.py notification/models.py notification/signals.py notification/urls.py notification/views.py notification/backends/__init__.py notification/backends/base.py notification/backends/email.py notification/management/__init__.py notification/management/commands/__init__.py notification/management/commands/emit_notices.py notification/templates/notification/base.html notification/templates/notification/email_body.txt notification/templates/notification/email_subject.txt notification/templates/notification/full.html notification/templates/notification/full.txt notification/templates/notification/notice.html notification/templates/notification/notice_settings.html notification/templates/notification/short.txt notification/tests/__init__.py notification/tests/models.py notification/tests/test_commands.py notification/tests/test_models.py notification/tests/test_views.pydjango-notification-1.2.0/django_notification.egg-info/top_level.txt0000644000076500000240000000001512354526115026453 0ustar paltmanstaff00000000000000notification django-notification-1.2.0/docs/0000755000076500000240000000000012354526115017153 5ustar paltmanstaff00000000000000django-notification-1.2.0/docs/changelog.rst0000644000076500000240000000240712207412206021627 0ustar paltmanstaff00000000000000.. _changelog: ChangeLog ========= BI = backward incompatible change 1.1.1 ----- * fixed a deprecation warning 1.1 --- * added Russian locale * added travis integration for tests/lints * added created_notice_type wrapper * cleaned up some small bugs identified by pylint 1.0 --- * removed unused `message.py` module * removed `captureas` templatetag * added `notice_settings.html` template * other minor fixes and tweaks, mostly to code style 0.3 --- * pluggable backends 0.2.0 ----- * BI: renamed Notice.user to Notice.recipient * BI: renamed {{ user }} context variable in notification templates to {{ recipient }} * BI: added nullable Notice.sender and modified send_now and queue to take an optional sender * added received and sent methods taking a User instance to Notice.objects * New default behavior: single notice view now marks unseen notices as seen * no longer optionally depend on mailer; use django.core.mail.send_mail and we now encourge use of Django 1.2+ for mailer support * notifications are not sent to inactive users * users which do not exist when sending notification are now ignored * BI: split settings part of notices view to its own view notice_settings 0.1.5 ----- * added support for DEFAULT_HTTP_PROTOCOL allowing https absolute URLs django-notification-1.2.0/docs/conf.py0000644000076500000240000000122112134247112020437 0ustar paltmanstaff00000000000000import sys, os extensions = [] templates_path = [] source_suffix = '.rst' master_doc = 'index' project = u'django-notification' package = 'notification' copyright_holder = 'Eldarion' copyright = u'2013, %s' % copyright_holder exclude_patterns = ['_build'] pygments_style = 'sphinx' html_theme = 'default' htmlhelp_basename = '%sdoc' % project latex_documents = [ ('index', '%s.tex' % project, u'%s Documentation' % project, copyright_holder, 'manual'), ] man_pages = [ ('index', project, u'%s Documentation' % project, [copyright_holder], 1) ] sys.path.insert(0, os.pardir) m = __import__(package) version = m.__version__ release = version django-notification-1.2.0/docs/index.rst0000644000076500000240000000164112134247112021007 0ustar paltmanstaff00000000000000.. django-notification documentation master file, created by sphinx-quickstart on Thu Jun 30 13:22:02 2011. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to django-notification's documentation! =============================================== Many sites need to notify users when certain events have occurred and to allow configurable options as to how those notifications are to be received. The project aims to provide a Django app for this sort of functionality. This includes: * Submission of notification messages by other apps. * Notification messages on signing in. * Notification messages via email (configurable by user). * Notification messages via feed. Contents: .. toctree:: :maxdepth: 2 usage settings changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-notification-1.2.0/docs/make.bat0000644000076500000240000001004412073444303020553 0ustar paltmanstaff00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-notification.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-notification.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-notification-1.2.0/docs/Makefile0000644000076500000240000001104212073444303020605 0ustar paltmanstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-notification.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-notification.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-notification" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-notification" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-notification-1.2.0/docs/settings.rst0000644000076500000240000000531512134247146021551 0ustar paltmanstaff00000000000000.. _settings: Notification specific Settings ================================ The following allows you to specify the behavior of django-notification in your project. Please be aware of the native Django settings which can affect the behavior of django-notification. NOTIFICATION_BACKENDS --------------------- **Default**:: [ ("email", "notification.backends.email.EmailBackend"), ] TODO: Describe usage. Look at Pinax DEFAULT_HTTP_PROTOCOL --------------------- **Default**: `http` This is used to specify the beginning of URLs in the default `email_body.txt` file. A common use-case for overriding this default might be `https` for use on more secure projects. NOTIFICATION_LANGUAGE_MODULE ---------------------------- **Default**: `Not defined` The default behavior for this setting is that it does not exist. It allows users to specify their own notification language. Example model in a `languages` app:: from django.conf import settings class Language(models.Model): user = models.ForeignKey(User) language = models.CharField(_("language"), choices=settings.LANGUAGES, max_length="10") Setting this value in `settings.py`:: NOTIFICATION_LANGUAGE_MODULE = "languages.Language" DEFAULT_FROM_EMAIL ------------------ **Default**: `webmaster@localhost` Docs: https://docs.djangoproject.com/en/1.3/ref/settings/#default-from-email Default e-mail address to use for various automated correspondence from notification.backends.email. Is actually part of Django core settings. LANGUAGES --------- **Default**: `A tuple of all available languages.` Docs: https://docs.djangoproject.com/en/1.3/ref/settings/#languages The default for this is specifically used for things like the Django admin. However, if you need to specify a subset of languages for your site's front end you can use this setting to override the default. In which case this is the definated pattern of usage:: gettext = lambda s: s LANGUAGES = ( ('en', gettext('English')), ('fr', gettext('French')), ) NOTIFICATION_QUEUE_ALL ---------------------- **Default**: False By default, calling `notification.send` will send the notification immediately, however, if you set this setting to True, then the default behavior of the `send` method will be to queue messages in the database for sending via the `emit_notices` command. NOTIFICATION_LOCK_WAIT_TIMEOUT ------------------------------ **Default**: -1 How long to wait for the lock to become available. Default of -1 means to never wait for the lock to become available. This only applies when using crontab setup to execute the `emit_notices` management command to send queued messages rather than sending immediately. django-notification-1.2.0/docs/usage.rst0000644000076500000240000001371712302543443021016 0ustar paltmanstaff00000000000000.. _usage: Usage ===== Integrating notification support into your app is a simple three-step process. * create your notice types * create your notice templates * send notifications Creating Notice Types --------------------- You need to call ``create_notice_type(label, display, description)`` once to create the notice types for your application in the database. ``label`` is just the internal shortname that will be used for the type, ``display`` is what the user will see as the name of the notification type and `description` is a short description. For example:: notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation") One good way to automatically do this notice type creation is in a ``management.py`` file for your app, attached to the syncdb signal. Here is an example:: from django.conf import settings from django.db.models import signals from django.utils.translation import ugettext_noop as _ if "notification" in settings.INSTALLED_APPS: from notification import models as notification def create_notice_types(app, created_models, verbosity, **kwargs): notification.create_notice_type("friends_invite", _("Invitation Received"), _("you have received an invitation")) notification.create_notice_type("friends_accept", _("Acceptance Received"), _("an invitation you sent has been accepted")) signals.post_syncdb.connect(create_notice_types, sender=notification) else: print "Skipping creation of NoticeTypes as notification app not found" Notice that the code is wrapped in a conditional clause so if django-notification is not installed, your app will proceed anyway. Note that the display and description arguments are marked for translation by using ugettext_noop. That will enable you to use Django's makemessages management command and use django-notification's i18n capabilities. Notification templates ---------------------- There are four different templates that can be written to for the actual content of the notices: * ``short.txt`` is a very short, text-only version of the notice (suitable for things like email subjects) * ``full.txt`` is a longer, text-only version of the notice (suitable for things like email bodies) * ``notice.html`` is a short, html version of the notice, displayed in a user's notice list on the website * ``full.html`` is a long, html version of the notice (not currently used for anything) Each of these should be put in a directory on the template path called ``notification//``. If any of these are missing, a default would be used. In practice, ``notice.html`` and ``full.txt`` should be provided at a minimum. For example, ``notification/friends_invite/notice.html`` might contain:: {% load i18n %}{% url invitations as invitation_page %}{% url profile_detail username=invitation.from_user.username as user_url %} {% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend (see invitations){% endblocktrans %} and ``notification/friends/full.txt`` might contain:: {% load i18n %}{% url invitations as invitation_page %}{% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend. You can accept their invitation at: http://{{ current_site }}{{ invitation_page }} {% endblocktrans %} The context variables are provided when sending the notification. Sending Notification ==================== There are two different ways of sending out notifications. We have support for blocking and non-blocking methods of sending notifications. The most simple way to send out a notification, for example:: notification.send([to_user], "friends_invite", {"from_user": from_user}) One thing to note is that ``send`` is a proxy around either ``send_now`` or ``queue``. They all have the same signature:: send(users, label, extra_context) The parameters are: * ``users`` is an iterable of ``User`` objects to send the notification to. * ``label`` is the label you used in the previous step to identify the notice type. * ``extra_content`` is a dictionary to add custom context entries to the template used to render to notification. This is optional. ``send_now`` vs. ``queue`` vs. ``send`` --------------------------------------- Lets first break down what each does. ``send_now`` ~~~~~~~~~~~~ This is a blocking call that will check each user for elgibility of the notice and actually peform the send. ``queue`` ~~~~~~~~~ This is a non-blocking call that will queue the call to ``send_now`` to be executed at a later time. To later execute the call you need to use the ``emit_notices`` management command. ``send`` ~~~~~~~~ A proxy around ``send_now`` and ``queue``. It gets its behavior from a global setting named ``NOTIFICATION_QUEUE_ALL``. By default it is ``False``. This setting is meant to help control whether you want to queue any call to ``send``. ``send`` also accepts ``now`` and ``queue`` keyword arguments. By default each option is set to ``False`` to honor the global setting which is ``False``. This enables you to override on a per call basis whether it should call ``send_now`` or ``queue``. Optional notification support ----------------------------- In case you want to use django-notification in your reusable app, you can wrap the import of django-notification in a conditional clause that tests if it's installed before sending a notice. As a result your app or project still functions without notification. For example:: from django.conf import settings if "notification" in settings.INSTALLED_APPS: from notification import models as notification else: notification = None and then, later:: if notification: notification.send([to_user], "friends_invite", {"from_user": from_user}) django-notification-1.2.0/LICENSE0000644000076500000240000000206012073443447017232 0ustar paltmanstaff00000000000000Copyright (c) 2008 James Tauber and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.django-notification-1.2.0/MANIFEST.in0000644000076500000240000000016112073443447017763 0ustar paltmanstaff00000000000000include AUTHORS include LICENSE recursive-include docs * recursive-include notification/templates/notification * django-notification-1.2.0/notification/0000755000076500000240000000000012354526115020711 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/__init__.py0000644000076500000240000000002612354526020023013 0ustar paltmanstaff00000000000000__version__ = "1.2.0" django-notification-1.2.0/notification/admin.py0000644000076500000240000000073412073444303022353 0ustar paltmanstaff00000000000000from django.contrib import admin from notification.models import NoticeType, NoticeSetting, NoticeQueueBatch class NoticeTypeAdmin(admin.ModelAdmin): list_display = ["label", "display", "description", "default"] class NoticeSettingAdmin(admin.ModelAdmin): list_display = ["id", "user", "notice_type", "medium", "send"] admin.site.register(NoticeQueueBatch) admin.site.register(NoticeType, NoticeTypeAdmin) admin.site.register(NoticeSetting, NoticeSettingAdmin) django-notification-1.2.0/notification/backends/0000755000076500000240000000000012354526115022463 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/backends/__init__.py0000644000076500000240000000340512302545225024572 0ustar paltmanstaff00000000000000import sys from django.conf import settings from django.core import exceptions from .base import BaseBackend # noqa # pylint: disable-msg=C0103 # mostly for backend compatibility default_backends = [ ("email", "notification.backends.email.EmailBackend"), ] def load_backends(): backends = [] configured_backends = getattr(settings, "NOTIFICATION_BACKENDS", default_backends) for medium_id, bits in enumerate(configured_backends): if len(bits) == 2: label, backend_path = bits spam_sensitivity = None elif len(bits) == 3: label, backend_path, spam_sensitivity = bits else: raise exceptions.ImproperlyConfigured( "NOTIFICATION_BACKENDS does not contain enough data." ) dot = backend_path.rindex(".") backend_mod, backend_class = backend_path[:dot], backend_path[dot + 1:] try: # import the module and get the module from sys.modules __import__(backend_mod) mod = sys.modules[backend_mod] except ImportError as e: raise exceptions.ImproperlyConfigured( "Error importing notification backend {}: \"{}\"".format(backend_mod, e) ) # add the backend label and an instantiated backend class to the # backends list. backend_instance = getattr(mod, backend_class)(medium_id, spam_sensitivity) backends.append(((medium_id, label), backend_instance)) return dict(backends) def load_media_defaults(backends): media = [] defaults = {} for key, backend in backends.items(): # key is a tuple (medium_id, backend_label) media.append(key) defaults[key[0]] = backend.spam_sensitivity return media, defaults django-notification-1.2.0/notification/backends/base.py0000644000076500000240000000371212207413251023743 0ustar paltmanstaff00000000000000from django.conf import settings from django.template import Context from django.template.loader import render_to_string from django.contrib.sites.models import Site class BaseBackend(object): """ The base backend. """ def __init__(self, medium_id, spam_sensitivity=None): self.medium_id = medium_id if spam_sensitivity is not None: self.spam_sensitivity = spam_sensitivity def can_send(self, user, notice_type): """ Determines whether this backend is allowed to send a notification to the given user and notice_type. """ from notification.models import NoticeSetting return NoticeSetting.for_user(user, notice_type, self.medium_id).send def deliver(self, recipient, sender, notice_type, extra_context): """ Deliver a notification to the given recipient. """ raise NotImplementedError() def get_formatted_messages(self, formats, label, context): """ Returns a dictionary with the format identifier as the key. The values are are fully rendered templates with the given context. """ format_templates = {} for fmt in formats: # conditionally turn off autoescaping for .txt extensions in format if fmt.endswith(".txt"): context.autoescape = False format_templates[fmt] = render_to_string(( "notification/%s/%s" % (label, fmt), "notification/%s" % fmt), context_instance=context) return format_templates def default_context(self): default_http_protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") current_site = Site.objects.get_current() base_url = "%s://%s" % (default_http_protocol, current_site.domain) return Context({ "default_http_protocol": default_http_protocol, "current_site": current_site, "base_url": base_url }) django-notification-1.2.0/notification/backends/email.py0000644000076500000240000000250312302545214024116 0ustar paltmanstaff00000000000000from django.conf import settings from django.core.mail import send_mail from django.template.loader import render_to_string from django.utils.translation import ugettext from notification import backends class EmailBackend(backends.BaseBackend): spam_sensitivity = 2 def can_send(self, user, notice_type): can_send = super(EmailBackend, self).can_send(user, notice_type) if can_send and user.email: return True return False def deliver(self, recipient, sender, notice_type, extra_context): # TODO: require this to be passed in extra_context context = self.default_context() context.update({ "recipient": recipient, "sender": sender, "notice": ugettext(notice_type.display), }) context.update(extra_context) messages = self.get_formatted_messages(( "short.txt", "full.txt" ), notice_type.label, context) subject = "".join(render_to_string("notification/email_subject.txt", { "message": messages["short.txt"], }, context).splitlines()) body = render_to_string("notification/email_body.txt", { "message": messages["full.txt"], }, context) send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [recipient.email]) django-notification-1.2.0/notification/compat.py0000644000076500000240000000112712302544062022541 0ustar paltmanstaff00000000000000import django from django.conf import settings from django.utils import six # Django 1.5 add support for custom auth user model if django.VERSION >= (1, 5): AUTH_USER_MODEL = settings.AUTH_USER_MODEL else: AUTH_USER_MODEL = "auth.User" try: from django.contrib.auth import get_user_model except ImportError: from django.contrib.auth.models import User get_user_model = lambda: User try: from urllib import quote except ImportError: from urllib.parse import quote # noqa if six.PY3: from threading import get_ident else: from thread import get_ident # noqa django-notification-1.2.0/notification/engine.py0000644000076500000240000000712112354526006022530 0ustar paltmanstaff00000000000000import sys import time import logging import traceback import base64 from django.conf import settings from django.core.mail import mail_admins from django.contrib.sites.models import Site from django.utils.six.moves import cPickle as pickle # pylint: disable-msg=F from notification.lockfile import FileLock, AlreadyLocked, LockTimeout from notification.models import NoticeQueueBatch from notification.signals import emitted_notices from notification import models as notification from .compat import get_user_model # lock timeout value. how long to wait for the lock to become available. # default behavior is to never wait for the lock to be available. LOCK_WAIT_TIMEOUT = getattr(settings, "NOTIFICATION_LOCK_WAIT_TIMEOUT", -1) def acquire_lock(*args): if len(args) == 1: lock = FileLock(args[0]) else: lock = FileLock("send_notices") logging.debug("acquiring lock...") try: lock.acquire(LOCK_WAIT_TIMEOUT) except AlreadyLocked: logging.debug("lock already in place. quitting.") return except LockTimeout: logging.debug("waiting for the lock timed out. quitting.") return logging.debug("acquired.") return lock def send_all(*args): lock = acquire_lock(*args) batches, sent, sent_actual = 0, 0, 0 start_time = time.time() try: # nesting the try statement to be Python 2.4 try: for queued_batch in NoticeQueueBatch.objects.all(): notices = pickle.loads(base64.b64decode(queued_batch.pickled_data)) for user, label, extra_context, sender in notices: try: user = get_user_model().objects.get(pk=user) logging.info("emitting notice {} to {}".format(label, user)) # call this once per user to be atomic and allow for logging to # accurately show how long each takes. if notification.send_now([user], label, extra_context, sender): sent_actual += 1 except get_user_model().DoesNotExist: # Ignore deleted users, just warn about them logging.warning( "not emitting notice {} to user {} since it does not exist".format( label, user) ) sent += 1 queued_batch.delete() batches += 1 emitted_notices.send( sender=NoticeQueueBatch, batches=batches, sent=sent, sent_actual=sent_actual, run_time="%.2f seconds" % (time.time() - start_time) ) except Exception: # pylint: disable-msg=W0703 # get the exception _, e, _ = sys.exc_info() # email people current_site = Site.objects.get_current() subject = "[{} emit_notices] {}".format(current_site.name, e) message = "\n".join( traceback.format_exception(*sys.exc_info()) # pylint: disable-msg=W0142 ) mail_admins(subject, message, fail_silently=True) # log it as critical logging.critical("an exception occurred: {}".format(e)) finally: logging.debug("releasing lock...") lock.release() logging.debug("released.") logging.info("") logging.info("{} batches, {} sent".format(batches, sent,)) logging.info("done in {:.2f} seconds".format(time.time() - start_time)) django-notification-1.2.0/notification/lockfile.py0000644000076500000240000003531712302545104023054 0ustar paltmanstaff00000000000000""" lockfile.py - Platform-independent advisory file locks. Requires Python 2.5 unless you apply 2.4.diff Locking is done on a per-thread basis instead of a per-process basis. Usage: >>> lock = FileLock("somefile") >>> try: ... lock.acquire() ... except AlreadyLocked: ... print "somefile", "is locked already." ... except LockFailed: ... print "somefile", "can\\'t be locked." ... else: ... print "got lock" got lock >>> print lock.is_locked() True >>> lock.release() >>> lock = FileLock("somefile") >>> print lock.is_locked() False >>> with lock: ... print lock.is_locked() True >>> print lock.is_locked() False >>> # It is okay to lock twice from the same thread... >>> with lock: ... lock.acquire() ... >>> # Though no counter is kept, so you can"t unlock multiple times... >>> print lock.is_locked() False Exceptions: Error - base class for other exceptions LockError - base class for all locking exceptions AlreadyLocked - Another thread or process already holds the lock LockFailed - Lock failed for some other reason UnlockError - base class for all unlocking exceptions AlreadyUnlocked - File was not locked. NotMyLock - File was locked but not by the current thread/process """ from __future__ import division import sys import socket import os import threading import time import errno from .compat import quote, get_ident # Work with PEP8 and non-PEP8 versions of threading module. if not hasattr(threading, "current_thread"): threading.current_thread = threading.currentThread if not hasattr(threading.Thread, "get_name"): threading.Thread.get_name = threading.Thread.getName __all__ = ["Error", "LockError", "LockTimeout", "AlreadyLocked", "LockFailed", "UnlockError", "NotLocked", "NotMyLock", "LinkFileLock", "MkdirFileLock", "SQLiteFileLock"] class Error(Exception): """ Base class for other exceptions. >>> try: ... raise Error ... except Exception: ... pass """ pass class LockError(Error): """ Base class for error arising from attempts to acquire the lock. >>> try: ... raise LockError ... except Error: ... pass """ pass class LockTimeout(LockError): """Raised when lock creation fails within a user-defined period of time. >>> try: ... raise LockTimeout ... except LockError: ... pass """ pass class AlreadyLocked(LockError): """Some other thread/process is locking the file. >>> try: ... raise AlreadyLocked ... except LockError: ... pass """ pass class LockFailed(LockError): """Lock file creation failed for some other reason. >>> try: ... raise LockFailed ... except LockError: ... pass """ pass class UnlockError(Error): """ Base class for errors arising from attempts to release the lock. >>> try: ... raise UnlockError ... except Error: ... pass """ pass class NotLocked(UnlockError): """Raised when an attempt is made to unlock an unlocked file. >>> try: ... raise NotLocked ... except UnlockError: ... pass """ pass class NotMyLock(UnlockError): """Raised when an attempt is made to unlock a file someone else locked. >>> try: ... raise NotMyLock ... except UnlockError: ... pass """ pass class LockBase: """Base class for platform-specific lock classes.""" def __init__(self, path, threaded=True): """ >>> lock = LockBase("somefile") >>> lock = LockBase("somefile", threaded=False) """ self.path = path self.lock_file = os.path.abspath(path) + ".lock" self.hostname = socket.gethostname() self.pid = os.getpid() if threaded: name = threading.current_thread().get_name() tname = "%s-" % quote(name, safe="") else: tname = "" dirname = os.path.dirname(self.lock_file) self.unique_name = os.path.join(dirname, "%s.%s%s" % (self.hostname, tname, self.pid)) def acquire(self, timeout=None): """ Acquire the lock. * If timeout is omitted (or None), wait forever trying to lock the file. * If timeout > 0, try to acquire the lock for that many seconds. If the lock period expires and the file is still locked, raise LockTimeout. * If timeout <= 0, raise AlreadyLocked immediately if the file is already locked. """ raise NotImplementedError("implement in subclass") def release(self): """ Release the lock. If the file is not locked, raise NotLocked. """ raise NotImplementedError("implement in subclass") def is_locked(self): """ Tell whether or not the file is locked. """ raise NotImplementedError("implement in subclass") def i_am_locking(self): """ Return True if this object is locking the file. """ raise NotImplementedError("implement in subclass") def break_lock(self): """ Remove a lock. Useful if a locking thread failed to unlock. """ raise NotImplementedError("implement in subclass") def __enter__(self): """ Context manager support. """ self.acquire() return self def __exit__(self, *_exc): """ Context manager support. """ self.release() class LinkFileLock(LockBase): """Lock access to a file using atomic property of link(2).""" def acquire(self, timeout=None): try: open(self.unique_name, "wb").close() except IOError: raise LockFailed("failed to create %s" % self.unique_name) end_time = time.time() if timeout is not None and timeout > 0: end_time += timeout while True: # Try and create a hard link to it. try: os.link(self.unique_name, self.lock_file) except OSError: # Link creation failed. Maybe we"ve double-locked? nlinks = os.stat(self.unique_name).st_nlink if nlinks == 2: # The original link plus the one I created == 2. We"re # good to go. return else: # Otherwise the lock creation failed. if timeout is not None and time.time() > end_time: os.unlink(self.unique_name) if timeout > 0: raise LockTimeout else: raise AlreadyLocked time.sleep(timeout is not None and (timeout / 10) or 0.1) else: # Link creation succeeded. We"re good to go. return def release(self): if not self.is_locked(): raise NotLocked elif not os.path.exists(self.unique_name): raise NotMyLock os.unlink(self.unique_name) os.unlink(self.lock_file) def is_locked(self): return os.path.exists(self.lock_file) def i_am_locking(self): return (self.is_locked() and os.path.exists(self.unique_name) and os.stat(self.unique_name).st_nlink == 2) def break_lock(self): if os.path.exists(self.lock_file): os.unlink(self.lock_file) class MkdirFileLock(LockBase): """Lock file by creating a directory.""" def __init__(self, path, threaded=True): """ >>> lock = MkdirFileLock("somefile") >>> lock = MkdirFileLock("somefile", threaded=False) """ LockBase.__init__(self, path, threaded) if threaded: tname = "%x-" % get_ident() else: tname = "" # Lock file itself is a directory. Place the unique file name into # it. self.unique_name = os.path.join( self.lock_file, "{}.{}{}".format(self.hostname, tname, self.pid) ) def attempt_acquire(self, timeout, end_time, wait): try: os.mkdir(self.lock_file) except OSError: err = sys.exc_info()[1] if err.errno == errno.EEXIST: # Already locked. if os.path.exists(self.unique_name): # Already locked by me. return if timeout is not None and time.time() > end_time: if timeout > 0: raise LockTimeout else: # Someone else has the lock. raise AlreadyLocked time.sleep(wait) else: # Couldn"t create the lock for some other reason raise LockFailed("failed to create %s" % self.lock_file) else: open(self.unique_name, "wb").close() return def acquire(self, timeout=None): end_time = time.time() if timeout is not None and timeout > 0: end_time += timeout if timeout is None: wait = 0.1 else: wait = max(0, timeout / 10) while True: self.attempt_acquire(self, timeout, end_time, wait) def release(self): if not self.is_locked(): raise NotLocked elif not os.path.exists(self.unique_name): raise NotMyLock os.unlink(self.unique_name) os.rmdir(self.lock_file) def is_locked(self): return os.path.exists(self.lock_file) def i_am_locking(self): return (self.is_locked() and os.path.exists(self.unique_name)) def break_lock(self): if os.path.exists(self.lock_file): for name in os.listdir(self.lock_file): os.unlink(os.path.join(self.lock_file, name)) os.rmdir(self.lock_file) class SQLiteFileLock(LockBase): """Demonstration of using same SQL-based locking.""" import tempfile _fd, testdb = tempfile.mkstemp() os.close(_fd) os.unlink(testdb) del _fd, tempfile def __init__(self, path, threaded=True): LockBase.__init__(self, path, threaded) self.lock_file = unicode(self.lock_file) self.unique_name = unicode(self.unique_name) import sqlite3 self.connection = sqlite3.connect(SQLiteFileLock.testdb) cursor = self.connection.cursor() try: cursor.execute("create table locks" "(" " lock_file varchar(32)," " unique_name varchar(32)" ")") except sqlite3.OperationalError: pass else: self.connection.commit() import atexit atexit.register(os.unlink, SQLiteFileLock.testdb) def create_lock(self, cursor): # Not locked. Try to lock it. cursor.execute("insert into locks" " (lock_file, unique_name)" " values" " (?, ?)", (self.lock_file, self.unique_name)) self.connection.commit() # Check to see if we are the only lock holder. cursor.execute("select * from locks where unique_name = ?", (self.unique_name,)) rows = cursor.fetchall() if len(rows) > 1: # Nope. Someone else got there. Remove our lock. cursor.execute("delete from locks where unique_name = ?", (self.unique_name,)) self.connection.commit() return False return True def i_am_the_only_lock(self, cursor): # Check to see if we are the only lock holder. cursor.execute("select * from locks where unique_name = ?", (self.unique_name,)) rows = cursor.fetchall() if len(rows) == 1: # We"re the locker, so go home. return def attempt_acquire(self, timeout, cursor, end_time, wait): if self.is_locked(): if self.i_am_the_only_lock(cursor): return else: if self.create_lock(cursor): return # Maybe we should wait a bit longer. if timeout is not None and time.time() > end_time: if timeout > 0: # No more waiting. raise LockTimeout else: # Someone else has the lock and we are impatient.. raise AlreadyLocked # Well, okay. We"ll give it a bit longer. time.sleep(wait) def acquire(self, timeout=None): end_time = time.time() if timeout is not None and timeout > 0: end_time += timeout if timeout is None: wait = 0.1 elif timeout <= 0: wait = 0 else: wait = timeout / 10 cursor = self.connection.cursor() while True: self.attempt_acquire(self, timeout, cursor, end_time, wait) def release(self): if not self.is_locked(): raise NotLocked if not self.i_am_locking(): raise NotMyLock((self._who_is_locking(), self.unique_name)) cursor = self.connection.cursor() cursor.execute("delete from locks" " where unique_name = ?", (self.unique_name,)) self.connection.commit() def _who_is_locking(self): cursor = self.connection.cursor() cursor.execute("select unique_name from locks" " where lock_file = ?", (self.lock_file,)) return cursor.fetchone()[0] def is_locked(self): cursor = self.connection.cursor() cursor.execute("select * from locks" " where lock_file = ?", (self.lock_file,)) rows = cursor.fetchall() return not not rows def i_am_locking(self): cursor = self.connection.cursor() cursor.execute("select * from locks" " where lock_file = ?" " and unique_name = ?", (self.lock_file, self.unique_name)) return not not cursor.fetchall() def break_lock(self): cursor = self.connection.cursor() cursor.execute("delete from locks" " where lock_file = ?", (self.lock_file,)) self.connection.commit() # pylint: disable-msg=C0103 if hasattr(os, "link"): FileLock = LinkFileLock else: FileLock = MkdirFileLock django-notification-1.2.0/notification/management/0000755000076500000240000000000012354526115023025 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/management/__init__.py0000644000076500000240000000000012073443447025130 0ustar paltmanstaff00000000000000django-notification-1.2.0/notification/management/commands/0000755000076500000240000000000012354526115024626 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/management/commands/__init__.py0000644000076500000240000000000012073443447026731 0ustar paltmanstaff00000000000000django-notification-1.2.0/notification/management/commands/emit_notices.py0000644000076500000240000000052512207413251027655 0ustar paltmanstaff00000000000000import logging from django.core.management.base import BaseCommand from notification.engine import send_all class Command(BaseCommand): help = "Emit queued notices." def handle(self, *args, **options): logging.basicConfig(level=logging.DEBUG, format="%(message)s") logging.info("-" * 72) send_all(*args) django-notification-1.2.0/notification/models.py0000644000076500000240000001577012302543443022554 0ustar paltmanstaff00000000000000from __future__ import unicode_literals from __future__ import print_function import base64 from django.db import models from django.db.models.query import QuerySet from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ from django.utils.translation import get_language, activate from django.utils.encoding import python_2_unicode_compatible from django.utils.six.moves import cPickle as pickle # pylint: disable-msg=F from .compat import AUTH_USER_MODEL from notification import backends DEFAULT_QUEUE_ALL = False QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", DEFAULT_QUEUE_ALL) NOTIFICATION_BACKENDS = backends.load_backends() NOTICE_MEDIA, NOTICE_MEDIA_DEFAULTS = backends.load_media_defaults( backends=NOTIFICATION_BACKENDS ) class LanguageStoreNotAvailable(Exception): pass def create_notice_type(label, display, description, **kwargs): NoticeType.create(label, display, description, **kwargs) @python_2_unicode_compatible class NoticeType(models.Model): label = models.CharField(_("label"), max_length=40) display = models.CharField(_("display"), max_length=50) description = models.CharField(_("description"), max_length=100) # by default only on for media with sensitivity less than or equal to this number default = models.IntegerField(_("default")) def __str__(self): return self.label class Meta: verbose_name = _("notice type") verbose_name_plural = _("notice types") @classmethod def create(cls, label, display, description, default=2, verbosity=1): """ Creates a new NoticeType. This is intended to be used by other apps as a post_syncdb manangement step. """ try: notice_type = cls._default_manager.get(label=label) updated = False if display != notice_type.display: notice_type.display = display updated = True if description != notice_type.description: notice_type.description = description updated = True if default != notice_type.default: notice_type.default = default updated = True if updated: notice_type.save() if verbosity > 1: print("Updated %s NoticeType" % label) except cls.DoesNotExist: cls(label=label, display=display, description=description, default=default).save() if verbosity > 1: print("Created %s NoticeType" % label) class NoticeSetting(models.Model): """ Indicates, for a given user, whether to send notifications of a given type to a given medium. """ user = models.ForeignKey(AUTH_USER_MODEL, verbose_name=_("user")) notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type")) medium = models.CharField(_("medium"), max_length=1, choices=NOTICE_MEDIA) send = models.BooleanField(_("send")) class Meta: verbose_name = _("notice setting") verbose_name_plural = _("notice settings") unique_together = ("user", "notice_type", "medium") @classmethod def for_user(cls, user, notice_type, medium): try: return cls._default_manager.get(user=user, notice_type=notice_type, medium=medium) except cls.DoesNotExist: default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default) setting = cls(user=user, notice_type=notice_type, medium=medium, send=default) setting.save() return setting class NoticeQueueBatch(models.Model): """ A queued notice. Denormalized data for a notice. """ pickled_data = models.TextField() def get_notification_language(user): """ Returns site-specific notification language for this user. Raises LanguageStoreNotAvailable if this site does not use translated notifications. """ if getattr(settings, "NOTIFICATION_LANGUAGE_MODULE", False): try: app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split(".") model = models.get_model(app_label, model_name) # pylint: disable-msg=W0212 language_model = model._default_manager.get(user__id__exact=user.id) if hasattr(language_model, "language"): return language_model.language except (ImportError, ImproperlyConfigured, model.DoesNotExist): raise LanguageStoreNotAvailable raise LanguageStoreNotAvailable def send_now(users, label, extra_context=None, sender=None): """ Creates a new notice. This is intended to be how other apps create new notices. notification.send(user, "friends_invite_sent", { "spam": "eggs", "foo": "bar", ) """ sent = False if extra_context is None: extra_context = {} notice_type = NoticeType.objects.get(label=label) current_language = get_language() for user in users: # get user language for user from language store defined in # NOTIFICATION_LANGUAGE_MODULE setting try: language = get_notification_language(user) except LanguageStoreNotAvailable: language = None if language is not None: # activate the user's language activate(language) for backend in NOTIFICATION_BACKENDS.values(): if backend.can_send(user, notice_type): backend.deliver(user, sender, notice_type, extra_context) sent = True # reset environment to original language activate(current_language) return sent def send(*args, **kwargs): """ A basic interface around both queue and send_now. This honors a global flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should be queued or not. A per call ``queue`` or ``now`` keyword argument can be used to always override the default global behavior. """ queue_flag = kwargs.pop("queue", False) now_flag = kwargs.pop("now", False) assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True." if queue_flag: return queue(*args, **kwargs) elif now_flag: return send_now(*args, **kwargs) else: if QUEUE_ALL: return queue(*args, **kwargs) else: return send_now(*args, **kwargs) def queue(users, label, extra_context=None, sender=None): """ Queue the notification in NoticeQueueBatch. This allows for large amounts of user notifications to be deferred to a seperate process running outside the webserver. """ if extra_context is None: extra_context = {} if isinstance(users, QuerySet): users = [row["pk"] for row in users.values("pk")] else: users = [user.pk for user in users] notices = [] for user in users: notices.append((user, label, extra_context, sender)) NoticeQueueBatch(pickled_data=base64.b64encode(pickle.dumps(notices))).save() django-notification-1.2.0/notification/signals.py0000644000076500000240000000024312134247146022722 0ustar paltmanstaff00000000000000import django.dispatch # pylint: disable-msg=C0103 emitted_notices = django.dispatch.Signal( providing_args=["batches", "sent", "sent_actual", "run_time"] ) django-notification-1.2.0/notification/templates/0000755000076500000240000000000012354526115022707 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/templates/notification/0000755000076500000240000000000012354526115025375 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/templates/notification/base.html0000644000076500000240000000011712134247112027165 0ustar paltmanstaff00000000000000{% extends "account/base.html" %} {% block body_class %}notices{% endblock %} django-notification-1.2.0/notification/templates/notification/email_body.txt0000644000076500000240000000053112134247112030232 0ustar paltmanstaff00000000000000{% load i18n %}{% load url from future %}{% url "notification_notices" as notices_url %}{% blocktrans %}You have received the following notice from {{ current_site }}: {{ message }} To see other notices or change how you receive notifications, please go to {{ default_http_protocol }}://{{ current_site }}{{ notices_url }} {% endblocktrans %} django-notification-1.2.0/notification/templates/notification/email_subject.txt0000644000076500000240000000012412073443447030745 0ustar paltmanstaff00000000000000{% load i18n %}{% blocktrans %}[{{ current_site }}] {{ message }}{% endblocktrans %}django-notification-1.2.0/notification/templates/notification/full.html0000644000076500000240000000007612073443447027234 0ustar paltmanstaff00000000000000{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}django-notification-1.2.0/notification/templates/notification/full.txt0000644000076500000240000000007712073443447027110 0ustar paltmanstaff00000000000000{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %} django-notification-1.2.0/notification/templates/notification/notice.html0000644000076500000240000000007612073443447027553 0ustar paltmanstaff00000000000000{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}django-notification-1.2.0/notification/templates/notification/notice_settings.html0000644000076500000240000000362512207414555031473 0ustar paltmanstaff00000000000000{% extends "notification/base.html" %} {% load i18n %} {% load url from future %} {% block body_id %}notification-settings{% endblock %} {% block head_title %}{% trans "Notification Settings" %}{% endblock %} {% block body %}

{% trans "Notification Settings" %}

{% url "account_settings" as email_url %} {% if not request.user.email %} {% blocktrans %}

Note: You do not have a verified email address to which notices can be sent. Add one now.

{% endblocktrans %} {% endif %}
{% csrf_token %} {% for header in notice_settings.column_headers %} {% endfor %} {% for row in notice_settings.rows %} {% for cell in row.cells %} {% endfor %} {% endfor %}
{% trans "Notification Type" %} {{ header.title }}
{{ row.notice_type.display }}
{{ row.notice_type.description }}
{% endblock %}django-notification-1.2.0/notification/templates/notification/short.txt0000644000076500000240000000007612073443447027304 0ustar paltmanstaff00000000000000{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}django-notification-1.2.0/notification/tests/0000755000076500000240000000000012354526115022053 5ustar paltmanstaff00000000000000django-notification-1.2.0/notification/tests/__init__.py0000644000076500000240000000026312302543443024161 0ustar paltmanstaff00000000000000from ..models import NOTICE_MEDIA def get_backend_id(backend_name): for bid, bname in NOTICE_MEDIA: if bname == backend_name: return bid return None django-notification-1.2.0/notification/tests/models.py0000644000076500000240000000031312302543443023701 0ustar paltmanstaff00000000000000from django.db import models from ..compat import AUTH_USER_MODEL class Language(models.Model): user = models.ForeignKey(AUTH_USER_MODEL) language = models.CharField("language", max_length=10) django-notification-1.2.0/notification/tests/test_commands.py0000644000076500000240000000155012302543443025262 0ustar paltmanstaff00000000000000from django.test import TestCase from django.test.utils import override_settings from django.core import management, mail from ..compat import get_user_model from ..models import create_notice_type, queue class TestManagementCmd(TestCase): def setUp(self): self.user = get_user_model().objects.create_user("test_user", "test@user.com", "123456") self.user2 = get_user_model().objects.create_user("test_user2", "test2@user.com", "123456") create_notice_type("label", "display", "description") @override_settings(SITE_ID=1) def test_emit_notices(self): users = [self.user, self.user2] queue(users, "label") management.call_command("emit_notices") self.assertEqual(len(mail.outbox), 2) self.assertIn(self.user.email, mail.outbox[0].to) self.assertIn(self.user2.email, mail.outbox[1].to) django-notification-1.2.0/notification/tests/test_models.py0000644000076500000240000001167112302543443024751 0ustar paltmanstaff00000000000000import base64 from django.test import TestCase from django.test.utils import override_settings from django.conf import settings from django.contrib.sites.models import Site from django.core import mail from django.utils.six.moves import cPickle as pickle # pylint: disable-msg=F from ..models import NoticeType, NoticeSetting, NoticeQueueBatch from ..models import LanguageStoreNotAvailable from ..models import get_notification_language, create_notice_type, send_now, send, queue from ..compat import get_user_model from .models import Language from . import get_backend_id class BaseTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user("test_user", "test@user.com", "123456") self.user2 = get_user_model().objects.create_user("test_user2", "test2@user.com", "123456") create_notice_type("label", "display", "description") self.notice_type = NoticeType.objects.get(label="label") def tearDown(self): self.user.delete() self.user2.delete() self.notice_type.delete() class TestNoticeType(TestCase): def test_create_notice_type(self): label = "friends_invite" create_notice_type(label, "Invitation Received", "you received an invitation") n = NoticeType.objects.get(label=label) self.assertEqual(str(n), label) def test_create(self): label = "friends_invite" NoticeType.create(label, "Invitation Received", "you received an invitation", default=2, verbosity=2) n = NoticeType.objects.get(label=label) self.assertEqual(str(n), label) # update NoticeType.create(label, "Invitation for you", "you got an invitation", default=1, verbosity=2) n = NoticeType.objects.get(pk=n.pk) self.assertEqual(n.display, "Invitation for you") self.assertEqual(n.description, "you got an invitation") self.assertEqual(n.default, 1) class TestNoticeSetting(BaseTest): def test_for_user(self): email_id = get_backend_id("email") notice_setting = NoticeSetting.objects.create(user=self.user, notice_type=self.notice_type, medium=email_id, send=False) self.assertEqual(NoticeSetting.for_user(self.user, self.notice_type, email_id), notice_setting) # test default fallback NoticeSetting.for_user(self.user2, self.notice_type, email_id) ns2 = NoticeSetting.objects.get(user=self.user2, notice_type=self.notice_type, medium=email_id) self.assertTrue(ns2.send) class TestProcedures(BaseTest): def setUp(self): super(TestProcedures, self).setUp() self.lang = Language.objects.create(user=self.user, language="en_US") mail.outbox = [] def tearDown(self): super(TestProcedures, self).tearDown() self.lang.delete() NoticeQueueBatch.objects.all().delete() @override_settings(NOTIFICATION_LANGUAGE_MODULE="tests.Language") def test_get_notification_language(self): self.assertEqual(get_notification_language(self.user), "en_US") self.assertRaises(LanguageStoreNotAvailable, get_notification_language, self.user2) del settings.NOTIFICATION_LANGUAGE_MODULE self.assertRaises(LanguageStoreNotAvailable, get_notification_language, self.user) @override_settings(SITE_ID=1, NOTIFICATION_LANGUAGE_MODULE="tests.Language") def test_send_now(self): Site.objects.create(domain="localhost", name="localhost") users = [self.user, self.user2] send_now(users, "label") self.assertEqual(len(mail.outbox), 2) self.assertIn(self.user.email, mail.outbox[0].to) self.assertIn(self.user2.email, mail.outbox[1].to) @override_settings(SITE_ID=1) def test_send(self): self.assertRaises(AssertionError, send, queue=True, now=True) users = [self.user, self.user2] send(users, "label", now=True) self.assertEqual(len(mail.outbox), 2) self.assertIn(self.user.email, mail.outbox[0].to) self.assertIn(self.user2.email, mail.outbox[1].to) send(users, "label", queue=True) self.assertEqual(NoticeQueueBatch.objects.count(), 1) batch = NoticeQueueBatch.objects.all()[0] notices = pickle.loads(base64.b64decode(batch.pickled_data)) self.assertEqual(len(notices), 2) @override_settings(SITE_ID=1) def test_send_default(self): # default behaviout, send_now users = [self.user, self.user2] send(users, "label") self.assertEqual(len(mail.outbox), 2) self.assertEqual(NoticeQueueBatch.objects.count(), 0) @override_settings(SITE_ID=1) def test_queue_queryset(self): users = get_user_model().objects.all() queue(users, "label") self.assertEqual(len(mail.outbox), 0) self.assertEqual(NoticeQueueBatch.objects.count(), 1) django-notification-1.2.0/notification/tests/test_views.py0000644000076500000240000000334112302543443024616 0ustar paltmanstaff00000000000000from django.test import TestCase from django.core.urlresolvers import reverse from ..compat import get_user_model from ..models import create_notice_type, NoticeSetting, NoticeType from . import get_backend_id class TestViews(TestCase): def setUp(self): self.user = get_user_model().objects.create_user("test_user", "test@user.com", "123456") def test_notice_settings_login_required(self): url = reverse("notification_notice_settings") response = self.client.get(url) self.assertRedirects(response, "/accounts/login/?next={}".format(url), target_status_code=404) def test_notice_settings(self): create_notice_type("label_1", "display", "description") notice_type_1 = NoticeType.objects.get(label="label_1") create_notice_type("label_2", "display", "description") notice_type_2 = NoticeType.objects.get(label="label_2") email_id = get_backend_id("email") setting = NoticeSetting.for_user(self.user, notice_type_2, email_id) setting.send = False setting.save() self.client.login(username="test_user", password="123456") response = self.client.get(reverse("notification_notice_settings")) self.assertEqual(response.status_code, 200) # pylint: disable-msg=E1103 post_data = { "label_2_{}".format(email_id): "on", } response = self.client.post(reverse("notification_notice_settings"), data=post_data) self.assertEqual(response.status_code, 302) # pylint: disable-msg=E1103 self.assertFalse(NoticeSetting.for_user(self.user, notice_type_1, email_id).send) self.assertTrue(NoticeSetting.for_user(self.user, notice_type_2, email_id).send) django-notification-1.2.0/notification/urls.py0000644000076500000240000000031612207414401022237 0ustar paltmanstaff00000000000000from django.conf.urls import patterns, url from notification.views import notice_settings urlpatterns = patterns( "", url(r"^settings/$", notice_settings, name="notification_notice_settings"), ) django-notification-1.2.0/notification/views.py0000644000076500000240000000444112207413250022413 0ustar paltmanstaff00000000000000from django.shortcuts import render_to_response from django.http import HttpResponseRedirect from django.template import RequestContext from django.contrib.auth.decorators import login_required from notification.models import NoticeSetting, NoticeType, NOTICE_MEDIA @login_required def notice_settings(request): """ The notice settings view. Template: :template:`notification/notice_settings.html` Context: notice_types A list of all :model:`notification.NoticeType` objects. notice_settings A dictionary containing ``column_headers`` for each ``NOTICE_MEDIA`` and ``rows`` containing a list of dictionaries: ``notice_type``, a :model:`notification.NoticeType` object and ``cells``, a list of tuples whose first value is suitable for use in forms and the second value is ``True`` or ``False`` depending on a ``request.POST`` variable called ``form_label``, whose valid value is ``on``. """ notice_types = NoticeType.objects.all() settings_table = [] for notice_type in notice_types: settings_row = [] for medium_id, medium_display in NOTICE_MEDIA: form_label = "%s_%s" % (notice_type.label, medium_id) setting = NoticeSetting.for_user(request.user, notice_type, medium_id) if request.method == "POST": if request.POST.get(form_label) == "on": if not setting.send: setting.send = True setting.save() else: if setting.send: setting.send = False setting.save() settings_row.append((form_label, setting.send)) settings_table.append({"notice_type": notice_type, "cells": settings_row}) if request.method == "POST": next_page = request.POST.get("next_page", ".") return HttpResponseRedirect(next_page) settings = { "column_headers": [medium_display for medium_id, medium_display in NOTICE_MEDIA], "rows": settings_table, } return render_to_response("notification/notice_settings.html", { "notice_types": notice_types, "notice_settings": settings, }, context_instance=RequestContext(request)) django-notification-1.2.0/PKG-INFO0000644000076500000240000001744612354526115017334 0ustar paltmanstaff00000000000000Metadata-Version: 1.1 Name: django-notification Version: 1.2.0 Summary: User notification management for the Django web framework Home-page: https://github.com/pinax/django-notification Author: James Tauber Author-email: jtauber@jtauber.com License: UNKNOWN Description: .. _usage: Usage ===== Integrating notification support into your app is a simple three-step process. * create your notice types * create your notice templates * send notifications Creating Notice Types --------------------- You need to call ``create_notice_type(label, display, description)`` once to create the notice types for your application in the database. ``label`` is just the internal shortname that will be used for the type, ``display`` is what the user will see as the name of the notification type and `description` is a short description. For example:: notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation") One good way to automatically do this notice type creation is in a ``management.py`` file for your app, attached to the syncdb signal. Here is an example:: from django.conf import settings from django.db.models import signals from django.utils.translation import ugettext_noop as _ if "notification" in settings.INSTALLED_APPS: from notification import models as notification def create_notice_types(app, created_models, verbosity, **kwargs): notification.create_notice_type("friends_invite", _("Invitation Received"), _("you have received an invitation")) notification.create_notice_type("friends_accept", _("Acceptance Received"), _("an invitation you sent has been accepted")) signals.post_syncdb.connect(create_notice_types, sender=notification) else: print "Skipping creation of NoticeTypes as notification app not found" Notice that the code is wrapped in a conditional clause so if django-notification is not installed, your app will proceed anyway. Note that the display and description arguments are marked for translation by using ugettext_noop. That will enable you to use Django's makemessages management command and use django-notification's i18n capabilities. Notification templates ---------------------- There are four different templates that can be written to for the actual content of the notices: * ``short.txt`` is a very short, text-only version of the notice (suitable for things like email subjects) * ``full.txt`` is a longer, text-only version of the notice (suitable for things like email bodies) * ``notice.html`` is a short, html version of the notice, displayed in a user's notice list on the website * ``full.html`` is a long, html version of the notice (not currently used for anything) Each of these should be put in a directory on the template path called ``notification//``. If any of these are missing, a default would be used. In practice, ``notice.html`` and ``full.txt`` should be provided at a minimum. For example, ``notification/friends_invite/notice.html`` might contain:: {% load i18n %}{% url invitations as invitation_page %}{% url profile_detail username=invitation.from_user.username as user_url %} {% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend (see invitations){% endblocktrans %} and ``notification/friends/full.txt`` might contain:: {% load i18n %}{% url invitations as invitation_page %}{% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend. You can accept their invitation at: http://{{ current_site }}{{ invitation_page }} {% endblocktrans %} The context variables are provided when sending the notification. Sending Notification ==================== There are two different ways of sending out notifications. We have support for blocking and non-blocking methods of sending notifications. The most simple way to send out a notification, for example:: notification.send([to_user], "friends_invite", {"from_user": from_user}) One thing to note is that ``send`` is a proxy around either ``send_now`` or ``queue``. They all have the same signature:: send(users, label, extra_context) The parameters are: * ``users`` is an iterable of ``User`` objects to send the notification to. * ``label`` is the label you used in the previous step to identify the notice type. * ``extra_content`` is a dictionary to add custom context entries to the template used to render to notification. This is optional. ``send_now`` vs. ``queue`` vs. ``send`` --------------------------------------- Lets first break down what each does. ``send_now`` ~~~~~~~~~~~~ This is a blocking call that will check each user for elgibility of the notice and actually peform the send. ``queue`` ~~~~~~~~~ This is a non-blocking call that will queue the call to ``send_now`` to be executed at a later time. To later execute the call you need to use the ``emit_notices`` management command. ``send`` ~~~~~~~~ A proxy around ``send_now`` and ``queue``. It gets its behavior from a global setting named ``NOTIFICATION_QUEUE_ALL``. By default it is ``False``. This setting is meant to help control whether you want to queue any call to ``send``. ``send`` also accepts ``now`` and ``queue`` keyword arguments. By default each option is set to ``False`` to honor the global setting which is ``False``. This enables you to override on a per call basis whether it should call ``send_now`` or ``queue``. Optional notification support ----------------------------- In case you want to use django-notification in your reusable app, you can wrap the import of django-notification in a conditional clause that tests if it's installed before sending a notice. As a result your app or project still functions without notification. For example:: from django.conf import settings if "notification" in settings.INSTALLED_APPS: from notification import models as notification else: notification = None and then, later:: if notification: notification.send([to_user], "friends_invite", {"from_user": from_user}) Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Framework :: Django django-notification-1.2.0/README.rst0000644000076500000240000000126012354526006017710 0ustar paltmanstaff00000000000000=================== django-notification =================== .. image:: https://travis-ci.org/pinax/django-notification.png :target: https://travis-ci.org/pinax/django-notification Many sites need to notify users when certain events have occurred and to allow configurable options as to how those notifications are to be received. The project aims to provide a Django app for this sort of functionality. This includes: * submission of notification messages by other apps * notification messages on signing in * notification messages via email (configurable by user) * notification messages via feed Documentation ============= See http://django-notification.readthedocs.org/ django-notification-1.2.0/setup.cfg0000644000076500000240000000007312354526115020044 0ustar paltmanstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 django-notification-1.2.0/setup.py0000644000076500000240000000165612302543443017741 0ustar paltmanstaff00000000000000from setuptools import setup, find_packages setup( name="django-notification", version=__import__("notification").__version__, description="User notification management for the Django web framework", long_description=open("docs/usage.rst").read(), author="James Tauber", author_email="jtauber@jtauber.com", url="https://github.com/pinax/django-notification", packages=find_packages(), classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Framework :: Django", ], include_package_data=True, test_suite='runtests', install_requires=[ 'django>=1.4', ], zip_safe=False, )