test_item.py 17.2 KB
Newer Older
1 2 3
"""Tests for items views."""

import json
4
from datetime import datetime
5 6 7
import ddt

from mock import Mock, patch
8
from pytz import UTC
9 10 11 12 13 14 15
from webob import Response

from django.http import Http404
from django.test import TestCase
from django.test.client import RequestFactory

from contentstore.views.component import component_handler
16

Calen Pennington committed
17
from contentstore.tests.utils import CourseTestCase
18
from student.tests.factories import UserFactory
19 20
from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore.django import modulestore
21 22
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
23
from xmodule.modulestore.exceptions import ItemNotFoundError
24 25


26 27
class ItemTest(CourseTestCase):
    """ Base test class for create, save, and delete """
28
    def setUp(self):
29 30 31 32 33 34 35 36
        super(ItemTest, self).setUp()

        self.unicode_locator = unicode(loc_mapper().translate_location(
            self.course.location.course_id, self.course.location, False, True
        ))

    def get_old_id(self, locator):
        """
37
        Converts new locator to old id format (forcing to non-draft).
38
        """
39
        return loc_mapper().translate_locator_to_location(BlockUsageLocator(locator)).replace(revision=None)
40 41 42 43 44

    def get_item_from_modulestore(self, locator, draft=False):
        """
        Get the item referenced by the locator from the modulestore
        """
45
        store = modulestore('draft') if draft else modulestore('direct')
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
        return store.get_item(self.get_old_id(locator))

    def response_locator(self, response):
        """
        Get the locator (unicode representation) from the response payload
        :param response:
        """
        parsed = json.loads(response.content)
        return parsed['locator']

    def create_xblock(self, parent_locator=None, display_name=None, category=None, boilerplate=None):
        data = {
            'parent_locator': self.unicode_locator if parent_locator is None else parent_locator,
            'category': category
        }
        if display_name is not None:
            data['display_name'] = display_name
        if boilerplate is not None:
            data['boilerplate'] = boilerplate
        return self.client.ajax_post('/xblock', json.dumps(data))

67

68 69 70 71 72 73 74 75 76 77 78 79 80 81
class GetItem(ItemTest):
    """Tests for '/xblock' GET url."""

    def test_get_vertical(self):
        # Add a vertical
        resp = self.create_xblock(category='vertical')
        self.assertEqual(resp.status_code, 200)

        # Retrieve it
        resp_content = json.loads(resp.content)
        resp = self.client.get('/xblock/' + resp_content['locator'])
        self.assertEqual(resp.status_code, 200)


82 83
class DeleteItem(ItemTest):
    """Tests for '/xblock' DELETE url."""
David Baumgold committed
84
    def test_delete_static_page(self):
85
        # Add static tab
86
        resp = self.create_xblock(category='static_tab')
87 88 89
        self.assertEqual(resp.status_code, 200)

        # Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
90
        resp_content = json.loads(resp.content)
91
        resp = self.client.delete('/xblock/' + resp_content['locator'])
92
        self.assertEqual(resp.status_code, 204)
93 94


95
class TestCreateItem(ItemTest):
96 97 98 99 100 101 102 103 104
    """
    Test the create_item handler thoroughly
    """
    def test_create_nicely(self):
        """
        Try the straightforward use cases
        """
        # create a chapter
        display_name = 'Nicely created'
105
        resp = self.create_xblock(display_name=display_name, category='chapter')
106 107 108
        self.assertEqual(resp.status_code, 200)

        # get the new item and check its category and display_name
109 110
        chap_locator = self.response_locator(resp)
        new_obj = self.get_item_from_modulestore(chap_locator)
Calen Pennington committed
111
        self.assertEqual(new_obj.scope_ids.block_type, 'chapter')
112
        self.assertEqual(new_obj.display_name, display_name)
113 114
        self.assertEqual(new_obj.location.org, self.course.location.org)
        self.assertEqual(new_obj.location.course, self.course.location.course)
115 116

        # get the course and ensure it now points to this one
117 118
        course = self.get_item_from_modulestore(self.unicode_locator)
        self.assertIn(self.get_old_id(chap_locator).url(), course.children)
119 120

        # use default display name
121
        resp = self.create_xblock(parent_locator=chap_locator, category='vertical')
122 123
        self.assertEqual(resp.status_code, 200)

124
        vert_locator = self.response_locator(resp)
125 126 127

        # create problem w/ boilerplate
        template_id = 'multiplechoice.yaml'
128 129 130 131
        resp = self.create_xblock(
            parent_locator=vert_locator,
            category='problem',
            boilerplate=template_id
132 133
        )
        self.assertEqual(resp.status_code, 200)
134 135
        prob_locator = self.response_locator(resp)
        problem = self.get_item_from_modulestore(prob_locator, True)
136 137 138 139 140 141 142 143 144 145 146 147 148
        # ensure it's draft
        self.assertTrue(problem.is_draft)
        # check against the template
        template = CapaDescriptor.get_template(template_id)
        self.assertEqual(problem.data, template['data'])
        self.assertEqual(problem.display_name, template['metadata']['display_name'])
        self.assertEqual(problem.markdown, template['metadata']['markdown'])

    def test_create_item_negative(self):
        """
        Negative tests for create_item
        """
        # non-existent boilerplate: creates a default
149
        resp = self.create_xblock(category='problem', boilerplate='nosuchboilerplate.yaml')
150 151
        self.assertEqual(resp.status_code, 200)

152 153 154 155 156 157 158
    def test_create_with_future_date(self):
        self.assertEqual(self.course.start, datetime(2030, 1, 1, tzinfo=UTC))
        resp = self.create_xblock(category='chapter')
        locator = self.response_locator(resp)
        obj = self.get_item_from_modulestore(locator)
        self.assertEqual(obj.start, datetime(2030, 1, 1, tzinfo=UTC))

David Baumgold committed
159

160
class TestEditItem(ItemTest):
161
    """
162
    Test xblock update.
163 164 165 166 167 168
    """
    def setUp(self):
        """ Creates the test course structure and a couple problems to 'edit'. """
        super(TestEditItem, self).setUp()
        # create a chapter
        display_name = 'chapter created'
169 170 171 172 173 174
        resp = self.create_xblock(display_name=display_name, category='chapter')
        chap_locator = self.response_locator(resp)
        resp = self.create_xblock(parent_locator=chap_locator, category='sequential')
        self.seq_locator = self.response_locator(resp)
        self.seq_update_url = '/xblock/' + self.seq_locator

175 176
        # create problem w/ boilerplate
        template_id = 'multiplechoice.yaml'
177 178 179 180 181
        resp = self.create_xblock(parent_locator=self.seq_locator, category='problem', boilerplate=template_id)
        self.problem_locator = self.response_locator(resp)
        self.problem_update_url = '/xblock/' + self.problem_locator

        self.course_update_url = '/xblock/' + self.unicode_locator
182 183 184 185 186

    def test_delete_field(self):
        """
        Sending null in for a field 'deletes' it
        """
187 188 189
        self.client.ajax_post(
            self.problem_update_url,
            data={'metadata': {'rerandomize': 'onreset'}}
190
        )
191
        problem = self.get_item_from_modulestore(self.problem_locator, True)
192
        self.assertEqual(problem.rerandomize, 'onreset')
193 194 195
        self.client.ajax_post(
            self.problem_update_url,
            data={'metadata': {'rerandomize': None}}
196
        )
197
        problem = self.get_item_from_modulestore(self.problem_locator, True)
198 199 200 201 202 203
        self.assertEqual(problem.rerandomize, 'never')

    def test_null_field(self):
        """
        Sending null in for a field 'deletes' it
        """
204
        problem = self.get_item_from_modulestore(self.problem_locator, True)
205
        self.assertIsNotNone(problem.markdown)
206 207 208
        self.client.ajax_post(
            self.problem_update_url,
            data={'nullout': ['markdown']}
209
        )
210
        problem = self.get_item_from_modulestore(self.problem_locator, True)
211
        self.assertIsNone(problem.markdown)
212 213 214 215 216

    def test_date_fields(self):
        """
        Test setting due & start dates on sequential
        """
217
        sequential = self.get_item_from_modulestore(self.seq_locator)
Calen Pennington committed
218
        self.assertIsNone(sequential.due)
219 220 221
        self.client.ajax_post(
            self.seq_update_url,
            data={'metadata': {'due': '2010-11-22T04:00Z'}}
222
        )
223
        sequential = self.get_item_from_modulestore(self.seq_locator)
224
        self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
225 226 227
        self.client.ajax_post(
            self.seq_update_url,
            data={'metadata': {'start': '2010-09-12T14:00Z'}}
228
        )
229
        sequential = self.get_item_from_modulestore(self.seq_locator)
230 231
        self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
        self.assertEqual(sequential.start, datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
232

233 234 235 236
    def test_delete_child(self):
        """
        Test deleting a child.
        """
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
        # Create 2 children of main course.
        resp_1 = self.create_xblock(display_name='child 1', category='chapter')
        resp_2 = self.create_xblock(display_name='child 2', category='chapter')
        chapter1_locator = self.response_locator(resp_1)
        chapter2_locator = self.response_locator(resp_2)

        course = self.get_item_from_modulestore(self.unicode_locator)
        self.assertIn(self.get_old_id(chapter1_locator).url(), course.children)
        self.assertIn(self.get_old_id(chapter2_locator).url(), course.children)

        # Remove one child from the course.
        resp = self.client.ajax_post(
            self.course_update_url,
            data={'children': [chapter2_locator]}
        )
        self.assertEqual(resp.status_code, 200)

        # Verify that the child is removed.
        course = self.get_item_from_modulestore(self.unicode_locator)
        self.assertNotIn(self.get_old_id(chapter1_locator).url(), course.children)
        self.assertIn(self.get_old_id(chapter2_locator).url(), course.children)
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286

    def test_reorder_children(self):
        """
        Test reordering children that can be in the draft store.
        """
        # Create 2 child units and re-order them. There was a bug about @draft getting added
        # to the IDs.
        unit_1_resp = self.create_xblock(parent_locator=self.seq_locator, category='vertical')
        unit_2_resp = self.create_xblock(parent_locator=self.seq_locator, category='vertical')
        unit1_locator = self.response_locator(unit_1_resp)
        unit2_locator = self.response_locator(unit_2_resp)

        # The sequential already has a child defined in the setUp (a problem).
        # Children must be on the sequential to reproduce the original bug,
        # as it is important that the parent (sequential) NOT be in the draft store.
        children = self.get_item_from_modulestore(self.seq_locator).children
        self.assertEqual(self.get_old_id(unit1_locator).url(), children[1])
        self.assertEqual(self.get_old_id(unit2_locator).url(), children[2])

        resp = self.client.ajax_post(
            self.seq_update_url,
            data={'children': [self.problem_locator, unit2_locator, unit1_locator]}
        )
        self.assertEqual(resp.status_code, 200)

        children = self.get_item_from_modulestore(self.seq_locator).children
        self.assertEqual(self.get_old_id(self.problem_locator).url(), children[0])
        self.assertEqual(self.get_old_id(unit1_locator).url(), children[2])
        self.assertEqual(self.get_old_id(unit2_locator).url(), children[1])
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335

    def test_make_public(self):
        """ Test making a private problem public (publishing it). """
        # When the problem is first created, it is only in draft (because of its category).
        with self.assertRaises(ItemNotFoundError):
            self.get_item_from_modulestore(self.problem_locator, False)
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'make_public'}
        )
        self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))

    def test_make_private(self):
        """ Test making a public problem private (un-publishing it). """
        # Make problem public.
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'make_public'}
        )
        self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
        # Now make it private
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'make_private'}
        )
        with self.assertRaises(ItemNotFoundError):
            self.get_item_from_modulestore(self.problem_locator, False)

    def test_make_draft(self):
        """ Test creating a draft version of a public problem. """
        # Make problem public.
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'make_public'}
        )
        self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
        # Now make it draft, which means both versions will exist.
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'create_draft'}
        )
        # Update the draft version and check that published is different.
        self.client.ajax_post(
            self.problem_update_url,
            data={'metadata': {'due': '2077-10-10T04:00Z'}}
        )
        published = self.get_item_from_modulestore(self.problem_locator, False)
        self.assertIsNone(published.due)
        draft = self.get_item_from_modulestore(self.problem_locator, True)
336
        self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
337 338 339 340 341 342 343 344 345 346 347

    def test_make_public_with_update(self):
        """ Update a problem and make it public at the same time. """
        self.client.ajax_post(
            self.problem_update_url,
            data={
                'metadata': {'due': '2077-10-10T04:00Z'},
                'publish': 'make_public'
            }
        )
        published = self.get_item_from_modulestore(self.problem_locator, False)
348
        self.assertEqual(published.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366

    def test_make_private_with_update(self):
        """ Make a problem private and update it at the same time. """
        # Make problem public.
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'make_public'}
        )
        self.client.ajax_post(
            self.problem_update_url,
            data={
                'metadata': {'due': '2077-10-10T04:00Z'},
                'publish': 'make_private'
            }
        )
        with self.assertRaises(ItemNotFoundError):
            self.get_item_from_modulestore(self.problem_locator, False)
        draft = self.get_item_from_modulestore(self.problem_locator, True)
367
        self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387

    def test_create_draft_with_update(self):
        """ Create a draft and update it at the same time. """
        # Make problem public.
        self.client.ajax_post(
            self.problem_update_url,
            data={'publish': 'make_public'}
        )
        self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
        # Now make it draft, which means both versions will exist.
        self.client.ajax_post(
            self.problem_update_url,
            data={
                'metadata': {'due': '2077-10-10T04:00Z'},
                'publish': 'create_draft'
            }
        )
        published = self.get_item_from_modulestore(self.problem_locator, False)
        self.assertIsNone(published.due)
        draft = self.get_item_from_modulestore(self.problem_locator, True)
388
        self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438


@ddt.ddt
class TestComponentHandler(TestCase):
    def setUp(self):
        self.request_factory = RequestFactory()

        patcher = patch('contentstore.views.component.modulestore')
        self.modulestore = patcher.start()
        self.addCleanup(patcher.stop)

        self.descriptor = self.modulestore.return_value.get_item.return_value

        self.usage_id = 'dummy_usage_id'

        self.user = UserFactory()

        self.request = self.request_factory.get('/dummy-url')
        self.request.user = self.user

    def test_invalid_handler(self):
        self.descriptor.handle.side_effect = Http404

        with self.assertRaises(Http404):
            component_handler(self.request, self.usage_id, 'invalid_handler')

    @ddt.data('GET', 'POST', 'PUT', 'DELETE')
    def test_request_method(self, method):

        def check_handler(handler, request, suffix):
            self.assertEquals(request.method, method)
            return Response()

        self.descriptor.handle = check_handler

        # Have to use the right method to create the request to get the HTTP method that we want
        req_factory_method = getattr(self.request_factory, method.lower())
        request = req_factory_method('/dummy-url')
        request.user = self.user

        component_handler(request, self.usage_id, 'dummy_handler')

    @ddt.data(200, 404, 500)
    def test_response_code(self, status_code):
        def create_response(handler, request, suffix):
            return Response(status_code=status_code)

        self.descriptor.handle = create_response

        self.assertEquals(component_handler(self.request, self.usage_id, 'dummy_handler').status_code, status_code)