A Django application to implement a follow system and a timeline using multiple backends (db, redis, etc.)
Project description
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
Either check out the package from GitHub or it pull from a release via PyPI
pip install django-sequere
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
haplocheirus: a Redis backed storage engine for timelines written in Scala
Case study from Redis documentation: write a twitter clone
Amico: relationships backed by Redis
django-constance: a multi-backends settings management application