courses.py 10.7 KB
Newer Older
1
from lettuce import world
2 3
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
4
from courseware.courses import get_course_by_id
5
from xmodule import seq_module, vertical_module
6

7 8
from logging import getLogger
logger = getLogger(__name__)
9 10

## support functions
Calen Pennington committed
11 12


13 14 15 16 17 18 19 20 21 22
def get_courses():
    '''
    Returns dict of lists of courses available, keyed by course.org (ie university).
    Courses are sorted by course.number.
    '''
    courses = [c for c in modulestore().get_courses()
               if isinstance(c, CourseDescriptor)]
    courses = sorted(courses, key=lambda course: course.number)
    return courses

23 24 25 26
# def get_courseware(course_id):
#     """
#     Given a course_id (string), return a courseware array of dictionaries for the
#     top two levels of navigation. Example:
27

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
#     [
#         {'chapter_name': 'Overview',
#             'sections': ['Welcome', 'System Usage Sequence', 'Lab0: Using the tools', 'Circuit Sandbox']
#         },
#         {'chapter_name': 'Week 1',
#             'sections': ['Administrivia and Circuit Elements', 'Basic Circuit Analysis', 'Resistor Divider', 'Week 1 Tutorials']
#             },
#         {'chapter_name': 'Midterm Exam',
#             'sections': ['Midterm Exam']
#         }
#     ]
#     """

#     course = get_course_by_id(course_id)
#     chapters = course.get_children()
#     courseware = [ {'chapter_name':c.display_name, 'sections':[s.display_name for s in c.get_children()]} for c in chapters]
#     return courseware
45

Calen Pennington committed
46

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
def get_courseware_with_tabs(course_id):
    """
    Given a course_id (string), return a courseware array of dictionaries for the
    top three levels of navigation. Same as get_courseware() except include
    the tabs on the right hand main navigation page.

    This hides the appropriate courseware as defined by the XML flag test:
    chapter.metadata.get('hide_from_toc','false').lower() == 'true'

    Example:

    [{
        'chapter_name': 'Overview',
        'sections': [{
            'clickable_tab_count': 0,
            'section_name': 'Welcome',
            'tab_classes': []
        }, {
            'clickable_tab_count': 1,
            'section_name': 'System Usage Sequence',
            'tab_classes': ['VerticalDescriptor']
        }, {
            'clickable_tab_count': 0,
            'section_name': 'Lab0: Using the tools',
            'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
        }, {
            'clickable_tab_count': 0,
            'section_name': 'Circuit Sandbox',
            'tab_classes': []
        }]
    }, {
        'chapter_name': 'Week 1',
        'sections': [{
            'clickable_tab_count': 4,
            'section_name': 'Administrivia and Circuit Elements',
            'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
        }, {
            'clickable_tab_count': 0,
            'section_name': 'Basic Circuit Analysis',
            'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
        }, {
            'clickable_tab_count': 0,
            'section_name': 'Resistor Divider',
            'tab_classes': []
        }, {
            'clickable_tab_count': 0,
            'section_name': 'Week 1 Tutorials',
            'tab_classes': []
        }]
    }, {
        'chapter_name': 'Midterm Exam',
        'sections': [{
            'clickable_tab_count': 2,
            'section_name': 'Midterm Exam',
            'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
        }]
    }]
    """

    course = get_course_by_id(course_id)
Calen Pennington committed
107 108 109 110 111 112 113
    chapters = [chapter for chapter in course.get_children() if chapter.metadata.get('hide_from_toc', 'false').lower() != 'true']
    courseware = [{'chapter_name': c.display_name,
                    'sections': [{'section_name': s.display_name, 
                                'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0, 
                                'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
                                        'class': t.__class__.__name__}
                                        for t in s.get_children()]}
114
                                for s in c.get_children() if s.metadata.get('hide_from_toc', 'false').lower() != 'true']}
Calen Pennington committed
115
                    for c in chapters]
116 117 118

    return courseware

Calen Pennington committed
119

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
def process_section(element, num_tabs=0):
    '''
    Process section reads through whatever is in 'course-content' and classifies it according to sequence module type.

    This function is recursive

    There are 6 types, with 6 actions.

    Sequence Module
    -contains one child module

    Vertical Module
    -contains other modules
    -process it and get its children, then process them

    Capa Module
    -problem type, contains only one problem
    -for this, the most complex type, we created a separate method, process_problem

    Video Module
    -video type, contains only one video
    -we only check to ensure that a section with class of video exists

    HTML Module
    -html text
    -we do not check anything about it

    Custom Tag Module
    -a custom 'hack' module type
    -there is a large variety of content that could go in a custom tag module, so we just pass if it is of this unusual type

    can be used like this:
    e = world.browser.find_by_css('section.course-content section')
    process_section(e)

    '''
    if element.has_class('xmodule_display xmodule_SequenceModule'):
        logger.debug('####### Processing xmodule_SequenceModule')
        child_modules = element.find_by_css("div>div>section[class^='xmodule']")
        for mod in child_modules:
            process_section(mod)

    elif element.has_class('xmodule_display xmodule_VerticalModule'):
        logger.debug('####### Processing xmodule_VerticalModule')
        vert_list = element.find_by_css("li section[class^='xmodule']")
        for item in vert_list:
            process_section(item)

    elif element.has_class('xmodule_display xmodule_CapaModule'):
        logger.debug('####### Processing xmodule_CapaModule')
        assert element.find_by_css("section[id^='problem']"), "No problems found in Capa Module"
        p = element.find_by_css("section[id^='problem']").first
        p_id = p['id']
        logger.debug('####################')
        logger.debug('id is "%s"' % p_id)
        logger.debug('####################')
        process_problem(p, p_id)

    elif element.has_class('xmodule_display xmodule_VideoModule'):
        logger.debug('####### Processing xmodule_VideoModule')
        assert element.find_by_css("section[class^='video']"), "No video found in Video Module"

    elif element.has_class('xmodule_display xmodule_HtmlModule'):
        logger.debug('####### Processing xmodule_HtmlModule')
        pass

    elif element.has_class('xmodule_display xmodule_CustomTagModule'):
        logger.debug('####### Processing xmodule_CustomTagModule')
        pass

    else:
        assert False, "Class for element not recognized!!"



def process_problem(element, problem_id):
    '''
    Process problem attempts to
    1) scan all the input fields and reset them
    2) click the 'check' button and look for an incorrect response (p.status text should be 'incorrect')
    3) click the 'show answer' button IF it exists and IF the answer is not already displayed
    4) enter the correct answer in each input box
    5) click the 'check' button and verify that answers are correct

    Because of all the ajax calls happening, sometimes the test fails because objects disconnect from the DOM.
    The basic functionality does exist, though, and I'm hoping that someone can take it over and make it super effective.
    '''

    prob_xmod = element.find_by_css("section.problem").first
    input_fields = prob_xmod.find_by_css("section[id^='input']")

    ## clear out all input to ensure an incorrect result
    for field in input_fields:
        field.find_by_css("input").first.fill('')

    ## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect'
    # This would need to be reworked because multiple choice problems don't have this status
    # if prob_xmod.find_by_css("p.status").first.text.strip().lower() != 'incorrect':
    prob_xmod.find_by_css("section.action input.check").first.click()

    ## all elements become disconnected after the click
    ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
    # Wait for the ajax reload
    assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
    element = world.browser.find_by_css("section[id='%s']" % problem_id).first
    prob_xmod = element.find_by_css("section.problem").first
    input_fields = prob_xmod.find_by_css("section[id^='input']")
    for field in input_fields:
        assert field.find_by_css("div.incorrect"), "The 'check' button did not work for %s" % (problem_id)

    show_button = element.find_by_css("section.action input.show").first
    ## this logic is to ensure we do not accidentally hide the answers
    if show_button.value.lower() == 'show answer':
        show_button.click()
    else:
        pass

    ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
    assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
    element = world.browser.find_by_css("section[id='%s']" % problem_id).first
    prob_xmod = element.find_by_css("section.problem").first
    input_fields = prob_xmod.find_by_css("section[id^='input']")

    ## in each field, find the answer, and send it to the field.
    ## Note that this does not work if the answer type is a strange format, e.g. "either a or b"
    for field in input_fields:
        field.find_by_css("input").first.fill(field.find_by_css("p[id^='answer']").first.text)

    prob_xmod.find_by_css("section.action input.check").first.click()

    ## assert that we entered the correct answers
    ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
    assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
    element = world.browser.find_by_css("section[id='%s']" % problem_id).first
    prob_xmod = element.find_by_css("section.problem").first
    input_fields = prob_xmod.find_by_css("section[id^='input']")
    for field in input_fields:
        ## if you don't use 'starts with ^=' the test will fail because the actual class is 'correct ' (with a space)
        assert field.find_by_css("div[class^='correct']"), "The check answer values were not correct for %s" % problem_id