Package: django-sequere
A Django application to implement a follow system and a timeline using multiple backends (db, redis, etc.).The timeline engine can be found insequere.contrib.timeline.InstallationEither check out the package fromGitHubor it pull from a release via PyPIpip install django-sequereAddsequereto yourINSTALLED_APPSINSTALLED_APPS=('sequere',)UsageIn Sequere any resources can follow any resources and vice versa.Let’s say you have two models:# models.pyfromdjango.dbimportmodelsclassUser(models.Model):username=models.CharField(max_length=150)classProject(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.pyfrom.modelsimportProject,Userimportsequeresequere.register(User)sequere.register(Project)Sequere uses the same concepts asDjango 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:>>>fromsequere.modelsimport(follow,unfollow,get_followings_count,is_following,get_followers_count,get_followers,get_followings)>>>frommyapp.modelsimportUser,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:Laclasseamericaine,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, useSequereMixin# models.pyfromdjango.dbimportmodelsfromsequere.mixinimportSequereMixinclassUser(SequereMixin,models.Model):username=models.Charfield(max_length=150)classProject(SequereMixin,models.Model):name=models.Charfield(max_length=150)Now you can use calls directly from the instance:>>>frommyapp.modelsimportUser,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:Laclasseamericaine,datetime.datetime(2013,10,25,4,41,31,612067))]So much fun!Backendssequere.backends.database.DatabaseBackendA database backend to store your follows in you favorite database using the Django’s
ORM.To use this backend you will have to addsequere.backends.databaseto yourINSTALLED_APPSINSTALLED_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.pyfrommyapp.modelsimportProjectfromsequere.baseimportModelBaseimportsequereclassProjectSequere(ModelBase):identifier='projet'# the french way ;)sequere.registry(Project,ProjectSequere)sequere.backends.redis.RedisBackendWe are using exclusivelySorted Setsin this Redis implementation.Create a uid for a new resourceINCR sequere:global:uid    =>  1
SET sequere:uid:{identifier}:{id} 1
HMSET sequere:uid::{id} identifier {identifier} object_id {id}Store followers countINCR sequere:uid:{to_uid}:followers:count => 1
INCR sequere:uid:{to_uid}:followers:{from_identifier}:count => 1Store followings countINCR sequere:uid:{from_uid}:followings:count => 1
INCR sequere:uid:{from_uid}:followings:{to_identifier}:count => 1Add a new followerZADD sequere:uid:{to_uid}:followers {from_uid} {timestamp}
ZADD sequere:uid:{to_uid}:followers:{from_identifier} {from_uid} {timestamp}Add a new followingZADD sequere:uid:{from_uid}:followings {to_uid} {timestamp}
ZADD sequere:uid:{from_uid}:followings{to_identifier} {to_uid} {timestamp}Retrieve the followers uidsZRANGEBYSCORE sequere:uid:{uid}:followers -inf +infRetrieve the followings uidsZRANGEBYSCORE sequere:uid:{uid}:followings =inf +infWith this implementation you can retrieve your followers orderedZREVRANGEBYSCORE sequere:uid:{uid}:followers +inf -infTimelineThe timeline engine is directly based onsequereresources system.ConceptATimelineis basically a list ofAction.AnActionis represented by:actorwhich is the actor of the actionverbwhich is the action nametargetwhich is the target of the action (not required)datewhich is the date when the action has been doneInstallationYou have to follow installation instructions ofsequerefirst before installingsequere.contrib.timeline.Addsequere.contrib.timelineto yourINSTALLED_APPSINSTALLED_APPS=('sequere.contrib.timeline',)sequere.contrib.timelinerequiresceleryto work properly,
so you will have to install it.UsageYou have to register your actions based on your resources, for example# sequere_registry.pyfrom.modelsimportProject,Userfromsequere.contrib.timelineimportActionfromsequereimportregisterfromsequere.baseimportModelBase# actionsclassJoinAction(Action):verb='join'classLikeAction(Action):verb='like'# resourcesclassProjectSequere(ModelBase):identifier='project'classUserSequere(ModelBase):identifier='user'actions=(JoinAction,LikeAction,)# register resourcesregister(User,UserSequere)register(Project,ProjectSequere)Now we have registered our actions we can play with the timeline API>>>fromsequere.modelsimport(follow,unfollow)>>>fromsequere.contrib.timelineimportTimeline>>>frommyapp.modelsimportUser,Project>>>frommyapp.sequere_registryimportJoinAction,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:thoasjoin>]>>>:timeline.get_public()[<JoinAction:thoasjoin>]When the resource is the actor of its own action then we push the action both
inprivateandpublictimelines.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:thoasjoin>]>>>Timeline(newbie).get_public()[]WhenAis followingBwe copy actions ofBin the private
timeline ofA,celeryis needed to handle these asynchronous tasks.>>>unfollow(newbie,thoas)>>>Timeline(newbie).get_private()[]WhenAis unfollowingBwe delete the actions ofBin the private
timeline ofA.As you may have noticed theJoinActionis an action which does not need a target,
some actions will need target,sequere.contrib.timelineprovides a quick way
to query actions for a specific target.>>>timeline=Timeline(thoas)>>>timeline.save(LikeAction(actor=thoas,target=project))>>>timeline.get_private()[<JoinAction:thoasjoin>,<LikeAction:thoaslikeLaclasseamericaine>]>>>timeline.get_private(target=Project)# only retrieve actions with Project resource as target[<LikeAction:thoaslikeLaclasseamericaine>]>>>timeline.get_private(target='project')# only retrieve actions with 'project' identifier as target[<LikeAction:thoaslikeLaclasseamericaine>]ConfigurationSEQUERE_BACKENDThe backend used to store followsDefaults tosequere.backends.database.DatabaseBackend.SEQUERE_BACKEND_OPTIONSA 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 tosequere:.SEQUERE_TIMELINE_BACKENDThe backend used to store followsDefaults tosequere.contrib.timeline.redis.RedisBackend.SEQUERE_TIMELINE_BACKEND_OPTIONSA 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 tosequere:timeline:.Resourceshaplocheirus: a Redis backed storage engine for timelines written in ScalaCase study from Redis documentation: write a twitter cloneAmico: relationships backed by Redisdjango-constance: a multi-backends settings management application