diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-05-02 04:24:09 -0400 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-05-02 04:24:09 -0400 |
commit | bb33c7a97208626ad3d3696aa8aad5a9212bfb5f (patch) | |
tree | 6c4940f10aa551779010c6220a533cf0d9bd3264 | |
parent | 641623332da36d70f43854981935b1e7b0360069 (diff) | |
download | askbot-bb33c7a97208626ad3d3696aa8aad5a9212bfb5f.tar.gz askbot-bb33c7a97208626ad3d3696aa8aad5a9212bfb5f.tar.bz2 askbot-bb33c7a97208626ad3d3696aa8aad5a9212bfb5f.zip |
created a simple "follower" app that allows users to follow any model
-rw-r--r-- | askbot/models/__init__.py | 19 | ||||
-rw-r--r-- | askbot/tests/__init__.py | 1 | ||||
-rw-r--r-- | askbot/tests/follow_tests.py | 23 | ||||
-rw-r--r-- | follower/TODO.rst | 3 | ||||
-rw-r--r-- | follower/__init__.py | 147 | ||||
-rw-r--r-- | follower/models.py | 3 | ||||
-rw-r--r-- | follower/tests.py | 39 | ||||
-rw-r--r-- | follower/views.py | 1 |
8 files changed, 227 insertions, 9 deletions
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 59967a88..a260cd11 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -15,6 +15,7 @@ from django.conf import settings as django_settings from django.contrib.contenttypes.models import ContentType from django.core import exceptions as django_exceptions from django_countries.fields import CountryField +import follow import askbot from askbot import exceptions as askbot_exceptions from askbot import const @@ -1719,6 +1720,16 @@ def user_follow_question(self, question = None): if self not in question.followed_by.all(): question.followed_by.add(self) +def user_follow_user(self, other_user): + """call when ``self`` user wants to follow ``other_user`` + """ + follow.follow(self, other_user) + +def user_unfollow_user(self, other_user): + """call when ``self`` user wants to unfollow ``other_user`` + """ + follow.unfollow(self, other_user) + def upvote(self, post, timestamp=None, cancel=False): return _process_vote( self,post, @@ -1885,6 +1896,8 @@ User.add_to_class('follow_question', user_follow_question) User.add_to_class('unfollow_question', user_unfollow_question) User.add_to_class('mark_tags', user_mark_tags) User.add_to_class('is_following', user_is_following) +User.add_to_class('follow_user', user_follow_user) +User.add_to_class('unfollow_user', user_unfollow_user) User.add_to_class('decrement_response_count', user_decrement_response_count) User.add_to_class('increment_response_count', user_increment_response_count) User.add_to_class('clean_response_counts', user_clean_response_counts) @@ -2448,9 +2461,9 @@ signals.post_updated.connect( signals.site_visited.connect(record_user_visit) #set up a possibility for the users to follow others -from follow import util as follow_util -follow_util.register(User, m2m = True) -follow_util.register(Question, m2m = True) +import follower +follower.register(User) +follower.register(Question) __all__ = [ 'signals', diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py index 1cb8d37b..a2d0bdc0 100644 --- a/askbot/tests/__init__.py +++ b/askbot/tests/__init__.py @@ -8,3 +8,4 @@ from askbot.tests.badge_tests import * from askbot.tests.management_command_tests import * from askbot.tests.search_state_tests import * from askbot.tests.form_tests import * +from askbot.tests.follow_tests import * diff --git a/askbot/tests/follow_tests.py b/askbot/tests/follow_tests.py index b6eb555a..460d7a01 100644 --- a/askbot/tests/follow_tests.py +++ b/askbot/tests/follow_tests.py @@ -7,17 +7,28 @@ class UserFollowTests(AskbotTestCase): self.u2 = self.create_user('user2') self.u3 = self.create_user('user3') - def test_user_follow(self): + def test_multiple_follow(self): - self.u1.follow(self.u2) - self.u1.follow(self.u3) - self.u2.follow(self.u1) + self.u1.follow_user(self.u2) + self.u1.follow_user(self.u3) + self.u2.follow_user(self.u1) self.assertEquals( - set(self.u1.followers()), + set(self.u1.get_followers()), set([self.u2]) ) self.assertEquals( - set(self.u1. + set(self.u2.get_followers()), + set([self.u1]) ) + + self.assertEquals( + set(self.u1.get_followed_users()), + set([self.u2, self.u3]) + ) + + def test_unfollow(self): + self.u1.follow_user(self.u2) + self.u1.unfollow_user(self.u2) + self.assertEquals(self.u1.get_followed_users().count(), 0) diff --git a/follower/TODO.rst b/follower/TODO.rst new file mode 100644 index 00000000..b71b92ae --- /dev/null +++ b/follower/TODO.rst @@ -0,0 +1,3 @@ +* create tests & test runner script +* create doc pages +* publish on pypi diff --git a/follower/__init__.py b/follower/__init__.py new file mode 100644 index 00000000..20d8fee9 --- /dev/null +++ b/follower/__init__.py @@ -0,0 +1,147 @@ +"""A Django App allowing :class:`~django.contrib.auth.models.User` to +follow instances of any other django models, including other users + +To use this module: +* add "follower" to the ``INSTALLED_APPS`` in your ``settings.py`` +* in your app's ``models.py`` add: + + import follower + follower.register(Thing) + +* run ``python manage.py syncdb`` +* then anywhere in your code you can do the following: + + user.follow(some_thing_instance) + user.unfollow(some_thing_instance) + + user.get_followed_things() #note that "things" is from the name of class Thing + some_thing.get_followers() + +Copyright 2011 Evgeny Fadeev evgeny.fadeev@gmail.com +""" +from django.contrib.auth.models import User +from django.db.models.fields.related import ManyToManyField, ForeignKey + +REGISTRY = [] + +def get_model_name(model): + return model._meta.module_name + + +def get_bridge_class_name(model): + return 'Follow' + get_model_name(model) + + +def get_bridge_model_for_object(obj): + """returns bridge model used to follow items + like the ``obj`` + """ + bridge_model_name = get_bridge_class_name(obj.__class__) + from django.db import models as django_models + return django_models.get_model('follower', bridge_model_name) + + +def get_object_followers(obj): + """returns query set of users following the object""" + bridge_lookup_field = get_bridge_class_name(obj.__class__).lower() + obj_model_name = get_model_name(obj.__class__) + filter_criterion = 'followed_' + obj_model_name + '_records__followed' + filter = {filter_criterion: obj} + return User.objects.filter(**filter) + + +def make_followed_objects_getter(model): + """returns query set of objects of a class ``model`` + that are followed by a user""" + + #something like followX_set__user + def followed_objects_getter(user): + filter = {'follower_records__follower': user} + return model.objects.filter(**filter) + + return followed_objects_getter + + +def make_follow_method(model): + """returns a method that adds a FollowX record + for an object + """ + def follow_method(user, obj): + """returns ``True`` if follow operation created a new record""" + bridge_model = get_bridge_model_for_object(obj) + bridge, created = bridge_model.objects.get_or_create(follower = user, followed = obj) + return created + return follow_method + + +def make_unfollow_method(model): + """returns a method that allows to unfollow an item + """ + def unfollow_method(user, obj): + """attempts to find an item and delete it, no + exstence checking + """ + bridge_model = get_bridge_model_for_object(obj) + objects = bridge_model.objects.get(follower = user, followed = obj) + objects.delete() + return unfollow_method + + +def register(model): + """returns model class that connects + User with the followed object + + ``model`` - is the model class to follow + + The ``model`` class gets new method - ``get_followers`` + and the User class - a method - ``get_followed_Xs``, where + the ``X`` is the name of the model + + Note, that proper pluralization of the model name is not supported, + just "s" is added + """ + from follower import models as follower_models + from django.db import models as django_models + + model_name = get_model_name(model) + if model in REGISTRY: + return + + #1) - create a new class FollowX + class Meta(object): + app_label = 'follower' + + fields = { + 'follower': ForeignKey( + User, + related_name = 'followed_' + model_name + '_records' + ), + 'followed': ForeignKey( + model, + related_name = 'follower_records' + ), + '__module__': follower_models.__name__, + 'Meta': Meta + } + + + #create the bridge model class + bridge_class_name = get_bridge_class_name(model) + bridge_model = type(bridge_class_name, (django_models.Model,), fields) + setattr(follower_models, bridge_class_name, bridge_model) + + #2) patch ``model`` with method ``get_followers()`` + model.add_to_class('get_followers', get_object_followers) + + #3) patch ``User`` with method ``get_followed_Xs`` + method_name = 'get_followed_' + model_name + 's' + getter_method = make_followed_objects_getter(model) + User.add_to_class(method_name, getter_method) + + #4) patch ``User`` with method ``follow_X`` + follow_method = make_follow_method(model) + User.add_to_class('follow_' + model_name, follow_method) + + #5) patch ``User`` with method ``unfollow_X`` + unfollow_method = make_unfollow_method(model) + User.add_to_class('unfollow_' + model_name, unfollow_method) diff --git a/follower/models.py b/follower/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/follower/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/follower/tests.py b/follower/tests.py new file mode 100644 index 00000000..806b2f2b --- /dev/null +++ b/follower/tests.py @@ -0,0 +1,39 @@ +""" +Test cases for the follower module +""" +from django.db import models, transaction +from django.contrib.contenttypes.management import update_contenttypes +from django.core.management.commands import syncdb +from django.test import TestCase + +import follower + +class SomeJunk(models.Model): + yeah = models.BooleanField(default = True) + +class SomeTrash(models.Model): + yeah = models.BooleanField(default = True) + +class FollowerTests(TestCase): + """idea taken from Dan Rosemans blog + http://blog.roseman.org.uk/2010/04/13/temporary-models-django/ + create a temp model in setUp(), run tests on it, then destroy it in dearDown() + """ + + def setUp(self): + models.register_models('followertests', SomeJunk) + models.signals.post_syncdb.disconnect(update_contenttypes) + + def test_register_fk_follow(self): + follower.register(SomeJunk) + #call_command('syncdb') + cmd = syncdb.Command()#south messes up here - call django's command directly + cmd.execute() + transaction.commit() + #test that table `follower_followsomejunk` exists + model = models.get_model('follower', 'FollowSomeJunk') + self.assertEquals(model.objects.count(), 0) + + #def tearDown(self): + # cursor = connection.cursor() + # cursor.execute('DROP TABLE `followertests_somejunk`') diff --git a/follower/views.py b/follower/views.py new file mode 100644 index 00000000..60f00ef0 --- /dev/null +++ b/follower/views.py @@ -0,0 +1 @@ +# Create your views here. |