Commit 1616de50 by muzaffaryousaf

Bookmarks API.

TNL-2180
parent 283623ab
......@@ -11,13 +11,13 @@ from .views import (
EnrollmentCourseDetailView
)
USERNAME_PATTERN = '(?P<username>[\w.@+-]+)'
urlpatterns = patterns(
'enrollment.views',
url(
r'^enrollment/{username},{course_key}$'.format(username=USERNAME_PATTERN,
course_key=settings.COURSE_ID_PATTERN),
r'^enrollment/{username},{course_key}$'.format(
username=settings.USERNAME_PATTERN, course_key=settings.COURSE_ID_PATTERN
),
EnrollmentView.as_view(),
name='courseenrollment'
),
......
"""
Bookmarks module.
"""
DEFAULT_FIELDS = [
'id',
'course_id',
'usage_id',
'created',
]
OPTIONAL_FIELDS = [
'display_name',
'path',
]
"""
Bookmarks Python API.
"""
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS
from .models import Bookmark
from .serializers import BookmarkSerializer
def get_bookmark(user, usage_key, fields=None):
"""
Return data for a bookmark.
Arguments:
user (User): The user of the bookmark.
usage_key (UsageKey): The usage_key of the bookmark.
fields (list): List of field names the data should contain (optional).
Returns:
Dict.
Raises:
ObjectDoesNotExist: If a bookmark with the parameters does not exist.
"""
bookmark = Bookmark.objects.get(user=user, usage_key=usage_key)
return BookmarkSerializer(bookmark, context={'fields': fields}).data
def get_bookmarks(user, course_key=None, fields=None, serialized=True):
"""
Return data for bookmarks of a user.
Arguments:
user (User): The user of the bookmarks.
course_key (CourseKey): The course_key of the bookmarks (optional).
fields (list): List of field names the data should contain (optional).
N/A if serialized is False.
serialized (bool): Whether to return a queryset or a serialized list of dicts.
Default is True.
Returns:
List of dicts if serialized is True else queryset.
"""
bookmarks_queryset = Bookmark.objects.filter(user=user)
if course_key:
bookmarks_queryset = bookmarks_queryset.filter(course_key=course_key)
bookmarks_queryset = bookmarks_queryset.order_by('-created')
if serialized:
return BookmarkSerializer(bookmarks_queryset, context={'fields': fields}, many=True).data
return bookmarks_queryset
def create_bookmark(user, usage_key):
"""
Create a bookmark.
Arguments:
user (User): The user of the bookmark.
usage_key (UsageKey): The usage_key of the bookmark.
Returns:
Dict.
Raises:
ItemNotFoundError: If no block exists for the usage_key.
"""
bookmark = Bookmark.create({
'user': user,
'usage_key': usage_key
})
return BookmarkSerializer(bookmark, context={'fields': DEFAULT_FIELDS + OPTIONAL_FIELDS}).data
def delete_bookmark(user, usage_key):
"""
Delete a bookmark.
Arguments:
user (User): The user of the bookmark.
usage_key (UsageKey): The usage_key of the bookmark.
Returns:
Dict.
Raises:
ObjectDoesNotExist: If a bookmark with the parameters does not exist.
"""
bookmark = Bookmark.objects.get(user=user, usage_key=usage_key)
bookmark.delete()
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Bookmark'
db.create_table('bookmarks_bookmark', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('usage_key', self.gf('xmodule_django.models.LocationKeyField')(max_length=255, db_index=True)),
('display_name', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
('path', self.gf('jsonfield.fields.JSONField')()),
))
db.send_create_signal('bookmarks', ['Bookmark'])
def backwards(self, orm):
# Deleting model 'Bookmark'
db.delete_table('bookmarks_bookmark')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'bookmarks.bookmark': {
'Meta': {'object_name': 'Bookmark'},
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'path': ('jsonfield.fields.JSONField', [], {}),
'usage_key': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['bookmarks']
"""
Models for Bookmarks.
"""
from django.contrib.auth.models import User
from django.db import models
from jsonfield.fields import JSONField
from model_utils.models import TimeStampedModel
from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField, LocationKeyField
class Bookmark(TimeStampedModel):
"""
Bookmarks model.
"""
user = models.ForeignKey(User, db_index=True)
course_key = CourseKeyField(max_length=255, db_index=True)
usage_key = LocationKeyField(max_length=255, db_index=True)
display_name = models.CharField(max_length=255, default='', help_text='Display name of block')
path = JSONField(help_text='Path in course tree to the block')
@classmethod
def create(cls, bookmark_data):
"""
Create a Bookmark object.
Arguments:
bookmark_data (dict): The data to create the object with.
Returns:
A Bookmark object.
Raises:
ItemNotFoundError: If no block exists for the usage_key.
"""
usage_key = bookmark_data.pop('usage_key')
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
block = modulestore().get_item(usage_key)
bookmark_data['course_key'] = usage_key.course_key
bookmark_data['display_name'] = block.display_name
bookmark_data['path'] = cls.get_path(block)
user = bookmark_data.pop('user')
bookmark, __ = cls.objects.get_or_create(usage_key=usage_key, user=user, defaults=bookmark_data)
return bookmark
@staticmethod
def get_path(block):
"""
Returns data for the path to the block in the course tree.
Arguments:
block (XBlock): The block whose path is required.
Returns:
list of dicts of the form {'usage_id': <usage_id>, 'display_name': <display_name>}.
"""
parent = block.get_parent()
parents_data = []
while parent is not None and parent.location.block_type not in ['course']:
parents_data.append({"display_name": parent.display_name, "usage_id": unicode(parent.location)})
parent = parent.get_parent()
parents_data.reverse()
return parents_data
"""
Serializers for Bookmarks.
"""
from rest_framework import serializers
from . import DEFAULT_FIELDS
from .models import Bookmark
class BookmarkSerializer(serializers.ModelSerializer):
"""
Serializer for the Bookmark model.
"""
id = serializers.SerializerMethodField('resource_id') # pylint: disable=invalid-name
course_id = serializers.Field(source='course_key')
usage_id = serializers.Field(source='usage_key')
path = serializers.Field(source='path')
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
try:
fields = kwargs['context'].pop('fields', DEFAULT_FIELDS) or DEFAULT_FIELDS
except KeyError:
fields = DEFAULT_FIELDS
# Instantiate the superclass normally
super(BookmarkSerializer, self).__init__(*args, **kwargs)
# Drop any fields that are not specified in the `fields` argument.
required_fields = set(fields)
all_fields = set(self.fields.keys())
for field_name in all_fields - required_fields:
self.fields.pop(field_name)
class Meta(object):
""" Serializer metadata. """
model = Bookmark
fields = (
'id',
'course_id',
'usage_id',
'display_name',
'path',
'created',
)
def resource_id(self, bookmark):
"""
Return the REST resource id: {username,usage_id}.
"""
return "{0},{1}".format(bookmark.user.username, bookmark.usage_key)
"""
Bookmarks service.
"""
import logging
from django.core.exceptions import ObjectDoesNotExist
from xmodule.modulestore.exceptions import ItemNotFoundError
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
log = logging.getLogger(__name__)
class BookmarksService(object):
"""
A service that provides access to the bookmarks API.
"""
def __init__(self, user, **kwargs):
super(BookmarksService, self).__init__(**kwargs)
self._user = user
def bookmarks(self, course_key):
"""
Return a list of bookmarks for the course for the current user.
Arguments:
course_key: CourseKey of the course for which to retrieve the user's bookmarks for.
Returns:
list of dict:
"""
return api.get_bookmarks(self._user, course_key=course_key, fields=DEFAULT_FIELDS + OPTIONAL_FIELDS)
def is_bookmarked(self, usage_key):
"""
Return whether the block has been bookmarked by the user.
Arguments:
usage_key: UsageKey of the block.
Returns:
Bool
"""
try:
api.get_bookmark(user=self._user, usage_key=usage_key)
except ObjectDoesNotExist:
log.error(u'Bookmark with usage_id: %s does not exist.', usage_key)
return False
return True
def set_bookmarked(self, usage_key):
"""
Adds a bookmark for the block.
Arguments:
usage_key: UsageKey of the block.
Returns:
Bool indicating whether the bookmark was added.
"""
try:
api.create_bookmark(user=self._user, usage_key=usage_key)
except ItemNotFoundError:
log.error(u'Block with usage_id: %s not found.', usage_key)
return False
return True
def unset_bookmarked(self, usage_key):
"""
Removes the bookmark for the block.
Arguments:
usage_key: UsageKey of the block.
Returns:
Bool indicating whether the bookmark was removed.
"""
try:
api.delete_bookmark(self._user, usage_key=usage_key)
except ObjectDoesNotExist:
log.error(u'Bookmark with usage_id: %s does not exist.', usage_key)
return False
return True
"""
Factories for Bookmark models.
"""
from factory.django import DjangoModelFactory
from factory import SubFactory
from functools import partial
from student.tests.factories import UserFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from ..models import Bookmark
COURSE_KEY = SlashSeparatedCourseKey(u'edX', u'test_course', u'test')
LOCATION = partial(COURSE_KEY.make_usage_key, u'problem')
class BookmarkFactory(DjangoModelFactory):
""" Simple factory class for generating Bookmark """
FACTORY_FOR = Bookmark
user = SubFactory(UserFactory)
course_key = COURSE_KEY
usage_key = LOCATION('usage_id')
display_name = ""
path = list()
"""
Tests for bookmarks api.
"""
from django.core.exceptions import ObjectDoesNotExist
from opaque_keys.edx.keys import UsageKey
from student.tests.factories import UserFactory
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from .factories import BookmarkFactory
from .. import api, DEFAULT_FIELDS, OPTIONAL_FIELDS
from ..models import Bookmark
class BookmarksAPITests(ModuleStoreTestCase):
"""
These tests cover the parts of the API methods.
"""
def setUp(self):
super(BookmarksAPITests, self).setUp()
self.user = UserFactory.create(password='test')
self.other_user = UserFactory.create(password='test')
self.course = CourseFactory.create(display_name='An Introduction to API Testing')
self.course_id = unicode(self.course.id)
self.chapter = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name='Week 1'
)
self.sequential = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name='Lesson 1'
)
self.vertical = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 1'
)
self.vertical_1 = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 1.1'
)
self.bookmark = BookmarkFactory.create(
user=self.user,
course_key=self.course_id,
usage_key=self.vertical.location,
display_name=self.vertical.display_name
)
self.course_2 = CourseFactory.create(display_name='An Introduction to API Testing 2')
self.chapter_2 = ItemFactory.create(
parent_location=self.course_2.location, category='chapter', display_name='Week 2'
)
self.sequential_2 = ItemFactory.create(
parent_location=self.chapter_2.location, category='sequential', display_name='Lesson 2'
)
self.vertical_2 = ItemFactory.create(
parent_location=self.sequential_2.location, category='vertical', display_name='Subsection 2'
)
self.bookmark_2 = BookmarkFactory.create(
user=self.user,
course_key=self.course_2.id,
usage_key=self.vertical_2.location,
display_name=self.vertical_2.display_name
)
self.all_fields = DEFAULT_FIELDS + OPTIONAL_FIELDS
def assert_bookmark_response(self, response_data, bookmark, optional_fields=False):
"""
Determines if the given response data (dict) matches the given bookmark.
"""
self.assertEqual(response_data['id'], '%s,%s' % (self.user.username, unicode(bookmark.usage_key)))
self.assertEqual(response_data['course_id'], unicode(bookmark.course_key))
self.assertEqual(response_data['usage_id'], unicode(bookmark.usage_key))
self.assertIsNotNone(response_data['created'])
if optional_fields:
self.assertEqual(response_data['display_name'], bookmark.display_name)
self.assertEqual(response_data['path'], bookmark.path)
def test_get_bookmark(self):
"""
Verifies that get_bookmark returns data as expected.
"""
bookmark_data = api.get_bookmark(user=self.user, usage_key=self.vertical.location)
self.assert_bookmark_response(bookmark_data, self.bookmark)
# With Optional fields.
bookmark_data = api.get_bookmark(
user=self.user,
usage_key=self.vertical.location,
fields=self.all_fields
)
self.assert_bookmark_response(bookmark_data, self.bookmark, optional_fields=True)
def test_get_bookmark_raises_error(self):
"""
Verifies that get_bookmark raises error as expected.
"""
with self.assertRaises(ObjectDoesNotExist):
api.get_bookmark(user=self.other_user, usage_key=self.vertical.location)
def test_get_bookmarks(self):
"""
Verifies that get_bookmarks returns data as expected.
"""
# Without course key.
bookmarks_data = api.get_bookmarks(user=self.user)
self.assertEqual(len(bookmarks_data), 2)
# Assert them in ordered manner.
self.assert_bookmark_response(bookmarks_data[0], self.bookmark_2)
self.assert_bookmark_response(bookmarks_data[1], self.bookmark)
# With course key.
bookmarks_data = api.get_bookmarks(user=self.user, course_key=self.course.id)
self.assertEqual(len(bookmarks_data), 1)
self.assert_bookmark_response(bookmarks_data[0], self.bookmark)
# With optional fields.
bookmarks_data = api.get_bookmarks(user=self.user, course_key=self.course.id, fields=self.all_fields)
self.assertEqual(len(bookmarks_data), 1)
self.assert_bookmark_response(bookmarks_data[0], self.bookmark, optional_fields=True)
# Without Serialized.
bookmarks = api.get_bookmarks(user=self.user, course_key=self.course.id, serialized=False)
self.assertEqual(len(bookmarks), 1)
self.assertTrue(bookmarks.model is Bookmark) # pylint: disable=no-member
self.assertEqual(bookmarks[0], self.bookmark)
def test_create_bookmark(self):
"""
Verifies that create_bookmark create & returns data as expected.
"""
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 1)
api.create_bookmark(user=self.user, usage_key=self.vertical_1.location)
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
def test_create_bookmark_do_not_create_duplicates(self):
"""
Verifies that create_bookmark do not create duplicate bookmarks.
"""
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 1)
bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_1.location)
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
bookmark_data_2 = api.create_bookmark(user=self.user, usage_key=self.vertical_1.location)
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
self.assertEqual(bookmark_data, bookmark_data_2)
def test_create_bookmark_raises_error(self):
"""
Verifies that create_bookmark raises error as expected.
"""
with self.assertRaises(ItemNotFoundError):
api.create_bookmark(user=self.user, usage_key=UsageKey.from_string('i4x://brb/100/html/340ef1771a0940'))
def test_delete_bookmark(self):
"""
Verifies that delete_bookmark removes bookmark as expected.
"""
self.assertEqual(len(api.get_bookmarks(user=self.user)), 2)
api.delete_bookmark(user=self.user, usage_key=self.vertical.location)
bookmarks_data = api.get_bookmarks(user=self.user)
self.assertEqual(len(bookmarks_data), 1)
self.assertNotEqual(unicode(self.vertical.location), bookmarks_data[0]['usage_id'])
def test_delete_bookmark_raises_error(self):
"""
Verifies that delete_bookmark raises error as expected.
"""
with self.assertRaises(ObjectDoesNotExist):
api.delete_bookmark(user=self.other_user, usage_key=self.vertical.location)
"""
Tests for Bookmarks models.
"""
from bookmarks.models import Bookmark
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class BookmarkModelTest(ModuleStoreTestCase):
"""
Test the Bookmark model.
"""
def setUp(self):
super(BookmarkModelTest, self).setUp()
self.user = UserFactory.create(password='test')
self.course = CourseFactory.create(display_name='An Introduction to API Testing')
self.course_id = unicode(self.course.id)
self.chapter = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name='Week 1'
)
self.sequential = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name='Lesson 1'
)
self.vertical = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 1'
)
self.vertical_2 = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 2'
)
self.path = [
{'display_name': self.chapter.display_name, 'usage_id': unicode(self.chapter.location)},
{'display_name': self.sequential.display_name, 'usage_id': unicode(self.sequential.location)}
]
def get_bookmark_data(self, block):
"""
Returns bookmark data for testing.
"""
return {
'user': self.user,
'course_key': self.course.id,
'usage_key': block.location,
'display_name': block.display_name,
}
def assert_valid_bookmark(self, bookmark_object, bookmark_data):
"""
Check if the given data matches the specified bookmark.
"""
self.assertEqual(bookmark_object.user, self.user)
self.assertEqual(bookmark_object.course_key, bookmark_data['course_key'])
self.assertEqual(bookmark_object.usage_key, self.vertical.location)
self.assertEqual(bookmark_object.display_name, bookmark_data['display_name'])
self.assertEqual(bookmark_object.path, self.path)
self.assertIsNotNone(bookmark_object.created)
def test_create_bookmark_success(self):
"""
Tests creation of bookmark.
"""
bookmark_data = self.get_bookmark_data(self.vertical)
bookmark_object = Bookmark.create(bookmark_data)
self.assert_valid_bookmark(bookmark_object, bookmark_data)
def test_get_path(self):
"""
Tests creation of path with given block.
"""
path_object = Bookmark.get_path(block=self.vertical)
self.assertEqual(path_object, self.path)
def test_get_path_with_given_chapter_block(self):
"""
Tests path for chapter level block.
"""
path_object = Bookmark.get_path(block=self.chapter)
self.assertEqual(len(path_object), 0)
def test_get_path_with_given_sequential_block(self):
"""
Tests path for sequential level block.
"""
path_object = Bookmark.get_path(block=self.sequential)
self.assertEqual(len(path_object), 1)
self.assertEqual(path_object[0], self.path[0])
def test_get_path_returns_empty_list_for_unreachable_parent(self):
"""
Tests get_path returns empty list if block has no parent.
"""
path = Bookmark.get_path(block=self.course)
self.assertEqual(path, [])
"""
Tests for bookmark services.
"""
from opaque_keys.edx.keys import UsageKey
from .factories import BookmarkFactory
from ..services import BookmarksService
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class BookmarksAPITests(ModuleStoreTestCase):
"""
Tests the Bookmarks service.
"""
def setUp(self):
super(BookmarksAPITests, self).setUp()
self.user = UserFactory.create(password='test')
self.other_user = UserFactory.create(password='test')
self.course = CourseFactory.create(display_name='An Introduction to API Testing')
self.course_id = unicode(self.course.id)
self.chapter = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name='Week 1'
)
self.sequential = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name='Lesson 1'
)
self.vertical = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 1'
)
self.vertical_1 = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 1.1'
)
self.bookmark = BookmarkFactory.create(
user=self.user,
course_key=self.course_id,
usage_key=self.vertical.location,
display_name=self.vertical.display_name
)
self.bookmark_service = BookmarksService(user=self.user)
def assert_bookmark_response(self, response_data, bookmark):
"""
Determines if the given response data (dict) matches the specified bookmark.
"""
self.assertEqual(response_data['id'], '%s,%s' % (self.user.username, unicode(bookmark.usage_key)))
self.assertEqual(response_data['course_id'], unicode(bookmark.course_key))
self.assertEqual(response_data['usage_id'], unicode(bookmark.usage_key))
self.assertIsNotNone(response_data['created'])
self.assertEqual(response_data['display_name'], bookmark.display_name)
self.assertEqual(response_data['path'], bookmark.path)
def test_get_bookmarks(self):
"""
Verifies get_bookmarks returns data as expected.
"""
bookmarks_data = self.bookmark_service.bookmarks(course_key=self.course.id)
self.assertEqual(len(bookmarks_data), 1)
self.assert_bookmark_response(bookmarks_data[0], self.bookmark)
def test_is_bookmarked(self):
"""
Verifies is_bookmarked returns Bool as expected.
"""
self.assertTrue(self.bookmark_service.is_bookmarked(usage_key=self.vertical.location))
self.assertFalse(self.bookmark_service.is_bookmarked(usage_key=self.vertical_1.location))
# Get bookmark that does not exist.
bookmark_service = BookmarksService(self.other_user)
self.assertFalse(bookmark_service.is_bookmarked(usage_key=self.vertical.location))
def test_set_bookmarked(self):
"""
Verifies set_bookmarked returns Bool as expected.
"""
# Assert False for item that does not exist.
self.assertFalse(
self.bookmark_service.set_bookmarked(usage_key=UsageKey.from_string("i4x://ed/ed/ed/interactive"))
)
self.assertTrue(self.bookmark_service.set_bookmarked(usage_key=self.vertical_1.location))
def test_unset_bookmarked(self):
"""
Verifies unset_bookmarked returns Bool as expected.
"""
self.assertFalse(
self.bookmark_service.unset_bookmarked(usage_key=UsageKey.from_string("i4x://ed/ed/ed/interactive"))
)
self.assertTrue(self.bookmark_service.unset_bookmarked(usage_key=self.vertical.location))
"""
Tests for bookmark views.
"""
import ddt
import json
import urllib
from django.core.urlresolvers import reverse
from rest_framework.test import APIClient
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .factories import BookmarkFactory
# pylint: disable=no-member
class BookmarksViewTestsMixin(ModuleStoreTestCase):
"""
Mixin for bookmarks views tests.
"""
test_password = 'test'
def setUp(self):
super(BookmarksViewTestsMixin, self).setUp()
self.anonymous_client = APIClient()
self.user = UserFactory.create(password=self.test_password)
self.create_test_data()
self.client = self.login_client(user=self.user)
def login_client(self, user):
"""
Helper method for getting the client and user and logging in. Returns client.
"""
client = APIClient()
client.login(username=user.username, password=self.test_password)
return client
def create_test_data(self):
"""
Creates the bookmarks test data.
"""
with self.store.default_store(ModuleStoreEnum.Type.split):
self.course = CourseFactory.create()
self.course_id = unicode(self.course.id)
chapter_1 = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name='Week 1'
)
sequential_1 = ItemFactory.create(
parent_location=chapter_1.location, category='sequential', display_name='Lesson 1'
)
self.vertical_1 = ItemFactory.create(
parent_location=sequential_1.location, category='vertical', display_name='Subsection 1'
)
self.bookmark_1 = BookmarkFactory.create(
user=self.user,
course_key=self.course_id,
usage_key=self.vertical_1.location,
display_name=self.vertical_1.display_name
)
chapter_2 = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name='Week 2'
)
sequential_2 = ItemFactory.create(
parent_location=chapter_2.location, category='sequential', display_name='Lesson 2'
)
vertical_2 = ItemFactory.create(
parent_location=sequential_2.location, category='vertical', display_name='Subsection 2'
)
self.vertical_3 = ItemFactory.create(
parent_location=sequential_2.location, category='vertical', display_name='Subsection 3'
)
self.bookmark_2 = BookmarkFactory.create(
user=self.user,
course_key=self.course_id,
usage_key=vertical_2.location,
display_name=vertical_2.display_name
)
# Other Course
self.other_course = CourseFactory.create(display_name='An Introduction to API Testing 2')
other_chapter = ItemFactory.create(
parent_location=self.other_course.location, category='chapter', display_name='Other Week 1'
)
other_sequential = ItemFactory.create(
parent_location=other_chapter.location, category='sequential', display_name='Other Lesson 1'
)
self.other_vertical = ItemFactory.create(
parent_location=other_sequential.location, category='vertical', display_name='Other Subsection 1'
)
self.other_bookmark = BookmarkFactory.create(
user=self.user,
course_key=unicode(self.other_course.id),
usage_key=self.other_vertical.location,
display_name=self.other_vertical.display_name
)
def assert_valid_bookmark_response(self, response_data, bookmark, optional_fields=False):
"""
Determines if the given response data (dict) matches the specified bookmark.
"""
self.assertEqual(response_data['id'], '%s,%s' % (self.user.username, unicode(bookmark.usage_key)))
self.assertEqual(response_data['course_id'], unicode(bookmark.course_key))
self.assertEqual(response_data['usage_id'], unicode(bookmark.usage_key))
self.assertIsNotNone(response_data['created'])
if optional_fields:
self.assertEqual(response_data['display_name'], bookmark.display_name)
self.assertEqual(response_data['path'], bookmark.path)
def send_get(self, client, url, query_parameters=None, expected_status=200):
"""
Helper method for sending a GET to the server. Verifies the expected status and returns the response.
"""
url = url + '?' + query_parameters if query_parameters else url
response = client.get(url)
self.assertEqual(expected_status, response.status_code)
return response
def send_post(self, client, url, data, content_type='application/json', expected_status=201):
"""
Helper method for sending a POST to the server. Verifies the expected status and returns the response.
"""
response = client.post(url, data=json.dumps(data), content_type=content_type)
self.assertEqual(expected_status, response.status_code)
return response
def send_delete(self, client, url, expected_status=204):
"""
Helper method for sending a DELETE to the server. Verifies the expected status and returns the response.
"""
response = client.delete(url)
self.assertEqual(expected_status, response.status_code)
return response
@ddt.ddt
class BookmarksListViewTests(BookmarksViewTestsMixin):
"""
This contains the tests for GET & POST methods of bookmark.views.BookmarksListView class
GET /api/bookmarks/v0/bookmarks/?course_id={course_id1}
POST /api/bookmarks/v0/bookmarks
"""
@ddt.data(
('course_id={}', False),
('course_id={}&fields=path,display_name', True),
)
@ddt.unpack
def test_get_bookmarks_successfully(self, query_params, check_optionals):
"""
Test that requesting bookmarks for a course returns records successfully in
expected order without optional fields.
"""
response = self.send_get(
client=self.client,
url=reverse('bookmarks'),
query_parameters=query_params.format(urllib.quote(self.course_id))
)
bookmarks = response.data['results']
self.assertEqual(len(bookmarks), 2)
self.assertEqual(response.data['count'], 2)
self.assertEqual(response.data['num_pages'], 1)
# As bookmarks are sorted by -created so we will compare in that order.
self.assert_valid_bookmark_response(bookmarks[0], self.bookmark_2, optional_fields=check_optionals)
self.assert_valid_bookmark_response(bookmarks[1], self.bookmark_1, optional_fields=check_optionals)
def test_get_bookmarks_with_pagination(self):
"""
Test that requesting bookmarks for a course return results with pagination 200 code.
"""
query_parameters = 'course_id={}&page_size=1'.format(urllib.quote(self.course_id))
response = self.send_get(client=self.client, url=reverse('bookmarks'), query_parameters=query_parameters)
bookmarks = response.data['results']
# Pagination assertions.
self.assertEqual(response.data['count'], 2)
self.assertIn('page=2&page_size=1', response.data['next'])
self.assertEqual(response.data['num_pages'], 2)
self.assertEqual(len(bookmarks), 1)
self.assert_valid_bookmark_response(bookmarks[0], self.bookmark_2)
def test_get_bookmarks_with_invalid_data(self):
"""
Test that requesting bookmarks with invalid data returns 0 records.
"""
# Invalid course id.
response = self.send_get(client=self.client, url=reverse('bookmarks'), query_parameters='course_id=invalid')
bookmarks = response.data['results']
self.assertEqual(len(bookmarks), 0)
def test_get_all_bookmarks_when_course_id_not_given(self):
"""
Test that requesting bookmarks returns all records for that user.
"""
# Without course id we would return all the bookmarks for that user.
response = self.send_get(client=self.client, url=reverse('bookmarks'))
bookmarks = response.data['results']
self.assertEqual(len(bookmarks), 3)
self.assert_valid_bookmark_response(bookmarks[0], self.other_bookmark)
self.assert_valid_bookmark_response(bookmarks[1], self.bookmark_2)
self.assert_valid_bookmark_response(bookmarks[2], self.bookmark_1)
def test_anonymous_access(self):
"""
Test that an anonymous client (not logged in) cannot call GET or POST.
"""
query_parameters = 'course_id={}'.format(self.course_id)
self.send_get(
client=self.anonymous_client,
url=reverse('bookmarks'),
query_parameters=query_parameters,
expected_status=401
)
self.send_post(
client=self.anonymous_client,
url=reverse('bookmarks'),
data={'usage_id': 'test'},
expected_status=401
)
def test_post_bookmark_successfully(self):
"""
Test that posting a bookmark successfully returns newly created data with 201 code.
"""
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
data={'usage_id': unicode(self.vertical_3.location)}
)
# Assert Newly created bookmark.
self.assertEqual(response.data['id'], '%s,%s' % (self.user.username, unicode(self.vertical_3.location)))
self.assertEqual(response.data['course_id'], self.course_id)
self.assertEqual(response.data['usage_id'], unicode(self.vertical_3.location))
self.assertIsNotNone(response.data['created'])
self.assertEqual(len(response.data['path']), 2)
self.assertEqual(response.data['display_name'], self.vertical_3.display_name)
def test_post_bookmark_with_invalid_data(self):
"""
Test that posting a bookmark for a block with invalid usage id returns a 400.
Scenarios:
1) Invalid usage id.
2) Without usage id.
3) With empty request.DATA
"""
# Send usage_id with invalid format.
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
data={'usage_id': 'invalid'},
expected_status=400
)
self.assertEqual(response.data['user_message'], u'Invalid usage_id: invalid.')
# Send data without usage_id.
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
data={'course_id': 'invalid'},
expected_status=400
)
self.assertEqual(response.data['user_message'], u'Parameter usage_id not provided.')
self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.')
# Send empty data dictionary.
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
data={},
expected_status=400
)
self.assertEqual(response.data['user_message'], u'No data provided.')
self.assertEqual(response.data['developer_message'], u'No data provided.')
def test_post_bookmark_for_non_existing_block(self):
"""
Test that posting a bookmark for a block that does not exist returns a 400.
"""
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
data={'usage_id': 'i4x://arbi/100/html/340ef1771a094090ad260ec940d04a21'},
expected_status=400
)
self.assertEqual(
response.data['user_message'],
u'Block with usage_id: i4x://arbi/100/html/340ef1771a094090ad260ec940d04a21 not found.'
)
self.assertEqual(
response.data['developer_message'],
u'Block with usage_id: i4x://arbi/100/html/340ef1771a094090ad260ec940d04a21 not found.'
)
def test_unsupported_methods(self):
"""
Test that DELETE and PUT are not supported.
"""
self.client.login(username=self.user.username, password=self.test_password)
self.assertEqual(405, self.client.put(reverse('bookmarks')).status_code)
self.assertEqual(405, self.client.delete(reverse('bookmarks')).status_code)
@ddt.ddt
class BookmarksDetailViewTests(BookmarksViewTestsMixin):
"""
This contains the tests for GET & DELETE methods of bookmark.views.BookmarksDetailView class
"""
@ddt.data(
('', False),
('fields=path,display_name', True)
)
@ddt.unpack
def test_get_bookmark_successfully(self, query_params, check_optionals):
"""
Test that requesting bookmark returns data with 200 code.
"""
response = self.send_get(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': self.user.username, 'usage_id': unicode(self.vertical_1.location)}
),
query_parameters=query_params
)
data = response.data
self.assertIsNotNone(data)
self.assert_valid_bookmark_response(data, self.bookmark_1, optional_fields=check_optionals)
def test_get_bookmark_that_belongs_to_other_user(self):
"""
Test that requesting bookmark that belongs to other user returns 404 status code.
"""
self.send_get(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': 'other', 'usage_id': unicode(self.vertical_1.location)}
),
expected_status=404
)
def test_get_bookmark_that_does_not_exist(self):
"""
Test that requesting bookmark that does not exist returns 404 status code.
"""
response = self.send_get(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': self.user.username, 'usage_id': 'i4x://arbi/100/html/340ef1771a0940'}
),
expected_status=404
)
self.assertEqual(
response.data['user_message'],
'Bookmark with usage_id: i4x://arbi/100/html/340ef1771a0940 does not exist.'
)
self.assertEqual(
response.data['developer_message'],
'Bookmark with usage_id: i4x://arbi/100/html/340ef1771a0940 does not exist.'
)
def test_get_bookmark_with_invalid_usage_id(self):
"""
Test that requesting bookmark with invalid usage id returns 400.
"""
response = self.send_get(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': self.user.username, 'usage_id': 'i4x'}
),
expected_status=404
)
self.assertEqual(response.data['user_message'], u'Invalid usage_id: i4x.')
def test_anonymous_access(self):
"""
Test that an anonymous client (not logged in) cannot call GET or DELETE.
"""
url = reverse('bookmarks_detail', kwargs={'username': self.user.username, 'usage_id': 'i4x'})
self.send_get(
client=self.anonymous_client,
url=url,
expected_status=401
)
self.send_delete(
client=self.anonymous_client,
url=url,
expected_status=401
)
def test_delete_bookmark_successfully(self):
"""
Test that delete bookmark returns 204 status code with success.
"""
query_parameters = 'course_id={}'.format(urllib.quote(self.course_id))
response = self.send_get(client=self.client, url=reverse('bookmarks'), query_parameters=query_parameters)
data = response.data
bookmarks = data['results']
self.assertEqual(len(bookmarks), 2)
self.send_delete(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': self.user.username, 'usage_id': unicode(self.vertical_1.location)}
)
)
response = self.send_get(client=self.client, url=reverse('bookmarks'), query_parameters=query_parameters)
bookmarks = response.data['results']
self.assertEqual(len(bookmarks), 1)
def test_delete_bookmark_that_belongs_to_other_user(self):
"""
Test that delete bookmark that belongs to other user returns 404.
"""
self.send_delete(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': 'other', 'usage_id': unicode(self.vertical_1.location)}
),
expected_status=404
)
def test_delete_bookmark_that_does_not_exist(self):
"""
Test that delete bookmark that does not exist returns 404.
"""
response = self.send_delete(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': self.user.username, 'usage_id': 'i4x://arbi/100/html/340ef1771a0940'}
),
expected_status=404
)
self.assertEqual(
response.data['user_message'],
u'Bookmark with usage_id: i4x://arbi/100/html/340ef1771a0940 does not exist.'
)
self.assertEqual(
response.data['developer_message'],
'Bookmark with usage_id: i4x://arbi/100/html/340ef1771a0940 does not exist.'
)
def test_delete_bookmark_with_invalid_usage_id(self):
"""
Test that delete bookmark with invalid usage id returns 400.
"""
response = self.send_delete(
client=self.client,
url=reverse(
'bookmarks_detail',
kwargs={'username': self.user.username, 'usage_id': 'i4x'}
),
expected_status=404
)
self.assertEqual(response.data['user_message'], u'Invalid usage_id: i4x.')
def test_unsupported_methods(self):
"""
Test that POST and PUT are not supported.
"""
url = reverse('bookmarks_detail', kwargs={'username': self.user.username, 'usage_id': 'i4x'})
self.client.login(username=self.user.username, password=self.test_password)
self.assertEqual(405, self.client.put(url).status_code)
self.assertEqual(405, self.client.post(url).status_code)
"""
URL routes for the bookmarks app.
"""
from django.conf import settings
from django.conf.urls import patterns, url
from .views import BookmarksListView, BookmarksDetailView
urlpatterns = patterns(
'bookmarks',
url(
r'^v1/bookmarks/$',
BookmarksListView.as_view(),
name='bookmarks'
),
url(
r'^v1/bookmarks/{username},{usage_key}/$'.format(
username=settings.USERNAME_PATTERN,
usage_key=settings.USAGE_ID_PATTERN
),
BookmarksDetailView.as_view(),
name='bookmarks_detail'
),
)
"""
HTTP end-points for the Bookmarks API.
For more information, see:
https://openedx.atlassian.net/wiki/display/TNL/Bookmarks+API
"""
import logging
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext as _, ugettext_noop
from rest_framework import status
from rest_framework import permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.generics import ListCreateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.lib.api.permissions import IsUserInUrl
from openedx.core.lib.api.serializers import PaginationSerializer
from xmodule.modulestore.exceptions import ItemNotFoundError
from lms.djangoapps.lms_xblock.runtime import unquote_slashes
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
from .serializers import BookmarkSerializer
log = logging.getLogger(__name__)
class BookmarksViewMixin(object):
"""
Shared code for bookmarks views.
"""
def fields_to_return(self, params):
"""
Returns names of fields which should be included in the response.
Arguments:
params (dict): The request parameters.
"""
optional_fields = params.get('fields', '').split(',')
return DEFAULT_FIELDS + [field for field in optional_fields if field in OPTIONAL_FIELDS]
def error_response(self, message, error_status=status.HTTP_400_BAD_REQUEST):
"""
Create and return a Response.
Arguments:
message (string): The message to put in the developer_message
and user_message fields.
status: The status of the response. Default is HTTP_400_BAD_REQUEST.
"""
return Response(
{
"developer_message": message,
"user_message": _(message) # pylint: disable=translation-of-non-string
},
status=error_status
)
class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
"""
**Use Case**
* Get a paginated list of bookmarks for a user.
The list can be filtered by passing parameter "course_id=<course_id>"
to only include bookmarks from a particular course.
The bookmarks are always sorted in descending order by creation date.
Each page in the list contains 10 bookmarks by default. The page
size can be altered by passing parameter "page_size=<page_size>".
To include the optional fields pass the values in "fields" parameter
as a comma separated list. Possible values are:
* "display_name"
* "path"
* Create a new bookmark for a user.
The POST request only needs to contain one parameter "usage_id".
Http400 is returned if the format of the request is not correct,
the usage_id is invalid or a block corresponding to the usage_id
could not be found.
**Example Requests**
GET /api/bookmarks/v1/bookmarks/?course_id={course_id1}&fields=display_name,path
POST /api/bookmarks/v1/bookmarks/
Request data: {"usage_id": <usage-id>}
**Response Values**
* count: The number of bookmarks in a course.
* next: The URI to the next page of bookmarks.
* previous: The URI to the previous page of bookmarks.
* num_pages: The number of pages listing bookmarks.
* results: A list of bookmarks returned. Each collection in the list
contains these fields.
* id: String. The identifier string for the bookmark: {user_id},{usage_id}.
* course_id: String. The identifier string of the bookmark's course.
* usage_id: String. The identifier string of the bookmark's XBlock.
* display_name: String. (optional) Display name of the XBlock.
* path: List. (optional) List of dicts containing {"usage_id": <usage-id>, display_name:<display-name>}
for the XBlocks from the top of the course tree till the parent of the bookmarked XBlock.
* created: ISO 8601 String. The timestamp of bookmark's creation.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
paginate_by = 10
max_paginate_by = 500
paginate_by_param = 'page_size'
pagination_serializer_class = PaginationSerializer
serializer_class = BookmarkSerializer
def get_serializer_context(self):
"""
Return the context for the serializer.
"""
context = super(BookmarksListView, self).get_serializer_context()
if self.request.method == 'GET':
context['fields'] = self.fields_to_return(self.request.QUERY_PARAMS)
return context
def get_queryset(self):
"""
Returns queryset of bookmarks for GET requests.
The results will only include bookmarks for the request's user.
If the course_id is specified in the request parameters,
the queryset will only include bookmarks from that course.
"""
course_id = self.request.QUERY_PARAMS.get('course_id', None)
if course_id:
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
log.error(u'Invalid course_id: %s.', course_id)
return []
else:
course_key = None
return api.get_bookmarks(user=self.request.user, course_key=course_key, serialized=False)
def post(self, request):
"""
POST /api/bookmarks/v1/bookmarks/
Request data: {"usage_id": "<usage-id>"}
"""
if not request.DATA:
return self.error_response(ugettext_noop(u'No data provided.'))
usage_id = request.DATA.get('usage_id', None)
if not usage_id:
return self.error_response(ugettext_noop(u'Parameter usage_id not provided.'))
try:
usage_key = UsageKey.from_string(unquote_slashes(usage_id))
except InvalidKeyError:
error_message = ugettext_noop(u'Invalid usage_id: {usage_id}.').format(usage_id=usage_id)
log.error(error_message)
return self.error_response(error_message)
try:
bookmark = api.create_bookmark(user=self.request.user, usage_key=usage_key)
except ItemNotFoundError:
error_message = ugettext_noop(u'Block with usage_id: {usage_id} not found.').format(usage_id=usage_id)
log.error(error_message)
return self.error_response(error_message)
return Response(bookmark, status=status.HTTP_201_CREATED)
class BookmarksDetailView(APIView, BookmarksViewMixin):
"""
**Use Cases**
Get or delete a specific bookmark for a user.
**Example Requests**:
GET /api/bookmarks/v1/bookmarks/{username},{usage_id}/?fields=display_name,path
DELETE /api/bookmarks/v1/bookmarks/{username},{usage_id}/
**Response for GET**
Users can only delete their own bookmarks. If the bookmark_id does not belong
to a requesting user's bookmark a Http404 is returned. Http404 will also be
returned if the bookmark does not exist.
* id: String. The identifier string for the bookmark: {user_id},{usage_id}.
* course_id: String. The identifier string of the bookmark's course.
* usage_id: String. The identifier string of the bookmark's XBlock.
* display_name: (optional) String. Display name of the XBlock.
* path: (optional) List of dicts containing {"usage_id": <usage-id>, display_name: <display-name>}
for the XBlocks from the top of the course tree till the parent of the bookmarked XBlock.
* created: ISO 8601 String. The timestamp of bookmark's creation.
**Response for DELETE**
Users can only delete their own bookmarks.
A successful delete returns a 204 and no content.
Users can only delete their own bookmarks. If the bookmark_id does not belong
to a requesting user's bookmark a 404 is returned. 404 will also be returned
if the bookmark does not exist.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, IsUserInUrl)
serializer_class = BookmarkSerializer
def get_usage_key_or_error_response(self, usage_id):
"""
Create and return usage_key or error Response.
Arguments:
usage_id (string): The id of required block.
"""
try:
return UsageKey.from_string(usage_id)
except InvalidKeyError:
error_message = ugettext_noop(u'Invalid usage_id: {usage_id}.').format(usage_id=usage_id)
log.error(error_message)
return self.error_response(error_message, status.HTTP_404_NOT_FOUND)
def get(self, request, username=None, usage_id=None): # pylint: disable=unused-argument
"""
GET /api/bookmarks/v1/bookmarks/{username},{usage_id}?fields=display_name,path
"""
usage_key_or_response = self.get_usage_key_or_error_response(usage_id=usage_id)
if isinstance(usage_key_or_response, Response):
return usage_key_or_response
try:
bookmark_data = api.get_bookmark(
user=request.user,
usage_key=usage_key_or_response,
fields=self.fields_to_return(request.QUERY_PARAMS)
)
except ObjectDoesNotExist:
error_message = ugettext_noop(
u'Bookmark with usage_id: {usage_id} does not exist.'
).format(usage_id=usage_id)
log.error(error_message)
return self.error_response(error_message, status.HTTP_404_NOT_FOUND)
return Response(bookmark_data)
def delete(self, request, username=None, usage_id=None): # pylint: disable=unused-argument
"""
DELETE /api/bookmarks/v1/bookmarks/{username},{usage_id}
"""
usage_key_or_response = self.get_usage_key_or_error_response(usage_id=usage_id)
if isinstance(usage_key_or_response, Response):
return usage_key_or_response
try:
api.delete_bookmark(user=request.user, usage_key=usage_key_or_response)
except ObjectDoesNotExist:
error_message = ugettext_noop(
u'Bookmark with usage_id: {usage_id} does not exist.'
).format(usage_id=usage_id)
log.error(error_message)
return self.error_response(error_message, status.HTTP_404_NOT_FOUND)
return Response(status=status.HTTP_204_NO_CONTENT)
......@@ -6,17 +6,16 @@ from django.conf import settings
from .views import UserDetail, UserCourseEnrollmentsList, UserCourseStatus
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
urlpatterns = patterns(
'mobile_api.users.views',
url('^' + USERNAME_PATTERN + '$', UserDetail.as_view(), name='user-detail'),
url('^' + settings.USERNAME_PATTERN + '$', UserDetail.as_view(), name='user-detail'),
url(
'^' + USERNAME_PATTERN + '/course_enrollments/$',
'^' + settings.USERNAME_PATTERN + '/course_enrollments/$',
UserCourseEnrollmentsList.as_view(),
name='courseenrollment-detail'
),
url('^{}/course_status_info/{}'.format(USERNAME_PATTERN, settings.COURSE_ID_PATTERN),
url('^{}/course_status_info/{}'.format(settings.USERNAME_PATTERN, settings.COURSE_ID_PATTERN),
UserCourseStatus.as_view(),
name='user-course-status')
)
......@@ -574,6 +574,7 @@ USAGE_KEY_PATTERN = r'(?P<usage_key_string>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@
ASSET_KEY_PATTERN = r'(?P<asset_key_string>(?:/?c4x(:/)?/[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
USAGE_ID_PATTERN = r'(?P<usage_id>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
USERNAME_PATTERN = r'(?P<username>[\w.@+-]+)'
############################## EVENT TRACKING #################################
LMS_SEGMENT_KEY = None
......@@ -1906,6 +1907,9 @@ INSTALLED_APPS = (
'xblock_django',
# Bookmarks
'bookmarks',
# programs support
'openedx.core.djangoapps.programs',
......
......@@ -89,6 +89,9 @@ urlpatterns = (
# User API endpoints
url(r'^api/user/', include('openedx.core.djangoapps.user_api.urls')),
# Bookmarks API endpoints
url(r'^api/bookmarks/', include('bookmarks.urls')),
# Profile Images API endpoints
url(r'^api/profile_images/', include('openedx.core.djangoapps.profile_images.urls')),
......
......@@ -9,18 +9,18 @@ NOTE: These views are deprecated. These routes are superseded by
from django.conf.urls import patterns, url
from .views import ProfileImageUploadView, ProfileImageRemoveView
from django.conf import settings
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
urlpatterns = patterns(
'',
url(
r'^v1/' + USERNAME_PATTERN + '/upload$',
r'^v1/' + settings.USERNAME_PATTERN + '/upload$',
ProfileImageUploadView.as_view(),
name="profile_image_upload"
),
url(
r'^v1/' + USERNAME_PATTERN + '/remove$',
r'^v1/' + settings.USERNAME_PATTERN + '/remove$',
ProfileImageRemoveView.as_view(),
name="profile_image_remove"
),
......
......@@ -8,6 +8,8 @@ from ..profile_images.views import ProfileImageView
from .accounts.views import AccountView
from .preferences.views import PreferencesView, PreferencesDetailView
from django.conf.urls import patterns, url
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
urlpatterns = patterns(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment