Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add acceptance test of image response #2015

Merged
Merged
73 changes: 8 additions & 65 deletions common/djangoapps/terrain/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,68 +9,11 @@
from lettuce import world


@world.absorb
class UserFactory(sf.UserFactory):
"""
User account for lms / cms
"""
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
pass


@world.absorb
class UserProfileFactory(sf.UserProfileFactory):
"""
Demographics etc for the User
"""
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass


@world.absorb
class RegistrationFactory(sf.RegistrationFactory):
"""
Activation key for registering the user account
"""
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass


@world.absorb
class GroupFactory(sf.GroupFactory):
"""
Groups for user permissions for courses
"""
pass


@world.absorb
class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory):
"""
Users allowed to enroll in the course outside of the usual window
"""
pass


@world.absorb
class CourseModeFactory(cmf.CourseModeFactory):
"""
Course modes
"""
pass


@world.absorb
class CourseFactory(xf.CourseFactory):
"""
Courseware courses
"""
pass


@world.absorb
class ItemFactory(xf.ItemFactory):
"""
Everything included inside a course
"""
pass
world.absorb(sf.UserFactory)
world.absorb(sf.UserProfileFactory)
world.absorb(sf.RegistrationFactory)
world.absorb(sf.GroupFactory)
world.absorb(sf.CourseEnrollmentAllowedFactory)
world.absorb(cmf.CourseModeFactory)
world.absorb(xf.CourseFactory)
world.absorb(xf.ItemFactory)
12 changes: 8 additions & 4 deletions common/djangoapps/terrain/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
# Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613

# django_url is assigned late in the process of loading lettuce,
# so we import this as a module, and then read django_url from
# it to get the correct value
import lettuce.django

from lettuce import world, step
from .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url
from nose.tools import assert_equals # pylint: disable=E0611

from logging import getLogger
Expand Down Expand Up @@ -135,7 +139,7 @@ def should_have_link_with_id_and_text(step, link_id, text):
def should_have_link_with_path_and_text(step, path, text):
link = world.browser.find_link_by_text(text)
assert len(link) > 0
assert_equals(link.first["href"], django_url(path))
assert_equals(link.first["href"], lettuce.django.django_url(path))


@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
Expand All @@ -154,7 +158,7 @@ def should_see_in_the_page(step, doesnt_appear, text):
def i_am_logged_in(step):
world.create_user('robot', 'test')
world.log_in(username='robot', password='test')
world.browser.visit(django_url('/'))
world.browser.visit(lettuce.django.django_url('/'))
dash_css = 'section.container.dashboard'
assert world.is_css_present(dash_css)

Expand All @@ -176,7 +180,7 @@ def dialogs_are_closed(step):

@step(u'visit the url "([^"]*)"')
def visit_url(step, url):
world.browser.visit(django_url(url))
world.browser.visit(lettuce.django.django_url(url))


@step(u'wait for AJAX to (?:finish|complete)')
Expand Down
13 changes: 10 additions & 3 deletions common/djangoapps/terrain/ui_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@
#pylint: disable=W0621

from lettuce import world

import time
import json
import re
import platform

# django_url is assigned late in the process of loading lettuce,
# so we import this as a module, and then read django_url from
# it to get the correct value
import lettuce.django


from textwrap import dedent
from urllib import quote_plus
from selenium.common.exceptions import (
Expand All @@ -14,7 +22,6 @@
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url
from nose.tools import assert_true # pylint: disable=E0611


Expand Down Expand Up @@ -247,13 +254,13 @@ def wait_for_ajax_complete():

@world.absorb
def visit(url):
world.browser.visit(django_url(url))
world.browser.visit(lettuce.django.django_url(url))
wait_for_js_to_load()


@world.absorb
def url_equals(url):
return world.browser.url == django_url(url)
return world.browser.url == lettuce.django.django_url(url)


@world.absorb
Expand Down
6 changes: 5 additions & 1 deletion common/lib/xmodule/xmodule/conditional_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ def _get_condition(self):
xml_value = self.descriptor.xml_attributes.get(xml_attr)
if xml_value:
return xml_value, attr_name
raise Exception('Error in conditional module: unknown condition "%s"' % xml_attr)
raise Exception(
'Error in conditional module: no known conditional found in {!r}'.format(
self.descriptor.xml_attributes.keys()
)
)

@lazy
def required_modules(self):
Expand Down
2 changes: 2 additions & 0 deletions common/lib/xmodule/xmodule/poll_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class PollFields(object):
poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.user_state_summary)

# List of answers, in the form {'id': 'some id', 'text': 'the answer text'}
answers = List(help="Poll answers from xml", scope=Scope.content, default=[])

question = String(help="Poll question", scope=Scope.content, default='')


Expand Down
2 changes: 2 additions & 0 deletions common/static/coffee/src/xblock/core.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
block.element = element
block.name = $element.data("name")

$element.trigger("xblock-initialized")
$element.data("initialized", true)
block

initializeBlocks: (element) ->
Expand Down
26 changes: 26 additions & 0 deletions lms/djangoapps/courseware/features/annotatable.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@shard_2
Feature: LMS.Annotatable Component
As a student, I want to view an Annotatable component in the LMS

Scenario: An Annotatable component can be rendered in the LMS
Given that a course has an annotatable component with 2 annotations
When I view the annotatable component
Then the annotatable component has rendered
And the annotatable component has 2 highlighted passages

Scenario: An Annotatable component links to annonation problems in the LMS
Given that a course has an annotatable component with 2 annotations
And the course has 2 annotatation problems
When I view the annotatable component
And I click "Reply to annotation" on passage <problem>
Then I am scrolled to that annotation problem
When I answer that annotation problem
Then I recieve feedback on that annotation problem
When I click "Return to annotation" on that problem
Then I am scrolled to the annotatable component

Examples:
| problem |
| 0 |
| 1 |

174 changes: 174 additions & 0 deletions lms/djangoapps/courseware/features/annotatable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import textwrap

from lettuce import world, steps
from nose.tools import assert_in, assert_equals, assert_true

from common import i_am_registered_for_the_course, visit_scenario_item

DATA_TEMPLATE = textwrap.dedent("""\
<annotatable>
<instructions>Instruction text</instructions>
<p>{}</p>
</annotatable>
""")

ANNOTATION_TEMPLATE = textwrap.dedent("""\
Before {0}.
<annotation title="region {0}" body="Comment {0}" highlight="yellow" problem="{0}">
Region Contents {0}
</annotation>
After {0}.
""")

PROBLEM_TEMPLATE = textwrap.dedent("""\
<problem max_attempts="1" weight="">
<annotationresponse>
<annotationinput>
<title>Question {number}</title>
<text>Region Contents {number}</text>
<comment>What number is this region?</comment>
<comment_prompt>Type your response below:</comment_prompt>
<tag_prompt>What number is this region?</tag_prompt>
<options>
{options}
</options>
</annotationinput>
</annotationresponse>
<solution>
This problem is checking region {number}
</solution>
</problem>
""")

OPTION_TEMPLATE = """<option choice="{correctness}">{number}</option>"""


def _correctness(choice, target):
if choice == target:
return "correct"
elif abs(choice - target) == 1:
return "partially-correct"
else:
return "incorrect"


@steps
class AnnotatableSteps(object):

def __init__(self):
self.annotations_count = None
self.active_problem = None

def define_component(self, step, count):
r"""that a course has an annotatable component with (?P<count>\d+) annotations$"""

count = int(count)
coursenum = 'test_course'
i_am_registered_for_the_course(step, coursenum)

world.scenario_dict['ANNOTATION_VERTICAL'] = world.ItemFactory(
parent_location=world.scenario_dict['SECTION'].location,
category='vertical',
display_name="Test Annotation Vertical"
)

world.scenario_dict['ANNOTATABLE'] = world.ItemFactory(
parent_location=world.scenario_dict['ANNOTATION_VERTICAL'].location,
category='annotatable',
display_name="Test Annotation Module",
data=DATA_TEMPLATE.format("\n".join(ANNOTATION_TEMPLATE.format(i) for i in xrange(count)))
)

self.annotations_count = count

def view_component(self, step):
r"""I view the annotatable component$"""
visit_scenario_item('ANNOTATABLE')

def check_rendered(self, step):
r"""the annotatable component has rendered$"""
world.wait_for_js_variable_truthy('$(".xblock-student_view[data-type=Annotatable]").data("initialized")')
annotatable_text = world.css_find('.xblock-student_view[data-type=Annotatable]').first.text
assert_in("Instruction text", annotatable_text)

for i in xrange(self.annotations_count):
assert_in("Region Contents {}".format(i), annotatable_text)

def count_passages(self, step, count):
r"""the annotatable component has (?P<count>\d+) highlighted passages$"""
count = int(count)
assert_equals(len(world.css_find('.annotatable-span')), count)
assert_equals(len(world.css_find('.annotatable-span.highlight')), count)
assert_equals(len(world.css_find('.annotatable-span.highlight-yellow')), count)

def add_problems(self, step, count):
r"""the course has (?P<count>\d+) annotatation problems$"""
count = int(count)

for i in xrange(count):
world.scenario_dict.setdefault('PROBLEMS', []).append(
world.ItemFactory(
parent_location=world.scenario_dict['ANNOTATION_VERTICAL'].location,
category='problem',
display_name="Test Annotation Problem {}".format(i),
data=PROBLEM_TEMPLATE.format(
number=i,
options="\n".join(
OPTION_TEMPLATE.format(
number=k,
correctness=_correctness(k, i)
)
for k in xrange(count)
)
)
)
)

def click_reply(self, step, problem):
r"""I click "Reply to annotation" on passage (?P<problem>\d+)$"""
problem = int(problem)

annotation_span_selector = '.annotatable-span[data-problem-id="{}"]'.format(problem)

world.css_find(annotation_span_selector).first.mouse_over()

annotation_reply_selector = '.annotatable-reply[data-problem-id="{}"]'.format(problem)
assert_equals(len(world.css_find(annotation_reply_selector)), 1)
world.css_click(annotation_reply_selector)

self.active_problem = problem

def active_problem_selector(self, subselector):
return 'section[data-problem-id="{}"] {}'.format(
world.scenario_dict['PROBLEMS'][self.active_problem].location.url(),
subselector,
)

def check_scroll_to_problem(self, step):
r"""I am scrolled to that annotation problem$"""
annotation_input_selector = self.active_problem_selector('.annotation-input')
assert_true(world.css_visible(annotation_input_selector))

def answer_problem(self, step):
r"""I answer that annotation problem$"""
world.css_fill(self.active_problem_selector('.comment'), 'Test Response')
world.css_click(self.active_problem_selector('.tag[data-id="{}"]'.format(self.active_problem)))
world.css_click(self.active_problem_selector('.check'))

def check_feedback(self, step):
r"""I recieve feedback on that annotation problem$"""
world.wait_for_visible(self.active_problem_selector('.tag-status.correct'))
assert_equals(len(world.css_find(self.active_problem_selector('.tag-status.correct'))), 1)
assert_equals(len(world.css_find(self.active_problem_selector('.show'))), 1)

def click_return_to(self, step):
r"""I click "Return to annotation" on that problem$"""
world.css_click(self.active_problem_selector('.annotation-return'))

def check_scroll_to_annotatable(self, step):
r"""I am scrolled to the annotatable component$"""
assert_true(world.css_visible('.annotation-header'))

# This line is required by @steps in order to actually bind the step
# regexes
AnnotatableSteps()
Loading