Bookmarks API.
TNL-2180
This commit is contained in:
@@ -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'
|
||||
),
|
||||
|
||||
15
lms/djangoapps/bookmarks/__init__.py
Normal file
15
lms/djangoapps/bookmarks/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Bookmarks module.
|
||||
"""
|
||||
|
||||
DEFAULT_FIELDS = [
|
||||
'id',
|
||||
'course_id',
|
||||
'usage_id',
|
||||
'created',
|
||||
]
|
||||
|
||||
OPTIONAL_FIELDS = [
|
||||
'display_name',
|
||||
'path',
|
||||
]
|
||||
93
lms/djangoapps/bookmarks/api.py
Normal file
93
lms/djangoapps/bookmarks/api.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
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()
|
||||
80
lms/djangoapps/bookmarks/migrations/0001_initial.py
Normal file
80
lms/djangoapps/bookmarks/migrations/0001_initial.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# -*- 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']
|
||||
0
lms/djangoapps/bookmarks/migrations/__init__.py
Normal file
0
lms/djangoapps/bookmarks/migrations/__init__.py
Normal file
71
lms/djangoapps/bookmarks/models.py
Normal file
71
lms/djangoapps/bookmarks/models.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
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
|
||||
50
lms/djangoapps/bookmarks/serializers.py
Normal file
50
lms/djangoapps/bookmarks/serializers.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
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)
|
||||
88
lms/djangoapps/bookmarks/services.py
Normal file
88
lms/djangoapps/bookmarks/services.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
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
|
||||
0
lms/djangoapps/bookmarks/tests/__init__.py
Normal file
0
lms/djangoapps/bookmarks/tests/__init__.py
Normal file
25
lms/djangoapps/bookmarks/tests/factories.py
Normal file
25
lms/djangoapps/bookmarks/tests/factories.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
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()
|
||||
180
lms/djangoapps/bookmarks/tests/test_api.py
Normal file
180
lms/djangoapps/bookmarks/tests/test_api.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
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)
|
||||
99
lms/djangoapps/bookmarks/tests/test_models.py
Normal file
99
lms/djangoapps/bookmarks/tests/test_models.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
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, [])
|
||||
101
lms/djangoapps/bookmarks/tests/test_services.py
Normal file
101
lms/djangoapps/bookmarks/tests/test_services.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
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))
|
||||
482
lms/djangoapps/bookmarks/tests/test_views.py
Normal file
482
lms/djangoapps/bookmarks/tests/test_views.py
Normal file
@@ -0,0 +1,482 @@
|
||||
"""
|
||||
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)
|
||||
26
lms/djangoapps/bookmarks/urls.py
Normal file
26
lms/djangoapps/bookmarks/urls.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
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'
|
||||
),
|
||||
)
|
||||
300
lms/djangoapps/bookmarks/views.py
Normal file
300
lms/djangoapps/bookmarks/views.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user