diff --git a/lms/djangoapps/courseware/features/course-section-content.feature b/lms/djangoapps/courseware/features/course-section-content.feature new file mode 100644 index 0000000000..bf6a9d8929 --- /dev/null +++ b/lms/djangoapps/courseware/features/course-section-content.feature @@ -0,0 +1,10 @@ +Feature: There are many different types of tabs + In order to validate tab types + As a staff member + I want to try out all the videos, buttons, and content + + Scenario: I visit a tabbed quiz + Given I am registered for course "MITx/6.002x-EE98/2012_Fall_SJSU" + And I log in + Given I visit and check 502 for "http://www.edx.org/courses/MITx/6.002x-EE98/2012_Fall_SJSU/courseware/Week_0/Administrivia_and_Circuit_Elements/" + I process diff --git a/lms/djangoapps/courseware/features/course-section-content.py b/lms/djangoapps/courseware/features/course-section-content.py new file mode 100644 index 0000000000..5513ab9d0c --- /dev/null +++ b/lms/djangoapps/courseware/features/course-section-content.py @@ -0,0 +1,197 @@ +from lettuce import * #before, world +from selenium import * +#import lettuce_webdriver.webdriver +import logging +import nose.tools +from selenium.webdriver import ActionChains +from selenium.webdriver.support.ui import WebDriverWait +import re + +## imported from lms/djangoapps/courseware/courses.py +from collections import defaultdict +from fs.errors import ResourceNotFoundError +from functools import wraps +import logging + +from path import path +from django.conf import settings +from django.http import Http404 + +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError +from static_replace import replace_urls, try_staticfiles_lookup +from courseware.access import has_access +## end import + +from django.core.urlresolvers import reverse +from courseware.courses import course_image_url, get_course_about_section, get_course_by_id +from courses import * +import os.path +import sys +path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'static')) +if not path in sys.path: + sys.path.insert(1, path) +del path +#from helpers import * + + +@step(u'I visit and check 502 for "(.*)"') +def i_visit_and_check_502_for_url(step, url): + world.browser.get(url) + check_for_502(url) + +@step(u'I process') +def i_make_sure_everything_is_there(step): + e = world.browser.find_element_by_css_selector('section.course-content section') + process_section(e) + + +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 5 types, with 5 actions. + + Sequence Module + -contains one child module + -to prevent from over-processing all its children (no easy way to specify only one level of depth), we only grab the first child + + 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 + + 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 + ''' + tab_type = element.get_attribute('class') + print 'processing a %s' % (tab_type) + if tab_type == "xmodule_display xmodule_SequenceModule": + child_modules = element.find_elements_by_css_selector("section[class^='xmodule']") + + ## ugly bit of code to get around not being able to specify only the first level of descendants + if child_modules[0].get_attribute('class') == "xmodule_display xmodule_VerticalModule": + process_section(child_modules[0]) + else: + for mod in child_modules: + process_section(mod) + + elif tab_type == "xmodule_display xmodule_VerticalModule": + vert_list = element.find_elements_by_css_selector("li section[class^='xmodule']") + print "I found %s items" % (str(len(vert_list))) + for item in vert_list: + print 'processing a child %s' % (item.get_attribute('class')) + process_section(item) + + elif tab_type == "xmodule_display xmodule_CapaModule": + assert element.find_element_by_css_selector("section[id^='problem']") , "No problems found in %s" % (tab_type) + p = element.find_element_by_css_selector("section[id^='problem']") + p_id = p.get_attribute('id') + process_problem(p, p_id) + + elif tab_type == "xmodule_display xmodule_VideoModule": + assert element.find_element_by_css_selector("section[class^='video']") , 'No video found in %s' % (tab_type) + + elif tab_type == "xmodule_display xmodule_CustomTagModule": + pass + + else: + assert False, "%s not recognized!!" % (tab_type) + + + +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_element_by_css_selector("section.problem") + input_fields = prob_xmod.find_elements_by_css_selector("section[id^='textinput']") + + ## clear out all input to ensure an incorrect result + for field in input_fields: + box = field.find_element_by_css_selector("input") + box.clear() + print "\n I cleared out the box %s \n" % (box.get_attribute('id')) + + ## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect' + if prob_xmod.find_element_by_css_selector("p.status").text.lower() != 'incorrect': + prob_xmod.find_element_by_css_selector("section.action input.check").click() + world.browser.implicitly_wait(4) + + ## all elements become disconnected after the click + element = world.browser.find_element_by_css_selector("section[id='"+problem_id+"']") + prob_xmod = element.find_element_by_css_selector("section.problem") + input_fields = prob_xmod.find_elements_by_css_selector("section[id^='textinput']") + for field in input_fields: + assert field.find_element_by_css_selector("div.incorrect") , "The 'check' button did not work for %s" % (problem_id) + print "\n So far so good! \n" + + + ## wait for the ajax changes to render + world.browser.implicitly_wait(4) + + ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) + element = world.browser.find_element_by_css_selector("section[id='"+problem_id+"']") + prob_xmod = element.find_element_by_css_selector("section.problem") + + + show_button = element.find_element_by_css_selector("section.action input.show") + ## this logic is to ensure we do not accidentally hide the answers + if show_button.get_attribute('value').lower() == 'show answer': + show_button.click() + print "\n I clicked show for %s \n" % (problem_id) + else: + pass + + + ## wait for the ajax changes to render + world.browser.implicitly_wait(4) + + ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) + element = world.browser.find_element_by_css_selector("section[id='"+problem_id+"']") + prob_xmod = element.find_element_by_css_selector("section.problem") + + ## find all the input fields + input_fields = prob_xmod.find_elements_by_css_selector("section[id^='textinput']") + + ## 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_element_by_css_selector("input").send_keys(field.find_element_by_css_selector("p[id^='answer']").text) + print "\n \n Entered %s into %s \n \n" % (field.find_element_by_css_selector("p[id^='answer']").text,field.find_element_by_css_selector("input").get_attribute('id') ) + prob_xmod = element.find_element_by_css_selector("section.problem") + prob_xmod.find_element_by_css_selector("section.action input.check").click() + world.browser.implicitly_wait(4) + + ## assert that we entered the correct answers + ## we have to redefine input-fields because apparently they become detached from the dom after clicking 'check' + + input_fields = world.browser.find_elements_by_css_selector("section[id='"+problem_id+"'] section[id^='textinput']") + 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 world.browser.find_element_by_css_selector("div[class^='correct']"), "The check answer values were not correct for %s" % (problem_id) + inputs = world.browser.find_elements_by_css_selector("section[id^='textinput'] input") + for el in inputs: + el.clear() + print "\n checked answers for %s \n" % (problem_id) diff --git a/lms/djangoapps/courseware/features/course_info.feature b/lms/djangoapps/courseware/features/course_info.feature new file mode 100644 index 0000000000..4a6a2daff6 --- /dev/null +++ b/lms/djangoapps/courseware/features/course_info.feature @@ -0,0 +1,21 @@ +Feature: View the Course Info tab + As a student in an edX course + In order to get background on the course + I want to view the info on the course info tab + + Scenario: I can get to the course info tab when logged in + Given I am logged in + And I am registered for a course + And I visit the dashboard + When I click on View Courseware + Then I am on an info page + And the Course Info tab is active + And I do not see "! Info section missing !" anywhere on the page + + # This test is currently failing + # see: https://www.pivotaltracker.com/projects/614553?classic=true#!/stories/38801223 + Scenario: I cannot get to the course info tab when not logged in + Given I am not logged in + And I visit the homepage + When I visit the course info URL + Then the login dialog is visible \ No newline at end of file diff --git a/lms/djangoapps/courseware/features/course_info.py b/lms/djangoapps/courseware/features/course_info.py new file mode 100644 index 0000000000..964a7af35f --- /dev/null +++ b/lms/djangoapps/courseware/features/course_info.py @@ -0,0 +1,15 @@ +from lettuce import world, step +from lettuce.django import django_url +#from portal.common import * + +@step('I am on an info page') +def i_am_on_an_info_page(step): + title = world.browser.title + url = world.browser.url + assert ('Course Info' in title) + assert (r'/info' in url) + +@step('I visit the course info URL$') +def i_visit_the_course_info_url(step): + url = django_url('/courses/MITx/6.002x/2012_Fall/info') + world.browser.visit(url) diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py new file mode 100644 index 0000000000..7b22c54231 --- /dev/null +++ b/lms/djangoapps/courseware/features/courses.py @@ -0,0 +1,129 @@ +from lettuce import * #before, world +from selenium import * +#import lettuce_webdriver.webdriver +import logging +import nose.tools +from selenium.webdriver import ActionChains +from selenium.webdriver.support.ui import WebDriverWait + +## imported from lms/djangoapps/courseware/courses.py +from collections import defaultdict +from fs.errors import ResourceNotFoundError +from functools import wraps +import logging + +from path import path +from django.conf import settings +from django.http import Http404 + +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError +from static_replace import replace_urls, try_staticfiles_lookup +from courseware.access import has_access +## end import + +from django.core.urlresolvers import reverse +from courseware.courses import course_image_url, get_course_about_section, get_course_by_id +import xmodule + +## support functions +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 + +def get_courseware(course_id): + """ + Given a course_id (string), return a courseware array of dictionaries for the + top two levels of navigation. Example: + + [ + {'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 + +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) + 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)==xmodule.seq_module.SequenceDescriptor) else 0, 'tab_classes':[t.__class__.__name__ for t in s.get_children() ]} for s in c.get_children() if s.metadata.get('hide_from_toc', 'false').lower() != 'true']} for c in chapters ] + + return courseware + diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature new file mode 100644 index 0000000000..cddf7527ae --- /dev/null +++ b/lms/djangoapps/courseware/features/courseware.feature @@ -0,0 +1,18 @@ +Feature: View the Courseware Tab + As a student in an edX course + In order to work on the course + I want to view the info on the courseware tab + + Scenario: I can get to the courseware tab when logged in + Given I am registered for a course + And I log in + And I click on View Courseware + When I click on the "Courseware" tab + Then the "Courseware" tab is active + + # TODO: fix this one? Not sure whether you should get a 404. + # Scenario: I cannot get to the courseware tab when not logged in + # Given I am not logged in + # And I visit the homepage + # When I visit the courseware URL + # Then the login dialog is visible \ No newline at end of file diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py new file mode 100644 index 0000000000..f90c7c3ba1 --- /dev/null +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -0,0 +1,36 @@ +from lettuce import world, step +from lettuce.django import django_url + +@step('I click on View Courseware') +def i_click_on_view_courseware(step): + css = 'p.enter-course' + world.browser.find_by_css(css).first.click() + +@step('I click on the "([^"]*)" tab$') +def i_click_on_the_tab(step, tab): + world.browser.find_link_by_text(tab).first.click() + +@step('I visit the courseware URL$') +def i_visit_the_course_info_url(step): + url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') + world.browser.visit(url) + +@step(u'I do not see "([^"]*)" anywhere on the page') +def i_do_not_see_text_anywhere_on_the_page(step, text): + assert world.browser.is_text_not_present(text) + +@step(u'I am on the dashboard page$') +def i_am_on_the_dashboard_page(step): + assert world.browser.is_element_present_by_css('section.courses') + assert world.browser.url == django_url('/dashboard') + +@step('the "([^"]*)" tab is active$') +def the_tab_is_active(step, tab): + css = '.course-tabs a.active' + active_tab = world.browser.find_by_css(css) + assert (active_tab.text == tab) + +@step('the login dialog is visible$') +def login_dialog_visible(step): + css = 'form#login_form.login_form' + assert world.browser.find_by_css(css).visible diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature new file mode 100644 index 0000000000..2e9c4f1886 --- /dev/null +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -0,0 +1,23 @@ +Feature: All the high level tabs should work + In order to preview the courseware + As a student + I want to navigate through the high level tabs + +# Note this didn't work as a scenario outline because +# before each scenario was not flushing the database +# TODO: break this apart so that if one fails the others +# will still run + Scenario: A student can see all tabs of the course + Given I am registered for a course + And I log in + And I click on View Courseware + When I click on the "Courseware" tab + Then the page title should be "6.002x Courseware" + When I click on the "Course Info" tab + Then the page title should be "6.002x Course Info" + When I click on the "Textbook" tab + Then the page title should be "6.002x Textbook" + When I click on the "Wiki" tab + Then the page title should be "6.002x | edX Wiki" + When I click on the "Progress" tab + Then the page title should be "6.002x Progress" diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature new file mode 100644 index 0000000000..98dad7131f --- /dev/null +++ b/lms/djangoapps/courseware/features/smart-accordion.feature @@ -0,0 +1,19 @@ +Feature: There are courses on the homepage + In order to compared rendered content to the database + As an acceptance test + I want to count all the chapters, sections, and tabs for each course + + Scenario: Navigate through course MITx/6.002x/2012_Fall + Given I am registered for course "MITx/6.002x/2012_Fall" + And I log in + Then I verify all the content of each course + + Scenario: Navigate through course edX/edx101/edX_Studio_Reference + Given I am registered for course "edX/edx101/edX_Studio_Reference" + And I log in + Then I verify all the content of each course + + Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall + Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall" + And I log in + Then I verify all the content of each course \ No newline at end of file diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py new file mode 100644 index 0000000000..ebc5f19db6 --- /dev/null +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -0,0 +1,149 @@ +from lettuce import world, step +import re +from nose.tools import assert_equals + +## imported from lms/djangoapps/courseware/courses.py +from collections import defaultdict +from fs.errors import ResourceNotFoundError +from functools import wraps + +from path import path +from django.conf import settings +from django.http import Http404 + +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError +from static_replace import replace_urls, try_staticfiles_lookup +from courseware.access import has_access +## end import + +from django.core.urlresolvers import reverse +from courseware.courses import course_image_url, get_course_about_section, get_course_by_id +from courses import * +import os.path +import sys +path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'static')) +if not path in sys.path: + sys.path.insert(1, path) +del path +#from helpers import * + +from logging import getLogger +logger = getLogger(__name__) + +def check_for_errors(): + e = world.browser.find_by_css('.outside-app') + if len(e) > 0: + assert False, 'there was a server error at %s' % (world.browser.url) + else: + assert True + +@step(u'I verify all the content of each course') +def i_verify_all_the_content_of_each_course(step): + all_possible_courses = get_courses() + ids = [c.id for c in all_possible_courses] + + # Get a list of all the registered courses + registered_courses = world.browser.find_by_css('article.my-course') + if len(all_possible_courses) < len(registered_courses): + assert False, "user is registered for more courses than are uniquely posssible" + else: + pass + + for test_course in registered_courses: + test_course.find_by_css('a').click() + check_for_errors() + + # Get the course. E.g. 'MITx/6.002x/2012_Fall' + current_course = re.sub('/info','',re.sub('.*/courses/','',world.browser.url)) + validate_course(current_course,ids) + + world.browser.find_link_by_text('Courseware').click() + assert world.browser.is_element_present_by_id('accordion',wait_time=2) + check_for_errors() + browse_course(current_course) + + # clicking the user link gets you back to the user's home page + world.browser.find_by_css('.user-link').click() + check_for_errors() + +def browse_course(course_id): + + ## count chapters from xml and page and compare + chapters = get_courseware_with_tabs(course_id) + num_chapters = len(chapters) + rendered_chapters = world.browser.find_by_css('#accordion > nav > div') + num_rendered_chapters = len(rendered_chapters) + assert num_chapters == num_rendered_chapters, '%d chapters expected, %d chapters found on page for %s' % (num_chapters, num_rendered_chapters, course_id) + + chapter_it = 0 + + ## Iterate the chapters + while chapter_it < num_chapters: + + ## click into a chapter + world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('h3').click() + + ## look for the "there was a server error" div + check_for_errors() + + ## count sections from xml and page and compare + sections = chapters[chapter_it]['sections'] + num_sections = len(sections) + + rendered_sections = world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li') + num_rendered_sections = len(rendered_sections) + assert num_sections == num_rendered_sections, '%d sections expected, %d sections found on page, iteration number %d on %s' % (num_sections, num_rendered_sections, chapter_it, course_id) + + section_it = 0 + + ## Iterate the sections + while section_it < num_sections: + + ## click on a section + world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click() + + ## sometimes the course-content takes a long time to load + assert world.browser.is_element_present_by_css('.course-content',wait_time=5) + + ## look for server error div + check_for_errors() + + ## count tabs from xml and page and compare + + ## count the number of tabs. If number of tabs is 0, there won't be anything rendered + ## so we explicitly set rendered_tabs because otherwise find_elements returns a None object with no length + num_tabs = sections[section_it]['clickable_tab_count'] + if num_tabs != 0: + rendered_tabs = world.browser.find_by_css('ol#sequence-list > li') + num_rendered_tabs = len(rendered_tabs) + else: + rendered_tabs = 0 + num_rendered_tabs = 0 + + assert num_tabs == num_rendered_tabs ,'%d tabs expected, %d tabs found, iteration number %d, on %s' % (num_tabs,num_rendered_tabs,section_it, course_id) + + tab_it = 0 + + ## Iterate the tabs + while tab_it < num_tabs: + + rendered_tabs[tab_it].find_by_tag('a').click() + + ## do something with the tab sections[section_it] + check_for_errors() + + tab_it += 1 + + section_it += 1 + + chapter_it += 1 + + +def validate_course(current_course, ids): + try: + ids.index(current_course) + except: + assert False, "invalid course id" diff --git a/lms/djangoapps/portal/README.md b/lms/djangoapps/portal/README.md new file mode 100644 index 0000000000..b4da273124 --- /dev/null +++ b/lms/djangoapps/portal/README.md @@ -0,0 +1,37 @@ +## acceptance_testing + +This fake django app is here to support acceptance testing using lettuce + +salad +(which uses splinter wrapping selenium). + +Some documentation for our efforts are located in basecamp at this link. + +First you need to make sure that you've installed the requirements. +This includes lettuce, salad, selenium, splinter, etc. +Do this with: +```pip install -r test-requirements.txt``` + +First set up the database that you need: +WARNING!!! THIS WILL OVERWRITE THE DATA IN YOUR DEV DATABASE +IF YOU WANT TO KEEP THAT DATA, SAVE A COPY OF YOUR ../db/mitx.db ELSEWHERE FIRST! + +