Django Vox Documentation

Getting Started

Installation

Getting the code

The recommended way to install django-vox is via pip (on Windows, replace pip3 with pip)

$ pip3 install django-vox[markdown,twilio,html]

Configuring Django

Add 'django_vox' to your INSTALLED_APPS setting:

INSTALLED_APPS = [
    # ...
    'django_vox',
]

Additionally, you may want to configure certain backends depending on exactly what sort of other notifications you want.

The Demo

While you’re welcome to use the setup instructions here, it may be easier to just try out the demo that comes with this package. The demo doesn’t cover all the use cases (yet) but it does cover most of the standard stuff.

Setting up the Models

There’s basically two parts to setting up the models. First, you have to add notifications to the models that you want notifications about. Second, you have to add channels to those notifications to specify where they can be sent. Finally, you need to implement the AbstractContactable interfaces for whatever your channels return so that we now how to contact them.

If you only ever want to send notifications to the site contacts, you can skip step 2 and 3, but that’s not very fun, is it.

Adding Model Notifications

Notifications in django vox are centered around models. This is important, because it makes it possible to predictably know what parameters will be available in the notification templates, and provides a measure of sanity to whoever is editing them.

To add notifications to a model, change the parent class from django.db.models.Model to django_vox.models.VoxModel. Also, add VoxMeta inner class (much like django’s Meta) which contains an attribute, named notifications. Set it to a VoxNotifications object, and each parameter you pass to it will specify the parameters for another notification. The parameter keys are the notification’s codename and the values are the the description (if they’re a plain string) or you can use a VoxNotification object to specify more parameters.

class User(VoxModel):

    class VoxMeta:
        notifications = VoxNotifications(
            create=_('Notification to that a user created an account'),
        )

    ...

    def save(self, *args, **kwargs):
        new = self.id is None
        super().save(*args, **kwargs)
        if new:
            self.issue_notification('create')

...

class PurchaseOrder(VoxModel):

    class VoxMeta:
        notifications = VoxNotifications(
            received = _('Notification that an order was received.'),
            on_hold = _('Notification that an order is on hold.'),
        )

    def save(self, *args, **kwargs):
        new = self.id is None
        if not new:
            old = PurchaseOrder.objects.get(pk=self.pk)
        super().save(*args, **kwargs)
        if new:
            self.issue_notification('received')
        if not new and not old.on_hold and self.on_hold:
            self.issue_notification('on_hold')

Here’s an example of the long-winded form to specify your parameters, This is more verbose, but makes it easier to specify extra notification parameters (like actor & target model) if you need them.

class User(VoxModel):

    class VoxMeta:
        notifications = VoxNotifications(
            create=VoxNotification(
                _('Notification to that a user created an account'),
                actor_type='myapp.mymodel'),
        )

Once you’ve finished adding these, you’ll need to regenerate the notifications table using the make_notifications management command:

python3 manage.py make_notifications

Adding Channels

Channels are what allow you to select different recipients. The site contacts channel is available by default, but if you want any other channels, you have to create them yourself using the channel registry at django_vox.registry.channels. You can add new channels using either the add or add_self method takes four arguments:

key
A slug that identifies the channel. Should be unique per model.
name
A name that shows up in the admin. Optional, defaults to various automatic values.
recipient_type
Model class of the objects returned by the function. Optional, defaults to the VoxModel subclass (i.e. Foo in Foo.add_channel).
func
A function or method that returns the instances of recipient_type. The function is called with a single argument which is the VoxModel instance that will eventually use it (i.e. the content object). Optional, defaults to lambda x: x

An example of channels given the above code might look like this:

class PurchaseOrder(VoxModel):
    ...
    def get_purchasers(self):
        yield self.purchaser

    def get_managers(self):
        yield self.shop.manager

...

from django_vox.registry import channels
channels[User].add_self()
channels[PurchaseOrder].add('purchaser', _('Purchaser'), User,
    PurchaseOrder.get_purchasers)
channels[PurchaseOrder].add('manager', _('Manager'), User,
    PurchaseOrder.get_managers)

Adding Contact Info

Now we have to implement the get_contacts_for_notification(notification) method for all the things that are return in channels. In our above example, that’s just the User model. This method takes a notification, and returns all of the contacts that the object has enabled for that notification. The idea behind this method is that it allows you to implement your own notification settings on a per-contact basis.

For now, we’re just going to make an implementation that assumes every user will get email notifications for all notifications. We can alter the user class to look like this:

from django_vox.models import VoxModel
from django_vox.base import Contact

class User(VoxModel):
    ...
    email = models.EmailField(max_length=254, unique=True)

    def get_contacts_for_notification(notification):
        return Contact(self.name, 'email', self.email)

Note

We haven’t covered actors or targets, but this example should be enough to get you started.

And there you have it. Now, in order for this to do anything useful, you’ll need to add some appropriate templates. In this case, you’ll want an email template for the “User” recipient of the “user created” notification, and possibly a template for a site contact too.

Backends

A backend is what is responsible for sending the actual messages. A backend implements a “protocol” like email or SMS. Multiple backends might implement the same protocol, so you might have a backend that sends email via SMTP, and another one that uses the mailgun API. The important thing is that all the backends implementing a specific protocol must accept the same form of addresses.

Most of these backends require extra dependencies that are not required for the base package. The specific extras are listed in the documentation, and you can mix/match them. For example:

# adds dependencies for markdown email, xmpp, twitter and the twilio
# backends
pip3 install django-vox[xmpp,markdown,twitter,twilio]

In order to add or remove backends, you need to set the DJANGO_VOX_BACKENDS setting in your projects settings.py file. The setting is a list of class names for backends that are in-use/enabled. If not set, the default is (assuming you have the required dependencies):

DJANGO_VOX_BACKENDS = (
    # disabled by default
    # 'django_vox.backends.activity.Backend',
    'django_vox.backends.html_email.Backend',
    'django_vox.backends.markdown_email.Backend',
    'django_vox.backends.postmark_email.Backend',
    'django_vox.backends.template_email.Backend',
    'django_vox.backends.twilio.Backend',
    'django_vox.backends.twitter.Backend',
    'django_vox.backends.slack.Backend',
    'django_vox.backends.json_webhook.Backend',
    'django_vox.backends.xmpp.Backend',
)

Django-vox provides a few built-in backends. Here’s how to set them up and use them.

Activity Backend

Protocol
activity
Class
django_vox.backends.activity.Backend

This is the backend for Activity Streams support. Setup is covered on the Activities page.

Email Backends

Protocol
email

These backends are a wrapper around Django’s internal mailing system. As such, it uses all of the built-in email settings including DEFAULT_FROM_EMAIL, and everything that starts with EMAIL in the standard django settings.

There are 3 backends included:

  • One that uses HTML (and converts it to text for you)
  • One that uses Markdown (and coverts it to HTML for you)
  • One that uses Django template blocks to specify both HTML & text (not recommended)

HTML Email Backend

Class
django_vox.backends.html_email.Backend

When using this, the content of your template will have to be HTML. If you don’t, it will be HTML anyways, but it will look real bad, and the god will frown on you. The backend automatically uses a library to convert the HTML into plain-text, so that there is a text version of the email, and so that the spam filters think better of you.

Incidentally, the subject field is not HTML formatted.

Markdown Email Backend

Class
django_vox.backends.markdown_email.Backend
Extra
[markdown]

This backend has you specify the content of your templates with markdown. While markdown doesn’t give you quite the flexibility as HTML, it’s a bit more intuitive.

Template-based Email Backend

Class
django_vox.backends.template_email.Backend

This backend isn’t recommended because it’s probably too confusing to be wroth it. However, if you really need to tailor-make your emails, it’s a solution that you can make work.

Writing notification templates for emails are a little more complicated than they are for the other backends, because emails can have multiple parts to them (subject, text, and html). The basic form looks like this:

{% block subject %}Email Subject{% endblock %}
{% block text_body %}Text body of email{% endblock %}
{% block html_body %}HTML body of email{% endblock %}

Postmark Templates

Class
django_vox.backends.postmark_email.Backend

This backend requires one config setting: DJANGO_VOX_POSTMARK_TOKEN. It should be, unsurprisingly, your token for interacting with the postmark API. When using this backend, the ‘Subject’ field refers to Postmark’s “template alias” and the template content should look something like this:

parameter_one: {{ content.attribute }}
parameter_two: {{ recipient.name }}

Twilio

Protocol
sms
Class
django_vox.backends.twilio.Backend
Extra
[twilio]

The twilio backend uses Twilio’s python library. It depends on 3 settings, all of which needs to be set for proper functioning.

DJANGO_VOX_TWILIO_ACCOUNT_SID Twilio account ID
DJANGO_VOX_TWILIO_AUTH_TOKEN Twilio authentication token
DJANGO_VOX_TWILIO_FROM_NUMBER Phone # to send Twilio SMS from

Twitter

Protocol
twitter
Class
django_vox.backends.twitter.Backend
Extra
[twitter]

The twitter backend allows you to post updates to twitter and (with the right permissions), send direct messages to your followers. In order to set it up, you first need to create a twitter application. The python-twitter docs explain the process well. Note that you can ignore callback URL, and you’ll want to set the name, description, and website fields to the name, description, and website of your application.

Once you’re done that, you may want to turn on “Read, Write and Access direct messages” in the “Permissions” tab. Then generate/regenerate your access token and secret.

Once you’re done that, you’ll want to set the following values in your settings.py file:

DJANGO_VOX_TWITTER_CONSUMER_KEY Consumer Key (API Key)
DJANGO_VOX_TWITTER_CONSUMER_SECRET Consumer Secret (API Secret)
DJANGO_VOX_TWITTER_TOKEN_KEY Access Token
DJANGO_VOX_TWITTER_TOKEN_SECRET Access Token Secret

Note

In order to post a message to your wall, make a site contact with the the twitter protocol and a blank address. In order to send a direct message, you’ll need a address that equals your user’s twitter handle (not including the “@” prefix).

Webhook (JSON)

Protocol
json-webhook
Class
django_vox.backends.json_webhook.Backend

This backend post JSON-formatted data to webhook. It’s useful for implementing generic webhooks or integrating with systems like Huginn or Zapier. The way you specify parameters is the same as with the Postmark backend:

parameter_one: {{ content.attribute }}
parameter_two: Hello World

This will translate into:

{'parameter_one': '<content.attribute>',
 'parameter_two': 'Hello World'}

Webhook (Slack)

Protocol
slack-webhook
Class
django_vox.backends.slack.Backend

This backend requires no configuration in django, all of the configuration is essentially part of the addresses used in the protocol. For setting up slack-webhook addresses, see the documentation on protocols.

XMPP

Protocol
xmpp
Class
django_vox.backends.xmpp.Backend
Extra
[xmpp]

This backends lets you send messages over xmpp to other xmpp users. It’s pretty straightforward; however, it’s also pretty slow right now, so don’t use it unless your also doing notifications in the background.

To set this up, you need to have the XMPP address and password in your settings. Here’s the relevant settings.

DJANGO_VOX_XMPP_JID XMPP address
DJANGO_VOX_XMPP_PASSWORD Password

Protocols and Addresses

A protocol is what lets us sort out and make sense of contact information in django-vox. Classic examples of a “protocol” are things like email, SMS, and XMPP. You can also have proprietary protocols like Slack webhooks, or things that use Google or Apple’s push notifications.

Each protocol has it’s own kind of addresses. When a contact is sent a message, django-vox automatically selects a backend that matches the available contacts (and addresses by extension) for that protocol.

Activity

ID: activity

This protocol is for Activity Streams (and only slightly supported ActivityPub). Message are stored locally in the database and are retrievable from an inbox. Setting this up is a bit of work, see the section on Activities.

Email

ID: email

The email protocol is really straightforward, the contact’s address is just the email address.

SMS

ID: sms

The contact’s address for SMS is the contact’s phone number in E.164 format. It’s recommended to use django-phonenumber-field if you want to store these numbers in a database.

Twitter

Addresses for the twitter protocol can take two forms:

  1. An empty string means the message will get posted as a status update for the account specified in the setting.
  2. Anything else will be sent as a direct message to the user with that handle. You shouldn’t prefix an ‘@’ to the address and you need to have the correct permissions set in order for this to work.

Webhook Protocols

While webhooks aren’t typically a convenient way to contact end-users, they can be pretty useful for setting up site contacts. Because of the templating system, you can be quite flexible in the way you set them up.

Name ID Purpose
JSON Webhook json-webhook Generic JSON data (specified in template)
Slack Webhook slack-webhook Posting messages to Slack

XMPP

XMPP (or Jabber) is a standardized, decentralized chat system. The contact’s address should just be their XMPP address (or JID, as it’s sometimes called).

Notification Templates

Notification templates use django’s build-in templating system. This gives us more than enough power to craft the kind of message we want, without making things too onerous.

Example

The way a template looks shouldn’t be too foreign to you if you’re already used to django; but just in case you’re wondering, here’s an example:

<p>Hi {{ contact.name }},</p>

<p>{{ content.poster.name }} has posted a comment on your blog titled
{{ content.article.title }}.</p>

Note that the exact variables available will depend on which model the notification is attached to.

Variables

Several variables are provided to you in the template context.

content
This refers to whatever model the notification is attached to. It is visible as the content-type field of the notification when you’re editing it in the admin. Most of the time, you’re probably going to be wanting to use this.
contact
The Contact object that the message is being sent to. This object has 3 attributes: name, protocol, and address.
recipient
The type of this depends on which channel is selected as the recipient of a notification, and what kind of objects that channel returns. In practice, it will probably be some sort of user/user-profile object. When site contacts are the recipient, the value is a SiteContact object.
actor
This is only available if actor_type is specified for the notification. It refers to whoever or whatever is causing action associated with the notification.
target
This is only available if target_model is specified for the notification. It refers to whoever or whatever the action associated with the notification is affecting.

Most of the time, it’s recommended to just try and use a field on the content variable instead of target or actor. Sometimes, though, this is just not possible, and you want to be able to differentiate between the two at runtime, so that’s why they exist.

Miscellaneous Notes

Escaping

Django’s template engine has been primarily designed for outputting HTML. The only place in which this really matters is when it comes to excaping content. Plain text and HTML content work fine, however, with other formats like Markdown we need to wrap all the template variables with a custom variable escaping object that escapes everything on the fly. This has a few consequences.

  1. Most variables will be wrapped in this class. While the class mostly mimics the behavior of the underlying object, any template filter using isinstance will fail.
  2. In order to maintain compatibility with template filters, we don’t try to escape any of the basic numeric or date/time objects. For the most part this is okay, but it is theoretically possible to end up with a weird result.
  3. The result of template filters is typically not escaped correctly.

Subjects

Templates have a subject field which is sometimes used, depending on the backend. Any backend which supports a subject, has the attribute USE_SUBJECT set to True. Setting a subject on a template who’s backend doesn’t use it has no effect.

Attachments

Attachments are an optional feature of django-vox. In order to to use attachments, two things must be in order. First, you need to set them up on your models, and second you need to be using a backend that supports them (which is just email right now).

Setting up the Models

Adding attachments is a lot like adding notifications. Instead of the notification attribute on VoxMeta, you specify the attachments field

To add notifications to a model, change the parent class from django.db.models.Model to django_vox.models.VoxModel. Also, add VoxMeta inner class (much like django’s Meta) which contains one attribute, a tuple named notifications. Each item in the tuple should be a django_vox.models.VoxParam instance. The result might look something like:

class User(VoxModel):

    class VoxMeta:
        attachments = VoxAttachments(
            vcard=VoxAttach(attr='make_vcard', mime_string='text/vcard',
                label=_('Contact Info')),
            photo=VoxAttach(mime_attr='photo_mimetype'))

        notifications = (
        ...
        )

In this case, there are two attachment options. The first, get’s the file contents from User.make_vcard, and has a mime type of text/vcard. The second will get its contents from User.photo, and will get its mime type from whatever’s in User.photo_mimetype.

Once these have been added, attachment options should show up when editing templates in the admin.

Activities

The activities backend provides support for the Activity Streams (and very slightly ActivityPub) standards. Message are stored locally in the database and are retrievable from an inbox. References to notions like actors and inboxes refer to the ideas in that standard.

Setting up the activities is fairly involved, and also entirely optional unless you actually want to use the activity backend. As a result, it’s got its own documentation. Note that this backend is somewhat experimental. Don’t use it in production unless you’ve tested it really well and know what you’re doing.

Settings

Because we need to generate full uri/iris (including the domain) and we need to be able to do it without an HTTP request, we need to have a way of finding out your domain & scheme. If you’re already using django.contrib.sites and you have it set up with a SITE_ID in your settings, that’ll work. Otherwise a simpler solution is to just set these two settings:

SITE_DOMAIN Your site’s domain name
SITE_SSL True if you use HTTPS, False otherwise

Caution

SITE_SSL should nearly always be True (the default) unless you’re in development testing on localhost.

Also, because this backend isn’t enabled by default, you’ll need to alter DJANGO_VOX_BACKENDS and add 'django_vox.backends.activity.Backend',. You can see an example on the Backends page.

Registering actors

Like we had to register channels before, now we have to register actors too. It’s mostly accurate to say that actors should be any users that you want to be able send/receive activities.

Actors all have endpoints, and inboxes (and some unimplemented things). When you add an actor you specify the route for his/her endpoint using a regex, much like you would make a normal Django 1 url route. The parameters in the regex should correspond to the identifying field in the user. Here’s an example:

actors[User].set_regex(r'^users/(?P<username>[0-9]+)/$')

Additionally, you’ll also need to return the activity contact from the get_contacts_for_notification method for the actors. If you want to send them all the possible notification, then add the following code:

def get_contacts_for_notification(self, _notification):
    yield Contact(self.name, 'activity', self.get_actor_address())

Setting up models

Just like we had to add a bunch of properties to the models for the basic features in django_vox, there’s a few more to add to get good results for the activity stream. These aren’t strictly necessary, but your results will be pretty plain without them. Code samples of all of these are available in the test site the comes with django_vox.

First, the notification parameters take a new parameter activity_type. It can be set to an aspy.Activity subclass. If it’s not set, django_vox with either match the notification name to an activity type, or use the default ‘Create’ type.

Note

This code makes use of the aspy library. It’s a dependency, so you should get it automatically, just import aspy.

Second, the object of your activity entries defaults to the plain activity streams “Object” with an id (based on get_absolute_url() and a name (based on __str__()). This is pretty bare-bones to say the least. You can specify something more colorful by implementing the __activity__() method and returning another subclass of aspy.Object with perhaps a few properties.

Finally, there’s a few rare cases where the same model might need to give you different objects for different kinds of notifications. If you need to do this, you can override VoxModel.get_activity_object().

Note

If your model is also an actor, it’s recommended to use VoxModel.get_actor_address() to get the object’s ID, otherwise you can use django_vox.base.full_iri(self.get_absolute_url())

Accessing the inboxes

At this point, you should be able to make up activity notifications, issue them, and then retrieve them using django_vox.models.InboxItem. However, if you want to use our hackish ActivityPub half-implementation, there’s one/two more steps. First we have to enable the inbox middleware. Add this to your settings.py:

MIDDLEWARE = [
    ...
    'django_vox.middleware.activity_inbox_middleware',
]

That’s probably it, but if your setup is more complicated than “each user has their own inbox” (for example, you have an organization-wide actor and you need several users to be able to access the inbox) you’ll have to add support for object level permissions and give each user access to the permission django_vox.view_inbox for the specific actor involved. Django doesn’t support this by default. We recommend using rules but there are other authentication backends that work too. Here’s an example with rules:

@rules.predicate
def inbox_owner(user, inbox_actor):
    # one set of rules for organization actors
    if isinstance(inbox_actor, models.Organization):
        return user.organization == inbox_actor
    # another for user actors
    return user == inbox_actor

rules.add_perm('django_vox.view_inbox', inbox_owner)

That’s it folks.

Running the Demo

The project comes with a demo that can be run from the project directory using:

make demo

The demo runs with everything in memory, so as soon as you stop it, everything you’ve done to it will be gone! It uses django’s development server, so if you change the source files, the server will reload and any data you had will get reset.

The Jist

The demo is a really basic blog app with the ability for people to “subscribe” by adding their email address and name. Subscribers get emailed, whenever a new article is posted, and the email contains a link that lets them comment. The article’s author gets notified whenever comments are posted. Finally, there’s also a site contact that gets notified whenever a new subscriber is added.

The demo is set up with django’s console email backend, so all the email notifications are printed to the console, where you can see what is going on.

Walkthrough

You can use the demo however you want, but in case you’re lost, here’s a walkthough you can follow that will show you its features.

  1. First, go to the admin site. The url should be http://127.0.0.1:8000/admin/. The username is author@example.org and the password is password. Once in, you can go to “Articles” (under Vox Demo) and add a new one.
  2. The demo comes with a subscriber already added, so once you add the article, an email should show up the console. The email contains a link to the new article. Click (or copy-paste) the link to open it in a browser. The loaded page should display the added article and an “add a comment” section. Go ahead and add a comment. After adding a comment you should see another email in the console addressed to the article’s author.
  3. Additionally, if you go to the blog’s index (http://127.0.0.1:8000/), you’ll see a form to subscribe to the blog. If you add a subscriber, you’ll get another email in the console notifying the site contact that somebody subscribed.
  4. Finally, you can look though the admin to see how things are set up. You can alter the site contacts or play with the notifications and templates. Bare in mind that while all the backends are enabled, and selectable, the users only have email contacts, so the other backends won’t do anything for anything besides site contacts.

Extras

Background Tasks

Django vox can integrate with django-backgroundtasks if available. Doing so is pretty simple, and (particularly if you have to do database lookups inside get_contacts_for_notification) can significantly reduce the work for an initial request.

Setup

In order to get this set up, you first need to go install and configure django-backgroundtasks yourself. It’s fairly straightforward, but exactly how you want the background tasks run is a question only you can answer.

Once it is set up, replace the following:

from django_vox.models import VoxModel

with this:

from django_vox.extra.background import BackgroundVoxModel \
    as VoxModel

Troubleshooting

If your messages aren’t being sent out, there’s a good chance that your background tasks just aren’t getting run at all. Try running manage.py process_tasks or check your queued background tasks in the django admin.

Comparison with similar projects

Django Vox is great if you:

  • Have content authors who are not programmers
  • Use a current versions of Django/Python
  • Want to be able to send notifications by multiple protocols
  • Don’t need “web-scale” (tip: if you’re using Django, you don’t need “web-scale”)
  • You don’t want to maintain a separate web service

That said here’s a more in-depth comparison between Django Vox and some specific alternatives.

  • Pinax notifications
    • Upsides
      • Less setup
      • Comes with notification settings editor
    • Downsides
      • Templates are part of django’s template system, so you can’t edit them from the admin.
      • Template parameters must be specified whenever sending a message. You need to know ahead of time, what parameters the template authors might want down the road.
      • Notification recipients must be users
      • Only supports email
      • Doesn’t support HTML email
    • Neither
      • Notification types can be created anywhere in the code, this means more flexibility, but potentially more confusion too.
  • django-courier
    • Upsides:
      • Uses signals, so you don’t have to specify your own notification types
      • No code required
    • Downside
      • Only supports emails
      • Doesn’t support HTML email
      • The documentation is very lacking
      • Specifying addresses is convoluted
      • Maybe dead
    • Neither
      • Supports (and requires) django.contrib.sites
  • universal_notifications
    • Upsides:
      • Supports many backends
    • Downsides:
      • Backends have to know about the structure of the user object
      • Notification recipients must be users
      • Email subjects are hard-coded
      • Templates aren’t easily editable in the admin
  • django-herald
    • Upsides:
      • Supports email attachments
    • Downsides:
      • Templates are part of django’s template system, so you can’t edit them from the admin.
      • Notification subjects are stored in the code
      • Only supports emails
      • Notification recipients must be users (though its possible to work around this)
  • django-notifier
    • Upsides:
      • Easy setup
    • Downsides:
      • Only supports emails by default
      • Doesn’t support HTML email
      • Notification recipients must be users
      • Custom backends must make assumptions about structure of user object
  • django-notifications
    • Upsides:
      • Supports more backends
      • Supports filters
    • Downsides:
      • Old, and depends on an long-out-of-date version of django
      • Notification recipients must be users
  • django-heythere
    • Downsides:
      • Notification types and templates are stored in settings.py
      • Only supports emails
      • Doesn’t support HTML email
  • django-push-notifications
    • Upsides:
      • Supports push notifications
    • Downsides:
      • Only supports push notifications
      • No templates
      • Notification recipients must be users
  • django-sitemessage
    • Upsides:
      • Supports many backends
    • Downsides:
      • Configuration is done in code
      • Not possible to specify different messages for different backends
  • django-gcm
    • Downsides:
      • Like django-push-notifications but worse
  • django-webpush
    • Downsides:
      • Like django-push-notifications but only supports web push
  • django-scarface
    • Downsides:
      • Like django-push-notifications but worse (requires Amazon SNS)

Actually Not Similar Projects

There’s also a good number of notification frameworks that solve a seeming-ly similar, but different problem: in-app notifications and activity feeds. These are the sort of things that might be a back-end to Django Vox. They’re listed here for completion:

Also, of honorable mention is Kawasemi which is more of a logging system than anything else.

Indices and tables