diff --git a/README.rst b/README.rst index 4336b5d..42fd119 100644 --- a/README.rst +++ b/README.rst @@ -13,10 +13,16 @@ :alt: Number of PyPI downloads -django-entity-event -=============== - -Newsfeed-style event tracking and subscription management for django-entity. +Django Entity Event +=================== + +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. Installation ------------ diff --git a/docs/advanced_features.rst b/docs/advanced_features.rst new file mode 100644 index 0000000..1c78daf --- /dev/null +++ b/docs/advanced_features.rst @@ -0,0 +1,154 @@ +Advanced Features +================= + +The :ref:`quickstart` 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-classing + :py:class:`~entity_event.models.Medium`. + + +Custom Context Loaders +---------------------- + +When events are created, it is up to the creator of the event to +decide what information gets stored in the event's ``context`` +field. In many cases it makes sense to persist all of the data +necessary to display the event to a user. + +In some cases, however the ``context`` of the event can be very large, +and storing all of the information would mean duplicating a large +amount of data that exists elsewhere in your database. It's desirable +to have all this data available in the ``context`` field, but it isn't +desirable to repeatedly duplicate large amounts of information. + +If the creator of the events can guarantee that some of the +information about the event will always be available in the database, +or computable from some subset of the data that could be stored in the +context, they can use the ``Source.context_loader`` field to provide a +path to an importable function to dynamically load the context when +the events are fetched. + +If for example, we are creating events about photo tags, and we don't +want to persist a full path to the photo, we can simply store a ``id`` +for the photo, and then use a context loader to load it +dynamically. The function we would write to load the context would +look something like + +.. code-block:: python + + # In module apps.photos.loaders + + def load_photo_context(context): + photo = Photo.objects.get(id=context['photo_id']) + context['photo_path'] = photo.path + return context + +Then, when defining our source for this type of event we would include +a path to this function in the ``context_loader`` field. + +.. code-block:: python + + from entity_event import Source + + photo_tag_source = Source.objects.create( + name="photo-tag", + display_name="Photo Tag", + description="You are tagged in a photo", + group=photo_group, + context_loader='apps.photos.loaders.load_photo_path' + ) + +With this setup, all of the additional information can by dynamically +loaded into events, simply by calling :py:meth:`Event.get_context +`. + +The ``Source`` model also uses django's ``clean`` method to ensure +that only valid importable functions get saved in the +database. However, if this function is removed from the codebase, +without a proper migration, attempting to load context for events with +this source will fail. + +There are a number of trade-offs in using a context loader. If the +underlying data is subject to change, accessing historic events could +cause errors in the application. Additionally, a context loader that +requires many runs to the database could cause accessing events to be +a much more expensive operation. In either of these cases it makes +more sense to store copies of the data in the ``context`` field of the +event. + + +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 +:py:class:`~entity_event.models.Medium` model: +:py:meth:`Medium.followers_of +` and +:py:meth:`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 +:py:class:`~entity_event.models.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. + +.. code-block:: python + + 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 + +.. code-block:: python + + followers_of(followed_by(entities)) == entities + +and + +.. code-block:: python + + followed_by(followers_of(entities)) == entities diff --git a/docs/conf.py b/docs/conf.py index f76a29e..b8968fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ def get_version(): source_suffix = '.rst' # The master toctree document. -master_doc = 'toc' +master_doc = 'index' # General information about the project. project = u'entity_event' @@ -150,43 +150,43 @@ def get_version(): ] -def process_django_model_docstring(app, what, name, obj, options, lines): - """ - Does special processing for django model docstrings, making docs for - fields in the model. - """ - # This causes import errors if left outside the function - from django.db import models +# def process_django_model_docstring(app, what, name, obj, options, lines): +# """ +# Does special processing for django model docstrings, making docs for +# fields in the model. +# """ +# # This causes import errors if left outside the function +# from django.db import models - # Only look at objects that inherit from Django's base model class - if inspect.isclass(obj) and issubclass(obj, models.Model): - # Grab the field list from the meta class - fields = obj._meta.fields +# # Only look at objects that inherit from Django's base model class +# if inspect.isclass(obj) and issubclass(obj, models.Model): +# # Grab the field list from the meta class +# fields = obj._meta.fields - for field in fields: - # Decode and strip any html out of the field's help text - help_text = strip_tags(force_unicode(field.help_text)) +# for field in fields: +# # Decode and strip any html out of the field's help text +# help_text = strip_tags(force_unicode(field.help_text)) - # Decode and capitalize the verbose name, for use if there isn't - # any help text - verbose_name = force_unicode(field.verbose_name).capitalize() +# # Decode and capitalize the verbose name, for use if there isn't +# # any help text +# verbose_name = force_unicode(field.verbose_name).capitalize() - if help_text: - # Add the model field to the end of the docstring as a param - # using the help text as the description - lines.append(u':param %s: %s' % (field.attname, help_text)) - else: - # Add the model field to the end of the docstring as a param - # using the verbose name as the description - lines.append(u':param %s: %s' % (field.attname, verbose_name)) +# if help_text: +# # Add the model field to the end of the docstring as a param +# # using the help text as the description +# lines.append(u':param %s: %s' % (field.attname, help_text)) +# else: +# # Add the model field to the end of the docstring as a param +# # using the verbose name as the description +# lines.append(u':param %s: %s' % (field.attname, verbose_name)) - # Add the field's type to the docstring - lines.append(u':type %s: %s' % (field.attname, type(field).__name__)) +# # Add the field's type to the docstring +# lines.append(u':type %s: %s' % (field.attname, type(field).__name__)) - # Return the extended docstring - return lines +# # Return the extended docstring +# return lines -def setup(app): - # Register the docstring processor with sphinx - app.connect('autodoc-process-docstring', process_django_model_docstring) \ No newline at end of file +# def setup(app): +# # Register the docstring processor with sphinx +# app.connect('autodoc-process-docstring', process_django_model_docstring) diff --git a/docs/index.rst b/docs/index.rst index 6760f4b..1e73533 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,23 @@ -django-entity-event Documentation -================================= +Django Entity Event +=================== -Please put a description here, followed by sections for configuration, basic usage, and code documentation. +Django Entity Event is a framework for storing 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. + +.. _Django Entity's: https://github.com/ambitioninc/django-entity/ + +Table of Contents +----------------- + +.. toctree:: + :maxdepth: 2 + + installation + quickstart + advanced_features + ref/entity_event + contributing + release_notes diff --git a/docs/installation.rst b/docs/installation.rst index 47117e2..970b08c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,10 +1,29 @@ Installation ============ -To install the latest release, type:: +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 -To install the latest code directly from source, type:: +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 + +.. _South: http://south.aeracode.org/ - pip install git+git://github.com/ambitioninc/django-entity-event.git \ No newline at end of file diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..c345406 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,337 @@ +.. _quickstart: + +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. + +1. Creating, and categorizing events. +2. Defining mediums and subscriptions. +3. 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 +:py:class:`~entity_event.models.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 +:py:class:`~entity_event.models.Medium` the event will be displayed +on, we do need to know what the +:py:class:`~entity_event.models.Source` of the events are. + +:py:class:`~entity_event.models.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 :py:class:`~entity_event.models.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 +:py:class:`~entity_event.models.SourceGroup` object. These objects are +very simple to create. Here we will make a single source group and two +different sources + +.. code-block:: python + + 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 +:py:class:`~entity_event.models.SourceGroup` object, it will often +make sense to define more logical +:py:class:`~entity_event.models.SourceGroup` objects. + +Once we have sources defined, we can begin creating events. To create +an event we use the :py:meth:`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 + +.. code-block:: python + + 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 :py:class:`~entity_event.models.Medium` +object for each method our users will consume events from. Storing +:py:class:`~entity_event.models.Medium` objects in the database has +two purposes. First, it is referenced when subscriptions are +created. Second the :py:class:`~entity_event.models.Medium` objects +provide an entry point to query for events and have all the +subscription logic and filtering taken care of for you. + +Like :py:class:`~entity_event.models.Source` objects, +:py:class:`~entity_event.models.Medium` objects are simple to create + +.. code-block:: python + + 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 +:py:class:`~entity_event.models.Subscription` object needs to be +created. Creating a :py:class:`~entity_event.models.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 + +.. code-block:: python + + 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 :py:class:`~entity_event.models.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 :py:class:`~entity_event.models.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 + +.. code-block:: python + + 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 :py:class:`~entity_event.models.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 :py:class:`~entity_event.models.Unsubscription` +object for them + +.. code-block:: python + + 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 :py:class:`~entity_event.models.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 +:py:class:`~entity_event.models.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 +:py:class:`~entity_event.models.Event` model directly while still +respecting all the :py:class:`~entity_event.models.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 +:py:class:`~entity_event.models.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 +:py:class:`~entity_event.models.Medium` class to perform queries. + +1. :py:meth:`Medium.events ` +2. :py:meth:`Medium.entity_events ` +3. :py:meth:`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 +:py:class:`~entity_event.models.Medium`, and the events returned will +only be events for which there is a corresponding +:py:class:`~entity_event.models.Subscription` object. + +The :py:meth:`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 + +.. code-block:: python + + all_products_medium.events() + +The :py:meth:`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 + +.. code-block:: python + + 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 +:py:meth:`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 + +.. code-block:: python + + 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``: passing ``False`` to this argument will filter the events + to only those which have not been marked as having been seen. +- ``include_expired``: defaults to ``False``, passing ``True`` to this + argument will include events that are expired. Events with + expiration are discussed in + :py:meth:`~entity_event.models.EventManager.create_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. diff --git a/docs/ref/entity_event.rst b/docs/ref/entity_event.rst index 3e90fbb..5707bdc 100644 --- a/docs/ref/entity_event.rst +++ b/docs/ref/entity_event.rst @@ -3,7 +3,47 @@ Code documentation ================== -entity_event ------------------- +.. automodule:: entity_event.models -.. automodule:: entity_event +.. autoclass:: Medium() + + .. automethod:: events(self, **event_filters) + + .. automethod:: entity_events(self, entity, **event_filters) + + .. automethod:: events_targets(self, entity_kind, **event_filters) + + .. automethod:: followed_by(self, entities) + + .. automethod:: followers_of(self, entities) + + +.. autoclass:: Source() + + .. automethod:: get_context(self, context) + +.. autoclass:: SourceGroup() + +.. autoclass:: Unsubscription() + +.. autoclass:: Subscription() + + .. automethod:: subscribed_entities(self) + +.. autoclass:: EventQuerySet() + + .. automethod:: mark_seen(self, medium) + +.. autoclass:: EventManager() + + .. automethod:: create_event(self, source, context, uuid, time_expires, actors, ignore_duplicates) + + .. automethod:: mark_seen(self, medium) + +.. autoclass:: Event() + + .. automethod:: get_context(self) + +.. autoclass:: EventActor() + +.. autoclass:: EventSeen() diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 1de8243..f5f5c1f 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -1,7 +1,17 @@ Release Notes ============= +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. \ No newline at end of file +* This is the initial release of django-entity-event. diff --git a/docs/toc.rst b/docs/toc.rst deleted file mode 100644 index 0109289..0000000 --- a/docs/toc.rst +++ /dev/null @@ -1,11 +0,0 @@ -Table of Contents -================= - -.. toctree:: - :maxdepth: 2 - - index - installation - ref/entity_event - contributing - release_notes diff --git a/entity_event/__init__.py b/entity_event/__init__.py index 72fb864..184b95e 100644 --- a/entity_event/__init__.py +++ b/entity_event/__init__.py @@ -1,2 +1,6 @@ # flake8: noqa -from .version import __version__ \ No newline at end of file +from .version import __version__ + +from .models import ( + Event, Medium, Source, SourceGroup, Subscription, Unsubscription +) diff --git a/entity_event/models.py b/entity_event/models.py index 4af9854..b815028 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -15,19 +15,123 @@ from entity.models import Entity, EntityKind, EntityRelationship -# TODO: add mark_seen function @python_2_unicode_compatible class Medium(models.Model): + """A ``Medium`` is an object in the database that defines the method + by which users will view events. The actual objects in the + database are fairly simple, only requiring a ``name``, + ``display_name`` and ``description``. Mediums can be created with + ``Medium.objects.create``, using the following parameters: + + :type name: str + :param name: A short, unique name for the medium. + + :type display_name: str + :param display_name: A short, human readable name for the medium. + Does not need to be unique. + + :type description: str + :param description: A human readable description of the + medium. + + Encoding a ``Medium`` object in the database serves 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. + + Any time a new way to display events to a user is created, a + corresponding ``Medium`` should be created. Some examples could + include a medium for sending email notifications, a medium for + individual newsfeeds, or a medium for a site wide notification + center. + + Once a medium object is created, and corresponding subscriptions + are created, there are three methods on the medium object that can + be used to query for events. They are ``events``, + ``entity_events`` and ``events_targets``. The differences between + these methods are described in their corresponding documentation. + + """ name = models.CharField(max_length=64, unique=True) display_name = models.CharField(max_length=64) description = models.TextField() def __str__(self): + """Readable representation of ``Medium`` objects.""" return self.display_name @transaction.atomic def events(self, **event_filters): """Return subscribed events, with basic filters. + + This method of getting events is useful when you want to + display events for your medium, independent of what entities + were involved in those events. For example, this method can be + used to display a list of site-wide events that happened in the + past 24 hours: + + .. code-block:: python + + TEMPLATE = ''' + +

Yoursite's Events

+
    + {% for event in events %} +
  • {{ event.context.event_text }}
  • + {% endfor %} +
+ + ''' + + def site_feed(request): + site_feed_medium = Medium.objects.get(name='site_feed') + start_time = datetime.utcnow() - timedelta(days=1) + context = {} + context['events'] = site_feed_medium.events(start_time=start_time) + return HttpResponse(TEMPLATE.render(context)) + + While the `events` method does not filter events based on what + entities are involved, filtering based on the properties of the events + themselves is supported, through the following arguments, all + of which are optional. + + :type start_time: datetime.datetime (optional) + :param start_time: Only return events that occurred after the + given time. If no time is given for this argument, no + filtering is done. + + :type end_time: datetime.datetime (optional) + :param end_time: Only return events that occurred before the + given time. If no time is given for this argument, no + filtering is done + + :type seen: Boolean (optional) + :param seen: This flag controls whether events that have + marked as seen are included. By default, both events that + have and have not been marked as seen are included. If + ``True`` is given for this parameter, only events that + have been marked as seen will be included. If ``False`` is + given, only events that have not been marked as seen will + be included. + + :type include_expired: Boolean (optional) + :param include_expired: By default, events that have a + expiration time, which has passed, are not included in the + results. Passing in ``True`` to this argument causes + expired events to be returned as well. + + :type actor: Entity (optional) + :param actor: Only include events with the given entity as an + actor. + + :type mark_seen: Boolean (optional) + :param mark_seen: Create a side effect in the database that + marks all the returned events as having been seen by this + medium. + + :rtype: EventQuerySet + :returns: A queryset of events. """ events = self.get_filtered_events(**event_filters) subscriptions = Subscription.objects.filter(medium=self) @@ -48,6 +152,81 @@ def events(self, **event_filters): @transaction.atomic def entity_events(self, entity, **event_filters): """Return subscribed events for a given entity. + + This method of getting events is useful when you want to see + only the events relevant to a single entity. The events + returned are events that the given entity is subscribed to, + either directly as an individual entity, or because they are + part of a group subscription. As an example, the + `entity_events` method can be used to implement a newsfeed for + a individual entity: + + .. code-block:: python + + TEMPLATE = ''' + +

{entity}'s Events

+
    + {% for event in events %} +
  • {{ event.context.event_text }}
  • + {% endfor %} +
+ + ''' + + def newsfeed(request): + newsfeed_medium = Medium.objects.get(name='newsfeed') + entity = Entity.get_for_obj(request.user) + context = {} + context['entity'] = entity + context['events'] = site_feed_medium.entity_events(entity, seen=False, mark_seen=True) + return HttpResponse(TEMPLATE.render(context)) + + + The only required argument for this method is the entity to + get events for. Filtering based on the properties of the + events themselves is supported, through the rest of the + following arguments, which are optional. + + :type_entity: Entity + :param entity: The entity to get events for. + + :type start_time: datetime.datetime (optional) + :param start_time: Only return events that occurred after the + given time. If no time is given for this argument, no + filtering is done. + + :type end_time: datetime.datetime (optional) + :param end_time: Only return events that occurred before the + given time. If no time is given for this argument, no + filtering is done + + :type seen: Boolean (optional) + :param seen: This flag controls whether events that have + marked as seen are included. By default, both events that + have and have not been marked as seen are included. If + ``True`` is given for this parameter, only events that + have been marked as seen will be included. If ``False`` is + given, only events that have not been marked as seen will + be included. + + :type include_expired: Boolean (optional) + :param include_expired: By default, events that have a + expiration time, which has passed, are not included in the + results. Passing in ``True`` to this argument causes + expired events to be returned as well. + + :type actor: Entity (optional) + :param actor: Only include events with the given entity as an + actor. + + :type mark_seen: Boolean (optional) + :param mark_seen: Create a side effect in the database that + marks all the returned events as having been seen by this + medium. + + :rtype: EventQuerySet + :returns: A queryset of events. """ events = self.get_filtered_events(**event_filters) @@ -72,7 +251,78 @@ def entity_events(self, entity, **event_filters): @transaction.atomic def events_targets(self, entity_kind=None, **event_filters): - """Return all events for this medium, with who the event is for. + """Return all events for this medium, with who each event is for. + + This method is useful for individually notifying every + entity concerned with a collection of events, while + still respecting subscriptions and usubscriptions. For + example, ``events_targets`` can be used to send email + notifications, by retrieving all unseen events (and marking + them as now having been seen), and then processing the + emails. In code, this could look like: + + .. code-block:: python + + email = Medium.objects.get(name='email') + new_emails = email.events_targets(seen=False, mark_seen=True) + + for event, targets in new_emails: + django.core.mail.send_mail( + subject = event.context["subject"] + message = event.context["message"] + recipient_list = [t.entity_meta["email"] for t in targets] + ) + + This ``events_targets`` method attempts to make bulk + processing of push-style notifications straightforward. This + sort of processing should normally occur in a separate thread + from any request/response cycle. + + Filtering based on the properties of the events themselves is + supported, through the rest of the following arguments, which + are optional. + + :type entity_kind: EntityKind + :param entity_kind: Only include targets of the given kind in + each targets list. + + :type start_time: datetime.datetime (optional) + :param start_time: Only return events that occurred after the + given time. If no time is given for this argument, no + filtering is done. + + :type end_time: datetime.datetime (optional) + :param end_time: Only return events that occurred before the + given time. If no time is given for this argument, no + filtering is done + + :type seen: Boolean (optional) + :param seen: This flag controls whether events that have + marked as seen are included. By default, both events that + have and have not been marked as seen are included. If + ``True`` is given for this parameter, only events that + have been marked as seen will be included. If ``False`` is + given, only events that have not been marked as seen will + be included. + + :type include_expired: Boolean (optional) + :param include_expired: By default, events that have a + expiration time, which has passed, are not included in the + results. Passing in ``True`` to this argument causes + expired events to be returned as well. + + :type actor: Entity (optional) + :param actor: Only include events with the given entity as an + actor. + + :type mark_seen: Boolean (optional) + :param mark_seen: Create a side effect in the database that + marks all the returned events as having been seen by this + medium. + + :rtype: List of tuples + :returns: A list of tuples in the form ``(event, targets)`` + where ``targets`` is a list of entities. """ events = self.get_filtered_events(**event_filters) subscriptions = Subscription.objects.filter(medium=self) @@ -107,6 +357,25 @@ def events_targets(self, entity_kind=None, **event_filters): def subset_subscriptions(self, subscriptions, entity=None): """Return only subscriptions the given entity is a part of. + + An entity is "part of a subscription" if either: + + 1. The subscription is for that entity, with no + sub-entity-kind. That is, it is not a group subscription. + + 2. The subscription is for a super-entity of the given entity, + and the subscription's sub-entity-kind is the same as that of + the entity's. + + :type subscriptions: QuerySet + :param subscriptions: A QuerySet of subscriptions to subset. + + :type entity: (optional) Entity + :param entity: Subset subscriptions to only those relevant for + this entity. + + :rtype: QuerySet + :returns: A queryset of filtered subscriptions. """ if entity is None: return subscriptions @@ -121,8 +390,13 @@ def subset_subscriptions(self, subscriptions, entity=None): @cached_property def unsubscriptions(self): - """ - Returns the unsubscribed entity IDs for each source as a dict keyed on source_id. + """Returns the unsubscribed entity IDs for each source as a dict, + keyed on source_id. + + :rtype: Dictionary + :returns: A dictionary of the form ``{source_id: entities}`` + where ``entities`` is a list of entities unsubscribed from + that source for this medium. """ unsubscriptions = defaultdict(list) for unsub in Unsubscription.objects.filter(medium=self).values('entity', 'source'): @@ -130,15 +404,23 @@ def unsubscriptions(self): return unsubscriptions def filter_source_targets_by_unsubscription(self, source_id, targets): - """ - Given a source id and targets, filter the targets by unsubscriptions. Return - the filtered list of targets. + """Given a source id and targets, filter the targets by + unsubscriptions. Return the filtered list of targets. """ unsubscriptions = self.unsubscriptions return [t for t in targets if t.id not in unsubscriptions[source_id]] def get_filtered_events_queries(self, start_time, end_time, seen, include_expired, actor): - """Return Q objects to filter events table. + """Return Q objects to filter events table to relevant events. + + The filters that are applied are those passed in from the + method that is querying the events table: One of ``events``, + ``entity_events`` or ``events_targets``. The arguments have + the behavior documented in those methods. + + :rtype: List of Q objects + :returns: A list of Q objects, which can be used as arguments + to ``Event.objects.filter``. """ now = datetime.utcnow() filters = [] @@ -165,17 +447,11 @@ def get_filtered_events_queries(self, start_time, end_time, seen, include_expire def get_filtered_events( self, start_time=None, end_time=None, seen=None, mark_seen=False, include_expired=False, actor=None): - """ - Retrieves events with time or seen filters and also marks them as seen if necessary. - - Possible event filters are: - - start_time: Filter everything including and after this time - - end_time: Filter everything up to and including this time - - include_expired: Include expired events in the result - - seen: Filter by if it is seen or not seen. None indicates no seen filtering is done - - mark_seen: Mark events as seen if they were not seen (and seen=False). Note that marking - as seen evaluates the queryset and performs a bulk update - - actor: Filter events that have the provided actor + """Retrieves events, filters by event level filters, and marks them as + seen if necessary. + + :rtype: EventQuerySet + :returns: All events which match the given filters. """ filtered_events_queries = self.get_filtered_events_queries(start_time, end_time, seen, include_expired, actor) events = Event.objects.filter(*filtered_events_queries) @@ -190,9 +466,36 @@ def get_filtered_events( return events def followed_by(self, entities): - """Return a queyset of the entities that the given entities are following. - - Entities follow themselves, and their super entities. + """Define what entities are followed by the entities passed to this + method. + + This method can be overridden by a class that concretely + inherits ``Medium``, to define custom semantics for the + ``only_following`` flag on relevant ``Subscription`` + objects. Overriding this method, and ``followers_of`` will be + sufficient to define that behavior. This method is not useful + to call directly, but is used by the methods that filter + events and targets. + + This implementation attempts to provide a sane default. In + this implementation, the entities followed by the ``entities`` + argument are the entities themselves, and their super entities. + + That is, individual entities follow themselves, and the groups + they are a part of. This works as a default implementation, + but, for example, an alternate medium may wish to define the + opposite behavior, where an individual entity follows + themselves and all of their sub-entities. + + Return a queryset of the entities that the given entities are + following. This needs to be the inverse of ``followers_of``. + + :type entities: Entity or EntityQuerySet + :param entities: The Entity, or QuerySet of Entities of interest. + + :rtype: EntityQuerySet + :returns: A QuerySet of all the entities followed by any of + those given. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) @@ -203,9 +506,38 @@ def followed_by(self, entities): return followed_by def followers_of(self, entities): - """Return a querset of the entities that follow the given entities. - - The followers of an entity are themselves and their sub entities. + """Define what entities are followers of the entities passed to this + method. + + This method can be overridden by a class that concretely + inherits ``Medium``, to define custom semantics for the + ``only_following`` flag on relevant ``Subscription`` + objects. Overriding this method, and ``followed_by`` will be + sufficient to define that behavior. This method is not useful + to call directly, but is used by the methods that filter + events and targets. + + This implementation attempts to provide a sane default. In + this implementation, the followers of the entities passed in + are defined to be the entities themselves, and their + sub-entities. + + That is, the followers of individual entities are themselves, + and if the entity has sub-entities, those sub-entities. This + works as a default implementation, but, for example, an + alternate medium may wish to define the opposite behavior, + where an the followers of an individual entity are themselves + and all of their super-entities. + + Return a queryset of the entities that follow the given + entities. This needs to be the inverse of ``followed_by``. + + :type entities: Entity or EntityQuerySet + :param entities: The Entity, or QuerySet of Entities of interest. + + :rtype: EntityQuerySet + :returns: A QuerySet of all the entities who are followers of + any of those given. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) @@ -218,6 +550,50 @@ def followers_of(self, entities): @python_2_unicode_compatible class Source(models.Model): + """A ``Source`` is an object in the database that represents where + events come from. These objects only require a few fields, + ``name``, ``display_name`` ``description``, ``group`` and + optionally ``context_loader``. Source objects categorize events + based on where they came from, or what type of information they + contain. Each source should be fairly fine grained, with broader + categorizations possible through ``SourceGroup`` objects. Sources + can be created with ``Source.objects.create`` using the following + parameters: + + :type name: str + :param name: A short, unique name for the source. + + :type display_name: str + :param display_name: A short, human readable name for the source. + Does not need to be unique. + + :type description: str + :param description: A human readable description of the source. + + :type group: SourceGroup + :param group: A SourceGroup object. A broad grouping of where the + events originate. + + :type context_loader: (optional) str + :param context_loader: A importable path to a function, which can + take a dictionary of context, and populate it with more + information from the database or other sources. + + Storing source objects in the database servers two purposes. The + first is to provide an object that Subscriptions can reference, + allowing different categories of events to be subscribed to over + different mediums. The second is to allow source instances to + store a reference to a function which can populate event contexts + with additional information that is relevant to the source. This + allows ``Event`` objects to be created with minimal data + duplication. + + Once sources are created, they will primarily be used to + categorize events, as each ``Event`` object requires a reference + to a source. Additionally they will be referenced by + ``Subscription`` objects to route events of the given source to be + handled by a given medium. + """ name = models.CharField(max_length=64, unique=True) display_name = models.CharField(max_length=64) description = models.TextField() @@ -227,15 +603,21 @@ class Source(models.Model): context_loader = models.CharField(max_length=256, default='', blank=True) def get_context_loader_function(self): - """ - Returns an imported, callable context loader function. + """Returns an imported, callable context loader function. """ return import_by_path(self.context_loader) def get_context(self, context): - """ - Gets the context for this source by loading it through the source's context - loader (if it has one) + """Gets the context for this source by loading it through the source's + context loader (if it has one). + + :type context: Dict + :param context: A dictionary of context for an event from this + source. + + :rtype: Dict + :returns: The context provided, with any additional context + loaded by the context loader function. """ if self.context_loader: return self.get_context_loader_function()(context) @@ -243,6 +625,11 @@ def get_context(self, context): return context def clean(self): + """Validation for the model. + + Check that: + - the context loader provided maps to an actual loadable function. + """ if self.context_loader: try: self.get_context_loader_function() @@ -250,30 +637,83 @@ def clean(self): raise ValidationError('Must provide a loadable context loader') def save(self, *args, **kwargs): + """Save the instance to the database after validation. + """ self.clean() return super(Source, self).save(*args, **kwargs) def __str__(self): + """Readable representation of ``Source`` objects.""" return self.display_name @python_2_unicode_compatible class SourceGroup(models.Model): + """A ``SourceGroup`` object is a high level categorization of + events. Since ``Source`` objects are meant to be very fine + grained, they are collected into ``SourceGroup`` objects. There is + no additional behavior associated with the source groups other + than further categorization. Source groups can be created with + ``SourceGroup.objects.create``, which takes the following + arguments: + + :type name: str + :param name: A short, unique name for the source group. + + :type display_name: str + :param display_name: A short, human readable name for the source + group. Does not need to be unique. + + :type description: str + :param description: A human readable description of the source + group. + """ name = models.CharField(max_length=64, unique=True) display_name = models.CharField(max_length=64) description = models.TextField() def __str__(self): + """Readable representation of ``SourceGroup`` objects.""" return self.display_name @python_2_unicode_compatible class Unsubscription(models.Model): + """Because django-entity-event allows for whole groups to be + subscribed to events at once, unsubscribing an entity is not as + simple as removing their subscription object. Instead, the + Unsubscription table provides a simple way to ensure that an + entity does not see events if they don't want to. + + Unsubscriptions are created for a single entity at a time, where + they are unsubscribed for events from a source on a medium. This + is stored as an ``Unsubscription`` object in the database, which + can be created using ``Unsubscription.objects.create`` using the + following arguments: + + :type entity: Entity + :param entity: The entity to unsubscribe. + + :type medium: Medium + :param medium: The ``Medium`` object representing where they don't + want to see the events. + + :type source: Source + :param source: The ``Source`` object representing what category + of event they no longer want to see. + + Once an ``Unsubscription`` object is created, all of the logic to + ensure that they do not see events form the given source by the + given medium is handled by the methods used to query for events + via the ``Medium`` object. That is, once the object is created, no + more work is needed to unsubscribe them. + """ entity = models.ForeignKey(Entity) medium = models.ForeignKey('Medium') source = models.ForeignKey('Source') def __str__(self): + """Readable representation of ``Unsubscription`` objects.""" s = '{entity} from {source} by {medium}' entity = self.entity.__str__() source = self.source.__str__() @@ -283,6 +723,82 @@ def __str__(self): @python_2_unicode_compatible class Subscription(models.Model): + """Which types of events are available to which mediums is controlled + through ``Subscription`` objects. By creating a ``Subscription`` + object in the database, you are storing that events from a given + ``Source`` object should be available to a given ``Medium`` + object. + + Each ``Subscription`` object can be one of two levels, either an + individual subscription or a group subscription. Additionally, + each ``Subscription`` object can be one of two types of + subscription, either a global subscription, or an "only following" + subscription. ``Subscription`` objects are created using + ``Subscription.objects.create`` which takes the following + arguments: + + :type medium: Medium + :param medium: The ``Medium`` object to make events available to. + + :type source: Source + :param source: The ``Source`` object that represents the category + of events to make available. + + :type entity: Entity + :param entity: The entity to subscribe in the case of an + individual subscription, or in the case of a group + subscription, the super-entity of the group. + + :type sub_entity_kind: (optional) EntityKind + :param sub_entity_kind: When creating a group subscription, this + is a foreign key to the ``EntityKind`` of the sub-entities to + subscribe. In the case of an individual subscription, this should + be ``None``. + + :type only_following: Boolean + :param only_following: If ``True``, events will be available to + entities through the medium only if the entities are following + the actors of the event. If ``False``, the events will all + be available to all the entities through the medium. + + When a ``Medium`` object is used to query for events, only the + events that have a subscription for their source to that medium + will ever be returned. This is an extremely useful property that + allows complex subscription logic to be handled simply by storing + subscription objects in the database. + + Storing subscriptions is made simpler by the ability to subscribe + groups of entities with a single subscription object. Groups of + entities of a given kind can be subscribed by subscribing their + super-entity and providing the ``sub_entity_kind`` argument. + + Subscriptions further are specified to be either an "only following" + subscription or not. This specification controls what + events will be returned when ``Medium.entity_events`` is called, + and controls what targets are returned when + ``Medium.events_targets`` is called. + + For example, if events are created for a new photo being uploaded + (from a single source called, say "photos"), and we want to provide + individuals with a notification in their newsfeed (through a + medium called "newsfeed"), we want to be able to display only the + events where the individual is tagged in the photo. By setting + ``only_following`` to true the following code would only return + events where the individual was included in the ``EventActor`` s, + rather than returning all "photos" events: + + .. code-block:: python + + user_entity = Entity.objects.get_for_obj(user) + newsfeed_medium = Medium.objects.get(name='newsfeed') + newsfeed.entity_events(user) + + The behavior of what constitutes "following" is controlled by the + Medium class. A default implementation of following is provided + and documented in the ``Medium.followers_of`` and + ``Medium.followed_by`` methods, but could be extended by + subclasses of Medium. + """ medium = models.ForeignKey('Medium') source = models.ForeignKey('Source') entity = models.ForeignKey(Entity, related_name='+') @@ -290,6 +806,7 @@ class Subscription(models.Model): only_following = models.BooleanField(default=True) def __str__(self): + """Readable representation of ``Subscription`` objects.""" s = '{entity} to {source} by {medium}' entity = self.entity.__str__() source = self.source.__str__() @@ -298,6 +815,14 @@ def __str__(self): def subscribed_entities(self): """Return a queryset of all subscribed entities. + + This will be a single entity in the case of an individual + subscription, otherwise it will be all the entities in the + group subscription. + + :rtype: EntityQuerySet + :returns: A QuerySet of all the entities that are a part of + this subscription. """ if self.sub_entity_kind is not None: sub_entities = self.entity.sub_relationships.filter( @@ -309,9 +834,16 @@ def subscribed_entities(self): class EventQuerySet(QuerySet): + """A custom QuerySet for Events. + """ def mark_seen(self, medium): - """ - Creates EventSeen objects for the provided medium for every event in the queryset. + """Creates EventSeen objects for the provided medium for every event + in the queryset. + + Creating these EventSeen objects ensures they will not be + returned when passing ``seen=False`` to any of the medium + event retrieval functions, ``events``, ``entity_events``, or + ``events_targets``. """ EventSeen.objects.bulk_create([ EventSeen(event=event, medium=medium) for event in self @@ -319,16 +851,77 @@ def mark_seen(self, medium): class EventManager(models.Manager): + """A custom Manager for Events. + """ def get_queryset(self): + """Return the EventQuerySet. + """ return EventQuerySet(self.model) def mark_seen(self, medium): + """Creates EventSeen objects for the provided medium for every event + in the queryset. + + Creating these EventSeen objects ensures they will not be + returned when passing ``seen=False`` to any of the medium + event retrieval functions, ``events``, ``entity_events``, or + ``events_targets``. + """ return self.get_queryset().mark_seen(medium) @transaction.atomic - def create_event(self, ignore_duplicates=False, actors=None, **kwargs): - """ - A utility method for creating events with actors. + def create_event(self, actors=None, ignore_duplicates=False, **kwargs): + """Create events with actors. + + This method can be used in place of ``Event.objects.create`` + to create events, and the appropriate actors. It takes all the + same keywords as ``Event.objects.create`` for the event + creation, but additionally takes a list of actors, and can be + told to not attempt to create an event if a duplicate event + exists. + + :type source: Source + :param source: A ``Source`` object representing where the + event came from. + + :type context: dict + :param context: A dictionary containing relevant + information about the event, to be serialized into + JSON. It is possible to load additional context + dynamically when events are fetched. See the + documentation on the ``context_loader`` field in + ``Source``. + + :type uuid: str + :param uuid: A unique string for the event. Requiring a + ``uuid`` allows code that creates events to ensure they do + not create duplicate events. This id could be, for example + some hash of the ``context``, or, if the creator is + unconcerned with creating duplicate events a call to + python's ``uuid1()`` in the ``uuid`` module. + + :type time_expires: datetime (optional) + :param time_expires: If given, the default methods for + querying events will not return this event after this time + has passed. + + :type actors: (optional) List of entities or list of entity ids. + :param actors: An ``EventActor`` object will be created for + each entity in the list. This allows for subscriptions + which are only following certain entities to behave + appropriately. + + :type ignore_duplicates: (optional) Boolean + :param ignore_duplicates: If ``True``, a check will be made to + ensure that an event with the give ``uuid`` does not exist + before attempting to create the event. Setting this to + ``True`` allows the creator of events to gracefully ensure + no duplicates are created. + + :rtype: Event + :returns: The created event. Alternatively if a duplicate + event already exists and ``ignore_duplicates`` is + ``True``, it will return ``None``. """ if ignore_duplicates and self.filter(uuid=kwargs.get('uuid', '')).exists(): return None @@ -347,6 +940,29 @@ def create_event(self, ignore_duplicates=False, actors=None, **kwargs): @python_2_unicode_compatible class Event(models.Model): + """``Event`` objects store information about events. By storing + events, from a given source, with some context, they are made + available to any ``Medium`` object with an appropriate + subscription. Events can be created with + ``Event.objects.create_event``, documented above. + + When creating an event, the information about what occurred is + stored in a JSON blob in the ``context`` field. This context can + be any type of information that could be useful for displaying + events on a given Medium. It is entirely the role of the + application developer to ensure that there is agreement between + what information is stored in ``Event.context`` and what + information the code the processes and displays events on each + medium expects. + + Events will usually be created by code that also created, or knows + about the ``Source`` object that is required to create the event. + To prevent storing unnecessary data in the context, this code can + define a context loader function when creating this source, which + can be used to dynamically fetch more data based on whatever + limited amount of data makes sense to store in the context. This + is further documented in the ``Source`` documentation. + """ source = models.ForeignKey('Source') context = jsonfield.JSONField() time = models.DateTimeField(auto_now_add=True, db_index=True) @@ -356,13 +972,21 @@ class Event(models.Model): objects = EventManager() def get_context(self): - """ - Retrieves the context for this event, passing it through the context loader of - the source if necessary. + """Retrieves and populates the context for this event. + + At the minimum, whatever context was stored in the event is + returned. If the source of the event provides a + ``context_loader``, any additional context created by that + function will be included. + + :rtype: Dict + :returns: A dictionary of the event's context, with any + additional context loaded. """ return self.source.get_context(self.context) def __str__(self): + """Readable representation of ``Event`` objects.""" s = '{source} event at {time}' source = self.source.__str__() time = self.time.strftime('%Y-%m-%d::%H:%M:%S') @@ -370,16 +994,29 @@ def __str__(self): class AdminEvent(Event): + """A proxy model used to provide a separate interface for event + creation through the django-admin interface. + """ class Meta: proxy = True @python_2_unicode_compatible class EventActor(models.Model): + """``EventActor`` objects encode what entities were involved in an + event. They provide the information necessary to create "only + following" subscriptions which route events only to the entities + that are involved in the event. + + ``EventActor`` objects should not be created directly, but should + be created as part of the creation of ``Event`` objects, using + ``Event.objects.create_event``. + """ event = models.ForeignKey('Event') entity = models.ForeignKey(Entity) def __str__(self): + """Readable representation of ``EventActor`` objects.""" s = 'Event {eventid} - {entity}' eventid = self.event.id entity = self.entity.__str__() @@ -388,6 +1025,16 @@ def __str__(self): @python_2_unicode_compatible class EventSeen(models.Model): + """``EventSeen`` objects store information about where and when an + event was seen. They store the medium that the event was seen on, + and what time it was seen. This information is used by the event + querying methods on ``Medium`` objects to filter events by whether + or not they have been seen on that medium. + + ``EventSeen`` objects should not be created directly, but should + be created by using the ``EventQuerySet.mark_seen`` method, + available on the QuerySets returned by the event querying methods. + """ event = models.ForeignKey('Event') medium = models.ForeignKey('Medium') time_seen = models.DateTimeField(default=datetime.utcnow) @@ -396,6 +1043,7 @@ class Meta: unique_together = ('event', 'medium') def __str__(self): + """Readable representation of ``EventSeen`` objects.""" s = 'Seen on {medium} at {time}' medium = self.medium.__str__() time = self.time_seen.strftime('%Y-%m-%d::%H:%M:%S') diff --git a/entity_event/version.py b/entity_event/version.py index 66a87bb..7fd229a 100644 --- a/entity_event/version.py +++ b/entity_event/version.py @@ -1 +1 @@ -__version__ = '0.1.5' +__version__ = '0.2.0'