├── __init__.py ├── rest_social ├── migrations │ ├── __init__.py │ ├── 0004_auto_20140926_2101.py │ ├── 0005_comment_related_tags.py │ ├── 0002_auto_20140913_2053.py │ ├── 0006_auto_20141016_1723.py │ ├── 0003_auto_20140926_0403.py │ └── 0001_initial.py ├── __init__.py ├── tests │ ├── __init__.py │ └── tests.py ├── urls.py ├── utils.py ├── serializers.py ├── pipeline.py ├── views.py └── models.py ├── .gitignore └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'baylee' 2 | -------------------------------------------------------------------------------- /rest_social/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rest_social/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'baylee' 2 | -------------------------------------------------------------------------------- /rest_social/tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'winnietong' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.db 4 | .DS_Store 5 | .coverage 6 | local_settings.py 7 | /static 8 | .idea/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rest-social 2 | ======================= 3 | 4 | Models and APIs for social functionality. Includes: 5 | - Hashtags 6 | - Likes 7 | - Follows (of users, or of other objects, such as tags) 8 | - Comments 9 | - @mentions 10 | - Flagging / Reporting -------------------------------------------------------------------------------- /rest_social/migrations/0004_auto_20140926_2101.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rest_social', '0003_auto_20140926_0403'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterUniqueTogether( 15 | name='share', 16 | unique_together=set([('user', 'content_type', 'object_id', 'id')]), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /rest_social/migrations/0005_comment_related_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rest_social', '0004_auto_20140926_2101'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='comment', 16 | name='related_tags', 17 | field=models.ManyToManyField(to='rest_social.Tag', null=True, blank=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /rest_social/migrations/0002_auto_20140913_2053.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | def add_social_providers(apps, schema_editor): 7 | SocialProvider = apps.get_model("rest_social", "SocialProvider") 8 | SocialProvider.objects.create(name="facebook") 9 | SocialProvider.objects.create(name="twitter") 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('rest_social', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.RunPython(add_social_providers), 19 | ] 20 | -------------------------------------------------------------------------------- /rest_social/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url, include 2 | from rest_social.rest_social import views 3 | from rest_framework import routers 4 | from rest_social.rest_social.views import TagViewSet 5 | 6 | 7 | router = routers.DefaultRouter() 8 | 9 | # These views will normally be overwritten by notification-related views to account for notifications 10 | # They can still be used as-is by registering with the main project router 11 | # router.register(r'follows', FollowViewSet, base_name='follows') 12 | # router.register(r'likes', LikeViewSet, base_name='likes') 13 | # router.register(r'shares', ShareViewSet, base_name='shares') 14 | # router.register(r'comments', CommentViewSet, base_name='comments') 15 | 16 | # These views do not expect app-specific notifications 17 | router.register(r'tags', TagViewSet, base_name='tags') 18 | 19 | urlpatterns = patterns('', 20 | url(r'^', include(router.urls)), 21 | url(r'^flag/$', views.FlagView.as_view(), name="flag"), 22 | url(r'^social_sign_up/$', views.SocialSignUp.as_view(), name="social_sign_up"), 23 | url(r'^social_friends/$', views.SocialFriends.as_view(), name="social_friends"), 24 | ) 25 | -------------------------------------------------------------------------------- /rest_social/migrations/0006_auto_20141016_1723.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rest_social', '0005_comment_related_tags'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='airshiptoken', 16 | name='user', 17 | ), 18 | migrations.DeleteModel( 19 | name='AirshipToken', 20 | ), 21 | migrations.RemoveField( 22 | model_name='notification', 23 | name='content_type', 24 | ), 25 | migrations.RemoveField( 26 | model_name='notification', 27 | name='reporter', 28 | ), 29 | migrations.RemoveField( 30 | model_name='notification', 31 | name='user', 32 | ), 33 | migrations.DeleteModel( 34 | name='Notification', 35 | ), 36 | migrations.AlterUniqueTogether( 37 | name='notificationsetting', 38 | unique_together=None, 39 | ), 40 | migrations.RemoveField( 41 | model_name='notificationsetting', 42 | name='user', 43 | ), 44 | migrations.DeleteModel( 45 | name='NotificationSetting', 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /rest_social/migrations/0003_auto_20140926_0403.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('contenttypes', '0001_initial'), 13 | ('rest_social', '0002_auto_20140913_2053'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Share', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 22 | ('object_id', models.PositiveIntegerField()), 23 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 24 | ('shared_with', models.ManyToManyField(related_name=b'shared_with', to=settings.AUTH_USER_MODEL)), 25 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 26 | ], 27 | options={ 28 | }, 29 | bases=(models.Model,), 30 | ), 31 | migrations.AlterUniqueTogether( 32 | name='share', 33 | unique_together=set([('user', 'content_type', 'object_id')]), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /rest_social/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from django.apps import apps 3 | import requests 4 | import urllib 5 | import urllib2 6 | from celery.task import task 7 | from django.conf import settings 8 | from twython import Twython 9 | 10 | 11 | def post_to_facebook(app_access_token, user_social_auth, message, link): 12 | url = "https://graph.facebook.com/%s/feed" % user_social_auth.uid 13 | 14 | params = { 15 | 'access_token': app_access_token, 16 | 'message': message, 17 | 'link': link 18 | } 19 | 20 | req = urllib2.Request(url, urllib.urlencode(params)) 21 | urllib2.urlopen(req) 22 | 23 | 24 | def post_to_facebook_og(app_access_token, user_social_auth, obj): 25 | og_info = obj.facebook_og_info() 26 | 27 | url = "https://graph.facebook.com/{0}/{1}:{2}".format( 28 | user_social_auth.uid, 29 | settings.FACEBOOK_OG_NAMESPACE, 30 | og_info['action'], 31 | ) 32 | 33 | params = { 34 | '{0}'.format(og_info['object']): '{0}'.format(og_info['url']), 35 | 'access_token': app_access_token, 36 | } 37 | 38 | requests.post(url, params=params) 39 | 40 | 41 | @task 42 | def post_social_media(user_social_auth, social_obj): 43 | message = social_obj.create_social_message(user_social_auth.provider) 44 | link = social_obj.url() 45 | 46 | if user_social_auth.provider == 'facebook': 47 | if settings.USE_FACEBOOK_OG: 48 | post_to_facebook_og(settings.SOCIAL_AUTH_FACEBOOK_APP_TOKEN, user_social_auth, social_obj) 49 | else: 50 | post_to_facebook(settings.SOCIAL_AUTH_FACEBOOK_APP_TOKEN, user_social_auth, message, link) 51 | elif user_social_auth.provider == 'twitter': 52 | twitter = Twython( 53 | app_key=settings.SOCIAL_AUTH_TWITTER_KEY, 54 | app_secret=settings.SOCIAL_AUTH_TWITTER_SECRET, 55 | oauth_token=user_social_auth.tokens['oauth_token'], 56 | oauth_token_secret=user_social_auth.tokens['oauth_token_secret'] 57 | ) 58 | 59 | full_message_url = "{0} {1}".format(message, link) 60 | 61 | # 140 characters minus the length of the link minus the space minus 3 characters for the ellipsis 62 | message_trunc = 140 - len(link) - 1 - 3 63 | 64 | # Truncate the message if the message + url is over 140 65 | if len(full_message_url) > 140: 66 | safe_message = "{0}... {1}".format(message[:message_trunc], link) 67 | else: 68 | safe_message = full_message_url 69 | 70 | twitter.update_status(status=safe_message, wrap_links=True) 71 | 72 | 73 | def get_social_model(): 74 | """ 75 | Returns the social model that is active in this project. 76 | """ 77 | try: 78 | return apps.get_model(settings.SOCIAL_MODEL) 79 | except ValueError: 80 | raise ImproperlyConfigured("SOCIAL_MODEL must be of the form 'app_label.model_name'") 81 | except LookupError: 82 | raise ImproperlyConfigured( 83 | "SOCIAL_MODEL refers to model '%s' that has not been installed" % settings.SOCIAL_MODEL) 84 | -------------------------------------------------------------------------------- /rest_social/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | from rest_framework.pagination import PaginationSerializer 4 | from rest_social.rest_social.models import Tag, Comment, Follow, Flag, Share, Like 5 | from rest_user.rest_user.serializers import UserSerializer, LoginSerializer 6 | 7 | __author__ = 'baylee' 8 | 9 | 10 | User = get_user_model() 11 | 12 | 13 | class TagSerializer(serializers.ModelSerializer): 14 | class Meta: 15 | model = Tag 16 | fields = ('name', 'id') 17 | 18 | 19 | class CommentSerializer(serializers.ModelSerializer): 20 | class Meta: 21 | model = Comment 22 | exclude = ('related_tags',) 23 | 24 | def __init__(self, *args, **kwargs): 25 | """ 26 | The `user` field is added here to help with recursive import issues mentioned in rest_user.serializers 27 | """ 28 | super(CommentSerializer, self).__init__(*args, **kwargs) 29 | self.fields["user"] = UserSerializer(read_only=True) 30 | 31 | 32 | class FollowSerializer(serializers.ModelSerializer): 33 | follower = UserSerializer(read_only=True, source="user") 34 | following = serializers.SerializerMethodField('get_user_follow') 35 | 36 | class Meta: 37 | model = Follow 38 | exclude = ('user',) 39 | 40 | def get_user_follow(self, obj): 41 | user = User.objects.get(pk=obj.object_id) 42 | serializer = UserSerializer(user) 43 | return serializer.data 44 | 45 | 46 | class ShareSerializer(serializers.ModelSerializer): 47 | user = UserSerializer(read_only=True) 48 | 49 | class Meta: 50 | model = Share 51 | 52 | 53 | class LikeSerializer(serializers.ModelSerializer): 54 | user = UserSerializer(read_only=True) 55 | 56 | class Meta: 57 | model = Like 58 | 59 | 60 | class PaginatedFollowSerializer(PaginationSerializer): 61 | class Meta: 62 | object_serializer_class = FollowSerializer 63 | 64 | 65 | class FlagSerializer(serializers.ModelSerializer): 66 | user = UserSerializer(read_only=True) 67 | 68 | class Meta: 69 | model = Flag 70 | 71 | 72 | class FollowPaginationSerializer(PaginationSerializer): 73 | def __init__(self, *args, **kwargs): 74 | """ 75 | Overrode BasePaginationSerializer init to set object serializer as Follow Serializer. 76 | """ 77 | super(FollowPaginationSerializer, self).__init__(*args, **kwargs) 78 | results_field = self.results_field 79 | object_serializer = FollowSerializer 80 | if 'context' in kwargs: 81 | context_kwarg = {'context': kwargs['context']} 82 | else: 83 | context_kwarg = {} 84 | 85 | self.fields[results_field] = object_serializer(source='object_list', 86 | many=True, 87 | **context_kwarg) 88 | 89 | 90 | class SocialSignUpSerializer(LoginSerializer): 91 | class Meta(LoginSerializer.Meta): 92 | fields = ('email', 'username', 'client_id', 'client_secret') 93 | read_only_fields = ('username',) 94 | -------------------------------------------------------------------------------- /rest_social/pipeline.py: -------------------------------------------------------------------------------- 1 | from social.apps.django_app.default.models import UserSocialAuth 2 | import urllib 3 | from urllib2 import URLError 4 | from django.core.files import File 5 | from manticore_django.manticore_django.utils import retry_cloudfiles 6 | 7 | 8 | def social_auth_user(strategy, uid, user=None, *args, **kwargs): 9 | """ 10 | Allows user to create a new account and associate a social account, 11 | even if that social account is already connected to a different 12 | user. It effectively 'steals' the social association from the 13 | existing user. This can be a useful option during the testing phase 14 | of a project. 15 | 16 | Return UserSocialAuth account for backend/uid pair or None if it 17 | doesn't exist. 18 | 19 | Delete UserSocialAuth if UserSocialAuth entry belongs to another 20 | user. 21 | """ 22 | social = UserSocialAuth.get_social_auth(kwargs['backend'].name, uid) 23 | if social: 24 | if user and social.user != user: 25 | # Delete UserSocialAuth pairing so this account can now connect 26 | social.delete() 27 | social = None 28 | elif not user: 29 | user = social.user 30 | return {'social': social, 31 | 'user': user, 32 | 'is_new': user is None, 33 | 'new_association': False} 34 | 35 | 36 | def save_extra_data(strategy, details, response, uid, user, social, *args, **kwargs): 37 | """Attempt to get extra information from facebook about the User""" 38 | 39 | if user is None: 40 | return 41 | 42 | if kwargs['backend'].name == "facebook": 43 | 44 | if 'email' in response: 45 | user.email = response['email'] 46 | 47 | # TODO: better placement of Location model. What do we do with Twitter or other locations? 48 | # if 'location' in response: 49 | # if not user.location: 50 | # Location.objects.create(name=response["location"]["name"], facebook_id=response["location"]["id"]) 51 | # else: 52 | # location = user.location 53 | # location.name = response['location']['name'] 54 | # location.facebook_id = response['location']['id'] 55 | 56 | if 'bio' in response: 57 | user.about = response['bio'] 58 | 59 | user.save() 60 | 61 | 62 | def get_profile_image(strategy, details, response, uid, user, social, is_new=False, *args, **kwargs): 63 | """Attempt to get a profile image for the User""" 64 | 65 | # If we don't have a user then just return 66 | if user is None: 67 | return 68 | 69 | # Save photo from FB 70 | if kwargs['backend'].name == "facebook": 71 | try: 72 | image_url = "https://graph.facebook.com/%s/picture?type=large" % uid 73 | result = urllib.urlretrieve(image_url) 74 | 75 | def save_image(user, uid, result): 76 | user.original_photo.save("%s.jpg" % uid, File(open(result[0]))) 77 | user.save(update_fields=['original_photo']) 78 | 79 | retry_cloudfiles(save_image, user, uid, result) 80 | except URLError: 81 | pass 82 | elif kwargs['backend'].name == "twitter" and social: 83 | try: 84 | # Get profile image to save 85 | if response['profile_image_url'] != '': 86 | image_result = urllib.urlretrieve(response['profile_image_url']) 87 | 88 | def save_image(user, uid, image_result): 89 | user.original_photo.save("%s.jpg" % uid, File(open(image_result[0]))) 90 | user.save(update_fields=['original_photo']) 91 | 92 | retry_cloudfiles(save_image, user, uid, image_result) 93 | except URLError: 94 | pass 95 | -------------------------------------------------------------------------------- /rest_social/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('contenttypes', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='AirshipToken', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 21 | ('token', models.CharField(max_length=100)), 22 | ('expired', models.BooleanField(default=False)), 23 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'abstract': False, 27 | }, 28 | bases=(models.Model,), 29 | ), 30 | migrations.CreateModel( 31 | name='Comment', 32 | fields=[ 33 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 34 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 35 | ('object_id', models.PositiveIntegerField(db_index=True)), 36 | ('description', models.CharField(max_length=140)), 37 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 38 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 39 | ], 40 | options={ 41 | 'ordering': ['created'], 42 | }, 43 | bases=(models.Model,), 44 | ), 45 | migrations.CreateModel( 46 | name='Flag', 47 | fields=[ 48 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 49 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 50 | ('object_id', models.PositiveIntegerField()), 51 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 52 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 53 | ], 54 | options={ 55 | }, 56 | bases=(models.Model,), 57 | ), 58 | migrations.CreateModel( 59 | name='Follow', 60 | fields=[ 61 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 62 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 63 | ('object_id', models.PositiveIntegerField(db_index=True)), 64 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 65 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 66 | ], 67 | options={ 68 | 'ordering': ['created'], 69 | }, 70 | bases=(models.Model,), 71 | ), 72 | migrations.CreateModel( 73 | name='FriendAction', 74 | fields=[ 75 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 76 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 77 | ('action_type', models.PositiveSmallIntegerField(choices=[(0, 'is following'), (1, 'favorited a post'), (2, 'replied to a post')])), 78 | ('object_id', models.PositiveIntegerField(db_index=True)), 79 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 80 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 81 | ], 82 | options={ 83 | 'ordering': ['-created'], 84 | }, 85 | bases=(models.Model,), 86 | ), 87 | migrations.CreateModel( 88 | name='Like', 89 | fields=[ 90 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 91 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 92 | ('object_id', models.PositiveIntegerField(db_index=True)), 93 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 94 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 95 | ], 96 | options={ 97 | }, 98 | bases=(models.Model,), 99 | ), 100 | migrations.CreateModel( 101 | name='Notification', 102 | fields=[ 103 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 104 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 105 | ('notification_type', models.PositiveSmallIntegerField(choices=[(0, 'started following you'), (1, 'favorited your post'), (2, 'replied to your post')])), 106 | ('object_id', models.PositiveIntegerField(db_index=True)), 107 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 108 | ('reporter', models.ForeignKey(related_name=b'reporter', blank=True, to=settings.AUTH_USER_MODEL, null=True)), 109 | ('user', models.ForeignKey(related_name=b'receiver', to=settings.AUTH_USER_MODEL, null=True)), 110 | ], 111 | options={ 112 | 'ordering': ['-created'], 113 | }, 114 | bases=(models.Model,), 115 | ), 116 | migrations.CreateModel( 117 | name='NotificationSetting', 118 | fields=[ 119 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 120 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 121 | ('notification_type', models.PositiveSmallIntegerField(choices=[(0, 'started following you'), (1, 'favorited your post'), (2, 'replied to your post')])), 122 | ('allow', models.BooleanField(default=True)), 123 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 124 | ], 125 | options={ 126 | }, 127 | bases=(models.Model,), 128 | ), 129 | migrations.CreateModel( 130 | name='SocialProvider', 131 | fields=[ 132 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 133 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 134 | ('name', models.CharField(max_length=20)), 135 | ], 136 | options={ 137 | 'abstract': False, 138 | }, 139 | bases=(models.Model,), 140 | ), 141 | migrations.CreateModel( 142 | name='Tag', 143 | fields=[ 144 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 145 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 146 | ('name', models.CharField(unique=True, max_length=75)), 147 | ], 148 | options={ 149 | 'abstract': False, 150 | }, 151 | bases=(models.Model,), 152 | ), 153 | migrations.AlterUniqueTogether( 154 | name='notificationsetting', 155 | unique_together=set([('notification_type', 'user')]), 156 | ), 157 | migrations.AlterUniqueTogether( 158 | name='like', 159 | unique_together=set([('user', 'content_type', 'object_id')]), 160 | ), 161 | migrations.AlterUniqueTogether( 162 | name='follow', 163 | unique_together=set([('user', 'content_type', 'object_id')]), 164 | ), 165 | migrations.AlterUniqueTogether( 166 | name='flag', 167 | unique_together=set([('user', 'content_type', 'object_id')]), 168 | ), 169 | ] 170 | -------------------------------------------------------------------------------- /rest_social/views.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from django.conf import settings 3 | import facebook 4 | from rest_framework.exceptions import AuthenticationFailed 5 | from rest_framework.permissions import IsAuthenticated 6 | from rest_framework.response import Response 7 | from rest_framework import viewsets, status, generics 8 | from rest_framework.decorators import detail_route, list_route 9 | from social.apps.django_app.default.models import UserSocialAuth 10 | from social.apps.django_app.utils import load_strategy, load_backend 11 | from social.backends.oauth import BaseOAuth1, BaseOAuth2 12 | from twython import Twython 13 | from rest_social.rest_social.models import Tag, Comment, Follow, Flag, Share, Like 14 | from rest_social.rest_social.serializers import TagSerializer, CommentSerializer, FollowSerializer, FlagSerializer, \ 15 | ShareSerializer, FollowPaginationSerializer, LikeSerializer, SocialSignUpSerializer 16 | from rest_social.rest_social.utils import post_social_media 17 | from rest_user.rest_user.serializers import UserSerializer 18 | from rest_user.rest_user.views import UserViewSet, SignUp 19 | from django.contrib.auth import get_user_model 20 | 21 | 22 | __author__ = 'baylee' 23 | 24 | 25 | User = get_user_model() 26 | 27 | 28 | class TagViewSet(viewsets.ModelViewSet): 29 | queryset = Tag.objects.all() 30 | serializer_class = TagSerializer 31 | 32 | 33 | class CommentViewSet(viewsets.ModelViewSet): 34 | queryset = Comment.objects.all() 35 | serializer_class = CommentSerializer 36 | 37 | def pre_save(self, obj): 38 | obj.user = self.request.user 39 | 40 | 41 | class FollowViewSet(viewsets.ModelViewSet): 42 | queryset = Follow.objects.all() 43 | serializer_class = FollowSerializer 44 | 45 | def pre_save(self, obj): 46 | obj.user = self.request.user 47 | 48 | @list_route(methods=['post']) 49 | def bulk_create(self, request): 50 | serializer = self.get_serializer(data=request.DATA, many=True) 51 | if serializer.is_valid(): 52 | [self.pre_save(obj) for obj in serializer.object] 53 | self.object = serializer.save(force_insert=True) 54 | [self.post_save(obj, created=True) for obj in self.object] 55 | return Response(serializer.data, status=status.HTTP_201_CREATED) 56 | 57 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 58 | 59 | 60 | class ShareViewSet(viewsets.ModelViewSet): 61 | queryset = Share.objects.all() 62 | serializer_class = ShareSerializer 63 | 64 | def pre_save(self, obj): 65 | obj.user = self.request.user 66 | 67 | 68 | class LikeViewSet(viewsets.ModelViewSet): 69 | queryset = Like.objects.all() 70 | serializer_class = LikeSerializer 71 | 72 | def pre_save(self, obj): 73 | obj.user = self.request.user 74 | 75 | 76 | class FlagView(generics.CreateAPIView): 77 | queryset = Flag.objects.all() 78 | serializer_class = FlagSerializer 79 | 80 | def pre_save(self, obj): 81 | obj.user = self.request.user 82 | 83 | 84 | class SocialUserViewSet(UserViewSet): 85 | serializer_class = UserSerializer 86 | 87 | @detail_route(methods=['get']) 88 | def following(self, request, pk): 89 | requested_user = User.objects.get(pk=pk) 90 | following = requested_user.user_following() 91 | page = self.paginate_queryset(following) 92 | serializer = FollowPaginationSerializer(instance=page) 93 | return Response(serializer.data) 94 | 95 | @detail_route(methods=['get']) 96 | def followers(self, request, pk): 97 | requested_user = User.objects.get(pk=pk) 98 | follower = requested_user.user_followers() 99 | page = self.paginate_queryset(follower) 100 | serializer = FollowPaginationSerializer(instance=page) 101 | return Response(serializer.data) 102 | 103 | 104 | class SocialSignUp(SignUp): 105 | serializer_class = SocialSignUpSerializer 106 | 107 | def create(self, request, *args, **kwargs): 108 | serializer = self.get_serializer(data=request.DATA, files=request.FILES) 109 | 110 | if serializer.is_valid(): 111 | self.pre_save(serializer.object) 112 | provider = request.DATA['provider'] 113 | 114 | # If this request was made with an authenticated user, try to associate this social account with it 115 | authed_user = request.user if not request.user.is_anonymous() else None 116 | 117 | strategy = load_strategy(request) 118 | backend = load_backend(strategy=strategy, name=provider, redirect_uri=None) 119 | 120 | if isinstance(backend, BaseOAuth1): 121 | token = { 122 | 'oauth_token': request.DATA['access_token'], 123 | 'oauth_token_secret': request.DATA['access_token_secret'], 124 | } 125 | elif isinstance(backend, BaseOAuth2): 126 | token = request.DATA['access_token'] 127 | 128 | user = backend.do_auth(token, user=authed_user) 129 | serializer.object = user 130 | 131 | if user and user.is_active: 132 | if not authed_user and request.DATA['password']: 133 | password = base64.decodestring(request.DATA['password']) 134 | user.set_password(password) 135 | user.save() 136 | self.post_save(serializer.object, created=True) 137 | headers = self.get_success_headers(serializer.data) 138 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 139 | else: 140 | return Response({"errors": "Error with social authentication"}, status=status.HTTP_400_BAD_REQUEST) 141 | 142 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 143 | 144 | 145 | class SocialShareMixin(object): 146 | 147 | @detail_route(methods=['post']) 148 | def social_share(self, request, pk): 149 | try: 150 | user_social_auth = UserSocialAuth.objects.get(user=request.user, provider=request.DATA['provider']) 151 | social_obj = self.get_object() 152 | post_social_media(user_social_auth, social_obj) 153 | return Response({'status': 'success'}) 154 | except UserSocialAuth.DoesNotExist: 155 | raise AuthenticationFailed("User is not authenticated with {}".format(request.DATA['provider'])) 156 | 157 | 158 | class SocialFriends(generics.ListAPIView): 159 | queryset = User.objects.all() 160 | serializer_class = UserSerializer 161 | permission_classes = (IsAuthenticated,) 162 | 163 | def get_queryset(self): 164 | provider = self.request.QUERY_PARAMS.get('provider', None) 165 | if provider == 'facebook': 166 | # TODO: what does it look like when a user has more than one social auth for a provider? Is this a thing 167 | # that can happen? How does it affect SocialShareMixin? The first one is the oldest--do we actually want 168 | # the last one? 169 | user_social_auth = self.request.user.social_auth.filter(provider='facebook').first() 170 | graph = facebook.GraphAPI(user_social_auth.extra_data['access_token']) 171 | facebook_friends = graph.request("v2.2/me/friends")["data"] 172 | friends = User.objects.filter(social_auth__provider='facebook', 173 | social_auth__uid__in=[user["id"] for user in facebook_friends]) 174 | return friends 175 | elif provider == 'twitter': 176 | user_social_auth = self.request.user.social_auth.filter(provider='twitter').first() 177 | twitter = Twython( 178 | app_key=settings.SOCIAL_AUTH_TWITTER_KEY, 179 | app_secret=settings.SOCIAL_AUTH_TWITTER_SECRET, 180 | oauth_token=user_social_auth.tokens['oauth_token'], 181 | oauth_token_secret=user_social_auth.tokens['oauth_token_secret'] 182 | ) 183 | twitter_friends = twitter.get_friends_ids()["ids"] 184 | friends = User.objects.filter(social_auth__provider='twitter', 185 | social_auth__uid__in=twitter_friends) 186 | return friends 187 | else: 188 | return Response({"errors": "{} is not a valid social provider".format(provider)}, 189 | status=status.HTTP_400_BAD_REQUEST) 190 | -------------------------------------------------------------------------------- /rest_social/models.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.contenttypes.fields import GenericRelation 4 | from django.contrib.sites.models import Site 5 | from django.utils.baseconv import base62 6 | import re 7 | from django.conf import settings 8 | from django.contrib.contenttypes import generic 9 | from django.contrib.contenttypes.models import ContentType 10 | from django.db import models 11 | from django.db.models.signals import post_save 12 | from model_utils import Choices 13 | from manticore_django.manticore_django.models import CoreModel 14 | from rest_user.rest_user.models import AbstractYeti 15 | 16 | 17 | class FollowableModel(): 18 | """ 19 | Abstract class that used as interface 20 | This class makes sure that child classes have 21 | my_method implemented 22 | """ 23 | __metaclass__ = abc.ABCMeta 24 | 25 | @abc.abstractmethod 26 | def identifier(self): 27 | return 28 | 29 | @abc.abstractmethod 30 | def type(self): 31 | return 32 | 33 | 34 | class Tag(CoreModel): 35 | name = models.CharField(max_length=75, unique=True) 36 | 37 | def identifier(self): 38 | return u"#%s" % self.name 39 | 40 | def type(self): 41 | return u"tag" 42 | 43 | def __unicode__(self): 44 | return u"%s" % self.name 45 | 46 | FollowableModel.register(Tag) 47 | 48 | 49 | def relate_tags(sender, **kwargs): 50 | """ 51 | Intended to be used as a receiver function for a `post_save` signal on models that have tags 52 | 53 | Expects tags is stored in a field called 'related_tags' on implementing model 54 | and it has a parameter called TAG_FIELD to be parsed 55 | """ 56 | # If we're saving related_tags, don't save again so we avoid duplicating notifications 57 | if kwargs['update_fields'] and 'related_tags' not in kwargs['update_fields']: 58 | return 59 | 60 | changed = False 61 | # Get the text of the field that holds tags. If there is no field specified, use an empty string. If the field's 62 | # value is None, use an empty string. 63 | message = getattr(kwargs['instance'], sender.TAG_FIELD, '') or '' 64 | for tag in re.findall(ur"#[a-zA-Z0-9_-]+", message): 65 | tag_obj, created = Tag.objects.get_or_create(name=tag[1:]) 66 | if tag_obj not in kwargs['instance'].related_tags.all(): 67 | kwargs['instance'].related_tags.add(tag_obj) 68 | changed = True 69 | 70 | if changed: 71 | kwargs['instance'].save() 72 | 73 | 74 | def mentions(sender, **kwargs): 75 | """ 76 | Intended to be used as a receiver function for a `post_save` signal on models that have @mentions 77 | Implementing model must have an attribute TAG_FIELD where @mentions are stored in raw form 78 | 79 | This function creates notifications but does not associate mentioned users with the created model instance 80 | """ 81 | try: 82 | from rest_notifications.rest_notifications.models import create_notification, Notification 83 | except ImportError: 84 | return 85 | 86 | if kwargs['created']: 87 | # Get the text of the field that holds tags. If there is no field specified, use an empty string. If the field's 88 | # value is None, use an empty string. 89 | message = getattr(kwargs['instance'], sender.TAG_FIELD, '') or '' 90 | content_object = getattr(kwargs['instance'], 'content_object', kwargs['instance']) 91 | 92 | for user in re.findall(ur"@[a-zA-Z0-9_.]+", message): 93 | User = get_user_model() 94 | try: 95 | receiver = User.objects.get(username=user[1:]) 96 | create_notification(receiver, kwargs['instance'].user, content_object, Notification.TYPES.mention) 97 | except User.DoesNotExist: 98 | pass 99 | 100 | 101 | class Comment(CoreModel): 102 | content_type = models.ForeignKey(ContentType) 103 | object_id = models.PositiveIntegerField(db_index=True) 104 | content_object = generic.GenericForeignKey() 105 | 106 | TAG_FIELD = 'description' 107 | related_tags = models.ManyToManyField(Tag, blank=True, null=True) 108 | 109 | description = models.CharField(max_length=140) 110 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 111 | 112 | class Meta: 113 | ordering = ['created'] 114 | 115 | post_save.connect(mentions, sender=Comment) 116 | post_save.connect(relate_tags, sender=Comment) 117 | 118 | 119 | # Allows a user to 'follow' objects 120 | class Follow(CoreModel): 121 | content_type = models.ForeignKey(ContentType) 122 | object_id = models.PositiveIntegerField(db_index=True) 123 | content_object = generic.GenericForeignKey() 124 | 125 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 126 | 127 | @property 128 | def object_type(self): 129 | return self.content_type.name 130 | 131 | @property 132 | def name(self): 133 | # object must be registered with FollowableModel 134 | return self.content_object.identifier() 135 | 136 | class Meta: 137 | unique_together = (("user", "content_type", "object_id"),) 138 | ordering = ['created'] 139 | 140 | 141 | class Like(CoreModel): 142 | content_type = models.ForeignKey(ContentType) 143 | object_id = models.PositiveIntegerField(db_index=True) 144 | content_object = generic.GenericForeignKey() 145 | 146 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 147 | 148 | class Meta: 149 | unique_together = (("user", "content_type", "object_id"),) 150 | 151 | 152 | # Flag an object for review 153 | class Flag(CoreModel): 154 | content_type = models.ForeignKey(ContentType) 155 | object_id = models.PositiveIntegerField() 156 | content_object = generic.GenericForeignKey() 157 | 158 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 159 | 160 | class Meta: 161 | unique_together = (("user", "content_type", "object_id"),) 162 | 163 | 164 | class Share(CoreModel): 165 | content_type = models.ForeignKey(ContentType) 166 | object_id = models.PositiveIntegerField() 167 | content_object = generic.GenericForeignKey() 168 | shared_with = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='shared_with') 169 | 170 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 171 | 172 | class Meta: 173 | unique_together = (("user", "content_type", "object_id", "id"),) 174 | 175 | 176 | class FriendAction(CoreModel): 177 | TYPES = Choices(*settings.SOCIAL_FRIEND_ACTIONS) 178 | 179 | # Unpack the list of social friend actions from the settings 180 | action_type = models.PositiveSmallIntegerField(choices=TYPES) 181 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 182 | 183 | content_type = models.ForeignKey(ContentType) 184 | object_id = models.PositiveIntegerField(db_index=True) 185 | content_object = generic.GenericForeignKey() 186 | 187 | def message(self): 188 | return unicode(self.TYPES[self.action_type][1]) 189 | 190 | def name(self): 191 | return u"{0}".format(self.TYPES._triples[self.action_type][1]) 192 | 193 | def display_name(self): 194 | return u"{0}".format(self.get_action_type_display()) 195 | 196 | class Meta: 197 | ordering = ['-created'] 198 | 199 | 200 | def create_friend_action(user, content_object, action_type): 201 | friend_action = FriendAction.objects.create(user=user, 202 | content_object=content_object, 203 | action_type=action_type) 204 | friend_action.save() 205 | 206 | 207 | # Currently available social providers 208 | class SocialProvider(CoreModel): 209 | name = models.CharField(max_length=20) 210 | 211 | 212 | class BaseSocialModel(models.Model): 213 | """ 214 | This is an abstract model to be inherited by the main "object" being used in feeds on a social media application. 215 | It expects that object to override the methods below. 216 | """ 217 | 218 | class Meta: 219 | abstract = True 220 | 221 | def url(self): 222 | current_site = Site.objects.get_current() 223 | return "http://{0}/{1}/".format(current_site.domain, base62.encode(self.pk)) 224 | 225 | def facebook_og_info(self): 226 | # return {'action': '', 'object': '', 'url': self.url()} 227 | raise NotImplementedError("This has not been implemented") 228 | 229 | def create_social_message(self, provider): 230 | raise NotImplementedError("This has not been implemented") 231 | 232 | 233 | class AbstractSocialYeti(AbstractYeti): 234 | follows = GenericRelation(Follow) 235 | 236 | class Meta: 237 | abstract = True 238 | 239 | def user_following(self): 240 | return self.follow_set.filter( 241 | content_type=ContentType.objects.get(app_label=settings.USER_APP_LABEL, model=settings.USER_MODEL) 242 | ) 243 | 244 | def user_followers(self): 245 | return Follow.objects.filter( 246 | content_type=ContentType.objects.get(app_label=settings.USER_APP_LABEL, model=settings.USER_MODEL), 247 | object_id=self.pk 248 | ) 249 | 250 | def user_following_count(self): 251 | return self.user_following().count() 252 | 253 | def user_followers_count(self): 254 | return self.user_followers().count() 255 | 256 | def identifier(self): 257 | return u"%s" % self.username 258 | 259 | def type(self): 260 | return u"user" 261 | -------------------------------------------------------------------------------- /rest_social/tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.core.urlresolvers import reverse 5 | from manticore_django.manticore_django.utils import get_class 6 | from rest_social.rest_social.utils import get_social_model 7 | from rest_user.rest_user.test.factories import UserFactory 8 | from rest_core.rest_core.test import ManticomTestCase 9 | from rest_social.rest_social.models import Follow, Comment, Tag 10 | 11 | __author__ = 'winnietong' 12 | 13 | 14 | User = get_user_model() 15 | SocialModel = get_social_model() 16 | SocialFactory = get_class(settings.SOCIAL_MODEL_FACTORY) 17 | 18 | 19 | class BaseAPITests(ManticomTestCase): 20 | def setUp(self): 21 | super(BaseAPITests, self).setUp() 22 | self.dev_user = UserFactory() 23 | 24 | 25 | class FlagTestCase(BaseAPITests): 26 | def test_users_can_flag_content(self): 27 | test_user = UserFactory() 28 | content_type = ContentType.objects.get_for_model(SocialModel) 29 | flag_url = reverse('flag') 30 | data = { 31 | 'content_type': content_type.pk, 32 | 'object_id': SocialFactory().pk 33 | } 34 | self.assertManticomPOSTResponse(flag_url, 35 | "$flagRequest", 36 | "$flagResponse", 37 | data, 38 | test_user 39 | ) 40 | 41 | 42 | class ShareTestCase(BaseAPITests): 43 | def test_users_can_share_content(self): 44 | test_user = UserFactory() 45 | content_type = ContentType.objects.get_for_model(SocialModel) 46 | shares_url = reverse('shares-list') 47 | data = { 48 | 'content_type': content_type.pk, 49 | 'object_id': SocialFactory().pk, 50 | 'shared_with': [test_user.pk] 51 | } 52 | self.assertManticomPOSTResponse(shares_url, 53 | "$shareRequest", 54 | "$shareResponse", 55 | data, 56 | self.dev_user 57 | ) 58 | 59 | 60 | class LikeTestCase(BaseAPITests): 61 | def test_users_can_like_content(self): 62 | content_type = ContentType.objects.get_for_model(SocialModel) 63 | likes_url = reverse('likes-list') 64 | data = { 65 | 'content_type': content_type.pk, 66 | 'object_id': SocialFactory().pk, 67 | } 68 | self.assertManticomPOSTResponse(likes_url, 69 | "$likeRequest", 70 | "$likeResponse", 71 | data, 72 | self.dev_user 73 | ) 74 | 75 | 76 | class CommentTestCase(BaseAPITests): 77 | def test_users_can_comment_on_content(self): 78 | content_type = ContentType.objects.get_for_model(SocialModel) 79 | comments_url = reverse('comments-list') 80 | data = { 81 | 'content_type': content_type.pk, 82 | 'object_id': SocialFactory().pk, 83 | 'description': 'This is a user comment.' 84 | } 85 | self.assertManticomPOSTResponse(comments_url, 86 | "$commentRequest", 87 | "$commentResponse", 88 | data, 89 | self.dev_user 90 | ) 91 | 92 | def test_comment_related_tags(self): 93 | content_type = ContentType.objects.get_for_model(SocialModel) 94 | Comment.objects.create(content_type=content_type, 95 | object_id=1, 96 | description='Testing of a hashtag. #django', 97 | user=self.dev_user) 98 | tags_url = reverse('tags-list') 99 | response = self.assertManticomGETResponse(tags_url, 100 | None, 101 | "$tagResponse", 102 | self.dev_user) 103 | self.assertEqual(response.data['results'][0]['name'], 'django') 104 | self.assertIsNotNone(Tag.objects.get(name='django')) 105 | 106 | 107 | class UserFollowingTestCase(BaseAPITests): 108 | def test_user_can_follow_each_other(self): 109 | test_user1 = UserFactory() 110 | user_content_type = ContentType.objects.get_for_model(User) 111 | follow_url = reverse('follows-list') 112 | # Dev User to follow Test User 1 113 | data = { 114 | 'content_type': user_content_type.pk, 115 | 'object_id': test_user1.pk 116 | } 117 | response = self.assertManticomPOSTResponse(follow_url, 118 | "$followRequest", 119 | "$followResponse", 120 | data, 121 | self.dev_user 122 | ) 123 | self.assertEqual(response.data['following']['username'], test_user1.username) 124 | 125 | def test_following_endpoint(self): 126 | test_user1 = UserFactory() 127 | test_user2 = UserFactory() 128 | user_content_type = ContentType.objects.get_for_model(User) 129 | # Dev User to follow User 1, User 2 to follow Dev User 130 | Follow.objects.create(content_type=user_content_type, object_id=test_user1.pk, user=self.dev_user) 131 | Follow.objects.create(content_type=user_content_type, object_id=self.dev_user.pk, user=test_user2) 132 | following_url = reverse('users-following', args=[self.dev_user.pk]) 133 | response = self.assertManticomGETResponse(following_url, 134 | None, 135 | "$followResponse", 136 | self.dev_user) 137 | self.assertEqual(response.data['count'], 1) 138 | self.assertEqual(response.data['results'][0]['following']['username'], test_user1.username) 139 | 140 | def test_follower_endpoint(self): 141 | test_user1 = UserFactory() 142 | test_user2 = UserFactory() 143 | user_content_type = ContentType.objects.get_for_model(User) 144 | # Dev User to follow User 1, User 2 to follow Dev User 145 | Follow.objects.create(content_type=user_content_type, object_id=test_user1.pk, user=self.dev_user) 146 | Follow.objects.create(content_type=user_content_type, object_id=self.dev_user.pk, user=test_user2) 147 | followers_url = reverse('users-followers', args=[self.dev_user.pk]) 148 | response = self.assertManticomGETResponse(followers_url, 149 | None, 150 | "$followResponse", 151 | self.dev_user) 152 | self.assertEqual(response.data['count'], 1) 153 | self.assertEqual(response.data['results'][0]['follower']['username'], test_user2.username) 154 | 155 | def test_user_can_unfollow_user(self): 156 | follower = UserFactory() 157 | user_content_type = ContentType.objects.get_for_model(User) 158 | follow_object = Follow.objects.create(content_type=user_content_type, object_id=self.dev_user.pk, user=follower) 159 | follows_url = reverse('follows-detail', kwargs={'pk': follow_object.pk}) 160 | 161 | # If you are not the follower of the user, you cannot unfollow the user 162 | self.assertManticomDELETEResponse(follows_url, self.dev_user, unauthorized=True) 163 | 164 | # If you are the follower of that user, you can unfollow the user 165 | self.assertManticomDELETEResponse(follows_url, follower) 166 | 167 | # Check that original follow object no longer exists 168 | self.assertEqual(Follow.objects.filter(pk=follow_object.pk).exists(), False) 169 | 170 | def test_user_following_and_follower_count(self): 171 | follower1 = UserFactory() 172 | follower2 = UserFactory() 173 | following = UserFactory() 174 | user_content_type = ContentType.objects.get_for_model(User) 175 | 176 | # Follower setup 177 | Follow.objects.create(content_type=user_content_type, object_id=following.pk, user=self.dev_user) 178 | Follow.objects.create(content_type=user_content_type, object_id=self.dev_user.pk, user=follower1) 179 | Follow.objects.create(content_type=user_content_type, object_id=self.dev_user.pk, user=follower2) 180 | 181 | users_url = reverse('users-detail', kwargs={'pk': self.dev_user.pk}) 182 | response = self.assertManticomGETResponse(users_url, 183 | None, 184 | "$userResponse", 185 | self.dev_user) 186 | self.assertEqual(response.data['user_following_count'], 1) 187 | self.assertEqual(response.data['user_followers_count'], 2) 188 | 189 | def test_bulk_follow(self): 190 | user1 = UserFactory() 191 | user2 = UserFactory() 192 | 193 | url = reverse('follows-bulk-create') 194 | user_content_type = ContentType.objects.get_for_model(User) 195 | data = [ 196 | {'content_type': user_content_type.pk, 'object_id': user1.pk}, 197 | {'content_type': user_content_type.pk, 'object_id': user2.pk} 198 | ] 199 | self.assertManticomPOSTResponse(url, "$followRequest", "$followResponse", data, self.dev_user) 200 | self.assertEqual(user1.user_followers_count(), 1) 201 | self.assertEqual(user2.user_followers_count(), 1) 202 | --------------------------------------------------------------------------------