Commit fa8ca11d by Nimisha Asthagiri

Remove unused Mobile Social Facebook endpoint

parent 080b7d7b
......@@ -73,10 +73,6 @@ class CourseMetadata(object):
if not settings.FEATURES.get('ENABLE_VIDEO_UPLOAD_PIPELINE'):
filtered_list.append('video_upload_pipeline')
# Do not show facebook_url if the feature is disabled.
if not settings.FEATURES.get('ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'):
filtered_list.append('facebook_url')
# Do not show social sharing url field if the feature is disabled.
if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or
not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")):
......
......@@ -338,15 +338,6 @@ class CourseFields(object):
help=_("Enter the unique identifier for your course's video files provided by edX."),
scope=Scope.settings
)
facebook_url = String(
help=_(
"Enter the URL for the official course Facebook group. "
"If you provide a URL, the mobile app includes a button that students can tap to access the group."
),
default=None,
display_name=_("Facebook URL"),
scope=Scope.settings
)
no_grade = Boolean(
display_name=_("Course Not Graded"),
help=_("Enter true or false. If true, the course will not be graded."),
......
"""
Social Facebook API
"""
# TODO
# There are still some performance and scalability issues that should be
# addressed for the various endpoints in this social_facebook djangoapp.
#
# For the Courses and Friends API:
# For both endpoints, we are retrieving the same data from the Facebook server.
# We are then simply organizing and filtering that data differently for each endpoint.
#
# Here are 3 ideas that can be explored further:
#
# Option 1. The app can just call one endpoint that provides a mapping between CourseIDs and Friends,
# and then cache that data once. The reverse map from Friends to CourseIDs can then be created on the app side.
#
# Option 2. The app once again calls just one endpoint (since the same data is computed for both),
# and caches the data once. The difference from #1 is that the server does the computation of the reverse-map and
# sends both maps down to the client. It's a tradeoff between bandwidth and client-side computation. So the payload
# could be something like:
#
# {
# courses: [
# {course_id: "c/ourse/1", friend_indices: [1, 2, 3]},
# {course_id: "c/ourse/2", friend_indices: [3, 4, 5]},
# ..
# ],
# friends: [
# {username: "friend1", facebook_id: "xxx", course_indices: [2, 7, 9]},
# {username: "friend2", facebook_id: "yyy", course_indices: [1, 4, 3]},
# ...
# ]
# }
#
# Option 3. Alternatively, continue to have separate endpoints, but have both endpoints call the same underlying method
# with a built-in cache.
#
# All 3 options can make use of a common cache of results from FB.
#
# At a minimum, some performance/load testing would need to be done
# so we have an idea of these endpoints' limitations and thresholds.
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for courses API
"""
from rest_framework import serializers
class CoursesWithFriendsSerializer(serializers.Serializer):
"""
Serializes oauth token for facebook groups request
"""
oauth_token = serializers.CharField(required=True)
# pylint: disable=E1101, W0201
"""
Tests for Courses
"""
import httpretty
import json
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory
from opaque_keys.edx.keys import CourseKey
from ..test_utils import SocialFacebookTestCase
class TestCourses(SocialFacebookTestCase):
"""
Tests for /api/mobile/v0.5/courses/...
"""
def setUp(self):
super(TestCourses, self).setUp()
self.course = CourseFactory.create(mobile_available=True)
@httpretty.activate
def test_one_course_with_friends(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
self.enroll_in_course(self.users[1], self.course)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
@httpretty.activate
def test_two_courses_with_friends(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.enroll_in_course(self.users[1], self.course)
self.course_2 = CourseFactory.create(mobile_available=True)
self.enroll_in_course(self.users[1], self.course_2)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101
@httpretty.activate
def test_three_courses_but_only_two_unique(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.course_2 = CourseFactory.create(mobile_available=True)
self.enroll_in_course(self.users[1], self.course_2)
self.enroll_in_course(self.users[1], self.course)
self.user_create_and_signin(2)
self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
self.set_sharing_preferences(self.users[2], True)
# Enroll another user in course_2
self.enroll_in_course(self.users[2], self.course_2)
self.set_facebook_interceptor_for_friends(
{'data': [
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
]}
)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101
# Assert that only two courses are returned
self.assertEqual(len(response.data), 2) # pylint: disable=E1101
@httpretty.activate
def test_two_courses_with_two_friends_on_different_paged_results(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.enroll_in_course(self.users[1], self.course)
self.user_create_and_signin(2)
self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
self.set_sharing_preferences(self.users[2], True)
self.course_2 = CourseFactory.create(mobile_available=True)
self.enroll_in_course(self.users[2], self.course_2)
self.set_facebook_interceptor_for_friends(
{
'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}],
"paging": {"next": "https://graph.facebook.com/v2.2/me/friends/next"},
"summary": {"total_count": 652}
}
)
# Set the interceptor for the paged
httpretty.register_uri(
httpretty.GET,
"https://graph.facebook.com/v2.2/me/friends/next",
body=json.dumps(
{
"data": [{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}],
"paging": {
"previous":
"https://graph.facebook.com/v2.2/10154805434030300/friends?limit=25&offset=25"
},
"summary": {"total_count": 652}
}
),
status=201
)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101
@httpretty.activate
def test_no_courses_with_friends_because_sharing_pref_off(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], False)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
self.enroll_in_course(self.users[1], self.course)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 0)
@httpretty.activate
def test_no_courses_with_friends_because_no_auth_token(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], False)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
self.enroll_in_course(self.users[1], self.course)
url = reverse('courses-with-friends')
response = self.client.get(url)
self.assertEqual(response.status_code, 400)
"""
URLs for courses API
"""
from django.conf.urls import patterns, url
from .views import CoursesWithFriends
urlpatterns = patterns(
'mobile_api.social_facebook.courses.views',
url(
r'^friends$',
CoursesWithFriends.as_view(),
name='courses-with-friends'
),
)
"""
Views for courses info API
"""
from rest_framework import generics, status
from rest_framework.response import Response
from courseware.access import is_mobile_available_for_user
from student.models import CourseEnrollment
from lms.djangoapps.mobile_api.social_facebook.courses import serializers
from ...users.serializers import CourseEnrollmentSerializer
from ...utils import mobile_view
from ..utils import get_friends_from_facebook, get_linked_edx_accounts, share_with_facebook_friends
@mobile_view()
class CoursesWithFriends(generics.ListAPIView):
"""
**Use Case**
API endpoint for retrieving all the courses that a user's friends are in.
Note that only friends that allow their courses to be shared will be included.
**Example request**
GET /api/mobile/v0.5/social/facebook/courses/friends
**Response Values**
See UserCourseEnrollmentsList in lms/djangoapps/mobile_api/users for the structure of the response values.
"""
serializer_class = serializers.CoursesWithFriendsSerializer
def list(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Get friends from Facebook
result = get_friends_from_facebook(serializer)
if not isinstance(result, list):
return result
friends_that_are_edx_users = get_linked_edx_accounts(result)
# Filter by sharing preferences
users_with_sharing = [
friend for friend in friends_that_are_edx_users if share_with_facebook_friends(friend)
]
# Get unique enrollments
enrollments = []
for friend in users_with_sharing:
query_set = CourseEnrollment.objects.filter(
user_id=friend['edX_id']
).exclude(course_id__in=[enrollment.course_id for enrollment in enrollments])
enrollments.extend(query_set)
# Get course objects
courses = [
enrollment for enrollment in enrollments if enrollment.course
and is_mobile_available_for_user(self.request.user, enrollment.course)
]
serializer = CourseEnrollmentSerializer(courses, context={'request': request}, many=True)
return Response(serializer.data)
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for Friends API
"""
from rest_framework import serializers
class FriendsInCourseSerializer(serializers.Serializer):
"""
Serializes oauth token for facebook groups request
"""
oauth_token = serializers.CharField(required=True)
"""
URLs for friends API
"""
from django.conf.urls import patterns, url
from django.conf import settings
from .views import FriendsInCourse
urlpatterns = patterns(
'mobile_api.social_facebook.friends.views',
url(
r'^course/{}$'.format(settings.COURSE_ID_PATTERN),
FriendsInCourse.as_view(),
name='friends-in-course'
),
)
"""
Views for friends info API
"""
from rest_framework import generics, status
from rest_framework.response import Response
from opaque_keys.edx.keys import CourseKey
from student.models import CourseEnrollment
from ...utils import mobile_view
from ..utils import get_friends_from_facebook, get_linked_edx_accounts, share_with_facebook_friends
from lms.djangoapps.mobile_api.social_facebook.friends import serializers
@mobile_view()
class FriendsInCourse(generics.ListAPIView):
"""
**Use Case**
API endpoint that returns all the users friends that are in the course specified.
Note that only friends that allow their courses to be shared will be included.
**Example request**:
GET /api/mobile/v0.5/social/facebook/friends/course/<course_id>
where course_id is in the form of /edX/DemoX/Demo_Course
**Response Values**
{
"friends": [
{
"name": "test",
"id": "12345",
},
...
]
}
"""
serializer_class = serializers.FriendsInCourseSerializer
def list(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Get all the user's FB friends
result = get_friends_from_facebook(serializer)
if not isinstance(result, list):
return result
def is_member(friend, course_key):
"""
Return true if friend is a member of the course specified by the course_key
"""
return CourseEnrollment.objects.filter(
course_id=course_key,
user_id=friend['edX_id']
).count() == 1
# For each friend check if they are a linked edX user
friends_with_edx_users = get_linked_edx_accounts(result)
# Filter by sharing preferences and enrollment in course
course_key = CourseKey.from_string(kwargs['course_id'])
friends_with_sharing_in_course = [
{'id': friend['id'], 'name': friend['name']}
for friend in friends_with_edx_users
if share_with_facebook_friends(friend) and is_member(friend, course_key)
]
return Response({'friends': friends_with_sharing_in_course})
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for user API
"""
from rest_framework import serializers
from django.core.validators import RegexValidator
class GroupSerializer(serializers.Serializer):
"""
Serializes facebook groups request
"""
name = serializers.CharField(max_length=150)
description = serializers.CharField(max_length=200, required=False)
privacy = serializers.ChoiceField(choices=[("open", "open"), ("closed", "closed")], required=False)
class GroupsMembersSerializer(serializers.Serializer):
"""
Serializes facebook invitations request
"""
member_ids = serializers.CharField(
required=True,
validators=[
RegexValidator(
regex=r'^([\d]+,?)*$',
message='A comma separated list of member ids must be provided',
code='member_ids error'
),
]
)
"""
Tests for groups
"""
import httpretty
from ddt import ddt, data
from django.conf import settings
from django.core.urlresolvers import reverse
from courseware.tests.factories import UserFactory
from ..test_utils import SocialFacebookTestCase
@ddt
class TestGroups(SocialFacebookTestCase):
"""
Tests for /api/mobile/v0.5/social/facebook/groups/...
"""
def setUp(self):
super(TestGroups, self).setUp()
self.user = UserFactory.create()
self.client.login(username=self.user.username, password='test')
# Group Creation and Deletion Tests
@httpretty.activate
def test_create_new_open_group(self):
group_id = '12345678'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_groups({'id': group_id}, status_code)
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{
'name': 'TheBestGroup',
'description': 'The group for the best people',
'privacy': 'open'
}
)
self.assertEqual(response.status_code, status_code)
self.assertTrue('id' in response.data) # pylint: disable=E1103
self.assertEqual(response.data['id'], group_id) # pylint: disable=E1103
@httpretty.activate
def test_create_new_closed_group(self):
group_id = '12345678'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_groups({'id': group_id}, status_code)
# Create new group
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{
'name': 'TheBestGroup',
'description': 'The group for the best people',
'privacy': 'closed'
}
)
self.assertEqual(response.status_code, status_code)
self.assertTrue('id' in response.data) # pylint: disable=E1103
self.assertEqual(response.data['id'], group_id) # pylint: disable=E1103
def test_create_new_group_no_name(self):
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(url, {})
self.assertEqual(response.status_code, 400)
def test_create_new_group_with_invalid_name(self):
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(url, {'invalid_name': 'TheBestGroup'})
self.assertEqual(response.status_code, 400)
def test_create_new_group_with_invalid_privacy(self):
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{'name': 'TheBestGroup', 'privacy': 'half_open_half_closed'}
)
self.assertEqual(response.status_code, 400)
@httpretty.activate
def test_delete_group_that_exists(self):
# Create new group
group_id = '12345678'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_groups({'id': group_id}, status_code)
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{
'name': 'TheBestGroup',
'description': 'The group for the best people',
'privacy': 'open'
}
)
self.assertEqual(response.status_code, status_code)
self.assertTrue('id' in response.data) # pylint: disable=E1103
# delete group
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/{}/{}/groups/{}?access_token=FakeToken&method=delete'.format(
settings.FACEBOOK_API_VERSION,
settings.FACEBOOK_APP_ID,
group_id
),
body='{"success": "true"}',
status=status_code
)
response = self.delete_group(response.data['id']) # pylint: disable=E1101
self.assertTrue(response.status_code, status_code)
@httpretty.activate
def test_delete(self):
group_id = '12345678'
status_code = 400
httpretty.register_uri(
httpretty.GET,
'https://graph.facebook.com/oauth/access_token?client_secret={}&grant_type=client_credentials&client_id={}'
.format(
settings.FACEBOOK_APP_SECRET,
settings.FACEBOOK_APP_ID
),
body='FakeToken=FakeToken',
status=200
)
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/{}/{}/groups/{}?access_token=FakeToken&method=delete'.format(
settings.FACEBOOK_API_VERSION,
settings.FACEBOOK_APP_ID,
group_id
),
body='{"error": {"message": "error message"}}',
status=status_code
)
response = self.delete_group(group_id)
self.assertTrue(response.status_code, status_code)
# Member addition and Removal tests
@data('1234,,,,5678,,', 'this00is00not00a00valid00id', '1234,abc,5678', '')
def test_invite_single_member_malformed_member_id(self, member_id):
group_id = '111111111111111'
response = self.invite_to_group(group_id, member_id)
self.assertEqual(response.status_code, 400)
@httpretty.activate
def test_invite_single_member(self):
group_id = '111111111111111'
member_id = '44444444444444444'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_members({'success': 'True'}, status_code, group_id, member_id)
response = self.invite_to_group(group_id, member_id)
self.assertEqual(response.status_code, status_code)
self.assertTrue('success' in response.data[member_id])
@httpretty.activate
def test_invite_multiple_members_successfully(self):
member_ids = '222222222222222,333333333333333,44444444444444444'
group_id = '111111111111111'
status_code = 200
self.set_facebook_interceptor_for_access_token()
for member_id in member_ids.split(','):
self.set_facebook_interceptor_for_members({'success': 'True'}, status_code, group_id, member_id)
response = self.invite_to_group(group_id, member_ids)
self.assertEqual(response.status_code, status_code)
for member_id in member_ids.split(','):
self.assertTrue('success' in response.data[member_id])
@httpretty.activate
def test_invite_single_member_unsuccessfully(self):
group_id = '111111111111111'
member_id = '44444444444444444'
status_code = 400
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_members(
{'error': {'message': 'error message'}},
status_code, group_id, member_id
)
response = self.invite_to_group(group_id, member_id)
self.assertEqual(response.status_code, 200)
self.assertTrue('error message' in response.data[member_id])
@httpretty.activate
def test_invite_multiple_members_unsuccessfully(self):
member_ids = '222222222222222,333333333333333,44444444444444444'
group_id = '111111111111111'
status_code = 400
self.set_facebook_interceptor_for_access_token()
for member_id in member_ids.split(','):
self.set_facebook_interceptor_for_members(
{'error': {'message': 'error message'}},
status_code, group_id, member_id
)
response = self.invite_to_group(group_id, member_ids)
self.assertEqual(response.status_code, 200)
for member_id in member_ids.split(','):
self.assertTrue('error message' in response.data[member_id])
"""
URLs for groups API
"""
from django.conf.urls import patterns, url
from .views import Groups, GroupsMembers
urlpatterns = patterns(
'mobile_api.social_facebook.groups.views',
url(
r'^(?P<group_id>[\d]*)$',
Groups.as_view(),
name='create-delete-group'
),
url(
r'^(?P<group_id>[\d]+)/member/(?P<member_id>[\d]*,*)$',
GroupsMembers.as_view(),
name='add-remove-member'
)
)
"""
Views for groups info API
"""
from rest_framework import generics, status, mixins
from rest_framework.response import Response
from django.conf import settings
import facebook
from ...utils import mobile_view
from . import serializers
@mobile_view()
class Groups(generics.CreateAPIView, mixins.DestroyModelMixin):
"""
**Use Case**
An API to Create or Delete course groups.
Note: The Delete is not invoked from the current version of the app
and is used only for testing with facebook dependencies.
**Creation Example request**:
POST /api/mobile/v0.5/social/facebook/groups/
Parameters: name : string,
description : string,
privacy : open/closed
**Creation Response Values**
{"id": group_id}
**Deletion Example request**:
DELETE /api/mobile/v0.5/social/facebook/groups/<group_id>
**Deletion Response Values**
{"success" : "true"}
"""
serializer_class = serializers.GroupSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
app_groups_response = facebook_graph_api().request(
settings.FACEBOOK_API_VERSION + '/' + settings.FACEBOOK_APP_ID + "/groups",
post_args=request.POST.dict()
)
return Response(app_groups_response)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, *args, **kwargs): # pylint: disable=unused-argument
"""
Deletes the course group.
"""
try:
return Response(
facebook_graph_api().request(
settings.FACEBOOK_API_VERSION + '/' + settings.FACEBOOK_APP_ID + "/groups/" + kwargs['group_id'],
post_args={'method': 'delete'}
)
)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
@mobile_view()
class GroupsMembers(generics.CreateAPIView, mixins.DestroyModelMixin):
"""
**Use Case**
An API to Invite and Remove members to a group
Note: The Remove is not invoked from the current version
of the app and is used only for testing with facebook dependencies.
**Invite Example request**:
POST /api/mobile/v0.5/social/facebook/groups/<group_id>/member/
Parameters: members : int,int,int...
**Invite Response Values**
{"member_id" : success/error_message}
A response with each member_id and whether or not the member was added successfully.
If the member was not added successfully the Facebook error message is provided.
**Remove Example request**:
DELETE /api/mobile/v0.5/social/facebook/groups/<group_id>/member/<member_id>
**Remove Response Values**
{"success" : "true"}
"""
serializer_class = serializers.GroupsMembersSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
graph = facebook_graph_api()
url = settings.FACEBOOK_API_VERSION + '/' + kwargs['group_id'] + "/members"
member_ids = serializer.data['member_ids'].split(',')
response = {}
for member_id in member_ids:
try:
if 'success' in graph.request(url, post_args={'member': member_id}):
response[member_id] = 'success'
except facebook.GraphAPIError, ex:
response[member_id] = ex.result['error']['message']
return Response(response, status=status.HTTP_200_OK)
def delete(self, request, *args, **kwargs): # pylint: disable=unused-argument
"""
Deletes the member from the course group.
"""
try:
return Response(
facebook_graph_api().request(
settings.FACEBOOK_API_VERSION + '/' + kwargs['group_id'] + "/members",
post_args={'method': 'delete', 'member': kwargs['member_id']}
)
)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
def facebook_graph_api():
"""
Returns the result from calling Facebook's Graph API with the app's access token.
"""
return facebook.GraphAPI(facebook.get_app_access_token(settings.FACEBOOK_APP_ID, settings.FACEBOOK_APP_SECRET))
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for Share Settings API
"""
from rest_framework import serializers
class UserSharingSerializar(serializers.Serializer):
"""
Serializes user social settings
"""
share_with_facebook_friends = serializers.BooleanField(required=True)
"""
Tests for users sharing preferences
"""
from django.core.urlresolvers import reverse
from ..test_utils import SocialFacebookTestCase
class StudentProfileViewTest(SocialFacebookTestCase):
""" Tests for the student profile views. """
USERNAME = u'bnotions'
PASSWORD = u'horse'
EMAIL = u'horse@bnotions.com'
FULL_NAME = u'bnotions horse'
def setUp(self):
super(StudentProfileViewTest, self).setUp()
self.user_create_and_signin(1)
def assert_shared_value(self, response, expected_value='True'):
"""
Tests whether the response is successful and whether the
share_with_facebook_friends value is set to the expected value.
"""
self.assertEqual(response.status_code, 200)
self.assertTrue('share_with_facebook_friends' in response.data)
self.assertTrue(expected_value in response.data['share_with_facebook_friends'])
def test_set_preferences_to_true(self):
url = reverse('preferences')
response = self.client.post(url, {'share_with_facebook_friends': 'True'})
self.assert_shared_value(response)
def test_set_preferences_to_false(self):
url = reverse('preferences')
response = self.client.post(url, {'share_with_facebook_friends': 'False'})
self.assert_shared_value(response, 'False')
def test_set_preferences_no_parameters(self):
# Note that if no value is given it will default to False
url = reverse('preferences')
response = self.client.post(url, {})
self.assert_shared_value(response, 'False')
def test_set_preferences_invalid_parameters(self):
# Note that if no value is given it will default to False
# also in the case of invalid parameters
url = reverse('preferences')
response = self.client.post(url, {'bad_param': 'False'})
self.assert_shared_value(response, 'False')
def test_get_preferences_after_setting_them(self):
url = reverse('preferences')
for boolean in ['True', 'False']:
# Set the preference
response = self.client.post(url, {'share_with_facebook_friends': boolean})
self.assert_shared_value(response, boolean)
# Get the preference
response = self.client.get(url)
self.assert_shared_value(response, boolean)
def test_get_preferences_without_setting_them(self):
url = reverse('preferences')
# Get the preference
response = self.client.get(url)
self.assert_shared_value(response, 'False')
"""
URLs for users sharing preferences
"""
from django.conf.urls import patterns, url
from .views import UserSharing
urlpatterns = patterns(
'mobile_api.social_facebook.preferences.views',
url(
r'^preferences/$',
UserSharing.as_view(),
name='preferences'
),
)
"""
Views for users sharing preferences
"""
from rest_framework import generics, status
from rest_framework.response import Response
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences, set_user_preference
from ...utils import mobile_view
from . import serializers
@mobile_view()
class UserSharing(generics.ListCreateAPIView):
"""
**Use Case**
An API to retrieve or update the users social sharing settings
**GET Example request**:
GET /api/mobile/v0.5/settings/preferences/
**GET Response Values**
{'share_with_facebook_friends': 'True'}
**POST Example request**:
POST /api/mobile/v0.5/settings/preferences/
paramters: share_with_facebook_friends : True
**POST Response Values**
{'share_with_facebook_friends': 'True'}
"""
serializer_class = serializers.UserSharingSerializar
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
value = serializer.data['share_with_facebook_friends']
set_user_preference(request.user, "share_with_facebook_friends", value)
return self.get(request, *args, **kwargs)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, *args, **kwargs):
preferences = get_user_preferences(request.user)
response = {'share_with_facebook_friends': preferences.get('share_with_facebook_friends', 'False')}
return Response(response)
"""
Test utils for Facebook functionality
"""
import httpretty
import json
from rest_framework.test import APITestCase
from django.conf import settings
from django.core.urlresolvers import reverse
from social.apps.django_app.default.models import UserSocialAuth
from course_modes.models import CourseMode
from student.models import CourseEnrollment
from student.views import login_oauth_token
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.factories import UserFactory
class SocialFacebookTestCase(ModuleStoreTestCase, APITestCase):
"""
Base Class for social test cases
"""
USERS = {
1: {'USERNAME': "TestUser One",
'EMAIL': "test_one@ebnotions.com",
'PASSWORD': "edx",
'FB_ID': "11111111111111111"},
2: {'USERNAME': "TestUser Two",
'EMAIL': "test_two@ebnotions.com",
'PASSWORD': "edx",
'FB_ID': "22222222222222222"},
3: {'USERNAME': "TestUser Three",
'EMAIL': "test_three@ebnotions.com",
'PASSWORD': "edx",
'FB_ID': "33333333333333333"}
}
BACKEND = "facebook"
USER_URL = "https://graph.facebook.com/me"
UID_FIELD = "id"
_FB_USER_ACCESS_TOKEN = 'ThisIsAFakeFacebookToken'
users = {}
def setUp(self):
super(SocialFacebookTestCase, self).setUp()
def set_facebook_interceptor_for_access_token(self):
"""
Facebook interceptor for groups access_token
"""
httpretty.register_uri(
httpretty.GET,
'https://graph.facebook.com/oauth/access_token?client_secret=' +
settings.FACEBOOK_APP_SECRET + '&grant_type=client_credentials&client_id=' +
settings.FACEBOOK_APP_ID,
body='FakeToken=FakeToken',
status=200
)
def set_facebook_interceptor_for_groups(self, data, status):
"""
Facebook interceptor for groups test
"""
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/' + settings.FACEBOOK_API_VERSION +
'/' + settings.FACEBOOK_APP_ID + '/groups',
body=json.dumps(data),
status=status
)
def set_facebook_interceptor_for_members(self, data, status, group_id, member_id):
"""
Facebook interceptor for group members tests
"""
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/' + settings.FACEBOOK_API_VERSION +
'/' + group_id + '/members?member=' + member_id +
'&access_token=FakeToken',
body=json.dumps(data),
status=status
)
def set_facebook_interceptor_for_friends(self, data):
"""
Facebook interceptor for friends tests
"""
httpretty.register_uri(
httpretty.GET,
"https://graph.facebook.com/v2.2/me/friends",
body=json.dumps(data),
status=201
)
def delete_group(self, group_id):
"""
Invoke the delete groups view
"""
url = reverse('create-delete-group', kwargs={'group_id': group_id})
response = self.client.delete(url)
return response
def invite_to_group(self, group_id, member_ids):
"""
Invoke the invite to group view
"""
url = reverse('add-remove-member', kwargs={'group_id': group_id, 'member_id': ''})
return self.client.post(url, {'member_ids': member_ids})
def remove_from_group(self, group_id, member_id):
"""
Invoke the remove from group view
"""
url = reverse('add-remove-member', kwargs={'group_id': group_id, 'member_id': member_id})
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
def link_edx_account_to_social(self, user, backend, social_uid):
"""
Register the user to the social auth backend
"""
reverse(login_oauth_token, kwargs={"backend": backend})
UserSocialAuth.objects.create(user=user, provider=backend, uid=social_uid)
def set_sharing_preferences(self, user, boolean_value):
"""
Sets self.user's share settings to boolean_value
"""
# Note that setting the value to boolean will result in the conversion to the unicode form of the boolean.
set_user_preference(user, 'share_with_facebook_friends', boolean_value)
self.assertEqual(get_user_preference(user, 'share_with_facebook_friends'), unicode(boolean_value))
def _change_enrollment(self, action, course_id=None, email_opt_in=None):
"""
Change the student's enrollment status in a course.
Args:
action (string): The action to perform (either "enroll" or "unenroll")
Keyword Args:
course_id (unicode): If provided, use this course ID. Otherwise, use the
course ID created in the setup for this test.
email_opt_in (unicode): If provided, pass this value along as
an additional GET parameter.
"""
if course_id is None:
course_id = unicode(self.course.id)
params = {
'enrollment_action': action,
'course_id': course_id
}
if email_opt_in:
params['email_opt_in'] = email_opt_in
return self.client.post(reverse('change_enrollment'), params)
def user_create_and_signin(self, user_number):
"""
Create a user and sign them in
"""
self.users[user_number] = UserFactory.create(
username=self.USERS[user_number]['USERNAME'],
email=self.USERS[user_number]['EMAIL'],
password=self.USERS[user_number]['PASSWORD']
)
self.client.login(username=self.USERS[user_number]['USERNAME'], password=self.USERS[user_number]['PASSWORD'])
def enroll_in_course(self, user, course):
"""
Enroll a user in the course
"""
resp = self._change_enrollment('enroll', course_id=course.id)
self.assertEqual(resp.status_code, 200)
self.assertTrue(CourseEnrollment.is_enrolled(user, course.id))
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
"""
URLs for Social Facebook
"""
from django.conf.urls import patterns, url, include
urlpatterns = patterns(
'',
url(r'^courses/', include('mobile_api.social_facebook.courses.urls')),
url(r'^friends/', include('mobile_api.social_facebook.friends.urls')),
url(r'^groups/', include('mobile_api.social_facebook.groups.urls')),
)
"""
Common utility methods and decorators for Social Facebook APIs.
"""
import json
import urllib2
import facebook
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from rest_framework.response import Response
from social.apps.django_app.default.models import UserSocialAuth
from openedx.core.djangoapps.user_api.models import UserPreference
from student.models import User
# TODO
# The pagination strategy needs to be further flushed out.
# What is the default page size for the facebook Graph API? 25? Is the page size a parameter that can be tweaked?
# If a user has a large number of friends, we would be calling the FB API num_friends/page_size times.
#
# However, on the app, we don't plan to display all those friends anyway.
# If we do, for scalability, the endpoints themselves would need to be paginated.
def get_pagination(friends):
"""
Get paginated data from FaceBook response
"""
data = friends['data']
while 'paging' in friends and 'next' in friends['paging']:
response = urllib2.urlopen(friends['paging']['next'])
friends = json.loads(response.read())
data = data + friends['data']
return data
def get_friends_from_facebook(serializer):
"""
Return a list with the result of a facebook /me/friends call
using the oauth_token contained within the serializer object.
If facebook returns an error, return a response object containing
the error message.
"""
try:
graph = facebook.GraphAPI(serializer.data['oauth_token'])
friends = graph.request(settings.FACEBOOK_API_VERSION + "/me/friends")
return get_pagination(friends)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
def get_linked_edx_accounts(data):
"""
Return a list of friends from the input that are edx users with the
additional attributes of edX_id and edX_username
"""
friends_that_are_edx_users = []
for friend in data:
query_set = UserSocialAuth.objects.filter(uid=unicode(friend['id']))
if query_set.count() == 1:
friend['edX_id'] = query_set[0].user_id
friend['edX_username'] = query_set[0].user.username
friends_that_are_edx_users.append(friend)
return friends_that_are_edx_users
def share_with_facebook_friends(friend):
"""
Return true if the user's share_with_facebook_friends preference is set to true.
"""
# Calling UserPreference directly because the requesting user may be different (and not is_staff).
try:
existing_user = User.objects.get(username=friend['edX_username'])
except ObjectDoesNotExist:
return False
return UserPreference.get_value(existing_user, 'share_with_facebook_friends') == 'True'
"""
URLs for mobile API
"""
from django.conf import settings
from django.conf.urls import patterns, url, include
from .users.views import my_user_info
......@@ -13,9 +12,3 @@ urlpatterns = patterns(
url(r'^video_outlines/', include('mobile_api.video_outlines.urls')),
url(r'^course_info/', include('mobile_api.course_info.urls')),
)
if settings.FEATURES["ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES"]:
urlpatterns += (
url(r'^social/facebook/', include('mobile_api.social_facebook.urls')),
url(r'^settings/', include('mobile_api.social_facebook.preferences.urls')),
)
......@@ -77,10 +77,7 @@ class CourseOverviewField(serializers.RelatedField):
request=request,
),
# Note: The following 2 should be deprecated.
'social_urls': {
'facebook': course_overview.facebook_url,
},
# Note: The following should be deprecated.
'latest_updates': {
'video': None
},
......
......@@ -253,23 +253,6 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
)
)
def test_no_facebook_url(self):
self.login_and_enroll()
response = self.api_response()
course_data = response.data[0]['course']
self.assertIsNone(course_data['social_urls']['facebook'])
def test_facebook_url(self):
self.login_and_enroll()
self.course.facebook_url = "http://facebook.com/test_group_page"
self.store.update_item(self.course, self.user.id)
response = self.api_response()
course_data = response.data[0]['course']
self.assertEquals(course_data['social_urls']['facebook'], self.course.facebook_url)
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_discussion_url(self):
self.login_and_enroll()
......
......@@ -271,7 +271,6 @@ FEATURES = {
# Expose Mobile REST API. Note that if you use this, you must also set
# ENABLE_OAUTH2_PROVIDER to True
'ENABLE_MOBILE_REST_API': False,
'ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES': False,
# Enable temporary APIs required for xBlocks on Mobile
'ENABLE_COURSE_BLOCKS_NAVIGATION_API': False,
......
......@@ -293,7 +293,6 @@ OIDC_COURSE_HANDLER_CACHE_TIMEOUT = 0
########################### External REST APIs #################################
FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
FEATURES['ENABLE_COURSE_BLOCKS_NAVIGATION_API'] = True
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_overviews', '0007_courseoverviewimageconfig'),
]
operations = [
migrations.RemoveField(
model_name='courseoverview',
name='facebook_url',
),
]
......@@ -66,7 +66,6 @@ class CourseOverview(TimeStampedModel):
# URLs
course_image_url = TextField()
facebook_url = TextField(null=True)
social_sharing_url = TextField(null=True)
end_of_course_survey_url = TextField(null=True)
......@@ -156,7 +155,6 @@ class CourseOverview(TimeStampedModel):
announcement=course.announcement,
course_image_url=course_image_url(course),
facebook_url=course.facebook_url,
social_sharing_url=course.social_sharing_url,
certificates_display_behavior=course.certificates_display_behavior,
......
......@@ -91,7 +91,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
'display_number_with_default',
'display_org_with_default',
'advertised_start',
'facebook_url',
'social_sharing_url',
'certificates_display_behavior',
'certificates_show_before_end',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment