From d4c5983a26594ac88089e8ed80d9c9ee98ede927 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Tue, 16 Dec 2014 19:02:48 -0500 Subject: [PATCH 01/43] Docs for events methods on Medium objects --- entity_event/models.py | 205 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/entity_event/models.py b/entity_event/models.py index e49aa4c..1558e9e 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -23,11 +23,74 @@ class Medium(models.Model): description = models.TextField() def __str__(self): + """Readable representation of ``Medium`` objects.""" return self.display_name @transaction.atomic def events(self, start_time=None, end_time=None, seen=None, include_expired=False, mark_seen=False): """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 happend in the + past 24 hours: + + .. code-block:: python + + TEMPLATE = ''' + +

Yoursite's Events

+ + + ''' + + 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 occured 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 occured 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 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. + """ events = self.get_filtered_events(start_time, end_time, seen, include_expired, mark_seen) subscriptions = Subscription.objects.filter(medium=self) @@ -50,6 +113,75 @@ def events(self, start_time=None, end_time=None, seen=None, include_expired=Fals @transaction.atomic def entity_events(self, entity, start_time=None, end_time=None, seen=None, include_expired=False, mark_seen=False): """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

+ + + ''' + + 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 occured 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 occured 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 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. + """ events = self.get_filtered_events(start_time, end_time, seen, include_expired, mark_seen) @@ -77,7 +209,71 @@ def entity_events(self, entity, start_time=None, end_time=None, seen=None, inclu def events_targets( self, entity_kind=None, start_time=None, end_time=None, seen=None, include_expired=False, mark_seen=False): - """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 occured 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 occured 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 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. + """ events = self.get_filtered_events(start_time, end_time, seen, include_expired, mark_seen) subscriptions = Subscription.objects.filter(medium=self) @@ -244,6 +440,7 @@ def save(self, *args, **kwargs): return super(Source, self).save(*args, **kwargs) def __str__(self): + """Readable representation of ``Source`` objects.""" return self.display_name @@ -254,6 +451,7 @@ class SourceGroup(models.Model): description = models.TextField() def __str__(self): + """Readable representation of ``SourceGroup`` objects.""" return self.display_name @@ -264,6 +462,7 @@ class Unsubscription(models.Model): 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__() @@ -280,6 +479,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__() @@ -353,6 +553,7 @@ def get_context(self): 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,6 +571,7 @@ class EventActor(models.Model): entity = models.ForeignKey(Entity) def __str__(self): + """Readable representation of ``EventActor`` objects.""" s = 'Event {eventid} - {entity}' eventid = self.event.id entity = self.entity.__str__() @@ -386,6 +588,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') From 80473da9aa55b57e2db67c61e6cf1da45edf7bfd Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 09:46:22 -0500 Subject: [PATCH 02/43] Docs for utility functions. --- entity_event/models.py | 74 ++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/entity_event/models.py b/entity_event/models.py index 1558e9e..c7ecc0c 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -308,6 +308,15 @@ def events_targets( 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 subscriptions's sub-entity-kind is the same as that of + the entity's. """ if entity is None: return subscriptions @@ -322,8 +331,8 @@ 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. """ unsubscriptions = defaultdict(list) for unsub in Unsubscription.objects.filter(medium=self).values('entity', 'source'): @@ -331,15 +340,14 @@ 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_event_filters(self, start_time, end_time, seen, include_expired): - """Return Q objects to filter events table. + """Return Q objects to filter events table to relevant events. """ now = datetime.utcnow() filters = [] @@ -360,8 +368,8 @@ def get_event_filters(self, start_time, end_time, seen, include_expired): return filters def get_filtered_events(self, start_time, end_time, seen, include_expired, mark_seen): - """ - Retrieves events with time or seen filters and also marks them as seen if necessary. + """Retrieves events, filters by event level filters, and marks them as + seen if necessary. """ event_filters = self.get_event_filters(start_time, end_time, seen, include_expired) events = Event.objects.filter(*event_filters) @@ -413,15 +421,13 @@ 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). """ if self.context_loader: return self.get_context_loader_function()(context) @@ -429,6 +435,11 @@ def get_context(self, context): return context def clean(self): + """Validatition 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() @@ -436,6 +447,8 @@ 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) @@ -488,6 +501,10 @@ 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. """ if self.sub_entity_kind is not None: sub_entities = self.entity.sub_relationships.filter( @@ -500,8 +517,13 @@ def subscribed_entities(self): class EventQuerySet(QuerySet): 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 @@ -510,15 +532,24 @@ def mark_seen(self, medium): class EventManager(models.Manager): 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. + """Create events with actors. """ if ignore_duplicates and self.filter(uuid=kwargs.get('uuid', '')).exists(): return None @@ -546,9 +577,12 @@ 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. """ return self.source.get_context(self.context) From fe988bf53f5949661a6a475ae0dde2db055a4981 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 10:09:17 -0500 Subject: [PATCH 03/43] followers_of and followed_by docs --- entity_event/models.py | 54 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/entity_event/models.py b/entity_event/models.py index c7ecc0c..a8fe0e8 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -384,9 +384,29 @@ def get_filtered_events(self, start_time, end_time, seen, include_expired, mark_ 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 sematics 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 queyset of the entities that the given entities are + following. This needs to be the inverse of ``followers_of``. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) @@ -397,9 +417,31 @@ 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 sematics 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 + subentities. + + 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 querset of the entities that follow the given + entities. This needs to be the inverse of ``followed_by``. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) From 6c6f145994266a4cc5ac87514b53cc1e04dacf65 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 10:51:13 -0500 Subject: [PATCH 04/43] create_events docs --- entity_event/models.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/entity_event/models.py b/entity_event/models.py index a8fe0e8..ca270da 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -590,8 +590,33 @@ def mark_seen(self, medium): return self.get_queryset().mark_seen(medium) @transaction.atomic - def create_event(self, ignore_duplicates=False, actors=None, **kwargs): + 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 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. + + :param kwargs: This method requires all the arguments for + creating an event to be present in keyword arguments. The + required arguments are ``source`` and ``context``, and + optionally ``time_expires`` and ``uuid``. """ if ignore_duplicates and self.filter(uuid=kwargs.get('uuid', '')).exists(): return None From e15d7a6e52e7815e32677aaf9e9e08db85da2ee2 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 11:27:48 -0500 Subject: [PATCH 05/43] Return type-annotations and descriptions --- entity_event/models.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index ca270da..6ad1d33 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -91,6 +91,8 @@ def site_feed(request): marks all the returned events as having been seen by this medium. + :rtype: EventQuerySet + :returns: A queryset of events. """ events = self.get_filtered_events(start_time, end_time, seen, include_expired, mark_seen) subscriptions = Subscription.objects.filter(medium=self) @@ -182,6 +184,8 @@ def newsfeed(request): marks all the returned events as having been seen by this medium. + :rtype: EventQuerySet + :returns: A queryset of events. """ events = self.get_filtered_events(start_time, end_time, seen, include_expired, mark_seen) @@ -274,6 +278,9 @@ def events_targets( 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(start_time, end_time, seen, include_expired, mark_seen) subscriptions = Subscription.objects.filter(medium=self) @@ -317,6 +324,16 @@ def subset_subscriptions(self, subscriptions, entity=None): 2. The subscription is for a super-entity of the given entity, and the subscriptions'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 @@ -333,6 +350,11 @@ def subset_subscriptions(self, subscriptions, entity=None): def unsubscriptions(self): """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'): @@ -348,6 +370,15 @@ def filter_source_targets_by_unsubscription(self, source_id, targets): def get_event_filters(self, start_time, end_time, seen, include_expired): """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 = [] @@ -370,6 +401,9 @@ def get_event_filters(self, start_time, end_time, seen, include_expired): def get_filtered_events(self, start_time, end_time, seen, include_expired, mark_seen): """Retrieves events, filters by event level filters, and marks them as seen if necessary. + + :rtype: EventQuerySet + :returns: All events which match the given filters. """ event_filters = self.get_event_filters(start_time, end_time, seen, include_expired) events = Event.objects.filter(*event_filters) @@ -407,6 +441,9 @@ def followed_by(self, entities): Return a queyset of the entities that the given entities are following. This needs to be the inverse of ``followers_of``. + + :rtype: EntityQuerySet + :returns: A QuerySet of entities followed by those given. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) @@ -442,6 +479,10 @@ def followers_of(self, entities): Return a querset of the entities that follow the given entities. This needs to be the inverse of ``followed_by``. + + :rtype: EntityQuerySet + :returns: A QuerySet of entities who are followers of those + given. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) @@ -470,6 +511,14 @@ def get_context_loader_function(self): def get_context(self, context): """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) @@ -547,6 +596,10 @@ def subscribed_entities(self): 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( @@ -617,6 +670,11 @@ def create_event(self, actors=None, ignore_duplicates=False, **kwargs): creating an event to be present in keyword arguments. The required arguments are ``source`` and ``context``, and optionally ``time_expires`` and ``uuid``. + + :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 @@ -650,6 +708,10 @@ def get_context(self): 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) From bf9bddbf5c581d83d7cb078749ce498f3c406641 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 15:04:28 -0500 Subject: [PATCH 06/43] Source and Medium documentation. --- entity_event/models.py | 80 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index 6ad1d33..4e66ead 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -18,6 +18,42 @@ # 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 server 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() @@ -495,6 +531,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() From 6b18a75e71d3ab0a9aaa444939993403f014a01c Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 15:18:42 -0500 Subject: [PATCH 07/43] SourceGroup and Unsubscription docs --- entity_event/models.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index 4e66ead..fea974f 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -630,6 +630,25 @@ def __str__(self): @python_2_unicode_compatible class SourceGroup(models.Model): + """A ``SourceGroup`` object is a high level categorization of + events. Since ``Source`` objecst 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() @@ -641,6 +660,35 @@ def __str__(self): @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') From 4aa13668ec039385fee58a8a418871ac9d0fe373 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 16:00:10 -0500 Subject: [PATCH 08/43] Subscription docs --- entity_event/models.py | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index fea974f..228fb49 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -704,6 +704,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 subsciption, 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 ``EventActors``, + 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='+') From 98dc2e7f7e6bea1d7370d17c722838eafd0ca033 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 16:11:27 -0500 Subject: [PATCH 09/43] Event documentation --- entity_event/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index 228fb49..9f8fde0 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -815,6 +815,8 @@ 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. @@ -830,6 +832,8 @@ def mark_seen(self, medium): class EventManager(models.Manager): + """A custom Manager for Events. + """ def get_queryset(self): """Return the EventQuerySet. """ @@ -897,6 +901,29 @@ def create_event(self, actors=None, ignore_duplicates=False, **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 occured is + stored in a JSON blob in the ``context`` field. This context can + be any type of information that could be usefull 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 + limitted 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) @@ -928,6 +955,9 @@ 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 From 4b6385d9283df141b0d1fe6655474840549b143b Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 16:17:18 -0500 Subject: [PATCH 10/43] EventActor, EventSeen docs --- entity_event/models.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index 9f8fde0..4a90f60 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -964,6 +964,15 @@ class Meta: @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) @@ -977,6 +986,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) From 60828af472b197c3079ef3bd10320182043d2056 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 16:18:25 -0500 Subject: [PATCH 11/43] remove mark-seen todo --- entity_event/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/entity_event/models.py b/entity_event/models.py index 4a90f60..ccb5e5c 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -15,7 +15,6 @@ 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 From 0b65fd482d89d1670e39d957bc3503bdc544205d Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 17:02:33 -0500 Subject: [PATCH 12/43] spelling corrections --- entity_event/models.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/entity_event/models.py b/entity_event/models.py index 3e78e14..7d8cf3d 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -68,7 +68,7 @@ def events(self, **event_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 happend in the + used to display a list of site-wide events that happened in the past 24 hours: .. code-block:: python @@ -97,12 +97,12 @@ def site_feed(request): of which are optional. :type start_time: datetime.datetime (optional) - :param start_time: Only return events that occured after the + :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 occured before the + :param end_time: Only return events that occurred before the given time. If no time is given for this argument, no filtering is done @@ -188,12 +188,12 @@ def newsfeed(request): :param entity: The entity to get events for. :type start_time: datetime.datetime (optional) - :param start_time: Only return events that occured after the + :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 occured before the + :param end_time: Only return events that occurred before the given time. If no time is given for this argument, no filtering is done @@ -279,12 +279,12 @@ def events_targets(self, entity_kind=None, **event_filters): each targets list. :type start_time: datetime.datetime (optional) - :param start_time: Only return events that occured after the + :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 occured before the + :param end_time: Only return events that occurred before the given time. If no time is given for this argument, no filtering is done @@ -352,7 +352,7 @@ def subset_subscriptions(self, subscriptions, entity=None): 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 subscriptions's sub-entity-kind is the same as that of + and the subscription's sub-entity-kind is the same as that of the entity's. :type subscriptions: QuerySet @@ -458,7 +458,7 @@ def followed_by(self, entities): method. This method can be overridden by a class that concretely - inherits ``Medium``, to define custom sematics for the + 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 @@ -475,7 +475,7 @@ def followed_by(self, entities): opposite behavior, where an individual entity follows themselves and all of their sub-entities. - Return a queyset of the entities that the given entities are + Return a queryset of the entities that the given entities are following. This needs to be the inverse of ``followers_of``. :rtype: EntityQuerySet @@ -494,7 +494,7 @@ def followers_of(self, entities): method. This method can be overridden by a class that concretely - inherits ``Medium``, to define custom sematics for the + 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 @@ -504,7 +504,7 @@ def followers_of(self, entities): 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 - subentities. + sub-entities. That is, the followers of individual entities are themselves, and if the entity has sub-entities, those sub-entities. This @@ -513,7 +513,7 @@ def followers_of(self, entities): where an the followers of an individual entity are themselves and all of their super-entities. - Return a querset of the entities that follow the given + Return a queryset of the entities that follow the given entities. This needs to be the inverse of ``followed_by``. :rtype: EntityQuerySet @@ -606,7 +606,7 @@ def get_context(self, context): return context def clean(self): - """Validatition for the model. + """Validation for the model. Check that: - the context loader provided maps to an actual loadable function. @@ -631,7 +631,7 @@ def __str__(self): @python_2_unicode_compatible class SourceGroup(models.Model): """A ``SourceGroup`` object is a high level categorization of - events. Since ``Source`` objecst are meant to be very fine + 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 @@ -733,7 +733,7 @@ class Subscription(models.Model): :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 subsciption, this should + subscribe. In the case of an individual subscription, this should be ``None``. :type only_following: Boolean @@ -765,7 +765,7 @@ class Subscription(models.Model): 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 ``EventActors``, + events where the individual was included in the ``EventActor``s, rather than returning all "photos" events: .. code-block:: python @@ -907,9 +907,9 @@ class Event(models.Model): subscription. Events can be created with ``Event.objects.create_event``, documented above. - When creating an event, the information about what occured is + 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 usefull for displaying + 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 @@ -921,7 +921,7 @@ class Event(models.Model): 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 - limitted amount of data makes sense to store in the context. This + limited amount of data makes sense to store in the context. This is further documented in the ``Source`` documentation. """ source = models.ForeignKey('Source') From f784e651e7d7a6b68caff5f6d6f52cc258a0dbdc Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 18:18:30 -0500 Subject: [PATCH 13/43] Sphinx Basic Setup and autodoc --- docs/conf.py | 4 ++-- docs/index.rst | 22 +++++++++++++++++++--- docs/installation.rst | 25 ++++++++++++++++++++++--- docs/quickstart.rst | 17 +++++++++++++++++ docs/ref/entity_event.rst | 29 ++++++++++++++++++++++++++--- docs/toc.rst | 11 ----------- entity_event/models.py | 8 ++++---- 7 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 docs/quickstart.rst delete mode 100644 docs/toc.rst diff --git a/docs/conf.py b/docs/conf.py index f76a29e..d7c1d1b 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' @@ -189,4 +189,4 @@ def process_django_model_docstring(app, what, name, obj, options, 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 + app.connect('autodoc-process-docstring', process_django_model_docstring) diff --git a/docs/index.rst b/docs/index.rst index 6760f4b..6baafed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,20 @@ -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. + +Table of Contents +----------------- + +.. toctree:: + :maxdepth: 2 + + installation + quickstart + ref/entity_event + contributing + release_notes diff --git a/docs/installation.rst b/docs/installation.rst index 47117e2..2889f76 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 +``'localized_recurrence'`` 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..611c28d --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,17 @@ +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 concers 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, categorizing, and storing events. +2. Displaying events to users in different ways. +3. Managing which events go where, and who are subscribed to them. diff --git a/docs/ref/entity_event.rst b/docs/ref/entity_event.rst index 3e90fbb..1851d9c 100644 --- a/docs/ref/entity_event.rst +++ b/docs/ref/entity_event.rst @@ -3,7 +3,30 @@ Code documentation ================== -entity_event ------------------- +.. automodule:: entity_event.models -.. automodule:: entity_event +.. autoclass:: Medium + :members: events, entity_events, events_targets, followed_by, followers_of + +.. autoclass:: Source + :members: get_context + +.. autoclass:: SourceGroup + +.. autoclass:: Unsubscription + +.. autoclass:: Subscription + :members: subscribed_entities + +.. autoclass:: EventQuerySet + :members: mark_seen + +.. autoclass:: EventManager + :members: create_event, mark_seen + +.. autoclass:: Event + :members: get_context + +.. autoclass:: EventActor + +.. autoclass:: EventSeen 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/models.py b/entity_event/models.py index 7d8cf3d..18b0e0b 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -753,19 +753,19 @@ class Subscription(models.Model): 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 + 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 + (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, + events where the individual was included in the ``EventActor`` s, rather than returning all "photos" events: .. code-block:: python From 342037f1a3dd19e56ea8a70163e5a394b1ce8b0d Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 19:29:49 -0500 Subject: [PATCH 14/43] Import models in __init__.py --- entity_event/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 +) From 4acb53605d3aa770302cf8759445800b66fe6c4a Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 19:50:54 -0500 Subject: [PATCH 15/43] Creating and Categorizing events docs --- docs/quickstart.rst | 91 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 611c28d..59ab638 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -13,5 +13,92 @@ This quickstart guide handles the three parts of managing events and notifications. 1. Creating, categorizing, and storing events. -2. Displaying events to users in different ways. -3. Managing which events go where, and who are subscribed to them. +2. Managing which events go where, and who are subscribed to them. +3. Displaying events to users in different ways. + + +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 + +.. 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' + ) + + update_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. + +..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 Subscriptions to Events +-------------------------------- + + +Querying and Displaying Events +------------------------------ + From 1dcdb87a4486ecedb573682c1df489b6dc547b23 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 20:44:18 -0500 Subject: [PATCH 16/43] Medium and Subscription creation docs --- docs/quickstart.rst | 135 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 8 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 59ab638..d01ad34 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -12,9 +12,13 @@ abstractions at each of these levels. This quickstart guide handles the three parts of managing events and notifications. -1. Creating, categorizing, and storing events. -2. Managing which events go where, and who are subscribed to them. -3. Displaying events to users in different ways. +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 @@ -52,7 +56,7 @@ different sources description='You have been tagged in a photo' ) - update_source = Source.objects.create( + product_source = Source.objects.create( group=yoursite_group, name='new-product', display_name='New Product', @@ -95,10 +99,125 @@ handlers. In either case, We're ready to discuss subscription management. -Managing Subscriptions to Events --------------------------------- +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 + +.. 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 ``Subscription`` object needs to be +created. Creating a ``Subscription`` object encodes that an entity, or +group of entities, wants to recieve 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 recieve 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, + subentity_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 recieve 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 recieve 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, + subentity_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 accomodate this, we can +create an ``Unsubscription`` object. These are used to unsubscribe a +single entity from recieving 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 + +.. 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 +recieve 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 and Displaying Events ------------------------------- +Querying and Events +------------------- From d56ccd0ea7ed8ffb5cd039e00a586910389ad798 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 21:41:17 -0500 Subject: [PATCH 17/43] Querying events docs. --- docs/quickstart.rst | 97 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index d01ad34..098fa06 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -219,5 +219,98 @@ events on those mediums, we can use the ``Medium`` objects to query for events, which we can then display to our users. -Querying and Events -------------------- +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. + +1. ``Medium.events`` +2. ``Medium.entity_events`` +3. ``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 + +.. code-block:: python + + 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 +querset 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 ``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 recieve 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 occured after this time +- ``end_time``: providing a datetime object to this paramter will + filter the events to only those that occured 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. +- ``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. From 050257647e6e01055709fdea187997f6159cd436 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 21:42:54 -0500 Subject: [PATCH 18/43] spelling fixes --- docs/quickstart.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 098fa06..42d7a55 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -6,7 +6,7 @@ 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 concers as simple as possible, and provides convenient +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 @@ -140,10 +140,10 @@ 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 recieve notifications of events from a +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 recieve notifications of new products in their newsfeed +will receive notifications of new products in their newsfeed .. code-block:: python @@ -165,7 +165,7 @@ 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 recieve emails about to the events where they +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`` @@ -173,7 +173,7 @@ 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 recieve emails about photos they're tagged +subscription for users to receive emails about photos they're tagged in, we'll define the subscription as follows .. code-block:: python @@ -192,9 +192,9 @@ 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 accomodate this, we can +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 recieving notifications of a given source on 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 @@ -211,7 +211,7 @@ object for them ) Once this object is stored in the database, this user will no longer -recieve this type of notification. +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 @@ -263,7 +263,7 @@ 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 -querset of events is as simple as +QuerySet of events is as simple as .. code-block:: python @@ -275,7 +275,7 @@ 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 recieve that +``targets`` is a list of the entities that should receive that notification. We could use this function to send emails about events as follows @@ -297,9 +297,9 @@ 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 occured after this time -- ``end_time``: providing a datetime object to this paramter will - filter the events to only those that occured before this time. + filter the events to only those that occurred after this time +- ``end_time``: providing a datetime object to this parameter will + filter the events to only those that occurred 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 From 45255558103d44df01bb8d27aa4ddf39562b621a Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 21:44:41 -0500 Subject: [PATCH 19/43] Code block syntax fix --- docs/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 42d7a55..af35df6 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -72,9 +72,9 @@ 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. +happened -..code-block:: python +.. code-block:: python from entity_event import Event From a67f57161afbc396d6e4d3d3af2db2d555b79ecc Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 17 Dec 2014 21:48:30 -0500 Subject: [PATCH 20/43] Minor version number bump. --- entity_event/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' From a37b794a66278d03ad2c9c1d366ec564b00f2799 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Thu, 18 Dec 2014 16:12:43 -0500 Subject: [PATCH 21/43] Advanced Features docs. --- docs/advanced_features.rst | 148 +++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 149 insertions(+) create mode 100644 docs/advanced_features.rst diff --git a/docs/advanced_features.rst b/docs/advanced_features.rst new file mode 100644 index 0000000..ca2cb2f --- /dev/null +++ b/docs/advanced_features.rst @@ -0,0 +1,148 @@ +Advanced Features +================= + +The 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 + ``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 ``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 ``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. + +.. 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/index.rst b/docs/index.rst index 6baafed..a0f8e6a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,6 +15,7 @@ Table of Contents installation quickstart + advanced_features ref/entity_event contributing release_notes From f1dd28e9dcb6a56d5512418ab0a4f6cde6d28730 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Thu, 18 Dec 2014 16:15:46 -0500 Subject: [PATCH 22/43] Release notes for v0.2 --- docs/release_notes.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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. From 8e444fe7d9178a6ea31f93dea3abd6fb3b5c240d Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Thu, 18 Dec 2014 16:54:14 -0500 Subject: [PATCH 23/43] Added cross referencing links --- docs/advanced_features.rst | 28 +++--- docs/index.rst | 8 +- docs/quickstart.rst | 171 ++++++++++++++++++++----------------- 3 files changed, 117 insertions(+), 90 deletions(-) diff --git a/docs/advanced_features.rst b/docs/advanced_features.rst index ca2cb2f..1c78daf 100644 --- a/docs/advanced_features.rst +++ b/docs/advanced_features.rst @@ -1,7 +1,7 @@ Advanced Features ================= -The Quickstart guide covers the common use cases of Django Entity +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. @@ -10,7 +10,7 @@ This guide will cover the following advanced use cases: - Dynamically loading context using ``context_loader`` - Customizing the behavior of ``only_following`` by sub-classing - ``Medium``. + :py:class:`~entity_event.models.Medium`. Custom Context Loaders @@ -65,7 +65,8 @@ a path to this function in the ``context_loader`` field. ) With this setup, all of the additional information can by dynamically -loaded into events, simply by calling ``Event.get_context``. +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 @@ -89,16 +90,21 @@ 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. +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 ``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. +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 diff --git a/docs/index.rst b/docs/index.rst index a0f8e6a..1e73533 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,9 +3,11 @@ Django Entity Event 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. +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 ----------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index af35df6..ce90ea6 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,3 +1,5 @@ +.. _quickstart: + Quickstart and Basic Usage ========================== @@ -25,17 +27,20 @@ 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 +: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 @@ -65,12 +70,14 @@ different sources 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. +: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 ``Event.objects.create_event`` method. To create -an event for the "photo-tag" group, we just need to know the source of +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 @@ -110,15 +117,16 @@ 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. +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 ``Source`` objects, ``Medium`` objects are simple to -create +Like :py:class:`~entity_event.models.Source` objects, +:py:class:`~entity_event.models.Medium` objects are simple to create .. code-block:: python @@ -138,12 +146,14 @@ create 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 +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 @@ -158,15 +168,16 @@ will receive notifications of new products in their newsfeed only_following=False ) -With this ``Subscription`` object defined, all events from the new -product source will be available to the newsfeed medium. +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 ``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. +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 @@ -193,10 +204,11 @@ 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`` +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 @@ -213,10 +225,11 @@ object for them 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. +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 @@ -225,45 +238,50 @@ 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 +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 -``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. +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. ``Medium.events`` -2. ``Medium.entity_events`` -3. ``Medium.events_targets`` +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 ``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 +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 ``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 +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 @@ -272,12 +290,13 @@ QuerySet of events is as simple as 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 +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 3553fcdce702303fbe07fc7010f0d49c6348c4a8 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Thu, 18 Dec 2014 16:56:18 -0500 Subject: [PATCH 24/43] Add actors argument documentation. --- entity_event/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index 18b0e0b..4317324 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -121,6 +121,10 @@ def site_feed(request): 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 @@ -212,6 +216,10 @@ def newsfeed(request): 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 @@ -303,6 +311,10 @@ def events_targets(self, entity_kind=None, **event_filters): 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 From cfeaf8c73cab8bdc3496d408941bcc77967e48ac Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Thu, 18 Dec 2014 17:00:02 -0500 Subject: [PATCH 25/43] Update readme --- README.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 ------------ From 54c786292de406f4be7dc6cdb34da7803de015f0 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 10:57:22 -0500 Subject: [PATCH 26/43] small corrections --- docs/installation.rst | 2 +- docs/quickstart.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 2889f76..970b08c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -16,7 +16,7 @@ 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 -``'localized_recurrence'`` in ``settings.INSTALLED_APPS``. After it is +``'entity_event'`` in ``settings.INSTALLED_APPS``. After it is included in your installed apps, run:: ./manage.py migrate entity_event diff --git a/docs/quickstart.rst b/docs/quickstart.rst index ce90ea6..47be2cd 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -164,7 +164,7 @@ their newsfeed medium=newsfeed_medium, source=product_source, entity=all_users, - subentity_kind=EntityKind.objects.get(name='user'), + sub_entity_kindd=EntityKind.objects.get(name='user'), only_following=False ) @@ -193,7 +193,7 @@ in, we'll define the subscription as follows medium=email_medium, source=photo_source, entity=all_users, - subentity_kind=EntityKind.objects.get(name='user'), + sub_entity_kindd=EntityKind.objects.get(name='user'), only_following=True ) From 0e75e8147d6ef4e08787f5bfe6d26b964a178d93 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 14:35:23 -0500 Subject: [PATCH 27/43] Quickstart corrections. --- docs/quickstart.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 47be2cd..9a3a1df 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -164,7 +164,7 @@ their newsfeed medium=newsfeed_medium, source=product_source, entity=all_users, - sub_entity_kindd=EntityKind.objects.get(name='user'), + sub_entity_kind=EntityKind.objects.get(name='user'), only_following=False ) @@ -193,7 +193,7 @@ in, we'll define the subscription as follows medium=email_medium, source=photo_source, entity=all_users, - sub_entity_kindd=EntityKind.objects.get(name='user'), + sub_entity_kind=EntityKind.objects.get(name='user'), only_following=True ) @@ -316,9 +316,9 @@ 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 after this time + 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 before this time. + 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 From 89cda826b9e8270a5cb23868c337b1f4e7a4912d Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 14:38:47 -0500 Subject: [PATCH 28/43] Medium typo --- entity_event/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entity_event/models.py b/entity_event/models.py index 4317324..52d0f53 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -34,7 +34,7 @@ class Medium(models.Model): :param description: A human readable description of the medium. - Encoding a ``Medium`` object in the database server two + 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 From 314e1914effa64c74bf8c4f4ebc571288943a2f4 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 15:30:35 -0500 Subject: [PATCH 29/43] Better documentation of create_event args --- docs/quickstart.rst | 4 +++- entity_event/models.py | 30 +++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9a3a1df..c345406 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -322,7 +322,9 @@ themselves. All three methods support the following arguments: - ``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. + 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. diff --git a/entity_event/models.py b/entity_event/models.py index 52d0f53..68c5b06 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -873,6 +873,31 @@ def create_event(self, actors=None, ignore_duplicates=False, **kwargs): 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 @@ -886,11 +911,6 @@ def create_event(self, actors=None, ignore_duplicates=False, **kwargs): ``True`` allows the creator of events to gracefully ensure no duplicates are created. - :param kwargs: This method requires all the arguments for - creating an event to be present in keyword arguments. The - required arguments are ``source`` and ``context``, and - optionally ``time_expires`` and ``uuid``. - :rtype: Event :returns: The created event. Alternatively if a duplicate event already exists and ``ignore_duplicates`` is From 69978823a14f59ec2b31c428e394058fd2d3f761 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 15:33:09 -0500 Subject: [PATCH 30/43] Param docs for followers_of, followed_by --- entity_event/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/entity_event/models.py b/entity_event/models.py index 68c5b06..22d2167 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -490,6 +490,9 @@ def followed_by(self, entities): Return a queryset of the entities that the given entities are following. This needs to be the inverse of ``followers_of``. + :type entities: EntityQuerySet + :param entities: The QuerySet of entities of interest. + :rtype: EntityQuerySet :returns: A QuerySet of entities followed by those given. """ @@ -528,6 +531,9 @@ def followers_of(self, entities): Return a queryset of the entities that follow the given entities. This needs to be the inverse of ``followed_by``. + :type entities: EntityQuerySet + :param entities: The QuerySet of entities of interest. + :rtype: EntityQuerySet :returns: A QuerySet of entities who are followers of those given. From 6608c5fa4c52d0ad18f43e245000748c84929498 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 15:35:10 -0500 Subject: [PATCH 31/43] Entity or EntityQuerySet clarification --- entity_event/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/entity_event/models.py b/entity_event/models.py index 22d2167..d147f38 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -490,8 +490,8 @@ def followed_by(self, entities): Return a queryset of the entities that the given entities are following. This needs to be the inverse of ``followers_of``. - :type entities: EntityQuerySet - :param entities: The QuerySet of entities of interest. + :type entities: Entity or EntityQuerySet + :param entities: The Entity, or QuerySet of Entities of interest. :rtype: EntityQuerySet :returns: A QuerySet of entities followed by those given. @@ -531,8 +531,8 @@ def followers_of(self, entities): Return a queryset of the entities that follow the given entities. This needs to be the inverse of ``followed_by``. - :type entities: EntityQuerySet - :param entities: The QuerySet of entities of interest. + :type entities: Entity or EntityQuerySet + :param entities: The Entity, or QuerySet of Entities of interest. :rtype: EntityQuerySet :returns: A QuerySet of entities who are followers of those From bbcd892b0069afc65e83baecd028f868f72f2cc6 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 15:48:40 -0500 Subject: [PATCH 32/43] Fixed double parameters and method signatures --- docs/conf.py | 64 +++++++++++++++++++-------------------- docs/ref/entity_event.rst | 49 ++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d7c1d1b..b8968fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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) +# def setup(app): +# # Register the docstring processor with sphinx +# app.connect('autodoc-process-docstring', process_django_model_docstring) diff --git a/docs/ref/entity_event.rst b/docs/ref/entity_event.rst index 1851d9c..5707bdc 100644 --- a/docs/ref/entity_event.rst +++ b/docs/ref/entity_event.rst @@ -5,28 +5,45 @@ Code documentation .. automodule:: entity_event.models -.. autoclass:: Medium - :members: events, entity_events, events_targets, followed_by, followers_of +.. autoclass:: Medium() -.. autoclass:: Source - :members: get_context + .. automethod:: events(self, **event_filters) -.. autoclass:: SourceGroup + .. automethod:: entity_events(self, entity, **event_filters) -.. autoclass:: Unsubscription + .. automethod:: events_targets(self, entity_kind, **event_filters) -.. autoclass:: Subscription - :members: subscribed_entities + .. automethod:: followed_by(self, entities) -.. autoclass:: EventQuerySet - :members: mark_seen + .. automethod:: followers_of(self, entities) -.. autoclass:: EventManager - :members: create_event, mark_seen -.. autoclass:: Event - :members: get_context +.. autoclass:: Source() -.. autoclass:: EventActor + .. automethod:: get_context(self, context) -.. autoclass:: EventSeen +.. 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() From deaa8c7d2929f39e145802aac8a2d66691a135cd Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Fri, 19 Dec 2014 16:00:51 -0500 Subject: [PATCH 33/43] Clarify behavior of followers_of and followed_by --- entity_event/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/entity_event/models.py b/entity_event/models.py index d147f38..b815028 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -494,7 +494,8 @@ def followed_by(self, entities): :param entities: The Entity, or QuerySet of Entities of interest. :rtype: EntityQuerySet - :returns: A QuerySet of entities followed by those given. + :returns: A QuerySet of all the entities followed by any of + those given. """ if isinstance(entities, Entity): entities = Entity.objects.filter(id=entities.id) @@ -535,8 +536,8 @@ def followers_of(self, entities): :param entities: The Entity, or QuerySet of Entities of interest. :rtype: EntityQuerySet - :returns: A QuerySet of entities who are followers of those - given. + :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) From ef1051f477327e8ef164dfeb2f24712207217fbd Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Mon, 29 Dec 2014 10:48:23 -0500 Subject: [PATCH 34/43] Freezegun version pin --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a0b7b5e..6b2f9d2 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def get_version(): 'south', 'mock>=1.0.1', 'coverage>=3.7.1', - 'freezegun', + 'freezegun==0.2.2', 'django-dynamic-fixture' ], test_suite='run_tests.run_tests', From d36e87be2976c3f87ff0e803096ea8b95550a5b1 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Mon, 29 Dec 2014 11:10:09 -0500 Subject: [PATCH 35/43] Update docs requirements --- entity_event/version.py | 2 +- requirements/docs.txt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/entity_event/version.py b/entity_event/version.py index 7fd229a..fc79d63 100644 --- a/entity_event/version.py +++ b/entity_event/version.py @@ -1 +1 @@ -__version__ = '0.2.0' +__version__ = '0.2.1' diff --git a/requirements/docs.txt b/requirements/docs.txt index eaaf0da..4adcab5 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,4 +1,8 @@ Sphinx>=1.2.2 sphinx_rtd_theme psycopg2>=2.4.5 -django>=1.6 \ No newline at end of file +django>=1.6,<1.7 +cached-property>=0.1.5 +django-entity>=1.7.1 +jsonfield>=0.9.20 +six From 6f2761eae24119f68acf4d9faa4e83a4a63c5feb Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Mon, 29 Dec 2014 12:28:51 -0500 Subject: [PATCH 36/43] Add djcelery to requirements/docs.txt --- requirements/docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/docs.txt b/requirements/docs.txt index 4adcab5..caba5d8 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -2,6 +2,7 @@ Sphinx>=1.2.2 sphinx_rtd_theme psycopg2>=2.4.5 django>=1.6,<1.7 +djcelery cached-property>=0.1.5 django-entity>=1.7.1 jsonfield>=0.9.20 From 10b3f4e92780c379a67b7069dc8f25410f69473c Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Mon, 29 Dec 2014 12:34:38 -0500 Subject: [PATCH 37/43] Bump version patch number --- entity_event/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entity_event/version.py b/entity_event/version.py index fc79d63..020ed73 100644 --- a/entity_event/version.py +++ b/entity_event/version.py @@ -1 +1 @@ -__version__ = '0.2.1' +__version__ = '0.2.2' From 4fe721cf4f44776b4dd5034b91e860794b2d1d73 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Mon, 29 Dec 2014 12:50:19 -0500 Subject: [PATCH 38/43] djcelery -> django-celery --- requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index caba5d8..af5ad28 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -2,7 +2,7 @@ Sphinx>=1.2.2 sphinx_rtd_theme psycopg2>=2.4.5 django>=1.6,<1.7 -djcelery +django-celery cached-property>=0.1.5 django-entity>=1.7.1 jsonfield>=0.9.20 From 9ef63a2d0ed012381e1f0eda82fd4507b30f0683 Mon Sep 17 00:00:00 2001 From: Wes Kendall Date: Wed, 21 Jan 2015 11:47:06 -0500 Subject: [PATCH 39/43] added more model tests for getting unseen events --- entity_event/tests/models_tests.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/entity_event/tests/models_tests.py b/entity_event/tests/models_tests.py index 2d8791d..b5d6c09 100644 --- a/entity_event/tests/models_tests.py +++ b/entity_event/tests/models_tests.py @@ -225,6 +225,27 @@ def test_super_not_included(self): self.assertEqual(subs.count(), 0) +class MediumGetFilteredEventsTest(TestCase): + def setUp(self): + self.medium = G(Medium) + + def test_get_unseen_events_some_seen_some_not(self): + seen_e = G(Event, context={}) + G(EventSeen, event=seen_e, medium=self.medium) + unseen_e = G(Event, context={}) + + events = self.medium.get_filtered_events(seen=False) + self.assertEquals(list(events), [unseen_e]) + + def test_get_unseen_events_some_seen_from_other_mediums(self): + seen_from_other_medium_e = G(Event, context={}) + G(EventSeen, event=seen_from_other_medium_e) + unseen_e = G(Event, context={}) + + events = self.medium.get_filtered_events(seen=False) + self.assertEquals(set(events), set([unseen_e, seen_from_other_medium_e])) + + class MediumGetEventFiltersTest(TestCase): def setUp(self): self.medium = G(Medium) From a8242637740fb8450b2ce2dfed25d8a680b21e2f Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 21 Jan 2015 12:44:30 -0500 Subject: [PATCH 40/43] _unseen_event_ids function, raw query --- entity_event/models.py | 17 +++++++++++++++++ entity_event/tests/models_tests.py | 12 +++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/entity_event/models.py b/entity_event/models.py index b815028..d1eb3f0 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -1048,3 +1048,20 @@ def __str__(self): medium = self.medium.__str__() time = self.time_seen.strftime('%Y-%m-%d::%H:%M:%S') return s.format(medium=medium, time=time) + + +def _unseen_event_ids(medium): + """Return all events that have not been seen on this medium. + """ + query = ''' + SELECT event.id + FROM entity_event_event AS event + LEFT OUTER JOIN (SELECT * + FROM entity_event_eventseen AS seen + WHERE seen.medium_id=%s) AS eventseen + ON event.id = eventseen.event_id + WHERE eventseen.medium_id IS NULL + ''' + unseen_events = Event.objects.raw(query, params=[medium.id]) + ids = [e.id for e in unseen_events] + return ids diff --git a/entity_event/tests/models_tests.py b/entity_event/tests/models_tests.py index b5d6c09..e31eba1 100644 --- a/entity_event/tests/models_tests.py +++ b/entity_event/tests/models_tests.py @@ -8,7 +8,7 @@ from six import text_type from entity_event.models import ( - Medium, Source, SourceGroup, Unsubscription, Subscription, Event, EventActor, EventSeen + Medium, Source, SourceGroup, Unsubscription, Subscription, Event, EventActor, EventSeen, _unseen_event_ids ) @@ -388,6 +388,16 @@ def test_length_indiv(self): self.assertEqual(indiv_qs.count(), 1) +class UnseenEventIdsTest(TestCase): + def test_filters_seen(self): + m = G(Medium) + e1 = G(Event, context={}) + e2 = G(Event, context={}) + Event.objects.filter(id=e2.id).mark_seen(m) + unseen_ids = _unseen_event_ids(m) + self.assertEqual(unseen_ids, [e1.id]) + + # Note: The following freeze_time a few more minutes than what we # want, in order to work around a strange off by a few seconds bug in # freezegun. I'm not sure what other way to fix it. Since we're only From dc54f6cda0fdc618b75c3a5535be69503088e81b Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 21 Jan 2015 12:51:20 -0500 Subject: [PATCH 41/43] Additional test for _unseen_event_ids --- entity_event/tests/models_tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/entity_event/tests/models_tests.py b/entity_event/tests/models_tests.py index e31eba1..f43c21c 100644 --- a/entity_event/tests/models_tests.py +++ b/entity_event/tests/models_tests.py @@ -397,6 +397,18 @@ def test_filters_seen(self): unseen_ids = _unseen_event_ids(m) self.assertEqual(unseen_ids, [e1.id]) + def test_multiple_mediums(self): + m1 = G(Medium) + m2 = G(Medium) + e1 = G(Event, context={}) + e2 = G(Event, context={}) + e3 = G(Event, context={}) + e4 = G(Event, context={}) + Event.objects.filter(id=e2.id).mark_seen(m1) + Event.objects.filter(id=e3.id).mark_seen(m2) + unseen_ids = _unseen_event_ids(m1) + self.assertEqual(set(unseen_ids), set([e1.id, e3.id, e4.id])) + # Note: The following freeze_time a few more minutes than what we # want, in order to work around a strange off by a few seconds bug in From d09d525c1c3929892a1f5c54a3822bf71b33fdeb Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 21 Jan 2015 12:55:46 -0500 Subject: [PATCH 42/43] Replace exclude filter with raw sql function --- entity_event/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/entity_event/models.py b/entity_event/models.py index d1eb3f0..4f1191a 100644 --- a/entity_event/models.py +++ b/entity_event/models.py @@ -437,7 +437,8 @@ def get_filtered_events_queries(self, start_time, end_time, seen, include_expire if seen is True: filters.append(Q(eventseen__medium=self)) elif seen is False: - filters.append(~Q(eventseen__medium=self)) + unseen_ids = _unseen_event_ids(medium=self) + filters.append(Q(id__in=unseen_ids)) # Filter by actor if actor is not None: From 1b848d9710f91d65c09284a7d3b467dcada21cd0 Mon Sep 17 00:00:00 2001 From: Erik Swanson Date: Wed, 21 Jan 2015 13:00:19 -0500 Subject: [PATCH 43/43] Bump patch version number --- entity_event/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entity_event/version.py b/entity_event/version.py index 020ed73..d93b5b2 100644 --- a/entity_event/version.py +++ b/entity_event/version.py @@ -1 +1 @@ -__version__ = '0.2.2' +__version__ = '0.2.3'