/django-sequere

A Django application to implement a follow system and a timeline using multiple backends (db, redis, etc.)

Primary LanguagePythonMIT LicenseMIT

django-sequere

Build Status

A Django application to implement a follow system and a timeline using multiple backends (db, redis, etc.).

The timeline engine can be found in sequere.contrib.timeline.

Installation

  1. Either check out the package from GitHub or it pull from a release via PyPI

    pip install django-sequere
    
  2. Add sequere to your INSTALLED_APPS

INSTALLED_APPS = (
    'sequere',
)

Usage

In Sequere any resources can follow any resources and vice versa.

Let's say you have two models:

# models.py

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=150)


class Project(models.Model):
    name = models.CharField(max_length=150)

Now you to register them in sequere to identify them when a resource is following another one.

# sequere_registry.py

from .models import Project, User

import sequere

sequere.register(User)
sequere.register(Project)

Sequere uses the same concepts as Django Admin, so if you have already used it, you will not be lost.

You can now use Sequere like any other application, let's play with it:

>>> from sequere.models import (follow, unfollow, get_followings_count, is_following,
                                    get_followers_count, get_followers, get_followings)

>>> from myapp.models import User, Project

>>> user = User.objects.create(username='thoas')

>>> project = Project.objects.create(name='La classe americaine')

>>> follow(user, project)  # thoas will now follow "La classe americaine"

>>> is_following(user, project)
True

>>> get_followers_count(project)
1

>>> get_followings_count(user)
1

>>> get_followers(user)
[]

>>> get_followers(project)
[(<User: thoas>, datetime.datetime(2013, 10, 25, 4, 41, 31, 612067))]

>>> get_followings(user)
[(<Project: La classe americaine, datetime.datetime(2013, 10, 25, 4, 41, 31, 612067))]

If you are as lazy as me to provide the original instance in each sequere calls, use SequereMixin

# models.py

from django.db import models

from sequere.mixin import SequereMixin

class User(SequereMixin, models.Model):
    username = models.Charfield(max_length=150)

class Project(SequereMixin, models.Model):
    name = models.Charfield(max_length=150)

Now you can use calls directly from the instance:

>>> from myapp.models import User, Project

>>> user = User.objects.create(username='thoas')

>>> project = Project.objects.create(name'La classe americaine')

>>> user.follow(project)  # thoas will now follow "La classe americaine"

>>> user.is_following(project)
True

>>> project.get_followers_count()
1

>>> user.get_followings_count()
1

>>> user.get_followers()
[]

>>> project.get_followers()
[(<User: thoas>, datetime.datetime(2013, 10, 25, 4, 41, 31, 612067))]

>>> user.get_followings()
[(<Project: La classe americaine, datetime.datetime(2013, 10, 25, 4, 41, 31, 612067))]

So much fun!

Backends

sequere.backends.database.DatabaseBackend

A database backend to store your follows in you favorite database using the Django's ORM.

To use this backend you will have to add sequere.backends.database to your INSTALLED_APPS

INSTALLED_APPS = (
    'sequere',
    'sequere.backends.database',
)

The follower will be identified by the couple (from_identifier, from_object_id) and the following by (to_identifier, to_object_id).

Each identifiers are taken from the registry. For example, if you want to create a custom identifier key from a model you can customized it like so:

# sequere_registry.py

from myapp.models import Project

from sequere.base import ModelBase

import sequere


class ProjectSequere(ModelBase):
    identifier = 'projet' # the french way ;)

sequere.registry(Project, ProjectSequere)

sequere.backends.redis.RedisBackend

We are using exclusively Sorted Sets in this Redis implementation.

Create a uid for a new resource

INCR sequere:global:uid    =>  1
SET sequere:uid:{identifier}:{id} 1
HMSET sequere:uid::{id} identifier {identifier} object_id {id}

Store followers count

INCR sequere:uid:{to_uid}:followers:count => 1
INCR sequere:uid:{to_uid}:followers:{from_identifier}:count => 1

Store followings count

INCR sequere:uid:{from_uid}:followings:count => 1
INCR sequere:uid:{from_uid}:followings:{to_identifier}:count => 1

Add a new follower

ZADD sequere:uid:{to_uid}:followers {from_uid} {timestamp}
ZADD sequere:uid:{to_uid}:followers:{from_identifier} {from_uid} {timestamp}

Add a new following

ZADD sequere:uid:{from_uid}:followings {to_uid} {timestamp}
ZADD sequere:uid:{from_uid}:followings{to_identifier} {to_uid} {timestamp}

Retrieve the followers uids

ZRANGEBYSCORE sequere:uid:{uid}:followers -inf +inf

Retrieve the followings uids

ZRANGEBYSCORE sequere:uid:{uid}:followings =inf +inf

With this implementation you can retrieve your followers ordered

ZREVRANGEBYSCORE sequere:uid:{uid}:followers +inf -inf

Timeline

The timeline engine is directly based on sequere resources system.

Concept

A Timeline is basically a list of Action.

An Action is represented by:

  • actor which is the actor of the action
  • verb which is the action name
  • target which is the target of the action (not required)
  • date which is the date when the action has been done

Installation

You have to follow installation instructions of sequere first before installing sequere.contrib.timeline.

Add sequere.contrib.timeline to your INSTALLED_APPS

INSTALLED_APPS = (
    'sequere.contrib.timeline',
)

sequere.contrib.timeline requires celery to work properly, so you will have to install it.

Usage

You have to register your actions based on your resources, for example

# sequere_registry.py

from .models import Project, User

from sequere.contrib.timeline import Action
from sequere import register
from sequere.base import ModelBase


# actions
class JoinAction(Action):
    verb = 'join'


class LikeAction(Action):
    verb = 'like'

# resources
class ProjectSequere(ModelBase):
    identifier = 'project'

class UserSequere(ModelBase):
    identifier = 'user'

    actions = (JoinAction, LikeAction, )

# register resources
register(User, UserSequere)
register(Project, ProjectSequere)

Now we have registered our actions we can play with the timeline API

>>> from sequere.models import (follow, unfollow)

>>> from sequere.contrib.timeline import Timeline

>>> from myapp.models import User, Project

>>> from myapp.sequere_registry import JoinAction, LikeAction

>>> thoas = User.objects.create(username='thoas')

>>> project = Project.objects.create(name='La classe americaine')

>>> timeline = Timeline(thoas) # create a timeline

>>> timeline.save(JoinAction(actor=thoas)) # save the action in the timeline

>>> timeline.get_private()
[<JoinAction: thoas join>]

>>>: timeline.get_public()
[<JoinAction: thoas join>]

When the resource is the actor of its own action then we push the action both in private and public timelines.

Now we have to test the system with the follow process

>>> newbie = User.objects.create(username='newbie')

>>> follow(newbie, thoas) # newbie is now following thoas

>>> Timeline(newbie).get_private() # thoas actions now appear in the private timeline of newbie
[<JoinAction: thoas join>]

>>> Timeline(newbie).get_public()
[]

When A is following B we copy actions of B in the private timeline of A, celery is needed to handle these asynchronous tasks.

>>> unfollow(newbie, thoas)

>>> Timeline(newbie).get_private()
[]

When A is unfollowing B we delete the actions of B in the private timeline of A.

As you may have noticed the JoinAction is an action which does not need a target, some actions will need target, sequere.contrib.timeline provides a quick way to query actions for a specific target.

>>> timeline = Timeline(thoas)

>>> timeline.save(LikeAction(actor=thoas, target=project))

>>> timeline.get_private()
[<JoinAction: thoas join>, <LikeAction: thoas like La classe americaine>]

>>> timeline.get_private(target=Project) # only retrieve actions with Project resource as target
[<LikeAction: thoas like La classe americaine>]

>>> timeline.get_private(target='project') # only retrieve actions with 'project' identifier as target
[<LikeAction: thoas like La classe americaine>]

Configuration

SEQUERE_BACKEND

The backend used to store follows

Defaults to sequere.backends.database.DatabaseBackend.

SEQUERE_BACKEND_OPTIONS

A dictionary of parameters to pass to the backend, for example with redis:

SEQUERE_BACKEND = 'sequere.backends.redis.RedisBackend'
SEQUERE_BACKEND_OPTIONS = {
    'client_class': 'myproject.myapp.mockup.Connection',
    'options': {
        'host': 'localhost',
        'port': 6379,
        'db': 0,
    },
    'prefix': 'prefix-used:'
}

The (optional) prefix to be used for the key when storing in the Redis database.

Defaults to sequere:.

SEQUERE_TIMELINE_BACKEND

The backend used to store follows

Defaults to sequere.contrib.timeline.redis.RedisBackend.

SEQUERE_TIMELINE_BACKEND_OPTIONS

A dictionary of parameters to pass to the backend, for example with redis:

SEQUERE_TIMELINE_BACKEND = 'sequere.contrib.timeline.redis.RedisBackend'
SEQUERE_TIMELINE_BACKEND_OPTIONS = {
    'client_class': 'myproject.myapp.mockup.Connection',
    'options': {
        'host': 'localhost',
        'port': 6379,
        'db': 0,
    },
    'prefix': 'prefix-used:'
}

The (optional) prefix to be used for the key when storing in the Redis database.

Defaults to sequere:timeline:.

Resources