diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 176669f9fe..ad253e1506 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -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")): diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 03a835244f..1f09dfce23 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -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."), diff --git a/lms/djangoapps/course_structure_api/v0/tests.py b/lms/djangoapps/course_structure_api/v0/tests.py index 726db58583..921b34d005 100644 --- a/lms/djangoapps/course_structure_api/v0/tests.py +++ b/lms/djangoapps/course_structure_api/v0/tests.py @@ -3,10 +3,8 @@ Run these tests @ Devstack: paver test_system -s lms --fasttest --verbose --test_id=lms/djangoapps/course_structure_api """ # pylint: disable=missing-docstring,invalid-name,maybe-no-member,attribute-defined-outside-init -from abc import ABCMeta from datetime import datetime from mock import patch, Mock -from itertools import product from django.core.urlresolvers import reverse @@ -15,13 +13,11 @@ from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory from opaque_keys.edx.locator import CourseLocator from xmodule.error_module import ErrorDescriptor from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.xml import CourseLocationManager from xmodule.tests import get_test_system -from student.tests.factories import UserFactory, CourseEnrollmentFactory from courseware.tests.factories import GlobalStaffFactory, StaffFactory from openedx.core.djangoapps.content.course_structures.models import CourseStructure from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure @@ -457,232 +453,3 @@ class CourseGradingPolicyMissingFieldsTests(CourseDetailTestMixin, CourseViewTes } ] self.assertListEqual(response.data, expected) - - -##################################################################################### -# -# The following Mixins/Classes collectively test the CourseBlocksAndNavigation view. -# -# The class hierarchy is: -# -# -----------------> CourseBlocksOrNavigationTestMixin <-------------- -# | ^ | -# | | | -# | CourseNavigationTestMixin | CourseBlocksTestMixin | -# | ^ ^ | ^ ^ | -# | | | | | | | -# | | | | | | | -# CourseNavigationTests CourseBlocksAndNavigationTests CourseBlocksTests -# -# -# Each Test Mixin is an abstract class that implements tests specific to its -# corresponding functionality. -# -# The concrete Test classes are expected to define the following class fields: -# -# block_navigation_view_type - The view's name as it should be passed to the django -# reverse method. -# container_fields - A list of fields that are expected to be included in the view's -# response for all container block types. -# block_fields - A list of fields that are expected to be included in the view's -# response for all block types. -# -###################################################################################### - - -class CourseBlocksOrNavigationTestMixin(CourseDetailTestMixin, CourseViewTestsMixin): - """ - A Mixin class for testing all views related to Course blocks and/or navigation. - """ - __metaclass__ = ABCMeta - - view_supports_debug_mode = False - - def setUp(self): - """ - Override the base `setUp` method to enroll the user in the course, since these views - require enrollment for non-staff users. - """ - super(CourseBlocksOrNavigationTestMixin, self).setUp() - CourseEnrollmentFactory(user=self.user, course_id=self.course.id) - - def create_user(self): - """ - Override the base `create_user` method to test with non-staff users for these views. - """ - self.user = UserFactory.create() - - @property - def view(self): - """ - Returns the name of the view for testing to use in the django `reverse` call. - """ - return 'course_structure_api:v0:' + self.block_navigation_view_type - - def test_get(self): - with check_mongo_calls(4): - response = super(CourseBlocksOrNavigationTestMixin, self).test_get() - - # verify root element - self.assertIn('root', response.data) - root_string = unicode(self.course.location) - self.assertEquals(response.data['root'], root_string) - - # verify ~blocks element - self.assertTrue(self.block_navigation_view_type in response.data) - blocks = response.data[self.block_navigation_view_type] - - # verify number of blocks - self.assertEquals(len(blocks), 5) - - # verify fields in blocks - for field, block in product(self.block_fields, blocks.values()): - self.assertIn(field, block) - - # verify container fields in container blocks - for field in self.container_fields: - self.assertIn(field, blocks[root_string]) - - def test_parse_error(self): - """ - Verifies the view returns a 400 when a query parameter is incorrectly formatted. - """ - response = self.http_get_for_course(data={'block_json': 'incorrect'}) - self.assertEqual(response.status_code, 400) - - @SharedModuleStoreTestCase.modifies_courseware - def test_no_access_to_block(self): - """ - Verifies the view returns only the top-level course block, excluding the sequential block - and its descendants when the user does not have access to the sequential. - """ - self.sequential.visible_to_staff_only = True - modulestore().update_item(self.sequential, self.user.id) - - response = super(CourseBlocksOrNavigationTestMixin, self).test_get() - self.assertEquals(len(response.data[self.block_navigation_view_type]), 1) - - -class CourseBlocksTestMixin(object): - """ - A Mixin class for testing all views related to Course blocks. - """ - __metaclass__ = ABCMeta - - view_supports_debug_mode = False - block_fields = ['id', 'type', 'display_name', 'web_url', 'block_url', 'graded', 'format'] - - def test_block_json(self): - """ - Verifies the view's response when the block_json data is requested. - """ - response = self.http_get_for_course( - data={'block_json': '{"video":{"profiles":["mobile_low"]}}'} - ) - self.assertEquals(response.status_code, 200) - video_block = response.data[self.block_navigation_view_type][unicode(self.video.location)] - self.assertIn('block_json', video_block) - - def test_block_count(self): - """ - Verifies the view's response when the block_count data is requested. - """ - response = self.http_get_for_course( - data={'block_count': 'problem'} - ) - self.assertEquals(response.status_code, 200) - root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)] - self.assertIn('block_count', root_block) - self.assertIn('problem', root_block['block_count']) - self.assertEquals(root_block['block_count']['problem'], 1) - - def test_multi_device_support(self): - """ - Verifies the view's response when multi_device support is requested. - """ - response = self.http_get_for_course( - data={'fields': 'multi_device'} - ) - self.assertEquals(response.status_code, 200) - - for block, expected_multi_device_support in ( - (self.problem, True), - (self.html, True), - (self.video, False) - ): - block_response = response.data[self.block_navigation_view_type][unicode(block.location)] - self.assertEquals(block_response['multi_device'], expected_multi_device_support) - - -class CourseNavigationTestMixin(object): - """ - A Mixin class for testing all views related to Course navigation. - """ - __metaclass__ = ABCMeta - - def test_depth_zero(self): - """ - Tests that all descendants are bundled into the root block when the navigation_depth is set to 0. - """ - response = self.http_get_for_course( - data={'navigation_depth': '0'} - ) - root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)] - self.assertIn('descendants', root_block) - self.assertEquals(len(root_block['descendants']), 4) - - def test_depth(self): - """ - Tests that all container blocks have descendants listed in their data. - """ - response = self.http_get_for_course() - - container_descendants = ( - (self.course.location, 1), - (self.sequential.location, 3), - ) - for container_location, expected_num_descendants in container_descendants: - block = response.data[self.block_navigation_view_type][unicode(container_location)] - self.assertIn('descendants', block) - self.assertEquals(len(block['descendants']), expected_num_descendants) - - -class CourseBlocksTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, SharedModuleStoreTestCase): - """ - A Test class for testing the Course 'blocks' view. - """ - block_navigation_view_type = 'blocks' - container_fields = ['children'] - - @classmethod - def setUpClass(cls): - super(CourseBlocksTests, cls).setUpClass() - cls.create_course_data() - - -class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationTestMixin, SharedModuleStoreTestCase): - """ - A Test class for testing the Course 'navigation' view. - """ - block_navigation_view_type = 'navigation' - container_fields = ['descendants'] - block_fields = [] - - @classmethod - def setUpClass(cls): - super(CourseNavigationTests, cls).setUpClass() - cls.create_course_data() - - -class CourseBlocksAndNavigationTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, - CourseNavigationTestMixin, SharedModuleStoreTestCase): - """ - A Test class for testing the Course 'blocks+navigation' view. - """ - block_navigation_view_type = 'blocks+navigation' - container_fields = ['children', 'descendants'] - - @classmethod - def setUpClass(cls): - super(CourseBlocksAndNavigationTests, cls).setUpClass() - cls.create_course_data() diff --git a/lms/djangoapps/course_structure_api/v0/urls.py b/lms/djangoapps/course_structure_api/v0/urls.py index 035b1a023a..11ba6aef45 100644 --- a/lms/djangoapps/course_structure_api/v0/urls.py +++ b/lms/djangoapps/course_structure_api/v0/urls.py @@ -20,27 +20,3 @@ urlpatterns = patterns( name='grading_policy' ), ) - -if settings.FEATURES.get('ENABLE_COURSE_BLOCKS_NAVIGATION_API'): - # TODO (MA-789) This endpoint still needs to be approved by the arch council. - # TODO (MA-704) This endpoint still needs to be made performant. - urlpatterns += ( - url( - r'^courses/{}/blocks/$'.format(COURSE_ID_PATTERN), - views.CourseBlocksAndNavigation.as_view(), - {'return_blocks': True, 'return_nav': False}, - name='blocks' - ), - url( - r'^courses/{}/navigation/$'.format(COURSE_ID_PATTERN), - views.CourseBlocksAndNavigation.as_view(), - {'return_blocks': False, 'return_nav': True}, - name='navigation' - ), - url( - r'^courses/{}/blocks\+navigation/$'.format(COURSE_ID_PATTERN), - views.CourseBlocksAndNavigation.as_view(), - {'return_blocks': True, 'return_nav': True}, - name='blocks+navigation' - ), - ) diff --git a/lms/djangoapps/course_structure_api/v0/views.py b/lms/djangoapps/course_structure_api/v0/views.py index 50359ecb9c..a61e94433a 100644 --- a/lms/djangoapps/course_structure_api/v0/views.py +++ b/lms/djangoapps/course_structure_api/v0/views.py @@ -1,7 +1,5 @@ """ API implementation for course-oriented interactions. """ -from collections import namedtuple -import json import logging from django.conf import settings @@ -12,20 +10,15 @@ from rest_framework.exceptions import AuthenticationFailed, ParseError from rest_framework.generics import RetrieveAPIView, ListAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.reverse import reverse from xmodule.modulestore.django import modulestore from opaque_keys.edx.keys import CourseKey from course_structure_api.v0 import serializers from courseware import courses from courseware.access import has_access -from courseware.model_data import FieldDataCache -from courseware.module_render import get_module_for_descriptor -from openedx.core.lib.api.view_utils import view_course_access, view_auth_classes from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors from openedx.core.lib.exceptions import CourseNotFoundError from student.roles import CourseInstructorRole, CourseStaffRole -from util.module_utils import get_dynamic_descriptor_children log = logging.getLogger(__name__) @@ -297,396 +290,3 @@ class CourseGradingPolicy(CourseViewMixin, ListAPIView): @CourseViewMixin.course_check def get(self, request, **kwargs): return Response(api.course_grading_policy(self.course_key)) - - -@view_auth_classes() -class CourseBlocksAndNavigation(ListAPIView): - """ - **Use Case** - - The following endpoints return the content of the course according to the requesting user's access level. - - * Blocks - Get the course's blocks. - - * Navigation - Get the course's navigation information per the navigation depth requested. - - * Blocks+Navigation - Get both the course's blocks and the course's navigation information. - - **Example requests**: - - GET api/course_structure/v0/courses/{course_id}/blocks/ - GET api/course_structure/v0/courses/{course_id}/navigation/ - GET api/course_structure/v0/courses/{course_id}/blocks+navigation/ - &block_count=video - &block_json={"video":{"profiles":["mobile_low"]}} - &fields=graded,format,multi_device - - **Parameters**: - - * block_json: (dict) Indicates for which block types to return student_view_json data. The key is the block - type and the value is the "context" that is passed to the block's student_view_json method. - - Example: block_json={"video":{"profiles":["mobile_high","mobile_low"]}} - - * block_count: (list) Indicates for which block types to return the aggregate count of the blocks. - - Example: block_count="video,problem" - - * fields: (list) Indicates which additional fields to return for each block. - Default is "children,graded,format,multi_device" - - Example: fields=graded,format,multi_device - - * navigation_depth (integer) Indicates how far deep to traverse into the course hierarchy before bundling - all the descendants. - Default is 3 since typical navigational views of the course show a maximum of chapter->sequential->vertical. - - Example: navigation_depth=3 - - **Response Values** - - The following fields are returned with a successful response. - Only either one of blocks, navigation, or blocks+navigation is returned depending on which endpoint is used. - The "root" field is returned for all endpoints. - - * root: The ID of the root node of the course blocks. - - * blocks: A dictionary that maps block usage IDs to a collection of information about each block. - Each block contains the following fields. Returned only if using the "blocks" endpoint. - - * id: (string) The usage ID of the block. - - * type: (string) The type of block. Possible values include course, chapter, sequential, vertical, html, - problem, video, and discussion. The type can also be the name of a custom type of block used for the course. - - * display_name: (string) The display name of the block. - - * children: (list) If the block has child blocks, a list of IDs of the child blocks. - Returned only if the "children" input parameter is True. - - * block_count: (dict) For each block type specified in the block_count parameter to the endpoint, the - aggregate number of blocks of that type for this block and all of its descendants. - Returned only if the "block_count" input parameter contains this block's type. - - * block_json: (dict) The JSON data for this block. - Returned only if the "block_json" input parameter contains this block's type. - - * block_url: (string) The URL to retrieve the HTML rendering of this block. The HTML could include - CSS and Javascript code. This URL can be used as a fallback if the custom block_json for this - block type is not requested and not supported. - - * web_url: (string) The URL to the website location of this block. This URL can be used as a further - fallback if the block_url and the block_json is not supported. - - * graded (boolean) Whether or not the block or any of its descendants is graded. - Returned only if "graded" is included in the "fields" parameter. - - * format: (string) The assignment type of the block. - Possible values can be "Homework", "Lab", "Midterm Exam", and "Final Exam". - Returned only if "format" is included in the "fields" parameter. - - * multi_device: (boolean) Whether or not the block's rendering obtained via block_url has support - for multiple devices. - Returned only if "multi_device" is included in the "fields" parameter. - - * navigation: A dictionary that maps block IDs to a collection of navigation information about each block. - Each block contains the following fields. Returned only if using the "navigation" endpoint. - - * descendants: (list) A list of IDs of the children of the block if the block's depth in the - course hierarchy is less than the navigation_depth. Otherwise, a list of IDs of the aggregate descendants - of the block. - - * blocks+navigation: A dictionary that combines both the blocks and navigation data. - Returned only if using the "blocks+navigation" endpoint. - - """ - class RequestInfo(object): - """ - A class for encapsulating the request information, including what optional fields are requested. - """ - DEFAULT_FIELDS = "children,graded,format,multi_device" - - def __init__(self, request, course): - self.request = request - self.course = course - self.field_data_cache = None - - # check what fields are requested - try: - # fields - self.fields = set(request.GET.get('fields', self.DEFAULT_FIELDS).split(",")) - - # block_count - self.block_count = request.GET.get('block_count', "") - self.block_count = ( - self.block_count.split(",") if self.block_count else [] - ) - - # navigation_depth - # See docstring for why we default to 3. - self.navigation_depth = int(request.GET.get('navigation_depth', '3')) - - # block_json - self.block_json = json.loads(request.GET.get('block_json', "{}")) - if self.block_json and not isinstance(self.block_json, dict): - raise ParseError - except: - raise ParseError - - class ResultData(object): - """ - A class for encapsulating the result information, specifically the blocks and navigation data. - """ - def __init__(self, return_blocks, return_nav): - self.blocks = {} - self.navigation = {} - if return_blocks and return_nav: - self.navigation = self.blocks - - def update_response(self, response, return_blocks, return_nav): - """ - Updates the response object with result information. - """ - if return_blocks and return_nav: - response["blocks+navigation"] = self.blocks - elif return_blocks: - response["blocks"] = self.blocks - elif return_nav: - response["navigation"] = self.navigation - - class BlockInfo(object): - """ - A class for encapsulating a block's information as needed during traversal of a block hierarchy. - """ - def __init__(self, block, request_info, parent_block_info=None): - # the block for which the recursion is being computed - self.block = block - - # the type of the block - self.type = block.category - - # the block's depth in the block hierarchy - self.depth = 0 - - # the block's children - self.children = [] - - # descendants_of_parent: the list of descendants for this block's parent - self.descendants_of_parent = [] - self.descendants_of_self = [] - - # if a parent block was provided, update this block's data based on the parent's data - if parent_block_info: - # increment this block's depth value - self.depth = parent_block_info.depth + 1 - - # set this blocks' descendants_of_parent - self.descendants_of_parent = parent_block_info.descendants_of_self - - # add ourselves to the parent's children, if requested. - if 'children' in request_info.fields: - parent_block_info.value.setdefault("children", []).append(unicode(block.location)) - - # the block's data to include in the response - self.value = { - "id": unicode(block.location), - "type": self.type, - "display_name": block.display_name, - "web_url": reverse( - "jump_to", - kwargs={"course_id": unicode(request_info.course.id), "location": unicode(block.location)}, - request=request_info.request, - ), - "block_url": reverse( - "courseware.views.render_xblock", - kwargs={"usage_key_string": unicode(block.location)}, - request=request_info.request, - ), - } - - @view_course_access(depth=None) - def list(self, request, course, return_blocks=True, return_nav=True, *args, **kwargs): - """ - REST API endpoint for listing all the blocks and/or navigation information in the course, - while regarding user access and roles. - - Arguments: - request - Django request object - course - course module object - return_blocks - If true, returns the blocks information for the course. - return_nav - If true, returns the navigation information for the course. - """ - # set starting point - start_block = course - - # initialize request and result objects - request_info = self.RequestInfo(request, course) - result_data = self.ResultData(return_blocks, return_nav) - - # create and populate a field data cache by pre-fetching for the course (with depth=None) - request_info.field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - course.id, request.user, course, depth=None, - ) - - # start the recursion with the start_block - self.recurse_blocks_nav(request_info, result_data, self.BlockInfo(start_block, request_info)) - - # return response - response = {"root": unicode(start_block.location)} - result_data.update_response(response, return_blocks, return_nav) - return Response(response) - - def recurse_blocks_nav(self, request_info, result_data, block_info): - """ - A depth-first recursive function that supports calculation of both the list of blocks in the course - and the navigation information up to the requested navigation_depth of the course. - - Arguments: - request_info - Object encapsulating the request information. - result_data - Running result data that is updated during the recursion. - block_info - Information about the current block in the recursion. - """ - # bind user data to the block - block_info.block = get_module_for_descriptor( - request_info.request.user, - request_info.request, - block_info.block, - request_info.field_data_cache, - request_info.course.id, - course=request_info.course - ) - - # verify the user has access to this block - if (block_info.block is None or not has_access( - request_info.request.user, - 'load', - block_info.block, - course_key=request_info.course.id - )): - return - - # add the block's value to the result - result_data.blocks[unicode(block_info.block.location)] = block_info.value - - # descendants - self.update_descendants(request_info, result_data, block_info) - - # children: recursively call the function for each of the children, while supporting dynamic children. - if block_info.block.has_children: - block_info.children = get_dynamic_descriptor_children(block_info.block, request_info.request.user.id) - for child in block_info.children: - self.recurse_blocks_nav( - request_info, - result_data, - self.BlockInfo(child, request_info, parent_block_info=block_info) - ) - - # block count - self.update_block_count(request_info, result_data, block_info) - - # block JSON data - self.add_block_json(request_info, block_info) - - # multi-device support - if 'multi_device' in request_info.fields: - block_info.value['multi_device'] = block_info.block.has_support( - getattr(block_info.block, 'student_view', None), - 'multi_device' - ) - - # additional fields - self.add_additional_fields(request_info, block_info) - - def update_descendants(self, request_info, result_data, block_info): - """ - Updates the descendants data for the current block. - - The current block is added to its parent's descendants if it is visible in the navigation - (i.e., the 'hide_from_toc' setting is False). - - Additionally, the block's depth is compared with the navigation_depth parameter to determine whether the - descendants of the block should be added to its own descendants (if block.depth <= navigation_depth) - or to the descendants of the block's parents (if block.depth > navigation_depth). - - block_info.descendants_of_self is the list of descendants that is passed to this block's children. - It should be either: - descendants_of_parent - if this block's depth is greater than the requested navigation_depth. - a dangling [] - if this block's hide_from_toc is True. - a referenced [] in navigation[block.location]["descendants"] - if this block's depth is within - the requested navigation depth. - """ - # Blocks with the 'hide_from_toc' setting are accessible, just not navigatable from the table-of-contents. - # If the 'hide_from_toc' setting is set on the block, do not add this block to the parent's descendants - # list and let the block's descendants add themselves to a dangling (unreferenced) descendants list. - if not block_info.block.hide_from_toc: - # add this block to the parent's descendants - block_info.descendants_of_parent.append(unicode(block_info.block.location)) - - # if this block's depth in the hierarchy is greater than the requested navigation depth, - # have the block's descendants add themselves to the parent's descendants. - if block_info.depth > request_info.navigation_depth: - block_info.descendants_of_self = block_info.descendants_of_parent - - # otherwise, have the block's descendants add themselves to this block's descendants by - # referencing/attaching descendants_of_self from this block's navigation value. - else: - result_data.navigation.setdefault( - unicode(block_info.block.location), {} - )["descendants"] = block_info.descendants_of_self - - def update_block_count(self, request_info, result_data, block_info): - """ - For all the block types that are requested to be counted, include the count of that block type as - aggregated from the block's descendants. - - Arguments: - request_info - Object encapsulating the request information. - result_data - Running result data that is updated during the recursion. - block_info - Information about the current block in the recursion. - """ - for b_type in request_info.block_count: - block_info.value.setdefault("block_count", {})[b_type] = ( - sum( - result_data.blocks.get(unicode(child.location), {}).get("block_count", {}).get(b_type, 0) - for child in block_info.children - ) + - (1 if b_type == block_info.type else 0) - ) - - def add_block_json(self, request_info, block_info): - """ - If the JSON data for this block's type is requested, and the block supports the 'student_view_json' - method, add the response from the 'student_view_json" method as the data for the block. - """ - if block_info.type in request_info.block_json: - if getattr(block_info.block, 'student_view_data', None): - block_info.value["block_json"] = block_info.block.student_view_data( - context=request_info.block_json[block_info.type] - ) - - # A mapping of API-exposed field names to xBlock field names and API field defaults. - BlockApiField = namedtuple('BlockApiField', 'block_field_name api_field_default') - FIELD_MAP = { - 'graded': BlockApiField(block_field_name='graded', api_field_default=False), - 'format': BlockApiField(block_field_name='format', api_field_default=None), - } - - def add_additional_fields(self, request_info, block_info): - """ - Add additional field names and values of the block as requested in the request_info. - """ - for field_name in request_info.fields: - if field_name in self.FIELD_MAP: - block_info.value[field_name] = getattr( - block_info.block, - self.FIELD_MAP[field_name].block_field_name, - self.FIELD_MAP[field_name].api_field_default, - ) - - def perform_authentication(self, request): - """ - Ensures that the user is authenticated (e.g. not an AnonymousUser) - """ - super(CourseBlocksAndNavigation, self).perform_authentication(request) - if request.user.is_anonymous(): - raise AuthenticationFailed diff --git a/lms/djangoapps/mobile_api/social_facebook/__init__.py b/lms/djangoapps/mobile_api/social_facebook/__init__.py deleted file mode 100644 index 3a45c5076a..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -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. diff --git a/lms/djangoapps/mobile_api/social_facebook/courses/__init__.py b/lms/djangoapps/mobile_api/social_facebook/courses/__init__.py deleted file mode 100644 index 738aa67827..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/courses/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Courses API -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/courses/models.py b/lms/djangoapps/mobile_api/social_facebook/courses/models.py deleted file mode 100644 index d2e8572729..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/courses/models.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -A models.py is required to make this an app (until we move to Django 1.7) -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/courses/serializers.py b/lms/djangoapps/mobile_api/social_facebook/courses/serializers.py deleted file mode 100644 index bccd9c3fd9..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/courses/serializers.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -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) diff --git a/lms/djangoapps/mobile_api/social_facebook/courses/tests.py b/lms/djangoapps/mobile_api/social_facebook/courses/tests.py deleted file mode 100644 index 26155b33eb..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/courses/tests.py +++ /dev/null @@ -1,147 +0,0 @@ -# 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) diff --git a/lms/djangoapps/mobile_api/social_facebook/courses/urls.py b/lms/djangoapps/mobile_api/social_facebook/courses/urls.py deleted file mode 100644 index 8e0b93093c..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/courses/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -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' - ), -) diff --git a/lms/djangoapps/mobile_api/social_facebook/courses/views.py b/lms/djangoapps/mobile_api/social_facebook/courses/views.py deleted file mode 100644 index b4f1a23aeb..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/courses/views.py +++ /dev/null @@ -1,65 +0,0 @@ -""" - 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) diff --git a/lms/djangoapps/mobile_api/social_facebook/friends/__init__.py b/lms/djangoapps/mobile_api/social_facebook/friends/__init__.py deleted file mode 100644 index 378fd53a74..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/friends/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Friends API -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/friends/models.py b/lms/djangoapps/mobile_api/social_facebook/friends/models.py deleted file mode 100644 index d2e8572729..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/friends/models.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -A models.py is required to make this an app (until we move to Django 1.7) -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/friends/serializers.py b/lms/djangoapps/mobile_api/social_facebook/friends/serializers.py deleted file mode 100644 index 449bbbced3..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/friends/serializers.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -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) diff --git a/lms/djangoapps/mobile_api/social_facebook/friends/tests.py b/lms/djangoapps/mobile_api/social_facebook/friends/tests.py deleted file mode 100644 index 3db80aae6f..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/friends/tests.py +++ /dev/null @@ -1,318 +0,0 @@ -# pylint: disable=E1101 -""" -Tests for friends -""" -import json -import httpretty -from django.core.urlresolvers import reverse -from xmodule.modulestore.tests.factories import CourseFactory -from ..test_utils import SocialFacebookTestCase - - -class TestFriends(SocialFacebookTestCase): - """ - Tests for /api/mobile/v0.5/friends/... - """ - - def setUp(self): - super(TestFriends, self).setUp() - self.course = CourseFactory.create() - - @httpretty.activate - def test_no_friends_enrolled(self): - # User 1 set up - self.user_create_and_signin(1) - # Link user_1's edX account to FB - self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID']) - self.set_sharing_preferences(self.users[1], True) - # Set the interceptor - 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']}, - {'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}, - ] - } - ) - course_id = unicode(self.course.id) - url = reverse('friends-in-course', kwargs={"course_id": course_id}) - response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}) - # Assert that no friends are returned - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data and len(response.data['friends']) == 0) - - @httpretty.activate - def test_no_friends_on_facebook(self): - # User 1 set up - self.user_create_and_signin(1) - # Enroll user_1 in the course - self.enroll_in_course(self.users[1], self.course) - self.set_sharing_preferences(self.users[1], True) - # Link user_1's edX account to FB - self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID']) - # Set the interceptor - self.set_facebook_interceptor_for_friends({'data': []}) - course_id = unicode(self.course.id) - url = reverse('friends-in-course', kwargs={"course_id": course_id}) - response = self.client.get( - url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN} - ) - # Assert that no friends are returned - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data and len(response.data['friends']) == 0) - - @httpretty.activate - def test_no_friends_linked_to_edx(self): - # User 1 set up - self.user_create_and_signin(1) - # Enroll user_1 in the course - self.enroll_in_course(self.users[1], self.course) - self.set_sharing_preferences(self.users[1], True) - # User 2 set up - self.user_create_and_signin(2) - # Enroll user_2 in the course - self.enroll_in_course(self.users[2], self.course) - self.set_sharing_preferences(self.users[2], True) - # User 3 set up - self.user_create_and_signin(3) - # Enroll user_3 in the course - self.enroll_in_course(self.users[3], self.course) - self.set_sharing_preferences(self.users[3], True) - - # Set the interceptor - 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']}, - {'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}, - ] - } - ) - course_id = unicode(self.course.id) - url = reverse('friends-in-course', kwargs={"course_id": course_id}) - response = self.client.get( - url, - {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN} - ) - # Assert that no friends are returned - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data and len(response.data['friends']) == 0) - - @httpretty.activate - def test_no_friends_share_settings_false(self): - # User 1 set up - self.user_create_and_signin(1) - self.enroll_in_course(self.users[1], self.course) - 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']}, - {'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}, - {'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}, - ] - } - ) - url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)}) - response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}) - - # Assert that USERNAME_1 is returned - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data) - self.assertTrue('friends' in response.data and len(response.data['friends']) == 0) - - @httpretty.activate - def test_no_friends_no_oauth_token(self): - # User 1 set up - self.user_create_and_signin(1) - self.enroll_in_course(self.users[1], self.course) - 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']}, - {'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}, - {'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}, - ] - } - ) - url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)}) - response = self.client.get(url, {'format': 'json'}) - # Assert that USERNAME_1 is returned - self.assertEqual(response.status_code, 400) - - @httpretty.activate - def test_one_friend_in_course(self): - # User 1 set up - self.user_create_and_signin(1) - self.enroll_in_course(self.users[1], self.course) - 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']}, - {'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}, - {'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}, - ] - } - ) - url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)}) - response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}) - - # Assert that USERNAME_1 is returned - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data) - self.assertTrue('id' in response.data['friends'][0]) - self.assertTrue(response.data['friends'][0]['id'] == self.USERS[1]['FB_ID']) - self.assertTrue('name' in response.data['friends'][0]) - self.assertTrue(response.data['friends'][0]['name'] == self.USERS[1]['USERNAME']) - - @httpretty.activate - def test_three_friends_in_course(self): - # User 1 set up - self.user_create_and_signin(1) - self.enroll_in_course(self.users[1], self.course) - self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID']) - self.set_sharing_preferences(self.users[1], True) - - # User 2 set up - self.user_create_and_signin(2) - self.enroll_in_course(self.users[2], self.course) - self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID']) - self.set_sharing_preferences(self.users[2], True) - - # User 3 set up - self.user_create_and_signin(3) - self.enroll_in_course(self.users[3], self.course) - self.link_edx_account_to_social(self.users[3], self.BACKEND, self.USERS[3]['FB_ID']) - self.set_sharing_preferences(self.users[3], True) - 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']}, - {'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}, - ] - } - ) - url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)}) - response = self.client.get( - url, - {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN} - ) - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data) - # Assert that USERNAME_1 is returned - self.assertTrue( - 'id' in response.data['friends'][0] and - response.data['friends'][0]['id'] == self.USERS[1]['FB_ID'] - ) - self.assertTrue( - 'name' in response.data['friends'][0] and - response.data['friends'][0]['name'] == self.USERS[1]['USERNAME'] - ) - # Assert that USERNAME_2 is returned - self.assertTrue( - 'id' in response.data['friends'][1] and - response.data['friends'][1]['id'] == self.USERS[2]['FB_ID'] - ) - self.assertTrue( - 'name' in response.data['friends'][1] and - response.data['friends'][1]['name'] == self.USERS[2]['USERNAME'] - ) - # Assert that USERNAME_3 is returned - self.assertTrue( - 'id' in response.data['friends'][2] and - response.data['friends'][2]['id'] == self.USERS[3]['FB_ID'] - ) - self.assertTrue( - 'name' in response.data['friends'][2] and - response.data['friends'][2]['name'] == self.USERS[3]['USERNAME'] - ) - - @httpretty.activate - def test_three_friends_in_paged_response(self): - # User 1 set up - self.user_create_and_signin(1) - self.enroll_in_course(self.users[1], self.course) - self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID']) - self.set_sharing_preferences(self.users[1], True) - - # User 2 set up - self.user_create_and_signin(2) - self.enroll_in_course(self.users[2], self.course) - self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID']) - self.set_sharing_preferences(self.users[2], True) - - # User 3 set up - self.user_create_and_signin(3) - self.enroll_in_course(self.users[3], self.course) - self.link_edx_account_to_social(self.users[3], self.BACKEND, self.USERS[3]['FB_ID']) - self.set_sharing_preferences(self.users[3], True) - - 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_1"}, - "summary": {"total_count": 652} - } - ) - # Set the interceptor for the first paged content - httpretty.register_uri( - httpretty.GET, - "https://graph.facebook.com/v2.2/me/friends/next_1", - body=json.dumps( - { - "data": [{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}], - "paging": {"next": "https://graph.facebook.com/v2.2/me/friends/next_2"}, - "summary": {"total_count": 652} - } - ), - status=201 - ) - # Set the interceptor for the last paged content - httpretty.register_uri( - httpretty.GET, - "https://graph.facebook.com/v2.2/me/friends/next_2", - body=json.dumps( - { - "data": [{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}], - "paging": { - "previous": - "https://graph.facebook.com/v2.2/10154805434030300/friends?limit=25&offset=25" - }, - "summary": {"total_count": 652} - } - ), - status=201 - ) - url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)}) - response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}) - self.assertEqual(response.status_code, 200) - self.assertTrue('friends' in response.data) - # Assert that USERNAME_1 is returned - self.assertTrue('id' in response.data['friends'][0]) - self.assertTrue(response.data['friends'][0]['id'] == self.USERS[1]['FB_ID']) - self.assertTrue('name' in response.data['friends'][0]) - self.assertTrue(response.data['friends'][0]['name'] == self.USERS[1]['USERNAME']) - # Assert that USERNAME_2 is returned - self.assertTrue('id' in response.data['friends'][1]) - self.assertTrue(response.data['friends'][1]['id'] == self.USERS[2]['FB_ID']) - self.assertTrue('name' in response.data['friends'][1]) - self.assertTrue(response.data['friends'][1]['name'] == self.USERS[2]['USERNAME']) - # Assert that USERNAME_3 is returned - self.assertTrue('id' in response.data['friends'][2]) - self.assertTrue(response.data['friends'][2]['id'] == self.USERS[3]['FB_ID']) - self.assertTrue('name' in response.data['friends'][2]) - self.assertTrue(response.data['friends'][2]['name'] == self.USERS[3]['USERNAME']) diff --git a/lms/djangoapps/mobile_api/social_facebook/friends/urls.py b/lms/djangoapps/mobile_api/social_facebook/friends/urls.py deleted file mode 100644 index e6e5f9141c..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/friends/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -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' - ), -) diff --git a/lms/djangoapps/mobile_api/social_facebook/friends/views.py b/lms/djangoapps/mobile_api/social_facebook/friends/views.py deleted file mode 100644 index 7e1904491d..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/friends/views.py +++ /dev/null @@ -1,71 +0,0 @@ -""" - 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/ - - 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}) diff --git a/lms/djangoapps/mobile_api/social_facebook/groups/__init__.py b/lms/djangoapps/mobile_api/social_facebook/groups/__init__.py deleted file mode 100644 index ca42614299..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/groups/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Groups API -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/groups/models.py b/lms/djangoapps/mobile_api/social_facebook/groups/models.py deleted file mode 100644 index d2e8572729..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/groups/models.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -A models.py is required to make this an app (until we move to Django 1.7) -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/groups/serializers.py b/lms/djangoapps/mobile_api/social_facebook/groups/serializers.py deleted file mode 100644 index 068d1a04b4..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/groups/serializers.py +++ /dev/null @@ -1,30 +0,0 @@ -""" - 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' - ), - ] - ) diff --git a/lms/djangoapps/mobile_api/social_facebook/groups/tests.py b/lms/djangoapps/mobile_api/social_facebook/groups/tests.py deleted file mode 100644 index 869946b7b2..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/groups/tests.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -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]) diff --git a/lms/djangoapps/mobile_api/social_facebook/groups/urls.py b/lms/djangoapps/mobile_api/social_facebook/groups/urls.py deleted file mode 100644 index a1cbcfc19c..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/groups/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -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[\d]*)$', - Groups.as_view(), - name='create-delete-group' - ), - url( - r'^(?P[\d]+)/member/(?P[\d]*,*)$', - GroupsMembers.as_view(), - name='add-remove-member' - ) -) diff --git a/lms/djangoapps/mobile_api/social_facebook/groups/views.py b/lms/djangoapps/mobile_api/social_facebook/groups/views.py deleted file mode 100644 index 28f0e12cc9..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/groups/views.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -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/ - - **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//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//member/ - - **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)) diff --git a/lms/djangoapps/mobile_api/social_facebook/models.py b/lms/djangoapps/mobile_api/social_facebook/models.py deleted file mode 100644 index d2e8572729..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/models.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -A models.py is required to make this an app (until we move to Django 1.7) -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/preferences/__init__.py b/lms/djangoapps/mobile_api/social_facebook/preferences/__init__.py deleted file mode 100644 index 7a7241c5ad..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/preferences/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Users Sharing preferences API -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/preferences/models.py b/lms/djangoapps/mobile_api/social_facebook/preferences/models.py deleted file mode 100644 index d2e8572729..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/preferences/models.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -A models.py is required to make this an app (until we move to Django 1.7) -""" diff --git a/lms/djangoapps/mobile_api/social_facebook/preferences/serializers.py b/lms/djangoapps/mobile_api/social_facebook/preferences/serializers.py deleted file mode 100644 index 1d51f87eda..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/preferences/serializers.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -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) diff --git a/lms/djangoapps/mobile_api/social_facebook/preferences/tests.py b/lms/djangoapps/mobile_api/social_facebook/preferences/tests.py deleted file mode 100644 index 19850b40f7..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/preferences/tests.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -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') diff --git a/lms/djangoapps/mobile_api/social_facebook/preferences/urls.py b/lms/djangoapps/mobile_api/social_facebook/preferences/urls.py deleted file mode 100644 index 70f95bc10c..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/preferences/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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' - ), -) diff --git a/lms/djangoapps/mobile_api/social_facebook/preferences/views.py b/lms/djangoapps/mobile_api/social_facebook/preferences/views.py deleted file mode 100644 index 8170483e6d..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/preferences/views.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -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) diff --git a/lms/djangoapps/mobile_api/social_facebook/test_utils.py b/lms/djangoapps/mobile_api/social_facebook/test_utils.py deleted file mode 100644 index c9c7304e10..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/test_utils.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -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) diff --git a/lms/djangoapps/mobile_api/social_facebook/urls.py b/lms/djangoapps/mobile_api/social_facebook/urls.py deleted file mode 100644 index 6118f592a6..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -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')), -) diff --git a/lms/djangoapps/mobile_api/social_facebook/utils.py b/lms/djangoapps/mobile_api/social_facebook/utils.py deleted file mode 100644 index 654dd97edd..0000000000 --- a/lms/djangoapps/mobile_api/social_facebook/utils.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -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' diff --git a/lms/djangoapps/mobile_api/urls.py b/lms/djangoapps/mobile_api/urls.py index 103aa3babb..864fb9fdf3 100644 --- a/lms/djangoapps/mobile_api/urls.py +++ b/lms/djangoapps/mobile_api/urls.py @@ -1,7 +1,6 @@ """ 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')), - ) diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index b2be7c81c8..1295af6dea 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -76,14 +76,6 @@ class CourseOverviewField(serializers.RelatedField): kwargs={'course_id': course_id}, request=request, ), - - # Note: The following 2 should be deprecated. - 'social_urls': { - 'facebook': course_overview.facebook_url, - }, - 'latest_updates': { - 'video': None - }, } diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index 1a831b777c..270587e8b7 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -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() diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 879e365577..6bf37f83de 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -236,7 +236,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView): it is enabled, otherwise null. * end: The end date of the course. * id: The unique ID of the course. - * latest_updates: Reserved for future use. * name: The name of the course. * number: The course number. * org: The organization that created the course. diff --git a/lms/envs/common.py b/lms/envs/common.py index 61b22d9573..031b4c14ad 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -271,10 +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, # Enable the combined login/registration form 'ENABLE_COMBINED_LOGIN_REGISTRATION': False, diff --git a/lms/envs/test.py b/lms/envs/test.py index 54411c6693..eff52628c1 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -293,9 +293,7 @@ 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 ###################### Payment ##############################3 # Enable fake payment processing page diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py b/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py new file mode 100644 index 0000000000..91ae94cad0 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py @@ -0,0 +1,18 @@ +# -*- 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', + ), + ] diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 5956ec55d3..0160dc15d5 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -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, diff --git a/openedx/core/djangoapps/content/course_overviews/tests.py b/openedx/core/djangoapps/content/course_overviews/tests.py index fe0e66bbf4..988cfda7af 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests.py +++ b/openedx/core/djangoapps/content/course_overviews/tests.py @@ -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',