Exemplo n.º 1
0
 def test_status(self):
     c = Client()
     start_time = datetime.now(tz=UTC())
     self.api_post(c, '/api/status/', data={'token': self.token.token})
     token = ApiToken.objects.get(id=self.token.id)
     self.assertTrue(token.last_seen >= start_time)
Exemplo n.º 2
0
    def _create_courseware_context(self):
        """
        Returns and creates the rendering context for the courseware.
        Also returns the table of contents for the courseware.
        """
        request = RequestCache.get_current_request()
        courseware_context = {
            'csrf':
            csrf(self.request)['csrf_token'],
            'COURSE_TITLE':
            self.course.display_name_with_default_escaped,
            'course':
            self.course,
            'init':
            '',
            'fragment':
            Fragment(),
            'staff_access':
            self.is_staff,
            'studio_url':
            get_studio_url(self.course, 'course'),
            'masquerade':
            self.masquerade,
            'real_user':
            self.real_user,
            'xqa_server':
            settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
            'bookmarks_api_url':
            reverse('bookmarks'),
            'language_preference':
            self._get_language_preference(),
            'disable_optimizely':
            True,
            'section_title':
            None,
            'sequence_title':
            None,
            'disable_accordion':
            waffle.flag_is_active(request, 'unified_course_view')
        }
        table_of_contents = toc_for_course(
            self.effective_user,
            self.request,
            self.course,
            self.chapter_url_name,
            self.section_url_name,
            self.field_data_cache,
        )
        courseware_context['accordion'] = render_accordion(
            self.request,
            self.course,
            table_of_contents['chapters'],
        )

        # entrance exam data
        self._add_entrance_exam_to_context(courseware_context)

        # staff masquerading data
        now = datetime.now(UTC())
        effective_start = _adjust_start_date_for_beta_testers(
            self.effective_user, self.course, self.course_key)
        if not in_preview_mode() and self.is_staff and now < effective_start:
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            courseware_context['disable_student_access'] = True

        if self.section:
            # chromeless data
            if self.section.chrome:
                chrome = [
                    s.strip() for s in self.section.chrome.lower().split(",")
                ]
                if 'accordion' not in chrome:
                    courseware_context['disable_accordion'] = True
                if 'tabs' not in chrome:
                    courseware_context['disable_tabs'] = True

            # default tab
            if self.section.default_tab:
                courseware_context['default_tab'] = self.section.default_tab

            # section data
            courseware_context[
                'section_title'] = self.section.display_name_with_default_escaped
            section_context = self._create_section_context(
                table_of_contents['previous_of_active_section'],
                table_of_contents['next_of_active_section'],
            )
            courseware_context['fragment'] = self.section.render(
                STUDENT_VIEW, section_context)
            if self.section.position and self.section.has_children:
                display_items = self.section.get_display_items()
                if display_items:
                    courseware_context['sequence_title'] = display_items[self.section.position - 1] \
                        .display_name_with_default

        return courseware_context
Exemplo n.º 3
0
def index(request, course_id, chapter=None, section=None, position=None):
    """
    Displays courseware accordion and associated content.  If course, chapter,
    and section are all specified, renders the page, or returns an error if they
    are invalid.

    If section is not specified, displays the accordion opened to the right chapter.

    If neither chapter or section are specified, redirects to user's most recent
    chapter, or the first chapter if this is the user's first visit.

    Arguments:

     - request    : HTTP request
     - course_id  : course id (str: ORG/course/URL_NAME)
     - chapter    : chapter url_name (str)
     - section    : section url_name (str)
     - position   : position in module, eg of <sequential> module (str)

    Returns:

     - HTTPresponse
    """

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)

    user = User.objects.prefetch_related("groups").get(id=request.user.id)

    # Redirecting to dashboard if the course is blocked due to un payment.
    redeemed_registration_codes = CourseRegistrationCode.objects.filter(
        course_id=course_key,
        registrationcoderedemption__redeemed_by=request.user)
    for redeemed_registration in redeemed_registration_codes:
        if not getattr(redeemed_registration.invoice, 'is_valid'):
            log.warning(
                u'User %s cannot access the course %s because payment has not yet been received',
                user, course_key.to_deprecated_string())
            return redirect(reverse('dashboard'))

    request.user = user  # keep just one instance of User
    course = get_course_with_access(user, 'load', course_key, depth=2)
    staff_access = has_access(user, 'staff', course)
    registered = registered_for_course(course, user)
    if not registered:
        # TODO (vshnayder): do course instructors need to be registered to see course?
        log.debug(u'User %s tried to view course %s but is not enrolled', user,
                  course.location.to_deprecated_string())
        return redirect(
            reverse('about_course', args=[course_key.to_deprecated_string()]))

    masq = setup_masquerade(request, staff_access)

    try:
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
            course_key, user, course, depth=2)

        course_module = get_module_for_descriptor(user, request, course,
                                                  field_data_cache, course_key)
        if course_module is None:
            log.warning(
                u'If you see this, something went wrong: if we got this'
                u' far, should have gotten a course module for this user')
            return redirect(
                reverse('about_course',
                        args=[course_key.to_deprecated_string()]))

        studio_url = get_studio_url(course, 'course')

        context = {
            'csrf':
            csrf(request)['csrf_token'],
            'accordion':
            render_accordion(request, course, chapter, section,
                             field_data_cache),
            'COURSE_TITLE':
            course.display_name_with_default,
            'course':
            course,
            'init':
            '',
            'fragment':
            Fragment(),
            'staff_access':
            staff_access,
            'studio_url':
            studio_url,
            'masquerade':
            masq,
            'xqa_server':
            settings.FEATURES.get(
                'USE_XQA_SERVER',
                'http://*****:*****@content-qa.mitx.mit.edu/xqa'),
            'reverifications':
            fetch_reverify_banner_info(request, course_key),
        }

        now = datetime.now(UTC())
        effective_start = _adjust_start_date_for_beta_testers(
            user, course, course_key)
        if staff_access and now < effective_start:
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True

        has_content = course.has_children_at_depth(CONTENT_DEPTH)
        if not has_content:
            # Show empty courseware for a course with no units
            return render_to_response('courseware/courseware.html', context)
        elif chapter is None:
            # passing CONTENT_DEPTH avoids returning 404 for a course with an
            # empty first section and a second section with content
            return redirect_to_course_position(course_module, CONTENT_DEPTH)

        # Only show the chat if it's enabled by the course and in the
        # settings.
        show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT']
        if show_chat:
            context['chat'] = chat_settings(course, user)
            # If we couldn't load the chat settings, then don't show
            # the widget in the courseware.
            if context['chat'] is None:
                show_chat = False

        context['show_chat'] = show_chat

        chapter_descriptor = course.get_child_by(
            lambda m: m.location.name == chapter)
        if chapter_descriptor is not None:
            save_child_position(course_module, chapter)
        else:
            raise Http404(
                'No chapter descriptor found with name {}'.format(chapter))

        chapter_module = course_module.get_child_by(
            lambda m: m.location.name == chapter)
        if chapter_module is None:
            # User may be trying to access a chapter that isn't live yet
            if masq == 'student':  # if staff is masquerading as student be kinder, don't 404
                log.debug('staff masq as student: no chapter %s' % chapter)
                return redirect(
                    reverse('courseware',
                            args=[course.id.to_deprecated_string()]))
            raise Http404

        if section is not None:
            section_descriptor = chapter_descriptor.get_child_by(
                lambda m: m.location.name == section)

            if section_descriptor is None:
                # Specifically asked-for section doesn't exist
                if masq == 'student':  # if staff is masquerading as student be kinder, don't 404
                    log.debug('staff masq as student: no section %s' % section)
                    return redirect(
                        reverse('courseware',
                                args=[course.id.to_deprecated_string()]))
                raise Http404

            ## Allow chromeless operation
            if section_descriptor.chrome:
                chrome = [
                    s.strip()
                    for s in section_descriptor.chrome.lower().split(",")
                ]
                if 'accordion' not in chrome:
                    context['disable_accordion'] = True
                if 'tabs' not in chrome:
                    context['disable_tabs'] = True

            if section_descriptor.default_tab:
                context['default_tab'] = section_descriptor.default_tab

            # cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
            # which will prefetch the children more efficiently than doing a recursive load
            section_descriptor = modulestore().get_item(
                section_descriptor.location, depth=None)

            # Load all descendants of the section, because we're going to display its
            # html, which in general will need all of its children
            section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
                course_key, user, section_descriptor, depth=None)

            # Verify that position a string is in fact an int
            if position is not None:
                try:
                    int(position)
                except ValueError:
                    raise Http404(
                        "Position {} is not an integer!".format(position))

            section_module = get_module_for_descriptor(
                request.user, request, section_descriptor,
                section_field_data_cache, course_key, position)

            if section_module is None:
                # User may be trying to be clever and access something
                # they don't have access to.
                raise Http404

            # Save where we are in the chapter
            save_child_position(chapter_module, section)
            context['fragment'] = section_module.render(STUDENT_VIEW)
            context[
                'section_title'] = section_descriptor.display_name_with_default
        else:
            # section is none, so display a message
            studio_url = get_studio_url(course, 'course')
            prev_section = get_current_child(chapter_module)
            if prev_section is None:
                # Something went wrong -- perhaps this chapter has no sections visible to the user.
                # Clearing out the last-visited state and showing "first-time" view by redirecting
                # to courseware.
                course_module.position = None
                course_module.save()
                return redirect(
                    reverse('courseware',
                            args=[course.id.to_deprecated_string()]))
            prev_section_url = reverse('courseware_section',
                                       kwargs={
                                           'course_id':
                                           course_key.to_deprecated_string(),
                                           'chapter':
                                           chapter_descriptor.url_name,
                                           'section':
                                           prev_section.url_name
                                       })
            context['fragment'] = Fragment(content=render_to_string(
                'courseware/welcome-back.html', {
                    'course': course,
                    'studio_url': studio_url,
                    'chapter_module': chapter_module,
                    'prev_section': prev_section,
                    'prev_section_url': prev_section_url
                }))

        result = render_to_response('courseware/courseware.html', context)
    except Exception as e:

        # Doesn't bar Unicode characters from URL, but if Unicode characters do
        # cause an error it is a graceful failure.
        if isinstance(e, UnicodeEncodeError):
            raise Http404("URL contains Unicode characters")

        if isinstance(e, Http404):
            # let it propagate
            raise

        # In production, don't want to let a 500 out for any reason
        if settings.DEBUG:
            raise
        else:
            log.exception(
                u"Error in index view: user={user}, course={course}, chapter={chapter}"
                u" section={section} position={position}".format(
                    user=user,
                    course=course,
                    chapter=chapter,
                    section=section,
                    position=position))
            try:
                result = render_to_response('courseware/courseware-error.html',
                                            {
                                                'staff_access': staff_access,
                                                'course': course
                                            })
            except:
                # Let the exception propagate, relying on global config to at
                # at least return a nice error message
                log.exception("Error while rendering courseware-error page")
                raise

    return result
Exemplo n.º 4
0
    def _get_html():

        if type(module) in [
                SequenceModule, VerticalModule
        ]:  # TODO: make this more general, eg use an XModule attribute instead
            return get_html()

        module_id = module.id
        if module.descriptor.has_score:
            histogram = grade_histogram(module_id)
            render_histogram = len(histogram) > 0
        else:
            histogram = None
            render_histogram = False

        if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
            [filepath, filename] = getattr(module.descriptor, 'xml_attributes',
                                           {}).get('filename', ['', None])
            osfs = module.system.filestore
            if filename is not None and osfs.exists(filename):
                # if original, unmangled filename exists then use it (github
                # doesn't like symlinks)
                filepath = filename
            data_dir = osfs.root_path.rsplit('/')[-1]
            giturl = getattr(module.lms, 'giturl',
                             '') or 'https://github.com/MITx'
            edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
        else:
            edit_link = False
            # Need to define all the variables that are about to be used
            giturl = ""
            data_dir = ""

        source_file = module.lms.source_file  # source used to generate the problem XML, eg latex or word

        # useful to indicate to staff if problem has been released or not
        # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
        now = datetime.datetime.now(UTC())
        is_released = "unknown"
        mstart = module.descriptor.lms.start

        if mstart is not None:
            is_released = "<font color='red'>Yes!</font>" if (
                now > mstart) else "<font color='green'>Not yet</font>"

        staff_context = {
            'fields': [(field.name, getattr(module, field.name))
                       for field in module.fields],
            'lms_fields': [(field.name, getattr(module.lms, field.name))
                           for field in module.lms.fields],
            'xml_attributes':
            getattr(module.descriptor, 'xml_attributes', {}),
            'location':
            module.location,
            'xqa_key':
            module.lms.xqa_key,
            'source_file':
            source_file,
            'source_url':
            '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
            'category':
            str(module.__class__.__name__),
            # Template uses element_id in js function names, so can't allow dashes
            'element_id':
            module.location.html_id().replace('-', '_'),
            'edit_link':
            edit_link,
            'user':
            user,
            'xqa_server':
            settings.MITX_FEATURES.get(
                'USE_XQA_SERVER',
                'http://*****:*****@content-qa.mitx.mit.edu/xqa'),
            'histogram':
            json.dumps(histogram),
            'render_histogram':
            render_histogram,
            'module_content':
            get_html(),
            'is_released':
            is_released,
        }
        return render_to_string("staff_problem_info.html", staff_context)
Exemplo n.º 5
0
class FilenameGeneratorTestCase(TestCase):
    """
    Tests for course_and_time_based_filename_generator
    """
    NOW = datetime.strptime('1974-06-22T01:02:03', '%Y-%m-%dT%H:%M:%S').replace(tzinfo=UTC())

    def setUp(self):
        super(FilenameGeneratorTestCase, self).setUp()
        datetime_patcher = patch.object(
            util.file, 'datetime',
            Mock(wraps=datetime)
        )
        mocked_datetime = datetime_patcher.start()
        mocked_datetime.now.return_value = self.NOW
        self.addCleanup(datetime_patcher.stop)

    @ddt.data(CourseLocator, SlashSeparatedCourseKey)
    def test_filename_generator(self, course_key_class):
        """
        Tests that the generator creates names based on course_id, base name, and date.
        """
        self.assertEqual(
            u'foo_bar_baz_file_1974-06-22-010203',
            course_and_time_based_filename_generator(course_key_class(org='foo', course='bar', run='baz'), 'file')
        )

        self.assertEqual(
            u'foo_bar_baz_base_name_ø_1974-06-22-010203',
            course_and_time_based_filename_generator(
                course_key_class(org='foo', course='bar', run='baz'), ' base` name ø '
            )
        )
    def test_course_metadata_utils(self):
        """
        Test every single function in course_metadata_utils.
        """

        def mock_strftime_localized(date_time, format_string):
            """
            Mock version of strftime_localized used for testing purposes.

            Because we don't have a real implementation of strftime_localized
            to work with (strftime_localized is provided by the XBlock runtime,
            which we don't have access to for this test case), we must declare
            this dummy implementation. This does NOT behave like a real
            strftime_localized should. It purposely returns a really dumb value
            that's only useful for testing purposes.

            Arguments:
                date_time (datetime): datetime to be formatted.
                format_string (str): format specifier. Valid values include:
                    - 'DATE_TIME'
                    - 'TIME'
                    - 'SHORT_DATE'
                    - 'LONG_DATE'

            Returns (str): format_string + " " + str(date_time)
            """
            if format_string in ['DATE_TIME', 'TIME', 'SHORT_DATE', 'LONG_DATE']:
                return format_string + " " + date_time.strftime("%Y-%m-%d %H:%M:%S")
            else:
                raise ValueError("Invalid format string :" + format_string)

        def nop_gettext(text):
            """Dummy implementation of gettext, so we don't need Django."""
            return text

        test_datetime = datetime(1945, 02, 06, 04, 20, 00, tzinfo=UTC())
        advertised_start_parsable = "2038-01-19 03:14:07"
        advertised_start_bad_date = "215-01-01 10:10:10"
        advertised_start_unparsable = "This coming fall"

        FunctionTest = namedtuple('FunctionTest', 'function scenarios')  # pylint: disable=invalid-name
        TestScenario = namedtuple('TestScenario', 'arguments expected_return')  # pylint: disable=invalid-name

        function_tests = [
            FunctionTest(clean_course_key, [
                # Test with a Mongo course and '=' as padding.
                TestScenario(
                    (self.demo_course.id, '='),
                    "course_MVSFQL2EMVWW6WBOGEXUMYLMNRPTEMBRGQ======"
                ),
                # Test with a Split course and '~' as padding.
                TestScenario(
                    (self.html_course.id, '~'),
                    "course_MNXXK4TTMUWXMMJ2KVXGS5TFOJZWS5DZLAVUGUZNGIYDGK2ZGIYDSNQ~"
                ),
            ]),
            FunctionTest(url_name_for_block, [
                TestScenario((self.demo_course,), self.demo_course.location.name),
                TestScenario((self.html_course,), self.html_course.location.name),
            ]),
            FunctionTest(display_name_with_default_escaped, [
                # Test course with no display name.
                TestScenario((self.demo_course,), "Empty"),
                # Test course with a display name that contains characters that need escaping.
                TestScenario((self.html_course,), "Intro to &lt;html&gt;"),
            ]),
            FunctionTest(display_name_with_default, [
                # Test course with no display name.
                TestScenario((self.demo_course,), "Empty"),
                # Test course with a display name that contains characters that need escaping.
                TestScenario((self.html_course,), "Intro to <html>"),
            ]),
            FunctionTest(number_for_course_location, [
                TestScenario((self.demo_course.location,), "DemoX.1"),
                TestScenario((self.html_course.location,), "CS-203"),
            ]),
            FunctionTest(has_course_started, [
                TestScenario((self.demo_course.start,), True),
                TestScenario((self.html_course.start,), False),
            ]),
            FunctionTest(has_course_ended, [
                TestScenario((self.demo_course.end,), True),
                TestScenario((self.html_course.end,), False),
            ]),
            FunctionTest(course_start_date_is_default, [
                TestScenario((test_datetime, advertised_start_parsable), False),
                TestScenario((test_datetime, None), False),
                TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False),
                TestScenario((DEFAULT_START_DATE, None), True),
            ]),
            FunctionTest(course_start_datetime_text, [
                # Test parsable advertised start date.
                # Expect start datetime to be parsed and formatted back into a string.
                TestScenario(
                    (DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME', nop_gettext, mock_strftime_localized),
                    mock_strftime_localized(Date().from_json(advertised_start_parsable), 'DATE_TIME') + " UTC"
                ),
                # Test un-parsable advertised start date.
                # Expect date parsing to throw a ValueError, and the advertised
                # start to be returned in Title Case.
                TestScenario(
                    (test_datetime, advertised_start_unparsable, 'DATE_TIME', nop_gettext, mock_strftime_localized),
                    advertised_start_unparsable.title()
                ),
                # Test parsable advertised start date from before January 1, 1900.
                # Expect mock_strftime_localized to throw a ValueError, and the
                # advertised start to be returned in Title Case.
                TestScenario(
                    (test_datetime, advertised_start_bad_date, 'DATE_TIME', nop_gettext, mock_strftime_localized),
                    advertised_start_bad_date.title()
                ),
                # Test without advertised start date, but with a set start datetime.
                # Expect formatted datetime to be returned.
                TestScenario(
                    (test_datetime, None, 'SHORT_DATE', nop_gettext, mock_strftime_localized),
                    mock_strftime_localized(test_datetime, 'SHORT_DATE')
                ),
                # Test without advertised start date and with default start datetime.
                # Expect TBD to be returned.
                TestScenario(
                    (DEFAULT_START_DATE, None, 'SHORT_DATE', nop_gettext, mock_strftime_localized),
                    'TBD'
                )
            ]),
            FunctionTest(course_end_datetime_text, [
                # Test with a set end datetime.
                # Expect formatted datetime to be returned.
                TestScenario(
                    (test_datetime, 'TIME', mock_strftime_localized),
                    mock_strftime_localized(test_datetime, 'TIME') + " UTC"
                ),
                # Test with default end datetime.
                # Expect empty string to be returned.
                TestScenario(
                    (None, 'TIME', mock_strftime_localized),
                    ""
                )
            ]),
            FunctionTest(may_certify_for_course, [
                TestScenario(('early_with_info', True, True), True),
                TestScenario(('early_no_info', False, False), True),
                TestScenario(('end', True, False), True),
                TestScenario(('end', False, True), True),
                TestScenario(('end', False, False), False),
            ]),
        ]

        for function_test in function_tests:
            for scenario in function_test.scenarios:
                actual_return = function_test.function(*scenario.arguments)
                self.assertEqual(actual_return, scenario.expected_return)

        # Even though we don't care about testing mock_strftime_localized,
        # we still need to test it with a bad format string in order to
        # satisfy the coverage checker.
        with self.assertRaises(ValueError):
            mock_strftime_localized(test_datetime, 'BAD_FORMAT_SPECIFIER')
Exemplo n.º 7
0
from fs.memoryfs import MemoryFS

from mock import Mock, patch

from xblock.runtime import KvsFieldData, DictKeyValueStore

import xmodule.course_module
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.utils.timezone import UTC

ORG = 'test_org'
COURSE = 'test_course'

NOW = datetime.strptime('2013-01-01T01:00:00',
                        '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC())


class CourseFieldsTestCase(unittest.TestCase):
    def test_default_start_date(self):
        self.assertEqual(xmodule.course_module.CourseFields.start.default,
                         datetime(2030, 1, 1, tzinfo=UTC()))


class DummySystem(ImportSystem):
    @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
    def __init__(self, load_error_modules):

        xmlstore = XMLModuleStore("data_dir",
                                  course_dirs=[],
                                  load_error_modules=load_error_modules)
Exemplo n.º 8
0
 def has_started(self):
     return datetime.now(UTC()) > self.first_eligible_appointment_date
Exemplo n.º 9
0
 def has_ended(self):
     return datetime.now(UTC()) > self.last_eligible_appointment_date
Exemplo n.º 10
0
 def is_past_due(self):
     """
     Is it now past this problem's due date, including grace period?
     """
     return (self.close_date is not None
             and datetime.datetime.now(UTC()) > self.close_date)
Exemplo n.º 11
0
    def check_problem(self, data):
        """
        Checks whether answers to a problem are correct

        Returns a map of correct/incorrect answers:
          {'success' : 'correct' | 'incorrect' | AJAX alert msg string,
           'contents' : html}
        """
        event_info = dict()
        event_info['state'] = self.lcp.get_state()
        event_info['problem_id'] = self.location.url()

        answers = self.make_dict_of_responses(data)
        event_info['answers'] = convert_files_to_filenames(answers)

        # Too late. Cannot submit
        if self.closed():
            event_info['failure'] = 'closed'
            self.system.track_function('problem_check_fail', event_info)
            raise NotFoundError('Problem is closed')

        # Problem submitted. Student should reset before checking again
        if self.done and self.rerandomize == "always":
            event_info['failure'] = 'unreset'
            self.system.track_function('problem_check_fail', event_info)
            raise NotFoundError(
                'Problem must be reset before it can be checked again')

        # Problem queued. Students must wait a specified waittime before they are allowed to submit
        if self.lcp.is_queued():
            current_time = datetime.datetime.now(UTC())
            prev_submit_time = self.lcp.get_recentmost_queuetime()
            waittime_between_requests = self.system.xqueue['waittime']
            if (current_time - prev_submit_time
                ).total_seconds() < waittime_between_requests:
                msg = u'You must wait at least {wait} seconds between submissions'.format(
                    wait=waittime_between_requests)
                return {
                    'success': msg,
                    'html': ''
                }  # Prompts a modal dialog in ajax callback

        try:
            correct_map = self.lcp.grade_answers(answers)
            self.attempts = self.attempts + 1
            self.lcp.done = True
            self.set_state_from_lcp()

        except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
            log.warning("StudentInputError in capa_module:problem_check",
                        exc_info=True)

            # Save the user's state before failing
            self.set_state_from_lcp()

            # If the user is a staff member, include
            # the full exception, including traceback,
            # in the response
            if self.system.user_is_staff:
                msg = u"Staff debug info: {tb}".format(
                    tb=cgi.escape(traceback.format_exc()))

            # Otherwise, display just an error message,
            # without a stack trace
            else:
                msg = u"Error: {msg}".format(msg=inst.message)

            return {'success': msg}

        except Exception as err:
            # Save the user's state before failing
            self.set_state_from_lcp()

            if self.system.DEBUG:
                msg = u"Error checking problem: {}".format(err.message)
                msg += u'\nTraceback:\n{}'.format(traceback.format_exc())
                return {'success': msg}
            raise

        published_grade = self.publish_grade()

        # success = correct if ALL questions in this problem are correct
        success = 'correct'
        for answer_id in correct_map:
            if not correct_map.is_correct(answer_id):
                success = 'incorrect'

        # NOTE: We are logging both full grading and queued-grading submissions. In the latter,
        #       'success' will always be incorrect
        event_info['grade'] = published_grade['grade']
        event_info['max_grade'] = published_grade['max_grade']
        event_info['correct_map'] = correct_map.get_dict()
        event_info['success'] = success
        event_info['attempts'] = self.attempts
        self.system.track_function('problem_check', event_info)

        if hasattr(self.system, 'psychometrics_handler'
                   ):  # update PsychometricsData using callback
            self.system.psychometrics_handler(self.get_state_for_lcp())

        # render problem into HTML
        html = self.get_problem_html(encapsulate=False)

        return {
            'success': success,
            'contents': html,
        }
Exemplo n.º 12
0
def add_staff_debug_info(user, block, view, frag, context):  # pylint: disable=unused-argument
    """
    Updates the supplied module with a new get_html function that wraps
    the output of the old get_html function with additional information
    for admin users only, including a histogram of student answers and the
    definition of the xmodule

    Does nothing if module is a SequenceModule or a VerticalModule.
    """
    # TODO: make this more general, eg use an XModule attribute instead
    if isinstance(block, (SequenceModule, VerticalModule)):
        return frag

    block_id = block.id
    if block.has_score and settings.FEATURES.get(
            'DISPLAY_HISTOGRAMS_TO_STAFF'):
        histogram = grade_histogram(block_id)
        render_histogram = len(histogram) > 0
    else:
        histogram = None
        render_histogram = False

    if settings.FEATURES.get('ENABLE_LMS_MIGRATION') and hasattr(
            block.runtime, 'filestore'):
        [filepath, filename] = getattr(block, 'xml_attributes',
                                       {}).get('filename', ['', None])
        osfs = block.runtime.filestore
        if filename is not None and osfs.exists(filename):
            # if original, unmangled filename exists then use it (github
            # doesn't like symlinks)
            filepath = filename
        data_dir = block.static_asset_path or osfs.root_path.rsplit('/')[-1]
        giturl = block.giturl or 'https://github.com/MITx'
        edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
    else:
        edit_link = False
        # Need to define all the variables that are about to be used
        giturl = ""
        data_dir = ""

    source_file = block.source_file  # source used to generate the problem XML, eg latex or word

    # useful to indicate to staff if problem has been released or not
    # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
    now = datetime.datetime.now(UTC())
    is_released = "unknown"
    mstart = block.start

    if mstart is not None:
        is_released = "<font color='red'>Yes!</font>" if (
            now > mstart) else "<font color='green'>Not yet</font>"

    field_contents = []
    for name, field in block.fields.items():
        try:
            field_contents.append((name, field.read_from(block)))
        except InvalidScopeError:
            log.warning("Unable to read field in Staff Debug information",
                        exc_info=True)
            field_contents.append((name, "WARNING: Unable to read field"))

    staff_context = {
        'fields':
        field_contents,
        'xml_attributes':
        getattr(block, 'xml_attributes', {}),
        'location':
        block.location,
        'xqa_key':
        block.xqa_key,
        'source_file':
        source_file,
        'source_url':
        '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
        'category':
        str(block.__class__.__name__),
        # Template uses element_id in js function names, so can't allow dashes
        'element_id':
        block.location.html_id().replace('-', '_'),
        'edit_link':
        edit_link,
        'user':
        user,
        'xqa_server':
        settings.FEATURES.get('USE_XQA_SERVER',
                              'http://*****:*****@content-qa.mitx.mit.edu/xqa'),
        'histogram':
        json.dumps(histogram),
        'render_histogram':
        render_histogram,
        'block_content':
        frag.content,
        'is_released':
        is_released,
    }
    return wrap_fragment(
        frag, render_to_string("staff_problem_info.html", staff_context))
Exemplo n.º 13
0
def now():
    return datetime.utcnow().replace(tzinfo=UTC())
Exemplo n.º 14
0
 def test_status_get(self):
     c = Client()
     start_time = datetime.now(tz=UTC())
     self.api_get(c, '/api/status/?token={0}'.format(self.token.token))
     token = ApiToken.objects.get(id=self.token.id)
     self.assertTrue(token.last_seen >= start_time)
Exemplo n.º 15
0
def get_discussion_category_map(course, user):
    """
    Get a mapping of categories and subcategories that are visible to
    the user within a course.
    """
    unexpanded_category_map = defaultdict(list)

    modules = get_accessible_discussion_modules(course, user)

    is_course_cohorted = course.is_cohorted
    cohorted_discussion_ids = course.cohorted_discussions

    for module in modules:
        id = module.discussion_id
        title = module.discussion_target
        sort_key = module.sort_key
        category = " / ".join(
            [x.strip() for x in module.discussion_category.split("/")])
        #Handle case where module.start is None
        entry_start_date = module.start if module.start else datetime.max.replace(
            tzinfo=pytz.UTC)
        unexpanded_category_map[category].append({
            "title": title,
            "id": id,
            "sort_key": sort_key,
            "start_date": entry_start_date
        })

    category_map = {
        "entries": defaultdict(dict),
        "subcategories": defaultdict(dict)
    }
    for category_path, entries in unexpanded_category_map.items():
        node = category_map["subcategories"]
        path = [x.strip() for x in category_path.split("/")]

        # Find the earliest start date for the entries in this category
        category_start_date = None
        for entry in entries:
            if category_start_date is None or entry[
                    "start_date"] < category_start_date:
                category_start_date = entry["start_date"]

        for level in path[:-1]:
            if level not in node:
                node[level] = {
                    "subcategories": defaultdict(dict),
                    "entries": defaultdict(dict),
                    "sort_key": level,
                    "start_date": category_start_date
                }
            else:
                if node[level]["start_date"] > category_start_date:
                    node[level]["start_date"] = category_start_date
            node = node[level]["subcategories"]

        level = path[-1]
        if level not in node:
            node[level] = {
                "subcategories": defaultdict(dict),
                "entries": defaultdict(dict),
                "sort_key": level,
                "start_date": category_start_date
            }
        else:
            if node[level]["start_date"] > category_start_date:
                node[level]["start_date"] = category_start_date

        dupe_counters = defaultdict(
            lambda: 0)  # counts the number of times we see each title
        for entry in entries:
            title = entry["title"]
            if node[level]["entries"][title]:
                # If we've already seen this title, append an incrementing number to disambiguate
                # the category from other categores sharing the same title in the course discussion UI.
                dupe_counters[title] += 1
                title = u"{title} ({counter})".format(
                    title=title, counter=dupe_counters[title])
            node[level]["entries"][title] = {
                "id": entry["id"],
                "sort_key": entry["sort_key"],
                "start_date": entry["start_date"],
                "is_cohorted": is_course_cohorted
            }

    # TODO.  BUG! : course location is not unique across multiple course runs!
    # (I think Kevin already noticed this)  Need to send course_id with requests, store it
    # in the backend.
    for topic, entry in course.discussion_topics.items():
        category_map['entries'][topic] = {
            "id":
            entry["id"],
            "sort_key":
            entry.get("sort_key", topic),
            "start_date":
            datetime.now(UTC()),
            "is_cohorted":
            is_course_cohorted and entry["id"] in cohorted_discussion_ids
        }

    _sort_map_entries(category_map, course.discussion_sort_alpha)

    return _filter_unstarted_categories(category_map)
Exemplo n.º 16
0
 def has_started_registration(self):
     return datetime.now(UTC()) > self.registration_start_date
    has_course_ended,
    DEFAULT_START_DATE,
    course_start_date_is_default,
    course_start_datetime_text,
    course_end_datetime_text,
    may_certify_for_course,
)
from xmodule.fields import Date
from xmodule.modulestore.tests.utils import (
    MongoModulestoreBuilder,
    VersioningModulestoreBuilder,
    MixedModulestoreBuilder
)


_TODAY = datetime.now(UTC())
_LAST_MONTH = _TODAY - timedelta(days=30)
_LAST_WEEK = _TODAY - timedelta(days=7)
_NEXT_WEEK = _TODAY + timedelta(days=7)


class CourseMetadataUtilsTestCase(TestCase):
    """
    Tests for course_metadata_utils.
    """

    def setUp(self):
        """
        Set up module store testing capabilities and initialize test courses.
        """
        super(CourseMetadataUtilsTestCase, self).setUp()
Exemplo n.º 18
0
 def has_ended_registration(self):
     return datetime.now(UTC()) > self.registration_end_date
from mock import Mock, patch
import itertools

from xblock.runtime import KvsFieldData, DictKeyValueStore

import xmodule.course_module
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.utils.timezone import UTC


ORG = 'test_org'
COURSE = 'test_course'

NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC())


class CourseFieldsTestCase(unittest.TestCase):
    def test_default_start_date(self):
        self.assertEqual(
            xmodule.course_module.CourseFields.start.default,
            datetime(2030, 1, 1, tzinfo=UTC())
        )


class DummySystem(ImportSystem):
    @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
    def __init__(self, load_error_modules):

        xmlstore = XMLModuleStore("data_dir", source_dirs=[],
Exemplo n.º 20
0
 def is_registering(self):
     now = datetime.now(UTC())
     return now >= self.registration_start_date and now <= self.registration_end_date
Exemplo n.º 21
0
 def test_default_start_date(self):
     self.assertEqual(xmodule.course_module.CourseFields.start.default,
                      datetime(2030, 1, 1, tzinfo=UTC()))
Exemplo n.º 22
0
class CourseFields(object):
    lti_passports = List(
        help="LTI tools passports as id:client_key:client_secret",
        scope=Scope.settings)
    textbooks = TextbookList(
        help="List of pairs of (title, url) for textbooks used in this course",
        default=[],
        scope=Scope.content)
    wiki_slug = String(help="Slug that points to the wiki for this course",
                       scope=Scope.content)
    enrollment_start = Date(
        help="Date that enrollment for this class is opened",
        scope=Scope.settings)
    enrollment_end = Date(help="Date that enrollment for this class is closed",
                          scope=Scope.settings)
    start = Date(
        help="Start time when this module is visible",
        # using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the
        # time of first invocation of this stmt on the server
        default=datetime.fromtimestamp(0, UTC()),
        scope=Scope.settings)
    end = Date(help="Date that this class ends", scope=Scope.settings)
    advertised_start = String(
        help="Date that this course is advertised to start",
        scope=Scope.settings)
    grading_policy = Dict(help="Grading policy definition for this class",
                          default={
                              "GRADER": [{
                                  "type": "Homework",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "short_label": "HW",
                                  "weight": 0.15
                              }, {
                                  "type": "Lab",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "weight": 0.15
                              }, {
                                  "type": "Midterm Exam",
                                  "short_label": "Midterm",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.3
                              }, {
                                  "type": "Final Exam",
                                  "short_label": "Final",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.4
                              }],
                              "GRADE_CUTOFFS": {
                                  "Pass": 0.5
                              }
                          },
                          scope=Scope.content)
    show_calculator = Boolean(
        help="Whether to show the calculator in this course",
        default=False,
        scope=Scope.settings)
    display_name = String(help="Display name for this module",
                          default="Empty",
                          display_name="Display Name",
                          scope=Scope.settings)
    show_chat = Boolean(help="Whether to show the chat widget in this course",
                        default=False,
                        scope=Scope.settings)
    tabs = List(help="List of tabs to enable in this course",
                scope=Scope.settings)
    end_of_course_survey_url = String(help="Url for the end-of-course survey",
                                      scope=Scope.settings)
    discussion_blackouts = List(
        help="List of pairs of start/end dates for discussion blackouts",
        scope=Scope.settings)
    discussion_topics = Dict(help="Map of topics names to ids",
                             scope=Scope.settings)
    discussion_sort_alpha = Boolean(
        scope=Scope.settings,
        default=False,
        help="Sort forum categories and subcategories alphabetically.")
    testcenter_info = Dict(help="Dictionary of Test Center info",
                           scope=Scope.settings)
    announcement = Date(help="Date this course is announced",
                        scope=Scope.settings)
    cohort_config = Dict(help="Dictionary defining cohort configuration",
                         scope=Scope.settings)
    is_new = Boolean(help="Whether this course should be flagged as new",
                     scope=Scope.settings)
    no_grade = Boolean(help="True if this course isn't graded",
                       default=False,
                       scope=Scope.settings)
    disable_progress_graph = Boolean(
        help="True if this course shouldn't display the progress graph",
        default=False,
        scope=Scope.settings)
    pdf_textbooks = List(
        help="List of dictionaries containing pdf_textbook configuration",
        scope=Scope.settings)
    html_textbooks = List(
        help="List of dictionaries containing html_textbook configuration",
        scope=Scope.settings)
    remote_gradebook = Dict(scope=Scope.settings)
    allow_anonymous = Boolean(scope=Scope.settings, default=True)
    allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
    advanced_modules = List(help="Beta modules used in your course",
                            scope=Scope.settings)
    has_children = True
    checklists = List(
        scope=Scope.settings,
        default=[{
            "short_description":
            "Getting Started With Studio",
            "items": [{
                "short_description": "Add Course Team Members",
                "long_description":
                "Grant your collaborators permission to edit your course so you can work together.",
                "is_checked": False,
                "action_url": "ManageUsers",
                "action_text": "Edit Course Team",
                "action_external": False
            }, {
                "short_description": "Set Important Dates for Your Course",
                "long_description":
                "Establish your course's student enrollment and launch dates on the Schedule and Details page.",
                "is_checked": False,
                "action_url": "SettingsDetails",
                "action_text": "Edit Course Details &amp; Schedule",
                "action_external": False
            }, {
                "short_description": "Draft Your Course's Grading Policy",
                "long_description":
                "Set up your assignment types and grading policy even if you haven't created all your assignments.",
                "is_checked": False,
                "action_url": "SettingsGrading",
                "action_text": "Edit Grading Settings",
                "action_external": False
            }, {
                "short_description": "Explore the Other Studio Checklists",
                "long_description":
                "Discover other available course authoring tools, and find help when you need it.",
                "is_checked": False,
                "action_url": "",
                "action_text": "",
                "action_external": False
            }]
        }, {
            "short_description":
            "Draft a Rough Course Outline",
            "items": [{
                "short_description":
                "Create Your First Section and Subsection",
                "long_description":
                "Use your course outline to build your first Section and Subsection.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }, {
                "short_description": "Set Section Release Dates",
                "long_description":
                "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }, {
                "short_description": "Designate a Subsection as Graded",
                "long_description":
                "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }, {
                "short_description": "Reordering Course Content",
                "long_description":
                "Use drag and drop to reorder the content in your course.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }, {
                "short_description": "Renaming Sections",
                "long_description":
                "Rename Sections by clicking the Section name from the Course Outline.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }, {
                "short_description": "Deleting Course Content",
                "long_description":
                "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }, {
                "short_description":
                "Add an Instructor-Only Section to Your Outline",
                "long_description":
                "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
                "is_checked": False,
                "action_url": "CourseOutline",
                "action_text": "Edit Course Outline",
                "action_external": False
            }]
        }, {
            "short_description":
            "Explore edX's Support Tools",
            "items": [{
                "short_description": "Explore the Studio Help Forum",
                "long_description":
                "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.",
                "is_checked": False,
                "action_url": "http://help.edge.edx.org/",
                "action_text": "Visit Studio Help",
                "action_external": True
            }, {
                "short_description": "Enroll in edX 101",
                "long_description":
                "Register for edX 101, edX's primer for course creation.",
                "is_checked": False,
                "action_url":
                "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
                "action_text": "Register for edX 101",
                "action_external": True
            }, {
                "short_description": "Download the Studio Documentation",
                "long_description":
                "Download the searchable Studio reference documentation in PDF form.",
                "is_checked": False,
                "action_url":
                "http://files.edx.org/Getting_Started_with_Studio.pdf",
                "action_text": "Download Documentation",
                "action_external": True
            }]
        }, {
            "short_description":
            "Draft Your Course About Page",
            "items": [{
                "short_description": "Draft a Course Description",
                "long_description":
                "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.",
                "is_checked": False,
                "action_url": "SettingsDetails",
                "action_text": "Edit Course Schedule &amp; Details",
                "action_external": False
            }, {
                "short_description": "Add Staff Bios",
                "long_description":
                "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.",
                "is_checked": False,
                "action_url": "SettingsDetails",
                "action_text": "Edit Course Schedule &amp; Details",
                "action_external": False
            }, {
                "short_description": "Add Course FAQs",
                "long_description":
                "Include a short list of frequently asked questions about your course.",
                "is_checked": False,
                "action_url": "SettingsDetails",
                "action_text": "Edit Course Schedule &amp; Details",
                "action_external": False
            }, {
                "short_description": "Add Course Prerequisites",
                "long_description":
                "Let students know what knowledge and/or skills they should have before they enroll in your course.",
                "is_checked": False,
                "action_url": "SettingsDetails",
                "action_text": "Edit Course Schedule &amp; Details",
                "action_external": False
            }]
        }])
    info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
    show_timezone = Boolean(
        help=
        "True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.",
        scope=Scope.settings,
        default=True)
    due_date_display_format = String(
        help=
        "Format supported by strftime for displaying due dates. Takes precedence over show_timezone.",
        scope=Scope.settings,
        default=None)
    enrollment_domain = String(
        help=
        "External login method associated with user accounts allowed to register in course",
        scope=Scope.settings)
    course_image = String(
        help="Filename of the course image",
        scope=Scope.settings,
        # Ensure that courses imported from XML keep their image
        default="images_course_image.jpg")

    # An extra property is used rather than the wiki_slug/number because
    # there are courses that change the number for different runs. This allows
    # courses to share the same css_class across runs even if they have
    # different numbers.
    #
    # TODO get rid of this as soon as possible or potentially build in a robust
    # way to add in course-specific styling. There needs to be a discussion
    # about the right way to do this, but arjun will address this ASAP. Also
    # note that the courseware template needs to change when this is removed.
    css_class = String(help="DO NOT USE THIS", scope=Scope.settings)

    # TODO: This is a quick kludge to allow CS50 (and other courses) to
    # specify their own discussion forums as external links by specifying a
    # "discussion_link" in their policy JSON file. This should later get
    # folded in with Syllabus, Course Info, and additional Custom tabs in a
    # more sensible framework later.
    discussion_link = String(help="DO NOT USE THIS", scope=Scope.settings)

    # TODO: same as above, intended to let internal CS50 hide the progress tab
    # until we get grade integration set up.
    # Explicit comparison to True because we always want to return a bool.
    hide_progress_tab = Boolean(help="DO NOT USE THIS", scope=Scope.settings)

    display_organization = String(
        help=
        "An optional display string for the course organization that will get rendered in the LMS",
        scope=Scope.settings)

    display_coursenumber = String(
        help=
        "An optional display string for the course number that will get rendered in the LMS",
        scope=Scope.settings)
Exemplo n.º 23
0
 def _closed(self, timeinfo):
     if timeinfo.close_date is not None and datetime.now(
             UTC()) > timeinfo.close_date:
         return True
     return False
Exemplo n.º 24
0
 def has_started(self):
     return datetime.now(UTC()) > self.start
Exemplo n.º 25
0
def get_discussion_category_map(course):
    course_id = course.id

    unexpanded_category_map = defaultdict(list)

    modules = _get_discussion_modules(course)

    is_course_cohorted = course.is_cohorted
    cohorted_discussion_ids = course.cohorted_discussions

    for module in modules:
        id = module.discussion_id
        title = module.discussion_target
        sort_key = module.sort_key
        category = " / ".join(
            [x.strip() for x in module.discussion_category.split("/")])
        #Handle case where module.start is None
        entry_start_date = module.start if module.start else datetime.max.replace(
            tzinfo=pytz.UTC)
        unexpanded_category_map[category].append({
            "title": title,
            "id": id,
            "sort_key": sort_key,
            "start_date": entry_start_date
        })

    category_map = {
        "entries": defaultdict(dict),
        "subcategories": defaultdict(dict)
    }
    for category_path, entries in unexpanded_category_map.items():
        node = category_map["subcategories"]
        path = [x.strip() for x in category_path.split("/")]

        # Find the earliest start date for the entries in this category
        category_start_date = None
        for entry in entries:
            if category_start_date is None or entry[
                    "start_date"] < category_start_date:
                category_start_date = entry["start_date"]

        for level in path[:-1]:
            if level not in node:
                node[level] = {
                    "subcategories": defaultdict(dict),
                    "entries": defaultdict(dict),
                    "sort_key": level,
                    "start_date": category_start_date
                }
            else:
                if node[level]["start_date"] > category_start_date:
                    node[level]["start_date"] = category_start_date
            node = node[level]["subcategories"]

        level = path[-1]
        if level not in node:
            node[level] = {
                "subcategories": defaultdict(dict),
                "entries": defaultdict(dict),
                "sort_key": level,
                "start_date": category_start_date
            }
        else:
            if node[level]["start_date"] > category_start_date:
                node[level]["start_date"] = category_start_date

        for entry in entries:
            node[level]["entries"][entry["title"]] = {
                "id": entry["id"],
                "sort_key": entry["sort_key"],
                "start_date": entry["start_date"],
                "is_cohorted": is_course_cohorted
            }

    # TODO.  BUG! : course location is not unique across multiple course runs!
    # (I think Kevin already noticed this)  Need to send course_id with requests, store it
    # in the backend.
    for topic, entry in course.discussion_topics.items():
        category_map['entries'][topic] = {
            "id":
            entry["id"],
            "sort_key":
            entry.get("sort_key", topic),
            "start_date":
            datetime.now(UTC()),
            "is_cohorted":
            is_course_cohorted and entry["id"] in cohorted_discussion_ids
        }

    _sort_map_entries(category_map, course.discussion_sort_alpha)

    return _filter_unstarted_categories(category_map)
Exemplo n.º 26
0
 def setUpClass(cls):
     super(MasqueradeTestCase, cls).setUpClass()
     cls.course = CourseFactory.create(number='masquerade-test', metadata={'start': datetime.now(UTC())})
     cls.info_page = ItemFactory.create(
         category="course_info", parent_location=cls.course.location,
         data="OOGIE BLOOGIE", display_name="updates"
     )
     cls.chapter = ItemFactory.create(
         parent_location=cls.course.location,
         category="chapter",
         display_name="Test Section",
     )
     cls.sequential_display_name = "Test Masquerade Subsection"
     cls.sequential = ItemFactory.create(
         parent_location=cls.chapter.location,
         category="sequential",
         display_name=cls.sequential_display_name,
     )
     cls.vertical = ItemFactory.create(
         parent_location=cls.sequential.location,
         category="vertical",
         display_name="Test Unit",
     )
     problem_xml = OptionResponseXMLFactory().build_xml(
         question_text='The correct answer is Correct',
         num_inputs=2,
         weight=2,
         options=['Correct', 'Incorrect'],
         correct_option='Correct'
     )
     cls.problem_display_name = "TestMasqueradeProblem"
     cls.problem = ItemFactory.create(
         parent_location=cls.vertical.location,
         category='problem',
         data=problem_xml,
         display_name=cls.problem_display_name
     )
Exemplo n.º 27
0
def get_discussion_category_map(course,
                                user,
                                divided_only_if_explicit=False,
                                exclude_unstarted=True):
    """
    Transform the list of this course's discussion xblocks into a recursive dictionary structure.  This is used
    to render the discussion category map in the discussion tab sidebar for a given user.

    Args:
        course: Course for which to get the ids.
        user:  User to check for access.
        divided_only_if_explicit (bool): If True, inline topics are marked is_divided only if they are
            explicitly listed in CourseDiscussionSettings.discussion_topics.

    Example:
        >>> example = {
        >>>               "entries": {
        >>>                   "General": {
        >>>                       "sort_key": "General",
        >>>                       "is_divided": True,
        >>>                       "id": "i4x-edx-eiorguegnru-course-foobarbaz"
        >>>                   }
        >>>               },
        >>>               "children": [
        >>>                     ["General", "entry"],
        >>>                     ["Getting Started", "subcategory"]
        >>>               ],
        >>>               "subcategories": {
        >>>                   "Getting Started": {
        >>>                       "subcategories": {},
        >>>                       "children": [
        >>>                           ["Working with Videos", "entry"],
        >>>                           ["Videos on edX", "entry"]
        >>>                       ],
        >>>                       "entries": {
        >>>                           "Working with Videos": {
        >>>                               "sort_key": None,
        >>>                               "is_divided": False,
        >>>                               "id": "d9f970a42067413cbb633f81cfb12604"
        >>>                           },
        >>>                           "Videos on edX": {
        >>>                               "sort_key": None,
        >>>                               "is_divided": False,
        >>>                               "id": "98d8feb5971041a085512ae22b398613"
        >>>                           }
        >>>                       }
        >>>                   }
        >>>               }
        >>>          }

    """
    unexpanded_category_map = defaultdict(list)

    xblocks = get_accessible_discussion_xblocks(course, user)

    discussion_settings = get_course_discussion_settings(course.id)
    discussion_division_enabled = course_discussion_division_enabled(
        discussion_settings)
    divided_discussion_ids = discussion_settings.divided_discussions

    for xblock in xblocks:
        discussion_id = xblock.discussion_id
        title = xblock.discussion_target
        sort_key = xblock.sort_key
        category = " / ".join(
            [x.strip() for x in xblock.discussion_category.split("/")])
        # Handle case where xblock.start is None
        entry_start_date = xblock.start if xblock.start else datetime.max.replace(
            tzinfo=pytz.UTC)
        unexpanded_category_map[category].append({
            "title": title,
            "id": discussion_id,
            "sort_key": sort_key,
            "start_date": entry_start_date
        })

    category_map = {
        "entries": defaultdict(dict),
        "subcategories": defaultdict(dict)
    }
    for category_path, entries in unexpanded_category_map.items():
        node = category_map["subcategories"]
        path = [x.strip() for x in category_path.split("/")]

        # Find the earliest start date for the entries in this category
        category_start_date = None
        for entry in entries:
            if category_start_date is None or entry[
                    "start_date"] < category_start_date:
                category_start_date = entry["start_date"]

        for level in path[:-1]:
            if level not in node:
                node[level] = {
                    "subcategories": defaultdict(dict),
                    "entries": defaultdict(dict),
                    "sort_key": level,
                    "start_date": category_start_date
                }
            else:
                if node[level]["start_date"] > category_start_date:
                    node[level]["start_date"] = category_start_date
            node = node[level]["subcategories"]

        level = path[-1]
        if level not in node:
            node[level] = {
                "subcategories": defaultdict(dict),
                "entries": defaultdict(dict),
                "sort_key": level,
                "start_date": category_start_date
            }
        else:
            if node[level]["start_date"] > category_start_date:
                node[level]["start_date"] = category_start_date

        divide_all_inline_discussions = (  # pylint: disable=invalid-name
            not divided_only_if_explicit
            and discussion_settings.always_divide_inline_discussions)
        dupe_counters = defaultdict(
            lambda: 0)  # counts the number of times we see each title
        for entry in entries:
            is_entry_divided = (discussion_division_enabled
                                and (divide_all_inline_discussions
                                     or entry["id"] in divided_discussion_ids))

            title = entry["title"]
            if node[level]["entries"][title]:
                # If we've already seen this title, append an incrementing number to disambiguate
                # the category from other categores sharing the same title in the course discussion UI.
                dupe_counters[title] += 1
                title = u"{title} ({counter})".format(
                    title=title, counter=dupe_counters[title])
            node[level]["entries"][title] = {
                "id": entry["id"],
                "sort_key": entry["sort_key"],
                "start_date": entry["start_date"],
                "is_divided": is_entry_divided
            }

    # TODO.  BUG! : course location is not unique across multiple course runs!
    # (I think Kevin already noticed this)  Need to send course_id with requests, store it
    # in the backend.
    for topic, entry in course.discussion_topics.items():
        category_map['entries'][topic] = {
            "id":
            entry["id"],
            "sort_key":
            entry.get("sort_key", topic),
            "start_date":
            datetime.now(UTC()),
            "is_divided": (discussion_division_enabled
                           and entry["id"] in divided_discussion_ids)
        }

    _sort_map_entries(category_map, course.discussion_sort_alpha)

    return _filter_unstarted_categories(
        category_map, course) if exclude_unstarted else category_map
Exemplo n.º 28
0
def add_staff_markup(user, has_instructor_access, disable_staff_debug_info,
                     block, view, frag, context):  # pylint: disable=unused-argument
    """
    Updates the supplied module with a new get_html function that wraps
    the output of the old get_html function with additional information
    for admin users only, including a histogram of student answers, the
    definition of the xmodule, and a link to view the module in Studio
    if it is a Studio edited, mongo stored course.

    Does nothing if module is a SequenceModule.
    """
    # TODO: make this more general, eg use an XModule attribute instead
    if isinstance(block, VerticalBlock) and (
            not context or not context.get('child_of_vertical', False)):
        # check that the course is a mongo backed Studio course before doing work
        is_studio_course = block.course_edit_method == "Studio"

        if is_studio_course:
            # build edit link to unit in CMS. Can't use reverse here as lms doesn't load cms's urls.py
            edit_link = "//" + settings.CMS_BASE + '/container/' + unicode(
                block.location)

            # return edit link in rendered HTML for display
            return wrap_fragment(
                frag,
                render_to_string("edit_unit_link.html", {
                    'frag_content': frag.content,
                    'edit_link': edit_link
                }))
        else:
            return frag

    if isinstance(block, SequenceModule) or getattr(block, 'HIDDEN', False):
        return frag

    block_id = block.location
    if block.has_score and settings.FEATURES.get(
            'DISPLAY_HISTOGRAMS_TO_STAFF'):
        histogram = grade_histogram(block_id)
        render_histogram = len(histogram) > 0
    else:
        histogram = None
        render_histogram = False

    if settings.FEATURES.get('ENABLE_LMS_MIGRATION') and hasattr(
            block.runtime, 'filestore'):
        [filepath, filename] = getattr(block, 'xml_attributes',
                                       {}).get('filename', ['', None])
        osfs = block.runtime.filestore
        if filename is not None and osfs.exists(filename):
            # if original, unmangled filename exists then use it (github
            # doesn't like symlinks)
            filepath = filename
        data_dir = block.static_asset_path or osfs.root_path.rsplit('/')[-1]
        giturl = block.giturl or 'https://github.com/MITx'
        edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
    else:
        edit_link = False
        # Need to define all the variables that are about to be used
        giturl = ""
        data_dir = ""

    source_file = block.source_file  # source used to generate the problem XML, eg latex or word

    # Useful to indicate to staff if problem has been released or not.
    # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access,
    # instead of now>mstart comparison here.
    now = datetime.datetime.now(UTC())
    is_released = "unknown"
    mstart = block.start

    if mstart is not None:
        is_released = "<font color='red'>Yes!</font>" if (
            now > mstart) else "<font color='green'>Not yet</font>"

    field_contents = []
    for name, field in block.fields.items():
        try:
            field_contents.append((name, field.read_from(block)))
        except InvalidScopeError:
            log.warning("Unable to read field in Staff Debug information",
                        exc_info=True)
            field_contents.append((name, "WARNING: Unable to read field"))

    staff_context = {
        'fields':
        field_contents,
        'xml_attributes':
        getattr(block, 'xml_attributes', {}),
        'tags':
        block._class_tags,  # pylint: disable=protected-access
        'location':
        block.location,
        'xqa_key':
        block.xqa_key,
        'source_file':
        source_file,
        'source_url':
        '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
        'category':
        str(block.__class__.__name__),
        'element_id':
        sanitize_html_id(block.location.html_id()),
        'edit_link':
        edit_link,
        'user':
        user,
        'xqa_server':
        settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
        'histogram':
        json.dumps(histogram),
        'render_histogram':
        render_histogram,
        'block_content':
        frag.content,
        'is_released':
        is_released,
        'has_instructor_access':
        has_instructor_access,
        'can_reset_attempts':
        'attempts' in block.fields,
        'can_rescore_problem':
        hasattr(block, 'rescore_problem'),
        'disable_staff_debug_info':
        disable_staff_debug_info,
    }
    return wrap_fragment(
        frag, render_to_string("staff_problem_info.html", staff_context))
Exemplo n.º 29
0
from mock import Mock, patch
import itertools

from xblock.runtime import KvsFieldData, DictKeyValueStore

import xmodule.course_module
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.utils.timezone import UTC

ORG = 'test_org'
COURSE = 'test_course'

NOW = datetime.strptime('2013-01-01T01:00:00',
                        '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC())

_TODAY = datetime.now(UTC())
_LAST_WEEK = _TODAY - timedelta(days=7)
_NEXT_WEEK = _TODAY + timedelta(days=7)


class CourseFieldsTestCase(unittest.TestCase):
    def test_default_start_date(self):
        self.assertEqual(xmodule.course_module.CourseFields.start.default,
                         datetime(2030, 1, 1, tzinfo=UTC()))


class DummySystem(ImportSystem):
    @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
    def __init__(self, load_error_modules):
Exemplo n.º 30
0
def create_new_course(request):
    """
    Create a new course
    """
    if not is_user_in_creator_group(request.user):
        raise PermissionDenied()

    # This logic is repeated in xmodule/modulestore/tests/factories.py
    # so if you change anything here, you need to also change it there.
    # TODO: write a test that creates two courses, one with the factory and
    # the other with this method, then compare them to make sure they are
    # equivalent.
    template = Location(request.POST['template'])
    org = request.POST.get('org')
    number = request.POST.get('number')
    display_name = request.POST.get('display_name')
    display_name = display_name.decode('utf-8')
    org = org.decode('utf-8')
    try:
        dest_location = Location('i4x', org, number, 'course',
                                 Location.clean(display_name))
    except InvalidLocationError as error:
        return JsonResponse({
            "ErrMsg":
            "Unable to create course '{name}'.\n\n{err}".format(
                name=display_name, err=error.message)
        })

    # see if the course already exists
    existing_course = None
    try:
        existing_course = modulestore('direct').get_item(dest_location)
    except ItemNotFoundError:
        pass

    if existing_course is not None:
        return JsonResponse(
            {'ErrMsg': 'There is already a course defined with this name.'})

    course_search_location = [
        'i4x', dest_location.org, dest_location.course, 'course', None
    ]
    courses = modulestore().get_items(course_search_location)

    if len(courses) > 0:
        return JsonResponse({
            'ErrMsg':
            'There is already a course defined with the same organization and course number.'
        })

    new_course = modulestore('direct').clone_item(template, dest_location)

    # clone a default 'about' module as well

    about_template_location = Location(
        ['i4x', 'edx', 'templates', 'about', 'overview'])
    dest_about_location = dest_location._replace(category='about',
                                                 name='overview')
    modulestore('direct').clone_item(about_template_location,
                                     dest_about_location)

    if display_name is not None:
        new_course.display_name = display_name

    # set a default start date to now
    new_course.start = datetime.datetime.now(UTC())

    initialize_course_tabs(new_course)

    create_all_course_groups(request.user, new_course.location)

    # seed the forums
    seed_permissions_roles(new_course.location.course_id)

    return JsonResponse({'id': new_course.location.url()})