Django Entity Event¶
Django Entity Event is a framework for storing and rendering events, managing users subscriptions to those events, and providing clean ways to make notifying users as easy as possible. It builds on the Django Entity’s powerful method of unifying individuals and groups into a consistent framework.
Table of Contents¶
Installation¶
Django Entity Event is compatible with Python versions 2.7, 3.3, and 3.4.
Installation with Pip¶
Entity Event is available on PyPi. It can be installed using pip
:
pip install django-entity-event
Use with Django¶
To use Entity Event with django, first be sure to install it and/or
include it in your requirements.txt
Then include
'entity_event'
in settings.INSTALLED_APPS
. After it is
included in your installed apps, run:
./manage.py migrate entity_event
if you are using South. Otherwise run:
./manage.py syncdb
Quickstart and Basic Usage¶
Django Entity Event is a great way to collect events that your users care about into a unified location. The parts of your code base that create these events are probably totally separate from the parts that display them, which are also separate from the parts that manage subscriptions to notifications. Django Entity Event makes separating these concerns as simple as possible, and provides convenient abstractions at each of these levels.
This quickstart guide handles the three parts of managing events and notifications.
- Creating, and categorizing events.
- Defining mediums and subscriptions.
- Querying events and presenting them to users.
If you are not already using Django Entity, this event framework won’t be particularly useful, and you should probably start by integrating Django Entity into your application.
Creating and Categorizing Events¶
Django Entity Event is structured such that all events come from a
Source
, and can be displayed to the
user from a variety of mediums. When we’re creating events, we
don’t need to worry much about what
Medium
the event will be displayed
on, we do need to know what the
Source
of the events are.
Source
objects are used to categorize
events. Categorizing events allows different types of events to be
consumed differently. So, before we can create an event, we need to
create a Source
object. It is a good
idea to use sources to do fine grained categorization of events. To
provide higher level groupings, all sources must reference a
SourceGroup
object. These objects are
very simple to create. Here we will make a single source group and two
different sources
from entity_event import Source, SourceGroup
yoursite_group = SourceGroup.objects.create(
name='yoursite',
display_name='Yoursite',
description='Events on Yoursite'
)
photo_source = Source.objects.create(
group=yoursite_group,
name='photo-tag',
display_name='Photo Tag',
description='You have been tagged in a photo'
)
product_source = Source.objects.create(
group=yoursite_group,
name='new-product',
display_name='New Product',
description='There is a new product on YourSite'
)
As seen above, the information required for these sources is fairly
minimal. It is worth noting that while we only defined a single
SourceGroup
object, it will often
make sense to define more logical
SourceGroup
objects.
Once we have sources defined, we can begin creating events. To create
an event we use the Event.objects.create_event
method. To create an
event for the “photo-tag” group, we just need to know the source of
the event, what entities are involved, and some information about what
happened
from entity_event import Event
# Assume we're within the photo tag processing code, and we'll
# have access to variables entities_tagged, photo_owner, and
# photo_location
Event.objects.create_event(
source=photo_source,
actors=entities_tagged,
context={
'photo_owner': photo_owner
'photo_location': photo_location
}
)
The code above is all that’s required to store an event. While this is a fairly simple interface for creating events, in some applications it may be easier to read, and less intrusive in application code to use django-signals in the application code, and create events in signal handlers. In either case, We’re ready to discuss subscription management.
Managing Mediums and Subscriptions to Events¶
Once the events are created, we need to define how the users of our application are going to interact with the events. There are a large number of possible ways to notify users of events. Email, newsfeeds, notification bars, are all examples. Django Entity Event doesn’t handle the display logic for notifying users, but it does handle the subscription and event routing/querying logic that determines which events go where.
To start, we must define a Medium
object for each method our users will consume events from. Storing
Medium
objects in the database has
two purposes. First, it is referenced when subscriptions are
created. Second the Medium
objects
provide an entry point to query for events and have all the
subscription logic and filtering taken care of for you.
Like Source
objects,
Medium
objects are simple to create
from entity_event import Medium
email_medium = Medium.objects.create(
name="email",
display_name="Email",
description="Email Notifications"
)
newsfeed_medium = Medium.objects.create(
name="newsfeed",
display_name="NewsFeed",
description="Your personal feed of events"
)
At first, none of the events we have been creating are accessible by
either of these mediums. In order for the mediums to have access to
the events, an appropriate
Subscription
object needs to be
created. Creating a Subscription
object encodes that an entity, or group of entities, wants to receive
notifications of events from a given source, by a given medium. For
example, we can create a subscription so that all the sub-entities of
an all_users
entity will receive notifications of new products in
their newsfeed
from entity import EntityKind
from entity_event import Subscription
Subscription.objects.create(
medium=newsfeed_medium,
source=product_source,
entity=all_users,
sub_entity_kind=EntityKind.objects.get(name='user'),
only_following=False
)
With this Subscription
object
defined, all events from the new product source will be available to
the newsfeed medium.
If we wanted to create a subscription for users to get email
notifications when they’ve been tagged in a photo, we will also create
a Subscription
object. However,
unlike the new product events, not every event from the photos source
is relevant to every user. We want to limit the events they receive
emails about to the events where they are tagged in the photo.
In code above, you may notice the only_following=False
argument. This argument controls whether all events are relevant for
the subscription, or if the events are only relevant if they are
related to the entities being subscribed. Since new products are
relevant to all users, we set this to False
. To create a
subscription for users to receive emails about photos they’re tagged
in, we’ll define the subscription as follows
Subscription.objects.create(
medium=email_medium,
source=photo_source,
entity=all_users,
sub_entity_kind=EntityKind.objects.get(name='user'),
only_following=True
)
This will only notify users if an entity they’re following is tagged in a photo. By default, entities follow themselves and their super entities.
Creating subscriptions for a whole group of people with a single entry
into the database is very powerful. However, some users may wish to
opt out of certain types of notifications. To accommodate this, we can
create an Unsubscription
object. These are used to unsubscribe a single entity from receiving
notifications of a given source on a given medium. For example if a
user wants to opt out of new product notifications in their newsfeed,
we can create an Unsubscription
object for them
from entity_event import Unsubscription
# Assume we have an entity, unsubscriber who wants to unsubscribe
Unsubscription.objects.create(
entity=unsubscriber,
source=product_source,
medium=newsfeed_medium
)
Once this object is stored in the database, this user will no longer receive this type of notification.
Once we have Medium
objects set up
for the methods of sending notifications, and we have our entities
subscribed to sources of events on those mediums, we can use the
Medium
objects to query for events,
which we can then display to our users.
Querying Events¶
Once we’ve got events being created, and subscriptions to them for a
given medium, we’ll want to display those events to our users. When
there are a large variety of events coming into the system from many
different sources, it would be very difficult to query the
Event
model directly while still
respecting all the Subscription
logic
that we hope to maintain.
For this reason, Django Entity Event provides three methods to make
querying for events` to display extremely simple. Since the
Medium
objects you’ve created should
correspond directly to a means by which you want to display events to
users, there are three methods of the
Medium
class to perform queries.
Medium.events
Medium.entity_events
Medium.events_targets
Each of these methods return somewhat different views into the events
that are being stored in the system. In each case, though, you will
call these methods from an instance of
Medium
, and the events returned will
only be events for which there is a corresponding
Subscription
object.
The Medium.events
method can be used to return all the events for that medium. This
method is useful for mediums that want to display events without any
particular regard for who performed the events. For example, we could
have a medium that aggregated all of the events from the new products
source. If we had a medium, all_products_medium
, with the
appropriate subscriptions set up, getting all the new product events
is as simple as
all_products_medium.events()
The Medium.entity_events
method can be used to get
all the events for a given entity on that medium. It takes a single
entity as an argument, and returns all the events for that entity on
that medium. We could use this method to get events for an individual
entity’s newsfeed. If we have a large number of sources creating
events, with subscriptions between those sources and the newsfeed,
aggregating them into one QuerySet of events is as simple as
newsfeed_medium.entity_events(user_entity)
There are some mediums that notify users of events independent of a
pageview’s request/response cycle. For example, an email medium will
want to process batches of events, and need information about who to
send the events to. For this use case, the
Medium.events_targets
method can be
used. Instead of providing a EventQueryset
, it provides a list of
tuples in the form (event, targets)
, where targets
is a list
of the entities that should receive that notification. We could use
this function to send emails about events as follows
from django.core.mail import send_mail
new_emails = email_medium.events_targets(seen=False, mark_seen=True)
for event, targets in new_emails:
send_mail(
subject = event.context["subject"]
message = event.context["message"]
recipient_list = [t.entity_meta["email"] for t in targets]
)
As seen in the last example, these methods also support a number of arguments for filtering the events based on properties of the events themselves. All three methods support the following arguments:
start_time
: providing a datetime object to this parameter will filter the events to only those that occurred at or after this time.end_time
: providing a datetime object to this parameter will filter the events to only those that occurred at or before this time.seen
: passingFalse
to this argument will filter the events to only those which have not been marked as having been seen.include_expired
: defaults toFalse
, passingTrue
to this argument will include events that are expired. Events with expiration are discussed increate_event()
.actor
: providing an entity to this parameter will filter the events to only those that include the given entity as an actor.
Finally, all of these methods take an argument mark_seen
. Passing
True
to this argument will mark the events as having been seen by
that medium so they will not show up if False
is passed to the
seen
filtering argument.
Using these three methods with any combination of the event filters should make virtually any event querying task simple.
Advanced Features¶
The Quickstart and Basic Usage guide covers the common use cases of Django Entity Event. In addition to the basic uses for creating, storing, and querying events, there are some more advanced uses supported for making Django Entity Event more efficient and flexible.
This guide will cover the following advanced use cases:
- Dynamically loading context using
context_loader
- Customizing the behavior of
only_following
by sub-classingMedium
.
Rendering Events¶
Django Entity Event comes complete with a rendering system for events. This is accomplished by the setup of two different models:
RenderingStyle
: Defines a style of rendering.ContextRenderer
: Defines the templates used for rendering, which rendering style it is, which source or source group it renders, and hints for fetching model PKs that are in event contexts.
When these models are in place, Medium
models can be configured
to point to a rendering_style
of their choice. Events that have sources or source groups that
match those configured in associated ContextRenderer
models
can then be rendered using the render
method on the medium.
The configuration and rendering is best explained using a complete example. First, let’s
imagine that we are storing events that have contexts with information about Django User models.
These events have a source called user_logged_in
and track every time a user logs in. An
example context is as follows:
{
'user': 1, # The PK of the Django User model
'login_time': 'Jan 10, 2014', # The time the user logged in
}
Now let’s say we have a Django template, user_logged_in.html
that looks like the following:
User {{ user.username }} logged in at {{ login_time }}
In order to render the event with this template, we first set up a rendering style. This rendering
style is pretty short and could probably be displayed in many places that want to display short
messages (like a notification bar). So, we can make a short
rendering style as followings:
short_rendering_style = RenderingStyle.objects.create(
name='short',
display_name='Short Rendering Style')
Now that we have our rendering style, we need to create a context renderer that has information about what templates, source, rendering style, and context hints to use when rendering the event. In our case, it would look like the following:
context_renderer = ContextRenderer.objects.create(
render_style=RenderingStyle.objects.get(name='short'),
name='short_login_renderer',
html_template_path='my_template_dir/user_logged_in.html',
source=Source.objects.get(name='user_logged_in'),
context_hints={
'user': {
'app_name': 'auth',
'model_name': 'User',
}
}
)
In the above, we set up the context renderer to use the short rendering style, pointed it to our html template
that we created, and also pointed it to the source of the event. As you can see from the html template, we
want to reach inside of the Django User object and display the username
field. In order to retrieve this
information, we have told our context renderer to treat the user
key from the event context as a PK
to a Django User
model that resides in the auth
app.
With this information, we can now render the event using whatever medium we have set up in Django Entity Event.
notification_medium = Medium.objects.get(name='notification')
events = notification_medium.events()
# Assume that two events were returned that have the following contexts
# e1.context = {
# 'user': 1, # Points to Jeff's user object
# 'login_time': 'January 1, 2015',
# }
# e1.context = {
# 'user': 2, # Points to Wes's user object
# 'login_time': 'February 28, 2015',
# }
#
# Pass the events into the medium's render method
rendered_events = notification_medium.render(events)
# The results are a dictionary keyed on each event. The keys point to a tuple
# of text and html renderings.
print(rendered_events[0][1])
'jeff logged in at January 1, 2015'
print(rendered_events[1][1])
'wes logged in at February 28, 2015'
With the notion of rendering styles, the notification medium and any medium that can display short
messages can utilize the renderings of the events. Other rendering styles can still be made for
more complex renderings such as emails with special styling. For more advanced options on how
to perform prefetch and select_relateds in the fetched contexts,
view ContextRenderer
.
Advanced Template Rendering Options¶
Along with the basic rendering capabilities, Django Entity Event comes with several other options and configurations for making rendering more robust.
Passing Additional Context to Templates¶
Sometimes mediums need to have subtle differences in the rendering of their contexts. For example, headers
might need to be added above and below a message or images might need to be displayed. For cases such as
this, mediums come with an additional_context
variable. Anything in this variable will always be
passed into the context when events are rendered for that particular medium.
Using a Default Rendering Style¶
It can be cumbersome to set up context renderers for every particular rendering style when it isn’t
necessary. For example, sometimes tailored emails need a special rendering style, however, many events
can be rendered in an email just fine with a simpler rendering style. For these cases, a user can set
a Django setting called DEFAULT_ENTITY_EVENT_RENDERING_STYLE
that points to the name of the
default rendering style to use. If this variable is set and an appropriate context loader cannot
be fetched for an event during rendering, the default rendering style will be used instead for
that event (if it has been configured).
Serialized Context Data¶
If your display mechanism needs access to the context data of the event this can be accomplished by calling:
Event.get_serialized_context
method on the
Event
model. This will return a serializer safe version of the context that is used
to generate the event output. This is useful if you want to make a completely custom rendering on the display device
or you need additional context information about the event that occurred.
Customizing Only-Following Behavior¶
In the quickstart, we discussed the use of “only following”
subscriptions to ensure that users only see the events that they are
interested in. In this discussion, we mentioned that by default,
entities follow themselves, and their super entities. This following
relationship is defined in two methods on the
Medium
model:
Medium.followers_of
and
Medium.followed_by
. These two methods are
inverses of each other and are used by the code that fetches events to
determine the semantics of “only following” subscriptions.
It is possible to customize the behavior of these types of
subscriptions by concretely inheriting from
Medium
, and overriding these two
functions. For example, we could define a type of medium that provides
the opposite behavior, where entities follow themselves and their
sub-entities.
from entity import Entity, EntityRelationship
from entity_event import Medium
class FollowSubEntitiesMedium(Medium):
def followers_of(self, entities):
if isinstance(entities, Entity):
entities = Entity.objects.filter(id=entities.id)
super_entities = EntityRelationship.objects.filter(
sub_entity__in=entities).values_list('super_entity')
followed_by = Entity.objects.filter(
Q(id__in=entities) | Q(id__in=super_entities))
return followed_by
def followed_by(self, entities):
if isinstance(entities, Entity):
entities = Entity.objects.filter(id=entities.id)
sub_entities = EntityRelationship.objects.filter(
super_entity__in=entities).values_list('sub_entity')
followers_of = Entity.objects.filter(
Q(id__in=entities) | Q(id__in=sub_entities))
return followers_of
With these methods overridden, the behavior of the methods
FollowsubEntitiesMedium.events
,
FollowsubEntitiesMedium.entity_events
, and
FollowsubEntitiesMedium.events_targets
should all behave as
expected.
It is entirely possible to define more complex following
relationships, potentially drawing on different source of information
for what entities should follow what entities. The only important
consideration is that the followers_of
method must be the inverse
of the followed_by
method. That is, for any set of entities, it
must hold that
followers_of(followed_by(entities)) == entities
and
followed_by(followers_of(entities)) == entities
Code documentation¶
Contributing¶
Contributions and issues are most welcome! All issues and pull requests are handled through github on the ambitioninc repository. Also, please check for any existing issues before filing a new one. If you have a great idea but it involves big changes, please file a ticket before making a pull request! We want to make sure you don’t spend your time coding something that might not fit the scope of the project.
Running the tests¶
To get the source source code and run the unit tests, run:
$ git clone git://github.com/ambitioninc/django-entity-event.git
$ cd django-entity-event
$ virtualenv env
$ . env/bin/activate
$ python setup.py install
$ coverage run setup.py test
$ coverage report --fail-under=100
While 100% code coverage does not make a library bug-free, it significantly reduces the number of easily caught bugs! Please make sure coverage is at 100% before submitting a pull request!
Code Styling¶
Please arrange imports with the following style
# Standard library imports
import os
# Third party package imports
from mock import patch
from django.conf import settings
# Local package imports
from entity_event.version import __version__
Please follow Google’s python style guide wherever possible.
Building the docs¶
When in the project directory:
pip install -r requirements/docs.txt
python setup.py build_sphinx
open docs/_build/html/index.html
Release Checklist¶
Before a new release, please go through the following checklist:
Bump version in entity_event/version.py
Add a release note in docs/release_notes.rst
Git tag the version
Upload to pypi:
pip install wheel python setup.py sdist bdist_wheel upload
Vulnerability Reporting¶
For any security issues, please do NOT file an issue or pull request on github! Please contact security@ambition.com with the GPG key provided on Ambition’s website.
Release Notes¶
v0.7.1¶
- Increase the uuid length
v0.7.0¶
- Add creation time for mediums so events can be queried per medium for after medium creation
v0.6.0¶
- Add python 3.5 support, remove django 1.7 support
v0.5.0¶
- Added django 1.9 support
v0.4.4¶
- Added some optimizations during event fetching to select and prefetch some related objects
v0.4.3¶
- Added ability to get a serialized version of an events context data
v0.4.0¶
- Added 1.8 support and dropped 1.6 support for Django
v0.3.4¶
- Fixed django-entity migration dependency for Django 1.6
v0.3.3¶
- Added Django 1.7 compatibility and app config
v0.3.2¶
- Added an additional_context field in the Medium object that allows passing of additional context to event renderings.
- Added ability to define a default rendering style for all sources or source groups if a context renderer is not defined for a particular rendering style.
v0.3.1¶
- Fixes a bug where contexts can have any numeric type as a pk
v0.3.0¶
- Adds a template and context rendering system to entity event
v0.2¶
- This release provides the core features of django-entity-event - Event Creation - Subscription Management - Event Querying - Admin Panel - Documentation
v0.1¶
- This is the initial release of django-entity-event.