453 lines
16 KiB
Python
453 lines
16 KiB
Python
"""
|
|
Unit tests for the notes app.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import json
|
|
|
|
import six
|
|
from django.contrib.auth.models import User
|
|
from django.core.exceptions import ValidationError
|
|
from django.test import RequestFactory, TestCase
|
|
from django.test.client import Client
|
|
from django.urls import reverse
|
|
from mock import Mock, patch
|
|
from opaque_keys.edx.locator import CourseLocator
|
|
from six import text_type
|
|
from six.moves import range
|
|
|
|
from courseware.tabs import CourseTab, get_course_tab_list
|
|
from notes import api, models, utils
|
|
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
|
|
|
|
class UtilsTest(ModuleStoreTestCase):
|
|
""" Tests for the notes utils. """
|
|
def setUp(self):
|
|
'''
|
|
Setup a dummy course-like object with a tabs field that can be
|
|
accessed via attribute lookup.
|
|
'''
|
|
super(UtilsTest, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
|
|
def test_notes_not_enabled(self):
|
|
'''
|
|
Tests that notes are disabled when the course tab configuration does NOT
|
|
contain a tab with type "notes."
|
|
'''
|
|
self.assertFalse(utils.notes_enabled_for_course(self.course))
|
|
|
|
def test_notes_enabled(self):
|
|
'''
|
|
Tests that notes are enabled when the course tab configuration contains
|
|
a tab with type "notes."
|
|
'''
|
|
with self.settings(FEATURES={'ENABLE_STUDENT_NOTES': True}):
|
|
self.course.advanced_modules = ["notes"]
|
|
self.assertTrue(utils.notes_enabled_for_course(self.course))
|
|
|
|
|
|
class CourseTabTest(ModuleStoreTestCase):
|
|
"""
|
|
Test that the course tab shows up the way we expect.
|
|
"""
|
|
def setUp(self):
|
|
'''
|
|
Setup a dummy course-like object with a tabs field that can be
|
|
accessed via attribute lookup.
|
|
'''
|
|
super(CourseTabTest, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
self.user = UserFactory()
|
|
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
|
|
|
def enable_notes(self):
|
|
"""Enable notes and add the tab to the course."""
|
|
self.course.tabs.append(CourseTab.load("notes"))
|
|
self.course.advanced_modules = ["notes"]
|
|
|
|
def has_notes_tab(self, course, user):
|
|
""" Returns true if the current course and user have a notes tab, false otherwise. """
|
|
request = RequestFactory().request()
|
|
request.user = user
|
|
all_tabs = get_course_tab_list(request, course)
|
|
return any([tab.name == u'My Notes' for tab in all_tabs])
|
|
|
|
def test_course_tab_not_visible(self):
|
|
# module not enabled in the course
|
|
self.assertFalse(self.has_notes_tab(self.course, self.user))
|
|
|
|
with self.settings(FEATURES={'ENABLE_STUDENT_NOTES': False}):
|
|
# setting not enabled and the module is not enabled
|
|
self.assertFalse(self.has_notes_tab(self.course, self.user))
|
|
|
|
# module is enabled and the setting is not enabled
|
|
self.course.advanced_modules = ["notes"]
|
|
self.assertFalse(self.has_notes_tab(self.course, self.user))
|
|
|
|
def test_course_tab_visible(self):
|
|
self.enable_notes()
|
|
self.assertTrue(self.has_notes_tab(self.course, self.user))
|
|
self.course.advanced_modules = []
|
|
self.assertFalse(self.has_notes_tab(self.course, self.user))
|
|
|
|
|
|
class ApiTest(TestCase):
|
|
|
|
def setUp(self):
|
|
super(ApiTest, self).setUp()
|
|
self.client = Client()
|
|
|
|
# Mocks
|
|
patcher = patch.object(api, 'api_enabled', Mock(return_value=True))
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
# Create two accounts
|
|
self.password = 'abc'
|
|
self.student = User.objects.create_user('student', 'student@test.com', self.password)
|
|
self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password)
|
|
self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
|
|
self.course_key = CourseLocator('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero')
|
|
self.note = {
|
|
'user': self.student,
|
|
'course_id': self.course_key,
|
|
'uri': '/',
|
|
'text': 'foo',
|
|
'quote': 'bar',
|
|
'range_start': 0,
|
|
'range_start_offset': 0,
|
|
'range_end': 100,
|
|
'range_end_offset': 0,
|
|
'tags': 'a,b,c'
|
|
}
|
|
|
|
# Make sure no note with this ID ever exists for testing purposes
|
|
self.NOTE_ID_DOES_NOT_EXIST = 99999
|
|
|
|
def login(self, as_student=None):
|
|
username = None
|
|
password = self.password
|
|
|
|
if as_student is None:
|
|
username = self.student.username
|
|
else:
|
|
username = as_student.username
|
|
|
|
self.client.login(username=username, password=password)
|
|
|
|
def url(self, name, args={}):
|
|
args.update({'course_id': text_type(self.course_key)})
|
|
return reverse(name, kwargs=args)
|
|
|
|
def create_notes(self, num_notes, create=True):
|
|
notes = []
|
|
for __ in range(num_notes):
|
|
note = models.Note(**self.note)
|
|
if create:
|
|
note.save()
|
|
notes.append(note)
|
|
return notes
|
|
|
|
def test_root(self):
|
|
self.login()
|
|
|
|
resp = self.client.get(self.url('notes_api_root'))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertNotEqual(resp.content, '')
|
|
|
|
content = json.loads(resp.content)
|
|
|
|
self.assertEqual(set(('name', 'version')), set(content.keys()))
|
|
self.assertIsInstance(content['version'], int)
|
|
self.assertEqual(content['name'], 'Notes API')
|
|
|
|
def test_index_empty(self):
|
|
self.login()
|
|
|
|
resp = self.client.get(self.url('notes_api_notes'))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertNotEqual(resp.content, '')
|
|
|
|
content = json.loads(resp.content)
|
|
self.assertEqual(len(content), 0)
|
|
|
|
def test_index_with_notes(self):
|
|
num_notes = 3
|
|
self.login()
|
|
self.create_notes(num_notes)
|
|
|
|
resp = self.client.get(self.url('notes_api_notes'))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertNotEqual(resp.content, '')
|
|
|
|
content = json.loads(resp.content)
|
|
self.assertIsInstance(content, list)
|
|
self.assertEqual(len(content), num_notes)
|
|
|
|
def test_index_max_notes(self):
|
|
self.login()
|
|
|
|
MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT')
|
|
num_notes = MAX_LIMIT + 1
|
|
self.create_notes(num_notes)
|
|
|
|
resp = self.client.get(self.url('notes_api_notes'))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertNotEqual(resp.content, '')
|
|
|
|
content = json.loads(resp.content)
|
|
self.assertIsInstance(content, list)
|
|
self.assertEqual(len(content), MAX_LIMIT)
|
|
|
|
def test_create_note(self):
|
|
self.login()
|
|
|
|
notes = self.create_notes(1)
|
|
self.assertEqual(len(notes), 1)
|
|
|
|
note_dict = notes[0].as_dict()
|
|
excluded_fields = ['id', 'user_id', 'created', 'updated']
|
|
note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
|
|
|
|
resp = self.client.post(self.url('notes_api_notes'),
|
|
json.dumps(note),
|
|
content_type='application/json',
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
|
|
self.assertEqual(resp.status_code, 303)
|
|
self.assertEqual(len(resp.content), 0)
|
|
|
|
def test_create_empty_notes(self):
|
|
self.login()
|
|
|
|
for empty_test in [None, [], '']:
|
|
resp = self.client.post(self.url('notes_api_notes'),
|
|
json.dumps(empty_test),
|
|
content_type='application/json',
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
self.assertEqual(resp.status_code, 400)
|
|
|
|
def test_create_note_missing_ranges(self):
|
|
self.login()
|
|
|
|
notes = self.create_notes(1)
|
|
self.assertEqual(len(notes), 1)
|
|
note_dict = notes[0].as_dict()
|
|
|
|
excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges']
|
|
note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
|
|
|
|
resp = self.client.post(self.url('notes_api_notes'),
|
|
json.dumps(note),
|
|
content_type='application/json',
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
self.assertEqual(resp.status_code, 400)
|
|
|
|
def test_read_note(self):
|
|
self.login()
|
|
|
|
notes = self.create_notes(3)
|
|
self.assertEqual(len(notes), 3)
|
|
|
|
for note in notes:
|
|
resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertNotEqual(resp.content, '')
|
|
|
|
content = json.loads(resp.content)
|
|
self.assertEqual(content['id'], note.pk)
|
|
self.assertEqual(content['user_id'], note.user_id)
|
|
|
|
def test_note_doesnt_exist_to_read(self):
|
|
self.login()
|
|
resp = self.client.get(self.url('notes_api_note', {
|
|
'note_id': self.NOTE_ID_DOES_NOT_EXIST
|
|
}))
|
|
self.assertEqual(resp.status_code, 404)
|
|
self.assertEqual(resp.content, '')
|
|
|
|
def test_student_doesnt_have_permission_to_read_note(self):
|
|
notes = self.create_notes(1)
|
|
self.assertEqual(len(notes), 1)
|
|
note = notes[0]
|
|
|
|
# set the student id to a different student (not the one that created the notes)
|
|
self.login(as_student=self.student2)
|
|
resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
|
|
self.assertEqual(resp.status_code, 403)
|
|
self.assertEqual(resp.content, '')
|
|
|
|
def test_delete_note(self):
|
|
self.login()
|
|
|
|
notes = self.create_notes(1)
|
|
self.assertEqual(len(notes), 1)
|
|
note = notes[0]
|
|
|
|
resp = self.client.delete(self.url('notes_api_note', {
|
|
'note_id': note.pk
|
|
}))
|
|
self.assertEqual(resp.status_code, 204)
|
|
self.assertEqual(resp.content, '')
|
|
|
|
with self.assertRaises(models.Note.DoesNotExist):
|
|
models.Note.objects.get(pk=note.pk)
|
|
|
|
def test_note_does_not_exist_to_delete(self):
|
|
self.login()
|
|
|
|
resp = self.client.delete(self.url('notes_api_note', {
|
|
'note_id': self.NOTE_ID_DOES_NOT_EXIST
|
|
}))
|
|
self.assertEqual(resp.status_code, 404)
|
|
self.assertEqual(resp.content, '')
|
|
|
|
def test_student_doesnt_have_permission_to_delete_note(self):
|
|
notes = self.create_notes(1)
|
|
self.assertEqual(len(notes), 1)
|
|
note = notes[0]
|
|
|
|
self.login(as_student=self.student2)
|
|
resp = self.client.delete(self.url('notes_api_note', {
|
|
'note_id': note.pk
|
|
}))
|
|
self.assertEqual(resp.status_code, 403)
|
|
self.assertEqual(resp.content, '')
|
|
|
|
try:
|
|
models.Note.objects.get(pk=note.pk)
|
|
except models.Note.DoesNotExist:
|
|
self.fail('note should exist and not be deleted because the student does not have permission to do so')
|
|
|
|
def test_update_note(self):
|
|
notes = self.create_notes(1)
|
|
note = notes[0]
|
|
|
|
updated_dict = note.as_dict()
|
|
updated_dict.update({
|
|
'text': 'itchy and scratchy',
|
|
'tags': ['simpsons', 'cartoons', 'animation']
|
|
})
|
|
|
|
self.login()
|
|
resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}),
|
|
json.dumps(updated_dict),
|
|
content_type='application/json',
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
self.assertEqual(resp.status_code, 303)
|
|
self.assertEqual(resp.content, '')
|
|
|
|
actual = models.Note.objects.get(pk=note.pk)
|
|
actual_dict = actual.as_dict()
|
|
for field in ['text', 'tags']:
|
|
self.assertEqual(actual_dict[field], updated_dict[field])
|
|
|
|
def test_search_note_params(self):
|
|
self.login()
|
|
|
|
total = 3
|
|
notes = self.create_notes(total)
|
|
invalid_uri = ''.join([note.uri for note in notes])
|
|
|
|
tests = [{'limit': 0, 'offset': 0, 'expected_rows': total},
|
|
{'limit': 0, 'offset': 2, 'expected_rows': total - 2},
|
|
{'limit': 0, 'offset': total, 'expected_rows': 0},
|
|
{'limit': 1, 'offset': 0, 'expected_rows': 1},
|
|
{'limit': 2, 'offset': 0, 'expected_rows': 2},
|
|
{'limit': total, 'offset': 2, 'expected_rows': 1},
|
|
{'limit': total, 'offset': total, 'expected_rows': 0},
|
|
{'limit': total + 1, 'offset': total + 1, 'expected_rows': 0},
|
|
{'limit': total + 1, 'offset': 0, 'expected_rows': total},
|
|
{'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0}]
|
|
|
|
for test in tests:
|
|
params = dict([(k, str(test[k]))
|
|
for k in ('limit', 'offset', 'uri')
|
|
if k in test])
|
|
resp = self.client.get(self.url('notes_api_search'),
|
|
params,
|
|
content_type='application/json',
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertNotEqual(resp.content, '')
|
|
|
|
content = json.loads(resp.content)
|
|
|
|
for expected_key in ('total', 'rows'):
|
|
self.assertIn(expected_key, content)
|
|
|
|
if 'expected_total' in test:
|
|
self.assertEqual(content['total'], test['expected_total'])
|
|
else:
|
|
self.assertEqual(content['total'], total)
|
|
|
|
self.assertEqual(len(content['rows']), test['expected_rows'])
|
|
|
|
for row in content['rows']:
|
|
self.assertIn('id', row)
|
|
|
|
|
|
class NoteTest(TestCase):
|
|
def setUp(self):
|
|
super(NoteTest, self).setUp()
|
|
|
|
self.password = 'abc'
|
|
self.student = User.objects.create_user('student', 'student@test.com', self.password)
|
|
self.course_key = CourseLocator('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero')
|
|
self.note = {
|
|
'user': self.student,
|
|
'course_id': self.course_key,
|
|
'uri': '/',
|
|
'text': 'foo',
|
|
'quote': 'bar',
|
|
'range_start': 0,
|
|
'range_start_offset': 0,
|
|
'range_end': 100,
|
|
'range_end_offset': 0,
|
|
'tags': 'a,b,c'
|
|
}
|
|
|
|
def test_clean_valid_note(self):
|
|
reference_note = models.Note(**self.note)
|
|
body = reference_note.as_dict()
|
|
|
|
note = models.Note(course_id=self.course_key, user=self.student)
|
|
try:
|
|
note.clean(json.dumps(body))
|
|
self.assertEqual(note.uri, body['uri'])
|
|
self.assertEqual(note.text, body['text'])
|
|
self.assertEqual(note.quote, body['quote'])
|
|
self.assertEqual(note.range_start, body['ranges'][0]['start'])
|
|
self.assertEqual(note.range_start_offset, body['ranges'][0]['startOffset'])
|
|
self.assertEqual(note.range_end, body['ranges'][0]['end'])
|
|
self.assertEqual(note.range_end_offset, body['ranges'][0]['endOffset'])
|
|
self.assertEqual(note.tags, ','.join(body['tags']))
|
|
except ValidationError:
|
|
self.fail('a valid note should not raise an exception')
|
|
|
|
def test_clean_invalid_note(self):
|
|
note = models.Note(course_id=self.course_key, user=self.student)
|
|
for empty_type in (None, '', 0, []):
|
|
with self.assertRaises(ValidationError):
|
|
note.clean(None)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
note.clean(json.dumps({
|
|
'text': 'foo',
|
|
'quote': 'bar',
|
|
'ranges': [{} for __ in range(10)] # too many ranges
|
|
}))
|
|
|
|
def test_as_dict(self):
|
|
note = models.Note(course_id=self.course_key, user=self.student)
|
|
d = note.as_dict()
|
|
self.assertNotIsInstance(d, six.string_types)
|
|
self.assertEqual(d['user_id'], self.student.id)
|
|
self.assertNotIn('course_id', d)
|