tschellenbach / Stream-Framework

Stream Framework is a Python library, which allows you to build news feed, activity streams and notification systems using Cassandra and/or Redis. The authors of Stream-Framework also provide a cloud service for feed technology:
https://getstream.io/
Other
4.73k stars 542 forks source link

Example for 'condensed activities' when retrieving items #119

Open yookore opened 9 years ago

yookore commented 9 years ago

@tbarbugli , I've been making greta progress with Stream Framework and its honestly a great piece of code. I've run into a little blocker which I think should be obvious but cant seem to wrap my head around it. How do i filter activities to only return the last action on an object in a user's feed. Here is the scenario User x has a feed that contains several actions carried out on a object (several posts, comments, likes etc) and when displaying the feed timeline, I want to be able to do two things,

  1. display only the last action in the stream and optionally
  2. aggregate actions perfomed on the that object.

I have a feeling that the Aggregated feed might be a good fit for this but I'd like to have some concrete examples on how to achieve this. Is there any portion of the code you could point me to?

tbarbugli commented 9 years ago

What about feed[:1]?

Il sabato 14 marzo 2015, yookore notiions@github.com ha scritto:

@tbarbugli https://github.com/tbarbugli , I've been making greta progress with Stream Framework and its honestly a great piece of code. I've run into a little blocker which I think should be obvious but cant seem to wrap my head around it. How do i filter activities to only return the last action on an object in a user's feed. Here is the scenario User x has a feed that contains several actions carried out on a object (several posts, comments, likes etc) and when displaying the feed timeline, I want to be able to do two things,

  1. display only the last action in the stream and optionally
  2. aggregate actions perfomed on the that object.

I have a feeling that the Aggregated feed might be a good fit for this but I'd like to have some concrete examples on how to achieve this. Is there any portion of the code you could point me to?

— Reply to this email directly or view it on GitHub https://github.com/tschellenbach/Stream-Framework/issues/119.

sent from iphone (sorry for the typos)

yookore commented 9 years ago

@tbarbugli maybe I did not clarify what I am asking. Lets say I have a feed that contains several activities carried out on the same object, say a post. One person creates the post, someone else likes the post and later someone else comments on the post. What would happen if i pull the feed would be three separate activity items for the same object. What I would like to do is only display the most recent activity on the object which would be the comment and hide previous activity on that same object. I hope this makes sense?

intellisense commented 9 years ago

I understand completely what you are asking for. I have devloped the same system and It is not that easy. I will try to explain it here as easy as I can. First we need to understand how activities are created based on Activity Streams Spec.

Action events are categorized by four main components.

So for example we have a user John and he posted something Object called it as Article

John (actor) posted (verb) the (preposition) Article (object) 12 hours ago

Now if any action is performed on the object, e.g. A comment on Article (A new object), that should create a new activity puting Article object as target:

John (actor) commented (verb) Article Comment (object) on (preposition) Article (target) 1 hour ago

Now based on above spec, what we want (In your case) is that we want to aggregate (group) activities based on main activity, e.g. object in first case and target in other case. So we need to write our own aggregator extending the BaseAggregator to write our own method get_group and rank, get_group is basically returns a unique key under which all activities are grouped for same object.

newsfeed/aggregators.py

from stream_framework.aggregators.base import BaseAggregator

from newsfeed.activity import NewsFeedAggregatedActivity # we will discuss this later

class NewsFeedAggregator(BaseAggregator):
    '''
    Aggregates which should aggregate as "User A, B and C commented on post"
    '''
    aggregated_activity_class = NewsFeedAggregatedActivity

    def rank(self, aggregated_activities):
        '''
        The ranking logic, for sorting aggregated activities
        '''
        aggregated_activities.sort(key=lambda a: a.updated_at, reverse=True)
        return aggregated_activities

    def get_group(self, activity):
        '''
        Returns a group based on the model and action object_id or target object_id (if present)
        '''
        activity_id = activity.target_id if activity.target_id else activity.object_id

        # Make sure to store model_name or database table name in extra_context of activity
        model = activity.extra_context['model_name']

        group = '%s-%s' % (model, id) #left part should always be the model name followed by dash
        return group

So far so good, now we will write NewsFeedAggregatedActivity which we have used in the above aggregator:

newsfeed/activity.py

from stream_framework.activity import AggregatedActivity
from stream_framework.utils import make_list_unique

from newsfeed.utils import proper_verb_display

class NewsFeedAggregatedActivity(AggregatedActivity):
    max_aggregated_activities_length = 500

    @property
    def main_activity(self):
        '''
        Returns the very first main activity object or target on which the future activities are performed and now being aggregated
        '''
        activity = self.recent_verb_activities[0]
        # I am assuming here that you are storing the objects intead of object references for simplicity
        # Storing the object references is prefered, then you need to query here to your database to fetch the object or target from database based on activity.object_id or activity.target_id
       # Always prefer target over object
        return activity.target if activity.target else activity.object

    @property
    def aggregated_verb_display(self, threshold=5):
        '''
        Returns verb display
        e.g. A, B and C commented on this
        e.g. A, B, C, D, E and 15 others commented on this
        '''

        activity = self.recent_verb_activities[-1]
        verb = self.recent_verb
        display = proper_verb_display(users=self.recent_verb_actors)
        display += ' %s %s this' % (verb.past_tense, verb.preposition)
        return display

    def verb_display(self):
        '''
        Returns verb display of the main activity
        e.g. User A posted the Article
        '''
        activity = self.main_activity
        verb = activity.verb
        display = proper_verb_display(users=[activity.actor])
        model = activity.extra_context['model_name']
        display += ' %s %s %s' % (verb.past_tense, verb.preposition, model.title())
        return display

    @property
    def recent_actor(self):
        return self.last_activity.actor

    @property
    def recent_verb(self):
        return self.last_activity.verb

    @property
    def recent_verb_activities(self):
        '''
        Creating groups for similar activities based on object or target if present
        '''
        last_activity = self.last_activity
        id = last_activity.target_id if last_activity.target_id else last_activity.object_id

        activities = [a for a in self.activities if a.verb.past_tense == self.recent_verb.past_tense]
        groups = {}
        for a in activities:
            key = a.target_id if a.target_id else a.object_id
            if not groups.get(key):
                groups[key] = []
            groups[key].append(a)
        return groups[id]

    @property
    def recent_verb_activities_count(self):
        return len(self.recent_verb_activities)

    @property
    def recent_verb_actors(self):
        activities = self.recent_verb_activities
        activities.reverse()
        return make_list_unique([a.actor for a in activities if a.verb.past_tense == self.recent_verb.past_tense])

Now we will write our own custom serializer to use our NewsFeedAggregatedActivity

newsfeed/serializers.py

from stream_framework.serializers.aggregated_activity_serializer import AggregatedActivitySerializer

from newsfeed.activity import NewsFeedAggregatedActivity

class NewsFeedAggregatedActivitySerializer(AggregatedActivitySerializer):
    aggregated_class = NewsFeedAggregatedActivity

We will write our own verbs to add the preposition, use these verbs when generating activities:

newsfeed/verbs.py

from stream_framework.verbs import register
from stream_framework.verbs.base import Verb

class PostVerb(Verb):
    id = 5 # 1-4 ids are already used in stream_framework.verbs.base
    infinitive = 'post'
    past_tense = 'posted'
    preposition = 'the'

class CommentVerb(Verb):
    id = 6
    infinitive = 'comment'
    past_tense = 'commented'
    preposition = 'on'

register(PostVerb)
register(CommentVerb)

Now we will create feeds which will use above custom classes:

newsfeed/feeds.py

from stream_framework.feeds.redis import RedisFeed
from stream_framework.feeds.aggregated_feed.redis import RedisAggregatedFeed
from stream_framework.activity import Activity

from newsfeed.aggregators import NewsFeedAggregator
from newsfeed.serializers import NewsFeedAggregatedActivitySerializer
from newsfeed.activity import NewsFeedAggregatedActivity

class AggregatedUserFeed(RedisAggregatedFeed):
    key_format = 'feed:aggregated:%(user_id)s'
    aggregated_activity_class = NewsFeedAggregatedActivity
    aggregator_class = NewsFeedAggregator
    timeline_serializer = NewsFeedAggregatedActivitySerializer
    max_length = 1000
    merge_max_length = 1000

class UserFeed(RedisFeed):
    key_format = 'feed:user:%(user_id)s'
    activity_class = Activity
    max_length = 1000
    merge_max_length = 1000

Now we will write our own manager to use feeds.py feeds:

newsfeed/managers.py

from stream_framework.feed_managers.base import Manager

from newsfeed.feeds import AggregatedUserFeed, UserFeed

class NewsFeedManager(Manager):
    # customize the feed classes we write to
    feed_classes = dict(
        aggregated=AggregatedUserFeed
    )
    # customize the user feed class
    user_feed_class = UserFeed
    # next add yourself the add_activity logic remove_activity logic

newsfeed_stream = NewsFeedManager()

Some helpers:

newsfeed/utils.py

def proper_verb_display(users=[], threshold=5):
    if users:
        count = len(users)
        if count <= 2:
            display = " and ".join([u.username for u in users]) 
        elif count < threshold:
            display = ", ".join([u.username for u in users[:-1]]) + " and " + users[-1].username)
        elif count == threshold:
            display = ", ".join([u.username for u in users[:-1]]) + " and 1 other"
        else:
            display = ", ".join([u.username for u in users[:threshold-1]]) + " and +{} others".format(count - (threshold - 1))

        return display
    return ''

Now to show user feed, you do this:

from newsfeed.managers import newsfeed_stream
newsfeed = newsfeed_stream.get_feeds(user.id)['aggregated']

And in html:

@TODO: figure out how to display aggregated acitivites, Everything you need to display aggregated activities is up there. I am leaving this intentionaly so that you can show some effort by yourself :p
yookore commented 9 years ago

@intellisense thanks for your response. I will dig in to this and see what gives....