From ed4b5bca88882e20398ae92777d470bee7571363 Mon Sep 17 00:00:00 2001 From: Brad Miller Date: Thu, 29 Aug 2019 12:36:35 +0000 Subject: [PATCH 1/2] Blacken all python files --- controllers/admin.py | 2038 +++++++++++------- controllers/ajax.py | 1045 ++++++--- controllers/appadmin.py | 643 +++--- controllers/assignments.py | 1269 +++++++---- controllers/books.py | 233 +- controllers/dashboard.py | 524 +++-- controllers/default.py | 474 ++-- controllers/designer.py | 97 +- controllers/everyday.py | 15 +- controllers/feed.py | 44 +- controllers/lti.py | 271 ++- controllers/oauth.py | 2 +- controllers/proxy.py | 47 +- controllers/sections.py | 166 +- models/0.py | 52 +- models/db.py | 287 ++- models/db_ebook.py | 331 +-- models/db_ebook_chapters.py | 104 +- models/db_sections.py | 101 +- models/grouped_assignments.py | 96 +- models/lti.py | 13 +- models/menu.py | 6 +- models/practice.py | 184 +- models/questions.py | 92 +- models/scheduler.py | 2 +- models/user_biography.py | 27 +- modules/db_dashboard.py | 424 ++-- modules/feedback.py | 311 ++- modules/outcome_request.py | 188 +- modules/outcome_response.py | 132 +- modules/pytsugi_utils.py | 7 +- modules/rs_grading.py | 1021 ++++++--- modules/stripe_form.py | 53 +- tests/ci_utils.py | 36 +- tests/conftest.py | 450 ++-- tests/locustfile.py | 53 +- tests/test_admin.py | 196 +- tests/test_ajax2.py | 949 ++++---- tests/test_autograder.py | 455 ++-- tests/test_course_1/_sources/lp_demo-test.py | 1 + tests/test_course_1/_sources/lp_demo.py | 2 + tests/test_course_1/conf.py | 148 +- tests/test_course_1/pavement.py | 63 +- tests/test_dashboard.py | 91 +- tests/test_designer.py | 49 +- tests/test_server.py | 835 ++++--- tests/utils.py | 26 +- 47 files changed, 8572 insertions(+), 5081 deletions(-) diff --git a/controllers/admin.py b/controllers/admin.py index d8df6e07b..e49c62373 100644 --- a/controllers/admin.py +++ b/controllers/admin.py @@ -18,28 +18,28 @@ logger.setLevel(settings.log_level) -ALL_AUTOGRADE_OPTIONS = ['manual', 'all_or_nothing', 'pct_correct', 'interact'] +ALL_AUTOGRADE_OPTIONS = ["manual", "all_or_nothing", "pct_correct", "interact"] AUTOGRADE_POSSIBLE_VALUES = dict( - clickablearea=['manual', 'all_or_nothing', 'interact'], + clickablearea=["manual", "all_or_nothing", "interact"], external=[], fillintheblank=ALL_AUTOGRADE_OPTIONS, activecode=ALL_AUTOGRADE_OPTIONS, actex=ALL_AUTOGRADE_OPTIONS, - dragndrop=['manual', 'all_or_nothing', 'interact'], + dragndrop=["manual", "all_or_nothing", "interact"], shortanswer=ALL_AUTOGRADE_OPTIONS, mchoice=ALL_AUTOGRADE_OPTIONS, codelens=ALL_AUTOGRADE_OPTIONS, parsonsprob=ALL_AUTOGRADE_OPTIONS, - video=['interact'], - youtube=['interact'], - poll=['interact'], - page=['interact'], - showeval=['interact'], + video=["interact"], + youtube=["interact"], + poll=["interact"], + page=["interact"], + showeval=["interact"], lp_build=ALL_AUTOGRADE_OPTIONS, - reveal = [] + reveal=[], ) -ALL_WHICH_OPTIONS = ['first_answer', 'last_answer', 'best_answer'] +ALL_WHICH_OPTIONS = ["first_answer", "last_answer", "best_answer"] WHICH_TO_GRADE_POSSIBLE_VALUES = dict( clickablearea=ALL_WHICH_OPTIONS, external=[], @@ -71,101 +71,122 @@ @auth.requires_login() def index(): - redirect(URL("admin","admin")) + redirect(URL("admin", "admin")) + @auth.requires_login() def doc(): response.title = "Documentation" return dict(course_id=auth.user.course_name, course=get_course_row(db.courses.ALL)) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def sections_list(): course = db(db.courses.id == auth.user.course_id).select().first() sections = db(db.sections.course_id == course.id).select() # get all sections - for course, list number of users in each section - return dict( - course = course, - sections = sections - ) + return dict(course=course, sections=sections) + -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def sections_create(): course = db(db.courses.id == auth.user.course_id).select().first() form = FORM( DIV( LABEL("Section Name", _for="section_name"), - INPUT(_id="section_name" ,_name="name", requires=IS_NOT_EMPTY(),_class="form-control"), - _class="form-group" + INPUT( + _id="section_name", + _name="name", + requires=IS_NOT_EMPTY(), + _class="form-control", ), + _class="form-group", + ), INPUT(_type="Submit", _value="Create Section", _class="btn"), - ) - if form.accepts(request,session): + ) + if form.accepts(request, session): section = db.sections.update_or_insert(name=form.vars.name, course_id=course.id) session.flash = "Section Created" - return redirect('/%s/admin/admin' % (request.application)) - return dict( - form = form, - ) + return redirect("/%s/admin/admin" % (request.application)) + return dict(form=form) + -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def sections_delete(): course = db(db.courses.id == auth.user.course_id).select().first() section = db(db.sections.id == request.vars.id).select().first() if not section or section.course_id != course.id: - return redirect(URL('admin','sections_list')) + return redirect(URL("admin", "sections_list")) section.clear_users() session.flash = "Deleted Section: %s" % (section.name) db(db.sections.id == section.id).delete() - return redirect(URL('admin','sections_list')) + return redirect(URL("admin", "sections_list")) + -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def sections_update(): course = db(db.courses.id == auth.user.course_id).select().first() section = db(db.sections.id == request.vars.id).select().first() if not section or section.course_id != course.id: - redirect(URL('admin','sections_list')) + redirect(URL("admin", "sections_list")) bulk_email_form = FORM( DIV( - TEXTAREA(_name="emails_csv", - requires=IS_NOT_EMPTY(), - _class="form-control", - ), - _class="form-group", + TEXTAREA( + _name="emails_csv", requires=IS_NOT_EMPTY(), _class="form-control" ), + _class="form-group", + ), LABEL( INPUT(_name="overwrite", _type="Checkbox"), "Overwrite Users In Section", _class="checkbox", - ), - INPUT(_type='Submit', _class="btn", _value="Update Section"), - ) - if bulk_email_form.accepts(request,session): + ), + INPUT(_type="Submit", _class="btn", _value="Update Section"), + ) + if bulk_email_form.accepts(request, session): if bulk_email_form.vars.overwrite: section.clear_users() users_added_count = 0 - for email_address in bulk_email_form.vars.emails_csv.split(','): + for email_address in bulk_email_form.vars.emails_csv.split(","): user = db(db.auth_user.email == email_address.lower()).select().first() if user: if section.add_user(user): users_added_count += 1 session.flash = "%d Emails Added" % (users_added_count) - return redirect('/%s/admin/sections_update?id=%d' % (request.application, section.id)) + return redirect( + "/%s/admin/sections_update?id=%d" % (request.application, section.id) + ) elif bulk_email_form.errors: response.flash = "Error Processing Request" return dict( - section = section, - users = section.get_users(), - bulk_email_form = bulk_email_form, - ) + section=section, users=section.get_users(), bulk_email_form=bulk_email_form + ) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def assignments(): """ This is called for the assignments tab on the instructor interface """ response.title = "Assignments" - cur_assignments = db(db.assignments.course == auth.user.course_id).select(orderby=db.assignments.duedate) + cur_assignments = db(db.assignments.course == auth.user.course_id).select( + orderby=db.assignments.duedate + ) assigndict = OrderedDict() for row in cur_assignments: assigndict[row.id] = row.name @@ -175,27 +196,35 @@ def assignments(): for tag in tag_query: tags.append(tag.tag_name) - course_url = path.join('/',request.application, 'static', auth.user.course_name, 'index.html') + course_url = path.join( + "/", request.application, "static", auth.user.course_name, "index.html" + ) course = get_course_row(db.courses.ALL) base_course = course.base_course chapter_labels = [] - chapters_query = db(db.chapters.course_id == base_course).select(db.chapters.chapter_label) + chapters_query = db(db.chapters.course_id == base_course).select( + db.chapters.chapter_label + ) for row in chapters_query: chapter_labels.append(row.chapter_label) - return dict(coursename=auth.user.course_name, - confirm=False, - course_id=auth.user.course_name, - course_url=course_url, - assignments=assigndict, - tags=tags, - chapters=chapter_labels, - toc=_get_toc_and_questions(), # <-- This Gets the readings and questions - course=course, - ) + return dict( + coursename=auth.user.course_name, + confirm=False, + course_id=auth.user.course_name, + course_url=course_url, + assignments=assigndict, + tags=tags, + chapters=chapter_labels, + toc=_get_toc_and_questions(), # <-- This Gets the readings and questions + course=course, + ) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def practice(): response.title = "Practice" course = db(db.courses.id == auth.user.course_id).select().first() @@ -224,7 +253,9 @@ def practice(): already_exists = 0 any_practice_settings = db(db.course_practice.auth_user_id == auth.user.id) - practice_settings = any_practice_settings(db.course_practice.course_name == course.course_name) + practice_settings = any_practice_settings( + db.course_practice.course_name == course.course_name + ) # If the instructor has created practice for other courses, don't randomize spacing and interleaving for the new # course. if not any_practice_settings.isempty(): @@ -234,9 +265,11 @@ def practice(): # Now checking to see if there are practice settings for this course. # If not, stick with the defaults. - if (not practice_settings.isempty() and - practice_settings.select().first().end_date is not None and - practice_settings.select().first().end_date != ""): + if ( + not practice_settings.isempty() + and practice_settings.select().first().end_date is not None + and practice_settings.select().first().end_date != "" + ): practice_setting = practice_settings.select().first() start_date = practice_setting.start_date end_date = practice_setting.end_date @@ -256,178 +289,207 @@ def practice(): if randint(0, 1) == 1: interleaving = 1 if practice_settings.isempty(): - db.course_practice.insert(auth_user_id=auth.user.id, - course_name=course.course_name, - start_date=start_date, - end_date=end_date, - max_practice_days=max_practice_days, - max_practice_questions=max_practice_questions, - day_points=day_points, - question_points=question_points, - questions_to_complete_day=questions_to_complete_day, - flashcard_creation_method=flashcard_creation_method, - graded=graded, - spacing=spacing, - interleaving=interleaving - ) - practice_settings = db((db.course_practice.auth_user_id == auth.user.id) & - (db.course_practice.course_name == course.course_name)) + db.course_practice.insert( + auth_user_id=auth.user.id, + course_name=course.course_name, + start_date=start_date, + end_date=end_date, + max_practice_days=max_practice_days, + max_practice_questions=max_practice_questions, + day_points=day_points, + question_points=question_points, + questions_to_complete_day=questions_to_complete_day, + flashcard_creation_method=flashcard_creation_method, + graded=graded, + spacing=spacing, + interleaving=interleaving, + ) + practice_settings = db( + (db.course_practice.auth_user_id == auth.user.id) + & (db.course_practice.course_name == course.course_name) + ) toc = "''" if flashcard_creation_method == 2: toc = _get_toc_and_questions() # If the GET request is to open the page for the first time (they're not submitting the form): - if not ('StartDate' in request.vars or - 'EndDate' in request.vars or - 'maxPracticeDays' in request.vars or - 'maxPracticeQuestions' in request.vars or - 'pointsPerDay' in request.vars or - 'pointsPerQuestion' in request.vars or - 'questionsPerDay' in request.vars or - 'flashcardsCreationType' in request.vars or - 'question_points' in request.vars or - 'graded' in request.vars): - return dict(course_start_date=course_start_date, - start_date=start_date, - end_date=end_date, - max_practice_days=max_practice_days, - max_practice_questions=max_practice_questions, - day_points=day_points, - question_points=question_points, - questions_to_complete_day=questions_to_complete_day, - flashcard_creation_method=flashcard_creation_method, - graded=graded, - spacing=spacing, - interleaving=interleaving, - toc=toc, - error_start_date=error_start_date, - error_end_date=error_end_date, - error_max_practice_days=error_max_practice_days, - error_max_practice_questions=error_max_practice_questions, - error_day_points=error_day_points, - error_question_points=error_question_points, - error_questions_to_complete_day=error_questions_to_complete_day, - error_flashcard_creation_method=error_flashcard_creation_method, - error_graded=error_graded, - complete=already_exists, - course=course, - ) + if not ( + "StartDate" in request.vars + or "EndDate" in request.vars + or "maxPracticeDays" in request.vars + or "maxPracticeQuestions" in request.vars + or "pointsPerDay" in request.vars + or "pointsPerQuestion" in request.vars + or "questionsPerDay" in request.vars + or "flashcardsCreationType" in request.vars + or "question_points" in request.vars + or "graded" in request.vars + ): + return dict( + course_start_date=course_start_date, + start_date=start_date, + end_date=end_date, + max_practice_days=max_practice_days, + max_practice_questions=max_practice_questions, + day_points=day_points, + question_points=question_points, + questions_to_complete_day=questions_to_complete_day, + flashcard_creation_method=flashcard_creation_method, + graded=graded, + spacing=spacing, + interleaving=interleaving, + toc=toc, + error_start_date=error_start_date, + error_end_date=error_end_date, + error_max_practice_days=error_max_practice_days, + error_max_practice_questions=error_max_practice_questions, + error_day_points=error_day_points, + error_question_points=error_question_points, + error_questions_to_complete_day=error_questions_to_complete_day, + error_flashcard_creation_method=error_flashcard_creation_method, + error_graded=error_graded, + complete=already_exists, + course=course, + ) else: try: - start_date = datetime.datetime.strptime(request.vars.get('StartDate', None), '%Y-%m-%d').date() + start_date = datetime.datetime.strptime( + request.vars.get("StartDate", None), "%Y-%m-%d" + ).date() if start_date < course_start_date: error_start_date = 1 except: error_start_date = 1 try: - end_date = datetime.datetime.strptime(request.vars.get('EndDate', None), '%Y-%m-%d').date() + end_date = datetime.datetime.strptime( + request.vars.get("EndDate", None), "%Y-%m-%d" + ).date() if end_date < start_date: error_end_date = 1 except: error_end_date = 1 if spacing == 1: try: - max_practice_days = int(request.vars.get('maxPracticeDays', None)) + max_practice_days = int(request.vars.get("maxPracticeDays", None)) except: error_max_practice_days = 1 else: try: - max_practice_questions = int(request.vars.get('maxPracticeQuestions', None)) + max_practice_questions = int( + request.vars.get("maxPracticeQuestions", None) + ) except: error_max_practice_questions = 1 if spacing == 1: try: - day_points = float(request.vars.get('pointsPerDay', None)) + day_points = float(request.vars.get("pointsPerDay", None)) except: error_day_points = 1 else: try: - question_points = float(request.vars.get('pointsPerQuestion', None)) + question_points = float(request.vars.get("pointsPerQuestion", None)) except: error_question_points = 1 if spacing == 1: try: - questions_to_complete_day = int(request.vars.get('questionsPerDay', None)) + questions_to_complete_day = int( + request.vars.get("questionsPerDay", None) + ) except: error_questions_to_complete_day = 1 try: - flashcard_creation_method = int(request.vars.get('flashcardsCreationType', None)) + flashcard_creation_method = int( + request.vars.get("flashcardsCreationType", None) + ) except: error_flashcard_creation_method = 1 try: - graded = int(request.vars.get('graded', None)) + graded = int(request.vars.get("graded", None)) except: error_graded = 1 no_error = 0 - if (error_start_date == 0 and - error_end_date == 0 and - error_max_practice_days == 0 and - error_max_practice_questions == 0 and - error_day_points == 0 and - error_question_points == 0 and - error_questions_to_complete_day == 0 and - error_flashcard_creation_method == 0 and - error_graded == 0): + if ( + error_start_date == 0 + and error_end_date == 0 + and error_max_practice_days == 0 + and error_max_practice_questions == 0 + and error_day_points == 0 + and error_question_points == 0 + and error_questions_to_complete_day == 0 + and error_flashcard_creation_method == 0 + and error_graded == 0 + ): no_error = 1 if no_error == 1: - practice_settings.update(start_date=start_date, - end_date=end_date, - max_practice_days=max_practice_days, - max_practice_questions=max_practice_questions, - day_points=day_points, - question_points=question_points, - questions_to_complete_day=questions_to_complete_day, - flashcard_creation_method=flashcard_creation_method, - graded=graded, - spacing=spacing, - interleaving=interleaving - ) + practice_settings.update( + start_date=start_date, + end_date=end_date, + max_practice_days=max_practice_days, + max_practice_questions=max_practice_questions, + day_points=day_points, + question_points=question_points, + questions_to_complete_day=questions_to_complete_day, + flashcard_creation_method=flashcard_creation_method, + graded=graded, + spacing=spacing, + interleaving=interleaving, + ) toc = "''" if flashcard_creation_method == 2: toc = _get_toc_and_questions() - return dict(course_id=auth.user.course_name, - course_start_date=course_start_date, - start_date=start_date, - end_date=end_date, - max_practice_days=max_practice_days, - max_practice_questions=max_practice_questions, - day_points=day_points, - question_points=question_points, - questions_to_complete_day=questions_to_complete_day, - flashcard_creation_method=flashcard_creation_method, - graded=graded, - spacing=spacing, - interleaving=interleaving, - error_graded=error_graded, - toc=toc, - error_start_date=error_start_date, - error_end_date=error_end_date, - error_max_practice_days=error_max_practice_days, - error_max_practice_questions=error_max_practice_questions, - error_day_points=error_day_points, - error_question_points=error_question_points, - error_questions_to_complete_day=error_questions_to_complete_day, - error_flashcard_creation_method=error_flashcard_creation_method, - complete=no_error, - course=course, - ) + return dict( + course_id=auth.user.course_name, + course_start_date=course_start_date, + start_date=start_date, + end_date=end_date, + max_practice_days=max_practice_days, + max_practice_questions=max_practice_questions, + day_points=day_points, + question_points=question_points, + questions_to_complete_day=questions_to_complete_day, + flashcard_creation_method=flashcard_creation_method, + graded=graded, + spacing=spacing, + interleaving=interleaving, + error_graded=error_graded, + toc=toc, + error_start_date=error_start_date, + error_end_date=error_end_date, + error_max_practice_days=error_max_practice_days, + error_max_practice_questions=error_max_practice_questions, + error_day_points=error_day_points, + error_question_points=error_question_points, + error_questions_to_complete_day=error_questions_to_complete_day, + error_flashcard_creation_method=error_flashcard_creation_method, + complete=no_error, + course=course, + ) # I was not sure if it's okay to import it from `assignmnets.py`. # Only questions that are marked for practice are eligible for the spaced practice. def _get_qualified_questions(base_course, chapter_label, sub_chapter_label): - return db((db.questions.base_course == base_course) & - ((db.questions.topic == "{}/{}".format(chapter_label, sub_chapter_label)) | - ((db.questions.chapter == chapter_label) & - (db.questions.topic == None) & - (db.questions.subchapter == sub_chapter_label))) & - (db.questions.practice == True)) + return db( + (db.questions.base_course == base_course) + & ( + (db.questions.topic == "{}/{}".format(chapter_label, sub_chapter_label)) + | ( + (db.questions.chapter == chapter_label) + & (db.questions.topic == None) + & (db.questions.subchapter == sub_chapter_label) + ) + ) + & (db.questions.practice == True) + ) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def add_practice_items(): response.title = "Add Practice Items" course = db(db.courses.course_name == auth.user.course_name).select().first() @@ -446,13 +508,21 @@ def add_practice_items(): for chapter in chapters: subchapters = db((db.sub_chapters.chapter_id == chapter.id)).select() for subchapter in subchapters: - subchapterTaught = db((db.sub_chapter_taught.course_name == auth.user.course_name) & - (db.sub_chapter_taught.chapter_label == chapter.chapter_label) & - (db.sub_chapter_taught.sub_chapter_label == subchapter.sub_chapter_label)) - questions = _get_qualified_questions(course.base_course, - chapter.chapter_label, - subchapter.sub_chapter_label) - if "{}/{}".format(chapter.chapter_name, subchapter.sub_chapter_name) in string_data: + subchapterTaught = db( + (db.sub_chapter_taught.course_name == auth.user.course_name) + & (db.sub_chapter_taught.chapter_label == chapter.chapter_label) + & ( + db.sub_chapter_taught.sub_chapter_label + == subchapter.sub_chapter_label + ) + ) + questions = _get_qualified_questions( + course.base_course, chapter.chapter_label, subchapter.sub_chapter_label + ) + if ( + "{}/{}".format(chapter.chapter_name, subchapter.sub_chapter_name) + in string_data + ): if subchapterTaught.isempty() and not questions.isempty(): db.sub_chapter_taught.insert( course_name=auth.user.course_name, @@ -461,10 +531,18 @@ def add_practice_items(): teaching_date=now_local.date(), ) for student in students: - flashcards = db((db.user_topic_practice.user_id == student.id) & - (db.user_topic_practice.course_name == course.course_name) & - (db.user_topic_practice.chapter_label == chapter.chapter_label) & - (db.user_topic_practice.sub_chapter_label == subchapter.sub_chapter_label)) + flashcards = db( + (db.user_topic_practice.user_id == student.id) + & (db.user_topic_practice.course_name == course.course_name) + & ( + db.user_topic_practice.chapter_label + == chapter.chapter_label + ) + & ( + db.user_topic_practice.sub_chapter_label + == subchapter.sub_chapter_label + ) + ) if flashcards.isempty(): db.user_topic_practice.insert( user_id=student.id, @@ -480,36 +558,53 @@ def add_practice_items(): last_presented=now.date() - datetime.timedelta(1), last_completed=now.date() - datetime.timedelta(1), creation_time=now, - timezoneoffset=float(session.timezoneoffset) + timezoneoffset=float(session.timezoneoffset), ) else: if not subchapterTaught.isempty(): subchapterTaught.delete() - db((db.user_topic_practice.course_name == course.course_name) & - (db.user_topic_practice.chapter_label == chapter.chapter_label) & - (db.user_topic_practice.sub_chapter_label == subchapter.sub_chapter_label)).delete() + db( + (db.user_topic_practice.course_name == course.course_name) + & ( + db.user_topic_practice.chapter_label + == chapter.chapter_label + ) + & ( + db.user_topic_practice.sub_chapter_label + == subchapter.sub_chapter_label + ) + ).delete() return json.dumps(dict(complete=True)) # This is the primary controller when the instructor goes to the admin page. -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def admin(): response.title = "Admin" sidQuery = db(db.courses.course_name == auth.user.course_name).select().first() courseid = sidQuery.id - sectionsQuery = db(db.sections.course_id == courseid).select() #Querying to find all sections for that given course_id found above + sectionsQuery = db( + db.sections.course_id == courseid + ).select() # Querying to find all sections for that given course_id found above sectionsList = [] for row in sectionsQuery: sectionsList.append(row.name) - #Now get the start date + # Now get the start date dateQuery = db(db.courses.course_name == auth.user.course_name).select() date = dateQuery[0].term_start_date date = date.strftime("%m/%d/%Y") cwd = os.getcwd() try: - os.chdir(path.join('applications',request.application,'books',sidQuery.base_course)) + os.chdir( + path.join( + "applications", request.application, "books", sidQuery.base_course + ) + ) master_build = sh("git describe --long", capture=True)[:-1] - with open('build_info','w') as bc: + with open("build_info", "w") as bc: bc.write(master_build) bc.write("\n") except: @@ -518,8 +613,14 @@ def admin(): os.chdir(cwd) try: - mbf_path = path.join('applications',request.application,'custom_courses',sidQuery.course_name,'build_info') - mbf = open(mbf_path,'r') + mbf_path = path.join( + "applications", + request.application, + "custom_courses", + sidQuery.course_name, + "build_info", + ) + mbf = open(mbf_path, "r") last_build = os.path.getmtime(mbf_path) my_build = mbf.read()[:-1] mbf.close() @@ -531,48 +632,76 @@ def admin(): mst_vers = 0 bugfix = False - cur_instructors = db(db.course_instructor.course == auth.user.course_id).select(db.course_instructor.instructor) + cur_instructors = db(db.course_instructor.course == auth.user.course_id).select( + db.course_instructor.instructor + ) instructordict = {} for row in cur_instructors: - name = db(db.auth_user.id == row.instructor).select(db.auth_user.first_name, db.auth_user.last_name) + name = db(db.auth_user.id == row.instructor).select( + db.auth_user.first_name, db.auth_user.last_name + ) for person in name: - instructordict[str(row.instructor)] = person.first_name + " " + person.last_name + instructordict[str(row.instructor)] = ( + person.first_name + " " + person.last_name + ) - cur_students = db(db.user_courses.course_id == auth.user.course_id).select(db.user_courses.user_id) + cur_students = db(db.user_courses.course_id == auth.user.course_id).select( + db.user_courses.user_id + ) studentdict = {} for row in cur_students: - person = db(db.auth_user.id == row.user_id).select(db.auth_user.username, db.auth_user.first_name, db.auth_user.last_name) + person = db(db.auth_user.id == row.user_id).select( + db.auth_user.username, db.auth_user.first_name, db.auth_user.last_name + ) for identity in person: name = identity.first_name + " " + identity.last_name if row.user_id not in instructordict: - studentdict[row.user_id]= name + studentdict[row.user_id] = name course = db(db.courses.course_name == auth.user.course_name).select().first() - instructor_course_list = db( (db.course_instructor.instructor == auth.user.id) & - (db.courses.id == db.course_instructor.course) & - (db.courses.base_course == course.base_course) & - (db.courses.course_name != course.course_name)).select(db.courses.course_name, db.courses.id) + instructor_course_list = db( + (db.course_instructor.instructor == auth.user.id) + & (db.courses.id == db.course_instructor.course) + & (db.courses.base_course == course.base_course) + & (db.courses.course_name != course.course_name) + ).select(db.courses.course_name, db.courses.id) curr_start_date = course.term_start_date.strftime("%m/%d/%Y") - return dict(sectionInfo=sectionsList,startDate=date, - coursename=auth.user.course_name, course_id=auth.user.course_name, - instructors=instructordict, students=studentdict, - curr_start_date=curr_start_date, confirm=True, - build_info=my_build, master_build=master_build, my_vers=my_vers, - mst_vers=mst_vers, - course=sidQuery, - instructor_course_list=instructor_course_list - ) + return dict( + sectionInfo=sectionsList, + startDate=date, + coursename=auth.user.course_name, + course_id=auth.user.course_name, + instructors=instructordict, + students=studentdict, + curr_start_date=curr_start_date, + confirm=True, + build_info=my_build, + master_build=master_build, + my_vers=my_vers, + mst_vers=mst_vers, + course=sidQuery, + instructor_course_list=instructor_course_list, + ) + # Called in admin.js from courseStudents to populate the list of students # eBookConfig.getCourseStudentsURL -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def course_students(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" cur_students = db( - (db.user_courses.course_id == auth.user.course_id) & - (db.auth_user.id == db.user_courses.user_id) - ).select(db.auth_user.username, db.auth_user.first_name,db.auth_user.last_name, orderby= db.auth_user.last_name|db.auth_user.first_name) + (db.user_courses.course_id == auth.user.course_id) + & (db.auth_user.id == db.user_courses.user_id) + ).select( + db.auth_user.username, + db.auth_user.first_name, + db.auth_user.last_name, + orderby=db.auth_user.last_name | db.auth_user.first_name, + ) searchdict = OrderedDict() for row in cur_students: name = row.first_name + " " + row.last_name @@ -580,8 +709,12 @@ def course_students(): searchdict[str(username)] = name return json.dumps(searchdict) + # Called when an instructor clicks on the grading tab -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def grading(): response.title = "Grading" assignments = {} @@ -600,59 +733,85 @@ def grading(): ).select( db.assignment_questions.question_id, db.assignment_questions.points, - orderby=db.assignment_questions.sorting_priority) + orderby=db.assignment_questions.sorting_priority, + ) questions = [] for q in assignment_questions: - question_name = db(db.questions.id == q.question_id).select(db.questions.name).first().name + question_name = ( + db(db.questions.id == q.question_id) + .select(db.questions.name) + .first() + .name + ) questions.append(question_name) question_points[question_name] = q.points assignments[row.name] = questions assignment_deadlines[row.name] = row.duedate.isoformat() - cur_students = db(db.user_courses.course_id == auth.user.course_id).select(db.user_courses.user_id) + cur_students = db(db.user_courses.course_id == auth.user.course_id).select( + db.user_courses.user_id + ) searchdict = {} for row in cur_students: - isinstructor = db((db.course_instructor.course == auth.user.course_id) & (db.course_instructor.instructor == row.user_id)).select() + isinstructor = db( + (db.course_instructor.course == auth.user.course_id) + & (db.course_instructor.instructor == row.user_id) + ).select() instructorlist = [] for line in isinstructor: instructorlist.append(line.instructor) if row.user_id not in instructorlist: - person = db(db.auth_user.id == row.user_id).select(db.auth_user.username, db.auth_user.first_name, - db.auth_user.last_name) + person = db(db.auth_user.id == row.user_id).select( + db.auth_user.username, db.auth_user.first_name, db.auth_user.last_name + ) for identity in person: name = identity.first_name + " " + identity.last_name - username = db(db.auth_user.id == int(row.user_id)).select(db.auth_user.username).first().username + username = ( + db(db.auth_user.id == int(row.user_id)) + .select(db.auth_user.username) + .first() + .username + ) searchdict[str(username)] = name - course = db(db.courses.id == auth.user.course_id).select().first() base_course = course.base_course chapter_labels = {} chapters_query = db(db.chapters.course_id == base_course).select() for row in chapters_query: q_list = [] - chapter_questions = db((db.questions.chapter == row.chapter_label) & (db.questions.base_course == base_course) & (db.questions.question_type != 'page')).select() + chapter_questions = db( + (db.questions.chapter == row.chapter_label) + & (db.questions.base_course == base_course) + & (db.questions.question_type != "page") + ).select() for chapter_q in chapter_questions: q_list.append(chapter_q.name) chapter_labels[row.chapter_label] = q_list - return dict(assignmentinfo=json.dumps(assignments), students=searchdict, - chapters=json.dumps(chapter_labels), - gradingUrl = URL('assignments', 'get_problem'), - autogradingUrl = URL('assignments', 'autograde'), - gradeRecordingUrl = URL('assignments', 'record_grade'), - calcTotalsURL = URL('assignments', 'calculate_totals'), - setTotalURL=URL('assignments', 'record_assignment_score'), - sendLTIGradeURL=URL('assignments', 'send_assignment_score_via_LTI'), - getCourseStudentsURL = URL('admin', 'course_students'), - get_assignment_release_statesURL= URL('admin', 'get_assignment_release_states'), - course_id = auth.user.course_name, - assignmentids=json.dumps(assignmentids), - assignment_deadlines=json.dumps(assignment_deadlines), - question_points=json.dumps(question_points), - course=course, - ) + return dict( + assignmentinfo=json.dumps(assignments), + students=searchdict, + chapters=json.dumps(chapter_labels), + gradingUrl=URL("assignments", "get_problem"), + autogradingUrl=URL("assignments", "autograde"), + gradeRecordingUrl=URL("assignments", "record_grade"), + calcTotalsURL=URL("assignments", "calculate_totals"), + setTotalURL=URL("assignments", "record_assignment_score"), + sendLTIGradeURL=URL("assignments", "send_assignment_score_via_LTI"), + getCourseStudentsURL=URL("admin", "course_students"), + get_assignment_release_statesURL=URL("admin", "get_assignment_release_states"), + course_id=auth.user.course_name, + assignmentids=json.dumps(assignmentids), + assignment_deadlines=json.dumps(assignment_deadlines), + question_points=json.dumps(question_points), + course=course, + ) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def removeStudents(): """ Remove one or more students from the current course @@ -663,11 +822,23 @@ def removeStudents(): the database and moves them to the basecourse. """ - baseCourseName = db(db.courses.course_name == auth.user.course_name).select(db.courses.base_course)[0].base_course - baseCourseID = db(db.courses.course_name == baseCourseName).select(db.courses.id)[0].id - answer_tables = ['mchoice_answers', 'clickablearea_answers', 'codelens_answers', - 'dragndrop_answers', 'fitb_answers','parsons_answers', - 'shortanswer_answers'] + baseCourseName = ( + db(db.courses.course_name == auth.user.course_name) + .select(db.courses.base_course)[0] + .base_course + ) + baseCourseID = ( + db(db.courses.course_name == baseCourseName).select(db.courses.id)[0].id + ) + answer_tables = [ + "mchoice_answers", + "clickablearea_answers", + "codelens_answers", + "dragndrop_answers", + "fitb_answers", + "parsons_answers", + "shortanswer_answers", + ] if not isinstance(request.vars["studentList"], str): # Multiple ids selected @@ -675,37 +846,64 @@ def removeStudents(): elif request.vars["studentList"] == "None": # No id selected session.flash = T("No valid students were selected") - return redirect('/%s/admin/admin' % (request.application)) + return redirect("/%s/admin/admin" % (request.application)) else: # One id selected studentList = [request.vars["studentList"]] for studentID in studentList: if studentID.isdigit() and int(studentID) != auth.user.id: - sid = db(db.auth_user.id == int(studentID)).select(db.auth_user.username).first() - db((db.user_courses.user_id == int(studentID)) & (db.user_courses.course_id == auth.user.course_id)).delete() - section = db((db.sections.course_id == auth.user.course_id) & - (db.section_users.auth_user == int(studentID)) & - (db.section_users.section == db.sections.id)).select().first() + sid = ( + db(db.auth_user.id == int(studentID)) + .select(db.auth_user.username) + .first() + ) + db( + (db.user_courses.user_id == int(studentID)) + & (db.user_courses.course_id == auth.user.course_id) + ).delete() + section = ( + db( + (db.sections.course_id == auth.user.course_id) + & (db.section_users.auth_user == int(studentID)) + & (db.section_users.section == db.sections.id) + ) + .select() + .first() + ) if section: db(db.section_users.id == section.section_users.id).delete() db.user_courses.insert(user_id=int(studentID), course_id=baseCourseID) - db(db.auth_user.id == int(studentID)).update(course_id=baseCourseID, course_name=baseCourseName, active='F') - db( (db.useinfo.sid == sid) & - (db.useinfo.course_id == auth.user.course_name)).update(course_id=baseCourseName) + db(db.auth_user.id == int(studentID)).update( + course_id=baseCourseID, course_name=baseCourseName, active="F" + ) + db( + (db.useinfo.sid == sid) + & (db.useinfo.course_id == auth.user.course_name) + ).update(course_id=baseCourseName) for tbl in answer_tables: - db( (db[tbl].sid == sid) & (db[tbl].course_name == auth.user.course_name)).update(course_name=baseCourseName) - db((db.code.sid == sid) & - (db.code.course_id == auth.user.course_id)).update(course_id=baseCourseID) - db((db.acerror_log.sid == sid) & - (db.acerror_log.course_id == auth.user.course_name)).update(course_id=baseCourseName) + db( + (db[tbl].sid == sid) + & (db[tbl].course_name == auth.user.course_name) + ).update(course_name=baseCourseName) + db( + (db.code.sid == sid) & (db.code.course_id == auth.user.course_id) + ).update(course_id=baseCourseID) + db( + (db.acerror_log.sid == sid) + & (db.acerror_log.course_id == auth.user.course_name) + ).update(course_id=baseCourseName) # leave user_chapter_progress and user_sub_chapter_progress alone for now. session.flash = T("You have successfully removed students") - return redirect('/%s/admin/admin' % (request.application)) + return redirect("/%s/admin/admin" % (request.application)) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def removeinstructor(): """ admin/removeinstructor/ @@ -713,7 +911,10 @@ def removeinstructor(): """ removed = [] if request.args[0] != str(auth.user.id): - db((db.course_instructor.instructor == request.args[0]) & (db.course_instructor.course == auth.user.course_id)).delete() + db( + (db.course_instructor.instructor == request.args[0]) + & (db.course_instructor.course == auth.user.course_id) + ).delete() removed.append(True) return json.dumps(removed) else: @@ -721,17 +922,21 @@ def removeinstructor(): removed.append(False) return json.dumps(removed) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def addinstructor(): """ admin/addinstructor/ """ - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" instructor = request.args(0) res = db(db.auth_user.id == instructor).select().first() if res: - db.course_instructor.insert(course=auth.user.course_id , instructor=instructor) + db.course_instructor.insert(course=auth.user.course_id, instructor=instructor) retval = "Success" else: retval = "Cannot add non-existent user as instructor" @@ -740,7 +945,10 @@ def addinstructor(): return json.dumps(retval) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def deletecourse(): course_name = auth.user.course_name cset = db(db.courses.course_name == course_name) @@ -749,36 +957,55 @@ def deletecourse(): courseid = res.id basecourse = res.base_course bcid = db(db.courses.course_name == basecourse).select(db.courses.id).first() - qset = db((db.course_instructor.course == courseid) & (db.course_instructor.instructor == auth.user.id)) + qset = db( + (db.course_instructor.course == courseid) + & (db.course_instructor.instructor == auth.user.id) + ) if not qset.isempty(): qset.delete() students = db(db.auth_user.course_id == courseid) students.update(course_id=bcid) - uset=db(db.user_courses.course_id == courseid) + uset = db(db.user_courses.course_id == courseid) uset.delete() db(db.courses.id == courseid).delete() try: - shutil.rmtree(path.join('applications', request.application, 'static', course_name)) - shutil.rmtree(path.join('applications', request.application, 'custom_courses', course_name)) + shutil.rmtree( + path.join( + "applications", request.application, "static", course_name + ) + ) + shutil.rmtree( + path.join( + "applications", + request.application, + "custom_courses", + course_name, + ) + ) session.clear() except: - session.flash = 'Error, %s does not appear to exist' % course_name + session.flash = "Error, %s does not appear to exist" % course_name else: - session.flash = 'You are not the instructor of %s' % course_name + session.flash = "You are not the instructor of %s" % course_name else: - session.flash = 'course, %s, not found' % course_name + session.flash = "course, %s, not found" % course_name - redirect(URL('default','index')) + redirect(URL("default", "index")) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def removeassign(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" try: - assignment_id = int(request.vars['assignid']) + assignment_id = int(request.vars["assignid"]) except: - session.flash = "Cannot remove assignment with id of {}".format(request.vars['assignid']) + session.flash = "Cannot remove assignment with id of {}".format( + request.vars["assignid"] + ) logger.error("Cannot Remove Assignment {}".format(request.args(0))) return "Error" @@ -790,83 +1017,116 @@ def removeassign(): else: return "Error" + # # This is only called by the create button in the popup where you give the assignment # its initial name. We might be able to refactor save_assignment to work in all cases. -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def createAssignment(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" due = None - name = '' + name = "" - if 'name' in request.vars and len(request.vars['name']) > 0: - name = request.vars['name'] + if "name" in request.vars and len(request.vars["name"]) > 0: + name = request.vars["name"] else: - return json.dumps('ERROR') + return json.dumps("ERROR") - course=auth.user.course_id + course = auth.user.course_id logger.debug("Adding new assignment {} for course".format(name, course)) - name_existsQ = len(db((db.assignments.name == name) & (db.assignments.course == course)).select()) + name_existsQ = len( + db((db.assignments.name == name) & (db.assignments.course == course)).select() + ) if name_existsQ > 0: return json.dumps("EXISTS") try: - newassignID = db.assignments.insert(course=course, name=name, duedate=datetime.datetime.utcnow() + datetime.timedelta(days=7)) + newassignID = db.assignments.insert( + course=course, + name=name, + duedate=datetime.datetime.utcnow() + datetime.timedelta(days=7), + ) db.commit() except Exception as ex: logger.error("ERROR CREATING ASSIGNMENT", ex) - return json.dumps('ERROR') + return json.dumps("ERROR") returndict = {name: newassignID} return json.dumps(returndict) - -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def renameAssignment(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" try: - logger.debug("Renaming {} to {} for course {}.".format(request.vars['original'],request.vars['name'],auth.user.course_id)) - assignment_id=request.vars['original'] - name=request.vars['name'] - course=auth.user.course_id - name_existsQ = len(db((db.assignments.name == name) & (db.assignments.course == course)).select()) - if name_existsQ>0: + logger.debug( + "Renaming {} to {} for course {}.".format( + request.vars["original"], request.vars["name"], auth.user.course_id + ) + ) + assignment_id = request.vars["original"] + name = request.vars["name"] + course = auth.user.course_id + name_existsQ = len( + db( + (db.assignments.name == name) & (db.assignments.course == course) + ).select() + ) + if name_existsQ > 0: return json.dumps("EXISTS") db(db.assignments.id == assignment_id).update(name=name) except Exception as ex: logger.error(ex) - return json.dumps('ERROR') + return json.dumps("ERROR") try: - returndict={name: assignment_id} + returndict = {name: assignment_id} return json.dumps(returndict) except Exception as ex: logger.error(ex) - return json.dumps('ERROR') + return json.dumps("ERROR") + -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def questionBank(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" logger.error("in questionbank") - row = db(db.courses.id == auth.user.course_id).select(db.courses.course_name, db.courses.base_course).first() + row = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.course_name, db.courses.base_course) + .first() + ) base_course = row.base_course tags = False - if request.vars['tags']: + if request.vars["tags"]: tags = True term = False - if request.vars['term']: + if request.vars["term"]: term = True chapterQ = None - if request.vars['chapter']: - chapter_label = db(db.chapters.chapter_label == request.vars['chapter']).select(db.chapters.chapter_label).first().chapter_label - chapterQ = db.questions.chapter == chapter_label + if request.vars["chapter"]: + chapter_label = ( + db(db.chapters.chapter_label == request.vars["chapter"]) + .select(db.chapters.chapter_label) + .first() + .chapter_label + ) + chapterQ = db.questions.chapter == chapter_label difficulty = False - if request.vars['difficulty']: + if request.vars["difficulty"]: difficulty = True authorQ = None - if request.vars['author'] : - authorQ = db.questions.author == request.vars['author'] + if request.vars["author"]: + authorQ = db.questions.author == request.vars["author"] rows = [] questions = [] @@ -888,13 +1148,21 @@ def questionBank(): else: questions_query = db(base_courseQ).select() - for question in questions_query: #Initially add all questions that we can to the list, and then remove the rows that don't match search criteria + for ( + question + ) in ( + questions_query + ): # Initially add all questions that we can to the list, and then remove the rows that don't match search criteria rows.append(question) for row in questions_query: removed_row = False if term: - if (request.vars['term'] not in row.name and row.question and request.vars['term'] not in row.question) or row.question_type == 'page': + if ( + request.vars["term"] not in row.name + and row.question + and request.vars["term"] not in row.question + ) or row.question_type == "page": try: rows.remove(row) removed_row = True @@ -902,7 +1170,7 @@ def questionBank(): ex = err if removed_row == False: if difficulty: - if int(request.vars['difficulty']) != row.difficulty: + if int(request.vars["difficulty"]) != row.difficulty: try: rows.remove(row) removed_row = True @@ -918,7 +1186,7 @@ def questionBank(): for tag_name in tag_names: tag_list.append(tag_name.tag_name) needsRemoved = False - for search_tag in request.vars['tags'].split(','): + for search_tag in request.vars["tags"].split(","): if search_tag not in tag_list: needsRemoved = True if needsRemoved: @@ -931,18 +1199,24 @@ def questionBank(): except Exception as ex: logger.error(ex) - return json.dumps('Error ' + str(ex)) + return json.dumps("Error " + str(ex)) return json.dumps(questions) # Deprecated; use add__or_update_assignment_question instead -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def addToAssignment(): return add__or_update_assignment_question() -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def getQuestionInfo(): """ called by the questionBank search interface @@ -950,10 +1224,19 @@ def getQuestionInfo(): * assignment -- integer assignment id * question -- the name of the question """ - assignment_id = int(request.vars['assignment']) - question_name = request.vars['question'] - base_course = db(db.courses.course_name == auth.user.course_name).select().first().base_course - row = db((db.questions.name == question_name) & (db.questions.base_course == base_course)).select().first() + assignment_id = int(request.vars["assignment"]) + question_name = request.vars["question"] + base_course = ( + db(db.courses.course_name == auth.user.course_name).select().first().base_course + ) + row = ( + db( + (db.questions.name == question_name) + & (db.questions.base_course == base_course) + ) + .select() + .first() + ) question_code = row.question htmlsrc = row.htmlsrc @@ -968,24 +1251,49 @@ def getQuestionInfo(): tag_name = db((db.tags.id == tag_id)).select(db.tags.tag_name).first().tag_name tags.append(" " + str(tag_name)) if question_difficulty != None: - returnDict = {'code':question_code, 'htmlsrc': htmlsrc, 'author':question_author, 'difficulty':int(question_difficulty), 'tags': tags} + returnDict = { + "code": question_code, + "htmlsrc": htmlsrc, + "author": question_author, + "difficulty": int(question_difficulty), + "tags": tags, + } else: - returnDict = {'code':question_code, 'htmlsrc': htmlsrc, 'author':question_author, 'difficulty':None, 'tags': tags} + returnDict = { + "code": question_code, + "htmlsrc": htmlsrc, + "author": question_author, + "difficulty": None, + "tags": tags, + } return json.dumps(returnDict) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def edit_question(): vars = request.vars - old_qname = vars['question'] - new_qname = vars['name'] + old_qname = vars["question"] + new_qname = vars["name"] try: - difficulty = int(vars['difficulty']) + difficulty = int(vars["difficulty"]) except: difficulty = 0 - tags = vars['tags'] - base_course = db(db.courses.id == auth.user.course_id).select(db.courses.base_course).first().base_course - old_question = db((db.questions.name == old_qname) & (db.questions.base_course == base_course)).select().first() + tags = vars["tags"] + base_course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.base_course) + .first() + .base_course + ) + old_question = ( + db((db.questions.name == old_qname) & (db.questions.base_course == base_course)) + .select() + .first() + ) if not old_question: return "Could not find question {} to update".format(old_qname) @@ -996,8 +1304,8 @@ def edit_question(): question_type = old_question.question_type subchapter = old_question.subchapter - question = vars['questiontext'] - htmlsrc = vars['htmlsrc'] + question = vars["questiontext"] + htmlsrc = vars["htmlsrc"] if old_qname == new_qname and old_question.author != author: return "You do not own this question, Please assign a new unique id" @@ -1009,58 +1317,94 @@ def edit_question(): try: new_qid = db.questions.update_or_insert( - (db.questions.name == new_qname) & (db.questions.base_course == base_course), - difficulty=difficulty, question=question, - name=new_qname, author=author, base_course=base_course, timestamp=timestamp, - chapter=chapter, subchapter=subchapter, question_type=question_type, - htmlsrc=htmlsrc) - if tags and tags != 'null': - tags = tags.split(',') + (db.questions.name == new_qname) + & (db.questions.base_course == base_course), + difficulty=difficulty, + question=question, + name=new_qname, + author=author, + base_course=base_course, + timestamp=timestamp, + chapter=chapter, + subchapter=subchapter, + question_type=question_type, + htmlsrc=htmlsrc, + ) + if tags and tags != "null": + tags = tags.split(",") for tag in tags: - logger.error("TAG = %s",tag) + logger.error("TAG = %s", tag) tag_id = db(db.tags.tag_name == tag).select(db.tags.id).first().id - db.question_tags.insert(question_id = new_qid, tag_id=tag_id) + db.question_tags.insert(question_id=new_qid, tag_id=tag_id) return "Success - Edited Question Saved" except Exception as ex: logger.error(ex) return "An error occurred saving your question {}".format(str(ex)) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def question_text(): - qname = request.vars['question_name'] - base_course = db(db.courses.id == auth.user.course_id).select(db.courses.base_course).first().base_course + qname = request.vars["question_name"] + base_course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.base_course) + .first() + .base_course + ) try: - q_text = db((db.questions.name == qname) & (db.questions.base_course == base_course)).select(db.questions.question).first().question + q_text = ( + db((db.questions.name == qname) & (db.questions.base_course == base_course)) + .select(db.questions.question) + .first() + .question + ) except: q_text = "Error: Could not find source for {} in the database".format(qname) - if q_text[0:2] == '\\x': # workaround Python2/3 SQLAlchemy/DAL incompatibility with text - q_text = q_text[2:].decode('hex') + if ( + q_text[0:2] == "\\x" + ): # workaround Python2/3 SQLAlchemy/DAL incompatibility with text + q_text = q_text[2:].decode("hex") logger.debug(q_text) return json.dumps(q_text) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def gettemplate(): template = request.args[0] returndict = {} - base = '' + base = "" - returndict['template'] = base + cmap.get(template,'').__doc__ + returndict["template"] = base + cmap.get(template, "").__doc__ - base_course = db(db.courses.id == auth.user.course_id).select(db.courses.base_course).first().base_course + base_course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.base_course) + .first() + .base_course + ) chapters = [] - chaptersrow = db(db.chapters.course_id == base_course).select(db.chapters.chapter_name, db.chapters.chapter_label) + chaptersrow = db(db.chapters.course_id == base_course).select( + db.chapters.chapter_name, db.chapters.chapter_label + ) for row in chaptersrow: - chapters.append((row['chapter_label'], row['chapter_name'])) + chapters.append((row["chapter_label"], row["chapter_name"])) logger.debug(chapters) - returndict['chapters'] = chapters + returndict["chapters"] = chapters return json.dumps(returndict) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def createquestion(): """ called from the questionBank interface when an instructor adds a new question to @@ -1080,90 +1424,128 @@ def createquestion(): * timed- is this part of a timed exam * htmlsrc htmlsrc from the previewer """ - row = db(db.courses.id == auth.user.course_id).select(db.courses.course_name, db.courses.base_course).first() + row = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.course_name, db.courses.base_course) + .first() + ) base_course = row.base_course - tab = request.vars['tab'] - aid = request.vars['assignmentid'] - if aid == 'undefined': - logger.error("undefined assignmentid by {} for name {} subchap {} question {}".format(auth.user.username, - request.vars.name, - request.vars.subchapter, - request.vars.question)) + tab = request.vars["tab"] + aid = request.vars["assignmentid"] + if aid == "undefined": + logger.error( + "undefined assignmentid by {} for name {} subchap {} question {}".format( + auth.user.username, + request.vars.name, + request.vars.subchapter, + request.vars.question, + ) + ) return json.dumps("ERROR") assignmentid = int(aid) - points = int(request.vars['points']) if request.vars['points'] else 1 - timed = request.vars['timed'] + points = int(request.vars["points"]) if request.vars["points"] else 1 + timed = request.vars["timed"] unittest = None - if re.search(':autograde:\s+unittest', request.vars.question): + if re.search(":autograde:\s+unittest", request.vars.question): unittest = "unittest" try: - newqID = db.questions.insert(base_course=base_course, name=request.vars['name'], - chapter=request.vars['chapter'], - subchapter=request.vars['subchapter'], + newqID = db.questions.insert( + base_course=base_course, + name=request.vars["name"], + chapter=request.vars["chapter"], + subchapter=request.vars["subchapter"], author=auth.user.first_name + " " + auth.user.last_name, autograde=unittest, - difficulty=request.vars['difficulty'], - question=request.vars['question'], + difficulty=request.vars["difficulty"], + question=request.vars["question"], timestamp=datetime.datetime.utcnow(), - question_type=request.vars['template'], - is_private=request.vars['isprivate'], + question_type=request.vars["template"], + is_private=request.vars["isprivate"], from_source=False, - htmlsrc=request.vars['htmlsrc']) + htmlsrc=request.vars["htmlsrc"], + ) - assignment_question = db.assignment_questions.insert(assignment_id=assignmentid, question_id=newqID, timed=timed, points=points) + assignment_question = db.assignment_questions.insert( + assignment_id=assignmentid, question_id=newqID, timed=timed, points=points + ) - returndict = {request.vars['name']: newqID, 'timed':timed, 'points': points} + returndict = {request.vars["name"]: newqID, "timed": timed, "points": points} return json.dumps(returndict) except Exception as ex: logger.error(ex) - return json.dumps('ERROR') + return json.dumps("ERROR") -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def htmlsrc(): - acid = request.vars['acid'] + acid = request.vars["acid"] htmlsrc = "" - res = db( - (db.questions.name == acid) & - (db.questions.base_course == db.courses.base_course) & - (db.courses.course_name == auth.user.course_name) - ).select(db.questions.htmlsrc).first() + res = ( + db( + (db.questions.name == acid) + & (db.questions.base_course == db.courses.base_course) + & (db.courses.course_name == auth.user.course_name) + ) + .select(db.questions.htmlsrc) + .first() + ) if res and res.htmlsrc: htmlsrc = res.htmlsrc else: - logger.error("HTML Source not found for %s in course %s", acid, auth.user.course_name) + logger.error( + "HTML Source not found for %s in course %s", acid, auth.user.course_name + ) htmlsrc = "

No preview Available

" - if htmlsrc and htmlsrc[0:2] == '\\x': # Workaround Python3/Python2 SQLAlchemy/DAL incompatibility with text columns - htmlsrc = htmlsrc.decode('hex') + if ( + htmlsrc and htmlsrc[0:2] == "\\x" + ): # Workaround Python3/Python2 SQLAlchemy/DAL incompatibility with text columns + htmlsrc = htmlsrc.decode("hex") return json.dumps(htmlsrc) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def getGradeComments(): - acid = request.vars['acid'] - sid = request.vars['sid'] + acid = request.vars["acid"] + sid = request.vars["sid"] - c = db((db.question_grades.sid == sid) \ - & (db.question_grades.div_id == acid) \ - & (db.question_grades.course_name == auth.user.course_name)\ - ).select().first() + c = ( + db( + (db.question_grades.sid == sid) + & (db.question_grades.div_id == acid) + & (db.question_grades.course_name == auth.user.course_name) + ) + .select() + .first() + ) if c != None: - return json.dumps({'grade':c.score, 'comments':c.comment}) + return json.dumps({"grade": c.score, "comments": c.comment}) else: return json.dumps("Error") + def _get_lti_record(oauth_consumer_key): if oauth_consumer_key: return db(db.lti_keys.consumer == oauth_consumer_key).select().first() -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def releasegrades(): try: - assignmentid = request.vars['assignmentid'] - released = (request.vars['released'] == 'yes') + assignmentid = request.vars["assignmentid"] + released = request.vars["released"] == "yes" assignment = db(db.assignments.id == assignmentid).select().first() assignment.update_record(released=released) @@ -1176,11 +1558,16 @@ def releasegrades(): assignment = _get_assignment(assignmentid) lti_record = _get_lti_record(session.oauth_consumer_key) if assignment and lti_record: - send_lti_grades(assignment.id, assignment.points, auth.user.course_id, lti_record, db) + send_lti_grades( + assignment.id, assignment.points, auth.user.course_id, lti_record, db + ) return "Success" -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def get_assignment_release_states(): # return a dictionary with the release status of whether grades have been # released for each of the assignments for the current course @@ -1191,281 +1578,364 @@ def get_assignment_release_states(): print(ex) return json.dumps({}) + def _get_toc_and_questions(): # return a dictionary with a nested dictionary representing everything the # picker will need in the instructor's assignment authoring tab # Format is documented at https://www.jstree.com/docs/json/ - #try: - course_row = get_course_row() - base_course = course_row.base_course - - # First get the chapters associated with the current course, and insert them into the tree - # Recurse, with each chapter: - # -- get the subchapters associated with it, and insert into the subdictionary - # -- Recurse; with each subchapter: - # -- get the divs associated with it, and insert into the sub-sub-dictionary - - question_picker = [] - # This one doesn't include the questions, but otherwise the same - reading_picker = [] - # This one is similar to reading_picker, but does not include sub-chapters with no practice question. - practice_picker = [] - subchapters_taught_query = db((db.sub_chapter_taught.course_name == auth.user.course_name) & - (db.chapters.course_id == base_course) & - (db.chapters.chapter_label == db.sub_chapter_taught.chapter_label) & - (db.sub_chapters.chapter_id == db.chapters.id) & - (db.sub_chapters.sub_chapter_label == db.sub_chapter_taught.sub_chapter_label) - ).select(db.chapters.chapter_name, - db.sub_chapters.sub_chapter_name) - chapters_and_subchapters_taught = [(row.chapters.chapter_name, row.sub_chapters.sub_chapter_name) - for row in subchapters_taught_query] - topic_query = db((db.courses.course_name == auth.user.course_name) & - (db.questions.base_course == db.courses.base_course) & - (db.questions.practice == True)).select(db.questions.topic, - db.questions.chapter, - db.questions.subchapter, - orderby=db.questions.id) - for q in topic_query: - # We know chapter_name and sub_chapter_name include spaces. - # So we cannot directly use the labels retrieved from q.topic as chapter_name and - # sub_chapter_name and we need to query the corresponding chapter_name and sub_chapter_name from the - # corresponding tables. - topic_not_found = True - if q.topic is not None: - topic_not_found = False - try: - chap, subch = q.topic.split('/') - except: - # a badly formed "topic" for the question; just ignore it - logger.info("Bad Topic: {}".format(q.topic)) - topic_not_found = True - try: - chapter = db((db.chapters.course_id == base_course) & - (db.chapters.chapter_label == chap)) \ - .select()[0] - - sub_chapter_name = db((db.sub_chapters.chapter_id == chapter.id) & - (db.sub_chapters.sub_chapter_label == subch)) \ - .select()[0].sub_chapter_name - except: - # topic's chapter and subchapter are not in the book; ignore this topic - logger.info("Missing Chapter {} or Subchapter {} for topic {}".format(chap, subch, q.topic)) - topic_not_found = True - - if topic_not_found: - topic_not_found = False - chap = q.chapter - subch = q.subchapter - try: - chapter = db((db.chapters.course_id == base_course) & - (db.chapters.chapter_label == chap)) \ - .select()[0] - - sub_chapter_name = db((db.sub_chapters.chapter_id == chapter.id) & - (db.sub_chapters.sub_chapter_label == subch)) \ - .select()[0].sub_chapter_name - except: - # topic's chapter and subchapter are not in the book; ignore this topic - logger.info("Missing Chapter {} or Subchapter {}".format(chap, subch)) - topic_not_found = True - - if not topic_not_found: - chapter_name = chapter.chapter_name - # Find the item in practice picker for this chapter - p_ch_info = None - for ch_info in practice_picker: - if ch_info['text'] == chapter_name: - p_ch_info = ch_info - if not p_ch_info: - # if there isn't one, add one - p_ch_info = {} - practice_picker.append(p_ch_info) - p_ch_info['text'] = chapter_name - p_ch_info['children'] = [] - # add the subchapter - p_sub_ch_info = {} - if sub_chapter_name not in [child['text'] for child in p_ch_info['children']]: - p_ch_info['children'].append(p_sub_ch_info) - p_sub_ch_info['id'] = "{}/{}".format(chapter_name, sub_chapter_name) - p_sub_ch_info['text'] = sub_chapter_name - # checked if - p_sub_ch_info['state'] = {'checked': - (chapter_name, sub_chapter_name) in chapters_and_subchapters_taught} - - # chapters are associated base_course. - chapters_query = db((db.chapters.course_id == base_course)).select(orderby=db.chapters.id) - ids = {row.chapter_name: row.id for row in chapters_query} - practice_picker.sort(key=lambda d: ids[d['text']]) - - for ch in chapters_query: - q_ch_info = {} - question_picker.append(q_ch_info) - q_ch_info['text'] = ch.chapter_name - q_ch_info['children'] = [] + # try: + course_row = get_course_row() + base_course = course_row.base_course + + # First get the chapters associated with the current course, and insert them into the tree + # Recurse, with each chapter: + # -- get the subchapters associated with it, and insert into the subdictionary + # -- Recurse; with each subchapter: + # -- get the divs associated with it, and insert into the sub-sub-dictionary + + question_picker = [] + # This one doesn't include the questions, but otherwise the same + reading_picker = [] + # This one is similar to reading_picker, but does not include sub-chapters with no practice question. + practice_picker = [] + subchapters_taught_query = db( + (db.sub_chapter_taught.course_name == auth.user.course_name) + & (db.chapters.course_id == base_course) + & (db.chapters.chapter_label == db.sub_chapter_taught.chapter_label) + & (db.sub_chapters.chapter_id == db.chapters.id) + & (db.sub_chapters.sub_chapter_label == db.sub_chapter_taught.sub_chapter_label) + ).select(db.chapters.chapter_name, db.sub_chapters.sub_chapter_name) + chapters_and_subchapters_taught = [ + (row.chapters.chapter_name, row.sub_chapters.sub_chapter_name) + for row in subchapters_taught_query + ] + topic_query = db( + (db.courses.course_name == auth.user.course_name) + & (db.questions.base_course == db.courses.base_course) + & (db.questions.practice == True) + ).select( + db.questions.topic, + db.questions.chapter, + db.questions.subchapter, + orderby=db.questions.id, + ) + for q in topic_query: + # We know chapter_name and sub_chapter_name include spaces. + # So we cannot directly use the labels retrieved from q.topic as chapter_name and + # sub_chapter_name and we need to query the corresponding chapter_name and sub_chapter_name from the + # corresponding tables. + topic_not_found = True + if q.topic is not None: + topic_not_found = False + try: + chap, subch = q.topic.split("/") + except: + # a badly formed "topic" for the question; just ignore it + logger.info("Bad Topic: {}".format(q.topic)) + topic_not_found = True + try: + chapter = db( + (db.chapters.course_id == base_course) + & (db.chapters.chapter_label == chap) + ).select()[0] + + sub_chapter_name = ( + db( + (db.sub_chapters.chapter_id == chapter.id) + & (db.sub_chapters.sub_chapter_label == subch) + ) + .select()[0] + .sub_chapter_name + ) + except: + # topic's chapter and subchapter are not in the book; ignore this topic + logger.info( + "Missing Chapter {} or Subchapter {} for topic {}".format( + chap, subch, q.topic + ) + ) + topic_not_found = True + + if topic_not_found: + topic_not_found = False + chap = q.chapter + subch = q.subchapter + try: + chapter = db( + (db.chapters.course_id == base_course) + & (db.chapters.chapter_label == chap) + ).select()[0] + + sub_chapter_name = ( + db( + (db.sub_chapters.chapter_id == chapter.id) + & (db.sub_chapters.sub_chapter_label == subch) + ) + .select()[0] + .sub_chapter_name + ) + except: + # topic's chapter and subchapter are not in the book; ignore this topic + logger.info("Missing Chapter {} or Subchapter {}".format(chap, subch)) + topic_not_found = True + + if not topic_not_found: + chapter_name = chapter.chapter_name + # Find the item in practice picker for this chapter + p_ch_info = None + for ch_info in practice_picker: + if ch_info["text"] == chapter_name: + p_ch_info = ch_info + if not p_ch_info: + # if there isn't one, add one + p_ch_info = {} + practice_picker.append(p_ch_info) + p_ch_info["text"] = chapter_name + p_ch_info["children"] = [] + # add the subchapter + p_sub_ch_info = {} + if sub_chapter_name not in [ + child["text"] for child in p_ch_info["children"] + ]: + p_ch_info["children"].append(p_sub_ch_info) + p_sub_ch_info["id"] = "{}/{}".format(chapter_name, sub_chapter_name) + p_sub_ch_info["text"] = sub_chapter_name + # checked if + p_sub_ch_info["state"] = { + "checked": (chapter_name, sub_chapter_name) + in chapters_and_subchapters_taught + } + + # chapters are associated base_course. + chapters_query = db((db.chapters.course_id == base_course)).select( + orderby=db.chapters.id + ) + ids = {row.chapter_name: row.id for row in chapters_query} + practice_picker.sort(key=lambda d: ids[d["text"]]) + + for ch in chapters_query: + q_ch_info = {} + question_picker.append(q_ch_info) + q_ch_info["text"] = ch.chapter_name + q_ch_info["children"] = [] + # Copy the same stuff for reading picker. + r_ch_info = {} + reading_picker.append(r_ch_info) + r_ch_info["text"] = ch.chapter_name + r_ch_info["children"] = [] + # practice_questions = db((db.questions.chapter == ch.chapter_label) & \ + # (db.questions.practice == True)) + # if not practice_questions.isempty(): + # # Copy the same stuff for practice picker. + # p_ch_info = {} + # practice_picker.append(p_ch_info) + # p_ch_info['text'] = ch.chapter_name + # p_ch_info['children'] = [] + # todo: check the chapters attribute to see if its available for readings + subchapters_query = db(db.sub_chapters.chapter_id == ch.id).select( + orderby=db.sub_chapters.id + ) + for sub_ch in subchapters_query: + q_sub_ch_info = {} + q_ch_info["children"].append(q_sub_ch_info) + q_sub_ch_info["text"] = sub_ch.sub_chapter_name + # Make the Exercises sub-chapters easy to access, since user-written problems will be added there. + if sub_ch.sub_chapter_name == "Exercises": + q_sub_ch_info["id"] = ch.chapter_name + " Exercises" + q_sub_ch_info["children"] = [] # Copy the same stuff for reading picker. - r_ch_info = {} - reading_picker.append(r_ch_info) - r_ch_info['text'] = ch.chapter_name - r_ch_info['children'] = [] + if ( + sub_ch.skipreading == "F" + or sub_ch.skipreading == False + or sub_ch.skipreading == None + ): + r_sub_ch_info = {} + r_ch_info["children"].append(r_sub_ch_info) + r_sub_ch_info["id"] = "{}/{}".format( + ch.chapter_name, sub_ch.sub_chapter_name + ) + r_sub_ch_info["text"] = sub_ch.sub_chapter_name # practice_questions = db((db.questions.chapter == ch.chapter_label) & \ - # (db.questions.practice == True)) + # (db.questions.subchapter == sub_ch.sub_chapter_label) & \ + # (db.questions.practice == True)) # if not practice_questions.isempty(): - # # Copy the same stuff for practice picker. - # p_ch_info = {} - # practice_picker.append(p_ch_info) - # p_ch_info['text'] = ch.chapter_name - # p_ch_info['children'] = [] - # todo: check the chapters attribute to see if its available for readings - subchapters_query = db(db.sub_chapters.chapter_id == ch.id).select(orderby=db.sub_chapters.id) - for sub_ch in subchapters_query: - q_sub_ch_info = {} - q_ch_info['children'].append(q_sub_ch_info) - q_sub_ch_info['text'] = sub_ch.sub_chapter_name - # Make the Exercises sub-chapters easy to access, since user-written problems will be added there. - if sub_ch.sub_chapter_name == 'Exercises': - q_sub_ch_info['id'] = ch.chapter_name + ' Exercises' - q_sub_ch_info['children'] = [] - # Copy the same stuff for reading picker. - if sub_ch.skipreading == 'F' or sub_ch.skipreading == False or sub_ch.skipreading == None: - r_sub_ch_info = {} - r_ch_info['children'].append(r_sub_ch_info) - r_sub_ch_info['id'] = "{}/{}".format(ch.chapter_name, sub_ch.sub_chapter_name) - r_sub_ch_info['text'] = sub_ch.sub_chapter_name - # practice_questions = db((db.questions.chapter == ch.chapter_label) & \ - # (db.questions.subchapter == sub_ch.sub_chapter_label) & \ - # (db.questions.practice == True)) - # if not practice_questions.isempty(): - # # Copy the same stuff for reading picker. - # p_sub_ch_info = {} - # p_ch_info['children'].append(p_sub_ch_info) - # p_sub_ch_info['id'] = "{}/{}".format(ch.chapter_name, sub_ch.sub_chapter_name) - # p_sub_ch_info['text'] = sub_ch.sub_chapter_name - # # checked if - # p_sub_ch_info['state'] = {'checked': - # (ch.chapter_name, sub_ch.sub_chapter_name) in chapters_and_subchapters_taught} - # include another level for questions only in the question picker - questions_query = db((db.courses.course_name == auth.user.course_name) & \ - (db.questions.base_course == db.courses.base_course) & \ - (db.questions.chapter == ch.chapter_label) & \ - (db.questions.question_type != 'page') & \ - (db.questions.subchapter == sub_ch.sub_chapter_label)).select(orderby=db.questions.id) - for question in questions_query: - q_info = dict( - text=question.questions.name + _add_q_meta_info(question), - id=question.questions.name, - ) - q_sub_ch_info['children'].append(q_info) - return json.dumps({'reading_picker': reading_picker, - 'practice_picker': practice_picker, - 'question_picker': question_picker}) - # except Exception as ex: - # print(ex) - # return json.dumps({}) + # # Copy the same stuff for reading picker. + # p_sub_ch_info = {} + # p_ch_info['children'].append(p_sub_ch_info) + # p_sub_ch_info['id'] = "{}/{}".format(ch.chapter_name, sub_ch.sub_chapter_name) + # p_sub_ch_info['text'] = sub_ch.sub_chapter_name + # # checked if + # p_sub_ch_info['state'] = {'checked': + # (ch.chapter_name, sub_ch.sub_chapter_name) in chapters_and_subchapters_taught} + # include another level for questions only in the question picker + questions_query = db( + (db.courses.course_name == auth.user.course_name) + & (db.questions.base_course == db.courses.base_course) + & (db.questions.chapter == ch.chapter_label) + & (db.questions.question_type != "page") + & (db.questions.subchapter == sub_ch.sub_chapter_label) + ).select(orderby=db.questions.id) + for question in questions_query: + q_info = dict( + text=question.questions.name + _add_q_meta_info(question), + id=question.questions.name, + ) + q_sub_ch_info["children"].append(q_info) + return json.dumps( + { + "reading_picker": reading_picker, + "practice_picker": practice_picker, + "question_picker": question_picker, + } + ) + + +# except Exception as ex: +# print(ex) +# return json.dumps({}) + def _add_q_meta_info(qrow): res = "" qt = { - 'mchoice': 'Mchoice ✓', - 'clickablearea':'Clickable ✓', - 'youtube': 'Video', - 'activecode': 'ActiveCode', - 'poll': 'Poll', - 'showeval': 'ShowEval', - 'video': 'Video', - 'dragndrop': 'Matching ✓', - 'parsonsprob': 'Parsons ✓', - 'codelens': 'CodeLens', - 'lp_build': 'LP ✓', - 'shortanswer': 'ShortAns', - 'actex': 'ActiveCode', - 'fillintheblank': 'FillB ✓' + "mchoice": "Mchoice ✓", + "clickablearea": "Clickable ✓", + "youtube": "Video", + "activecode": "ActiveCode", + "poll": "Poll", + "showeval": "ShowEval", + "video": "Video", + "dragndrop": "Matching ✓", + "parsonsprob": "Parsons ✓", + "codelens": "CodeLens", + "lp_build": "LP ✓", + "shortanswer": "ShortAns", + "actex": "ActiveCode", + "fillintheblank": "FillB ✓", } - res += qt.get(qrow.questions.question_type,"") + res += qt.get(qrow.questions.question_type, "") if qrow.questions.autograde: - res += ' ✓' + res += " ✓" if res != "": res = """ [{}] """.format(res) return res -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def get_assignment(): - assignment_id = request.vars['assignmentid'] + assignment_id = request.vars["assignmentid"] # Assemble the assignment-level properties - if assignment_id == 'undefined': - logger.error('UNDEFINED assignment {} {}'.format(auth.user.course_name, auth.user.username)) - session.flash = 'Error assignment ID is undefined' - return redirect(URL('assignments','index')) + if assignment_id == "undefined": + logger.error( + "UNDEFINED assignment {} {}".format( + auth.user.course_name, auth.user.username + ) + ) + session.flash = "Error assignment ID is undefined" + return redirect(URL("assignments", "index")) _set_assignment_max_points(assignment_id) assignment_data = {} assignment_row = db(db.assignments.id == assignment_id).select().first() - assignment_data['assignment_points'] = assignment_row.points + assignment_data["assignment_points"] = assignment_row.points try: - assignment_data['due_date'] = assignment_row.duedate.strftime("%Y/%m/%d %H:%M") + assignment_data["due_date"] = assignment_row.duedate.strftime("%Y/%m/%d %H:%M") except Exception as ex: logger.error(ex) - assignment_data['due_date'] = None - assignment_data['description'] = assignment_row.description - assignment_data['visible'] = assignment_row.visible + assignment_data["due_date"] = None + assignment_data["description"] = assignment_row.description + assignment_data["visible"] = assignment_row.visible # Still need to get: # -- timed properties of assignment # (See https://github.com/RunestoneInteractive/RunestoneServer/issues/930) - base_course = db(db.courses.id == auth.user.course_id).select(db.courses.base_course).first().base_course + base_course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.base_course) + .first() + .base_course + ) # Assemble the readings (subchapters) that are part of the assignment - a_q_rows = db((db.assignment_questions.assignment_id == assignment_id) & - (db.assignment_questions.question_id == db.questions.id) & - (db.questions.question_type == 'page') - ).select(orderby=db.assignment_questions.sorting_priority) + a_q_rows = db( + (db.assignment_questions.assignment_id == assignment_id) + & (db.assignment_questions.question_id == db.questions.id) + & (db.questions.question_type == "page") + ).select(orderby=db.assignment_questions.sorting_priority) pages_data = [] for row in a_q_rows: - if row.questions.question_type == 'page': + if row.questions.question_type == "page": # get the count of 'things to do' in this chap/subchap - activity_count = db((db.questions.chapter==row.questions.chapter) & - (db.questions.subchapter==row.questions.subchapter) & - (db.questions.from_source == 'T') & - (db.questions.base_course == base_course)).count() - - pages_data.append(dict( - name = row.questions.name, - points = row.assignment_questions.points, - autograde = row.assignment_questions.autograde, - activity_count = activity_count, - activities_required = row.assignment_questions.activities_required, - which_to_grade = row.assignment_questions.which_to_grade, - autograde_possible_values = AUTOGRADE_POSSIBLE_VALUES[row.questions.question_type], - which_to_grade_possible_values = WHICH_TO_GRADE_POSSIBLE_VALUES[row.questions.question_type] - )) + activity_count = db( + (db.questions.chapter == row.questions.chapter) + & (db.questions.subchapter == row.questions.subchapter) + & (db.questions.from_source == "T") + & (db.questions.base_course == base_course) + ).count() + + pages_data.append( + dict( + name=row.questions.name, + points=row.assignment_questions.points, + autograde=row.assignment_questions.autograde, + activity_count=activity_count, + activities_required=row.assignment_questions.activities_required, + which_to_grade=row.assignment_questions.which_to_grade, + autograde_possible_values=AUTOGRADE_POSSIBLE_VALUES[ + row.questions.question_type + ], + which_to_grade_possible_values=WHICH_TO_GRADE_POSSIBLE_VALUES[ + row.questions.question_type + ], + ) + ) # Assemble the questions that are part of the assignment - a_q_rows = db((db.assignment_questions.assignment_id == assignment_id) & - (db.assignment_questions.question_id == db.questions.id) & - (db.assignment_questions.reading_assignment == None) - ).select(orderby=db.assignment_questions.sorting_priority) - #return json.dumps(db._lastsql) + a_q_rows = db( + (db.assignment_questions.assignment_id == assignment_id) + & (db.assignment_questions.question_id == db.questions.id) + & (db.assignment_questions.reading_assignment == None) + ).select(orderby=db.assignment_questions.sorting_priority) + # return json.dumps(db._lastsql) questions_data = [] for row in a_q_rows: logger.debug(row.questions.question_type) - if row.questions.question_type != 'page': - questions_data.append(dict( - name = row.questions.name, - points = row.assignment_questions.points, - autograde = row.assignment_questions.autograde, - which_to_grade = row.assignment_questions.which_to_grade, - autograde_possible_values = AUTOGRADE_POSSIBLE_VALUES[row.questions.question_type], - which_to_grade_possible_values = WHICH_TO_GRADE_POSSIBLE_VALUES[row.questions.question_type] - )) - - return json.dumps(dict(assignment_data=assignment_data, - pages_data=pages_data, - questions_data=questions_data)) - -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + if row.questions.question_type != "page": + questions_data.append( + dict( + name=row.questions.name, + points=row.assignment_questions.points, + autograde=row.assignment_questions.autograde, + which_to_grade=row.assignment_questions.which_to_grade, + autograde_possible_values=AUTOGRADE_POSSIBLE_VALUES[ + row.questions.question_type + ], + which_to_grade_possible_values=WHICH_TO_GRADE_POSSIBLE_VALUES[ + row.questions.question_type + ], + ) + ) + + return json.dumps( + dict( + assignment_data=assignment_data, + pages_data=pages_data, + questions_data=questions_data, + ) + ) + + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def save_assignment(): # This endpoint is for saving (updating) an assignment's top-level information, without any # questions or readings that might be part of the assignment @@ -1476,11 +1946,11 @@ def save_assignment(): # -- description # -- duedate - assignment_id = request.vars.get('assignment_id') - isVisible = request.vars['visible'] + assignment_id = request.vars.get("assignment_id") + isVisible = request.vars["visible"] try: - d_str = request.vars['due'] + d_str = request.vars["due"] format_str = "%Y/%m/%d %H:%M" due = datetime.datetime.strptime(d_str, format_str) except: @@ -1490,19 +1960,21 @@ def save_assignment(): total = _set_assignment_max_points(assignment_id) db(db.assignments.id == assignment_id).update( course=auth.user.course_id, - description=request.vars['description'], + description=request.vars["description"], points=total, duedate=due, - visible=request.vars['visible'] + visible=request.vars["visible"], ) - return json.dumps({request.vars['name']: assignment_id, - 'status': 'success'}) + return json.dumps({request.vars["name"]: assignment_id, "status": "success"}) except Exception as ex: logger.error(ex) - return json.dumps('ERROR') + return json.dumps("ERROR") -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def add__or_update_assignment_question(): # This endpoint is for adding a question to an assignment, or updating an existing assignment_question @@ -1513,22 +1985,39 @@ def add__or_update_assignment_question(): # -- autograde # -- which_to_grade # -- reading_assignment (boolean, true if it's a page to visit rather than a directive to interact with) - if request.vars.assignment == 'undefined': - session.flash = "Error: Unable to update assignment in DB. No assignment is selected" - return redirect(URL('admin','assignments')) + if request.vars.assignment == "undefined": + session.flash = ( + "Error: Unable to update assignment in DB. No assignment is selected" + ) + return redirect(URL("admin", "assignments")) - assignment_id = int(request.vars['assignment']) - question_name = request.vars['question'] - logger.debug("adding or updating assign id {} question_name {}".format(assignment_id, question_name)) + assignment_id = int(request.vars["assignment"]) + question_name = request.vars["question"] + logger.debug( + "adding or updating assign id {} question_name {}".format( + assignment_id, question_name + ) + ) # This assumes that question will always be in DB already, before an assignment_question is created - logger.debug("course_id %s",auth.user.course_id) + logger.debug("course_id %s", auth.user.course_id) question_id = _get_question_id(question_name, auth.user.course_id) if question_id == None: - logger.error("Question Not found for name = {} course = {}".format(question_name, auth.user.course_id)) - session.flash = "Error: Cannot find question {} in the database".format(question_name) - return redirect(URL('admin','assignments')) + logger.error( + "Question Not found for name = {} course = {}".format( + question_name, auth.user.course_id + ) + ) + session.flash = "Error: Cannot find question {} in the database".format( + question_name + ) + return redirect(URL("admin", "assignments")) - base_course = db(db.courses.id == auth.user.course_id).select(db.courses.base_course).first().base_course + base_course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.base_course) + .first() + .base_course + ) logger.debug("base course %s", base_course) question_type = db.questions[question_id].question_type chapter = db.questions[question_id].chapter @@ -1546,17 +2035,19 @@ def add__or_update_assignment_question(): sp = tmpSp activity_count = 0 - if question_type == 'page': - reading_assignment = 'T' + if question_type == "page": + reading_assignment = "T" # get the count of 'things to do' in this chap/subchap - activity_count = db((db.questions.chapter == chapter) & - (db.questions.subchapter == subchapter) & - (db.questions.from_source == 'T') & - (db.questions.base_course == base_course)).count() + activity_count = db( + (db.questions.chapter == chapter) + & (db.questions.subchapter == subchapter) + & (db.questions.from_source == "T") + & (db.questions.base_course == base_course) + ).count() try: - activities_required = int(request.vars.get('activities_required')) + activities_required = int(request.vars.get("activities_required")) if activities_required == -1: - activities_required = max(int(activity_count * .8),1) + activities_required = max(int(activity_count * 0.8), 1) except: logger.error("No Activities set for RA %s", question_name) activities_required = None @@ -1568,49 +2059,59 @@ def add__or_update_assignment_question(): # Have to use try/except here instead of request.vars.get in case the points is '', # which doesn't convert to int try: - points = int(request.vars['points']) + points = int(request.vars["points"]) except: points = activity_count - - autograde = request.vars.get('autograde') - which_to_grade = request.vars.get('which_to_grade') + autograde = request.vars.get("autograde") + which_to_grade = request.vars.get("which_to_grade") # Make sure the defaults are set correctly for activecode Qs - if question_type in ('activecode', 'actex'): - if auto_grade != 'unittest': - autograde = 'manual' + if question_type in ("activecode", "actex"): + if auto_grade != "unittest": + autograde = "manual" which_to_grade = "" try: # save the assignment_question db.assignment_questions.update_or_insert( - (db.assignment_questions.assignment_id==assignment_id) & (db.assignment_questions.question_id==question_id), - assignment_id = assignment_id, - question_id = question_id, + (db.assignment_questions.assignment_id == assignment_id) + & (db.assignment_questions.question_id == question_id), + assignment_id=assignment_id, + question_id=question_id, activities_required=activities_required, points=points, autograde=autograde, - which_to_grade = which_to_grade, - reading_assignment = reading_assignment, - sorting_priority = sp + which_to_grade=which_to_grade, + reading_assignment=reading_assignment, + sorting_priority=sp, ) total = _set_assignment_max_points(assignment_id) - return json.dumps(dict( - total = total, - activity_count=activity_count, - activities_required=activities_required, - autograde_possible_values=AUTOGRADE_POSSIBLE_VALUES[question_type], - which_to_grade_possible_values=WHICH_TO_GRADE_POSSIBLE_VALUES[question_type], - status = 'success' - )) + return json.dumps( + dict( + total=total, + activity_count=activity_count, + activities_required=activities_required, + autograde_possible_values=AUTOGRADE_POSSIBLE_VALUES[question_type], + which_to_grade_possible_values=WHICH_TO_GRADE_POSSIBLE_VALUES[ + question_type + ], + status="success", + ) + ) except Exception as ex: logger.error(ex) return json.dumps("Error") + def _get_question_id(question_name, course_id): - question = db((db.questions.name == question_name) & - (db.questions.base_course == db.courses.base_course) & - (db.courses.id == course_id) - ).select(db.questions.id).first() + question = ( + db( + (db.questions.name == question_name) + & (db.questions.base_course == db.courses.base_course) + & (db.courses.id == course_id) + ) + .select(db.questions.id) + .first() + ) if question: return int(question.id) else: @@ -1622,6 +2123,7 @@ def _get_question_id(question_name, course_id): # (db.courses.id == course_id) # ).select(db.questions.id).first().id) + def _get_max_sorting_priority(assignment_id): max = db.assignment_questions.sorting_priority.max() return ( @@ -1630,49 +2132,62 @@ def _get_max_sorting_priority(assignment_id): .first()[max] ) + def _get_question_sorting_priority(assignment_id, question_id): res = ( - db((db.assignment_questions.assignment_id == assignment_id) - & (db.assignment_questions.question_id == question_id) + db( + (db.assignment_questions.assignment_id == assignment_id) + & (db.assignment_questions.question_id == question_id) ) .select(db.assignment_questions.sorting_priority) .first() ) if res is not None: - return res['sorting_priority'] + return res["sorting_priority"] else: return res -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def delete_assignment_question(): ## Deletes one assignment_question try: - question_name = request.vars['name'] - assignment_id = int(request.vars['assignment_id']) + question_name = request.vars["name"] + assignment_id = int(request.vars["assignment_id"]) question_id = _get_question_id(question_name, auth.user.course_id) logger.debug("DELETEING A: %s Q:%s ", assignment_id, question_id) - db((db.assignment_questions.assignment_id == assignment_id) & \ - (db.assignment_questions.question_id == question_id)).delete() + db( + (db.assignment_questions.assignment_id == assignment_id) + & (db.assignment_questions.question_id == question_id) + ).delete() total = _set_assignment_max_points(assignment_id) - return json.dumps({'total': total}) + return json.dumps({"total": total}) except Exception as ex: logger.error(ex) return json.dumps("Error") + def _set_assignment_max_points(assignment_id): """Called after a change to assignment questions. Recalculate the total, save it in the assignment row and return it.""" sum_op = db.assignment_questions.points.sum() - total = db(db.assignment_questions.assignment_id == assignment_id).select(sum_op).first()[sum_op] - db(db.assignments.id == assignment_id).update( - points=total + total = ( + db(db.assignment_questions.assignment_id == assignment_id) + .select(sum_op) + .first()[sum_op] ) + db(db.assignments.id == assignment_id).update(points=total) return total -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def reorder_assignment_questions(): """Called when the questions are reordered in the instructor assignments interface. request.vars must include: @@ -1683,19 +2198,24 @@ def reorder_assignment_questions(): boolean reading_assignment flag set to True, or all that have it set to False). We will reassign sorting_priorities to all of them. """ - question_names = request.vars['names[]'] # a list of question_names - assignment_id = int(request.vars['assignment_id']) + question_names = request.vars["names[]"] # a list of question_names + assignment_id = int(request.vars["assignment_id"]) i = 0 for name in question_names: i += 1 question_id = _get_question_id(name, auth.user.course_id) - db((db.assignment_questions.question_id == question_id) & - (db.assignment_questions.assignment_id == assignment_id)) \ - .update(sorting_priority = i) + db( + (db.assignment_questions.question_id == question_id) + & (db.assignment_questions.assignment_id == assignment_id) + ).update(sorting_priority=i) return json.dumps("Reordered in DB") -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def copy_assignment(): """ vars: @@ -1704,63 +2224,85 @@ def copy_assignment(): """ res = None - if not verifyInstructorStatus(request.vars['course'], auth.user): + if not verifyInstructorStatus(request.vars["course"], auth.user): return "Error: Not Authorized" else: - if request.vars.oldassignment == '-1': - assignments = db((db.assignments.course == db.courses.id) & - (db.courses.course_name == request.vars['course'])).select() + if request.vars.oldassignment == "-1": + assignments = db( + (db.assignments.course == db.courses.id) + & (db.courses.course_name == request.vars["course"]) + ).select() for a in assignments: print("A = {}".format(a)) - res = _copy_one_assignment(request.vars['course'], a.assignments['id']) + res = _copy_one_assignment(request.vars["course"], a.assignments["id"]) if res != "success": break else: - res = _copy_one_assignment(request.vars['course'], request.vars['oldassignment']) + res = _copy_one_assignment( + request.vars["course"], request.vars["oldassignment"] + ) if res is None: return "Error: No Assignments to copy" else: return res + def _copy_one_assignment(course, oldid): - old_course = db(db.courses.course_name == course).select().first() - this_course = db(db.courses.course_name == auth.user.course_name).select().first() - old_assignment = db(db.assignments.id == int(oldid)).select().first() - due_delta = old_assignment.duedate.date() - old_course.term_start_date - due_date = this_course.term_start_date + due_delta - newassign_id = db.assignments.insert(course=auth.user.course_id, name=old_assignment.name, - duedate=due_date, description=old_assignment.description, - points=old_assignment.points, - threshold_pct=old_assignment.threshold_pct) - - old_questions = db(db.assignment_questions.assignment_id == old_assignment.id).select() - for q in old_questions: - dq = q.as_dict() - dq['assignment_id'] = newassign_id - del dq['id'] - db.assignment_questions.insert(**dq) - - return "success" - - -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + old_course = db(db.courses.course_name == course).select().first() + this_course = db(db.courses.course_name == auth.user.course_name).select().first() + old_assignment = db(db.assignments.id == int(oldid)).select().first() + due_delta = old_assignment.duedate.date() - old_course.term_start_date + due_date = this_course.term_start_date + due_delta + newassign_id = db.assignments.insert( + course=auth.user.course_id, + name=old_assignment.name, + duedate=due_date, + description=old_assignment.description, + points=old_assignment.points, + threshold_pct=old_assignment.threshold_pct, + ) + + old_questions = db( + db.assignment_questions.assignment_id == old_assignment.id + ).select() + for q in old_questions: + dq = q.as_dict() + dq["assignment_id"] = newassign_id + del dq["id"] + db.assignment_questions.insert(**dq) + + return "success" + + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def courselog(): thecourse = db(db.courses.id == auth.user.course_id).select().first() course = auth.user.course_name - data = pd.read_sql_query(""" + data = pd.read_sql_query( + """ select sid, useinfo.timestamp, event, act, div_id, chapter, subchapter from useinfo left outer join questions on div_id = name and questions.base_course = '{}' where course_id = '{}' order by useinfo.id - """.format(thecourse.base_course, course), settings.database_uri) - data = data[~data.sid.str.contains('@')] + """.format( + thecourse.base_course, course + ), + settings.database_uri, + ) + data = data[~data.sid.str.contains("@")] - response.headers['Content-Type']='application/vnd.ms-excel' - response.headers['Content-Disposition']= 'attachment; filename=data_for_{}.csv'.format(auth.user.course_name) + response.headers["Content-Type"] = "application/vnd.ms-excel" + response.headers[ + "Content-Disposition" + ] = "attachment; filename=data_for_{}.csv".format(auth.user.course_name) return data.to_csv(na_rep=" ") + def killer(): print(routes_onerror) x = 5 / 0 - return 'ERROR' + return "ERROR" diff --git a/controllers/ajax.py b/controllers/ajax.py index e7d0e0e3f..c007f944a 100644 --- a/controllers/ajax.py +++ b/controllers/ajax.py @@ -15,42 +15,53 @@ logger = logging.getLogger(settings.logger) logger.setLevel(settings.log_level) -response.headers['Access-Control-Allow-Origin'] = '*' - -EVENT_TABLE = {'mChoice':'mchoice_answers', - 'fillb':'fitb_answers', - 'dragNdrop':'dragndrop_answers', - 'clickableArea':'clickablearea_answers', - 'parsons':'parsons_answers', - 'codelens1':'codelens_answers', - 'shortanswer':'shortanswer_answers', - 'fillintheblank': 'fitb_answers', - 'mchoice': 'mchoice_answers', - 'dragndrop': 'dragndrop_answers', - 'clickablearea':'clickablearea_answers', - 'parsonsprob': 'parsons_answers' } +response.headers["Access-Control-Allow-Origin"] = "*" + +EVENT_TABLE = { + "mChoice": "mchoice_answers", + "fillb": "fitb_answers", + "dragNdrop": "dragndrop_answers", + "clickableArea": "clickablearea_answers", + "parsons": "parsons_answers", + "codelens1": "codelens_answers", + "shortanswer": "shortanswer_answers", + "fillintheblank": "fitb_answers", + "mchoice": "mchoice_answers", + "dragndrop": "dragndrop_answers", + "clickablearea": "clickablearea_answers", + "parsonsprob": "parsons_answers", +} def compareAndUpdateCookieData(sid): - if 'ipuser' in request.cookies and request.cookies['ipuser'].value != sid and request.cookies['ipuser'].value.endswith("@"+request.client): - db.useinfo.update_or_insert(db.useinfo.sid == request.cookies['ipuser'].value, sid=sid) + if ( + "ipuser" in request.cookies + and request.cookies["ipuser"].value != sid + and request.cookies["ipuser"].value.endswith("@" + request.client) + ): + db.useinfo.update_or_insert( + db.useinfo.sid == request.cookies["ipuser"].value, sid=sid + ) + def hsblog(): setCookie = False if auth.user: sid = auth.user.username compareAndUpdateCookieData(sid) - setCookie = True # we set our own cookie anyway to eliminate many of the extraneous anonymous - # log entries that come from auth timing out even but the user hasn't reloaded - # the page. + setCookie = ( + True + ) # we set our own cookie anyway to eliminate many of the extraneous anonymous + # log entries that come from auth timing out even but the user hasn't reloaded + # the page. else: - if 'ipuser' in request.cookies: - sid = request.cookies['ipuser'].value + if "ipuser" in request.cookies: + sid = request.cookies["ipuser"].value setCookie = True else: - sid = str(uuid.uuid1().int)+"@"+request.client + sid = str(uuid.uuid1().int) + "@" + request.client setCookie = True - act = request.vars.get('act', '') + act = request.vars.get("act", "") div_id = request.vars.div_id event = request.vars.event course = request.vars.course @@ -62,40 +73,76 @@ def hsblog(): tt = 0 try: - db.useinfo.insert(sid=sid,act=act[0:512],div_id=div_id,event=event,timestamp=ts,course_id=course) + db.useinfo.insert( + sid=sid, + act=act[0:512], + div_id=div_id, + event=event, + timestamp=ts, + course_id=course, + ) except: - logger.debug('failed to insert log record for {} in {} : {} {} {}'.format(sid, course, div_id, event, act)) + logger.debug( + "failed to insert log record for {} in {} : {} {} {}".format( + sid, course, div_id, event, act + ) + ) - if event == 'timedExam' and (act == 'finish' or act == 'reset'): + if event == "timedExam" and (act == "finish" or act == "reset"): logger.debug(act) - if act == 'reset': - r = 'T' + if act == "reset": + r = "T" else: r = None try: - db.timed_exam.insert(sid=sid, course_name=course, correct=int(request.vars.correct), - incorrect=int(request.vars.incorrect), skipped=int(request.vars.skipped), - time_taken=int(tt), timestamp=ts, - div_id=div_id,reset=r) + db.timed_exam.insert( + sid=sid, + course_name=course, + correct=int(request.vars.correct), + incorrect=int(request.vars.incorrect), + skipped=int(request.vars.skipped), + time_taken=int(tt), + timestamp=ts, + div_id=div_id, + reset=r, + ) except Exception as e: - logger.debug('failed to insert a timed exam record for {} in {} : {}'.format(sid, course, div_id)) - logger.debug('correct {} incorrect {} skipped {} time {}'.format(request.vars.correct, request.vars.incorrect, request.vars.skipped, request.vars.time)) - logger.debug('Error: {}'.format(e.message)) + logger.debug( + "failed to insert a timed exam record for {} in {} : {}".format( + sid, course, div_id + ) + ) + logger.debug( + "correct {} incorrect {} skipped {} time {}".format( + request.vars.correct, + request.vars.incorrect, + request.vars.skipped, + request.vars.time, + ) + ) + logger.debug("Error: {}".format(e.message)) # Produce a default result. res = dict(log=True, timestamp=str(ts)) # Process this event. - if event == 'mChoice' and auth.user: + if event == "mChoice" and auth.user: # # has user already submitted a correct answer for this question? # if db((db.mchoice_answers.sid == sid) & # (db.mchoice_answers.div_id == div_id) & # (db.mchoice_answers.course_name == auth.user.course_name) & # (db.mchoice_answers.correct == 'T')).count() == 0: - answer = request.vars.answer - correct = request.vars.correct - db.mchoice_answers.insert(sid=sid,timestamp=ts, div_id=div_id, answer=answer, correct=correct, course_name=course) + answer = request.vars.answer + correct = request.vars.correct + db.mchoice_answers.insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=answer, + correct=correct, + course_name=course, + ) elif event == "fillb" and auth.user: answer_json = request.vars.answer correct = request.vars.correct @@ -106,60 +153,106 @@ def hsblog(): res.update(res_update) # Save this data. - db.fitb_answers.insert(sid=sid, timestamp=ts, div_id=div_id, answer=answer_json, correct=correct, course_name=course) + db.fitb_answers.insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=answer_json, + correct=correct, + course_name=course, + ) elif event == "dragNdrop" and auth.user: # if db((db.dragndrop_answers.sid == sid) & # (db.dragndrop_answers.div_id == div_id) & # (db.dragndrop_answers.course_name == auth.user.course_name) & # (db.dragndrop_answers.correct == 'T')).count() == 0: - answers = request.vars.answer - minHeight = request.vars.minHeight - correct = request.vars.correct + answers = request.vars.answer + minHeight = request.vars.minHeight + correct = request.vars.correct - db.dragndrop_answers.insert(sid=sid, timestamp=ts, div_id=div_id, answer=answers, correct=correct, course_name=course, minHeight=minHeight) + db.dragndrop_answers.insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=answers, + correct=correct, + course_name=course, + minHeight=minHeight, + ) elif event == "clickableArea" and auth.user: # if db((db.clickablearea_answers.sid == sid) & # (db.clickablearea_answers.div_id == div_id) & # (db.clickablearea_answers.course_name == auth.user.course_name) & # (db.clickablearea_answers.correct == 'T')).count() == 0: - correct = request.vars.correct - db.clickablearea_answers.insert(sid=sid, timestamp=ts, div_id=div_id, answer=act, correct=correct, course_name=course) + correct = request.vars.correct + db.clickablearea_answers.insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=act, + correct=correct, + course_name=course, + ) elif event == "parsons" and auth.user: # if db((db.parsons_answers.sid == sid) & # (db.parsons_answers.div_id == div_id) & # (db.parsons_answers.course_name == auth.user.course_name) & # (db.parsons_answers.correct == 'T')).count() == 0: - correct = request.vars.correct - answer = request.vars.answer - source = request.vars.source - db.parsons_answers.insert(sid=sid, timestamp=ts, div_id=div_id, answer=answer, source=source, correct=correct, course_name=course) + correct = request.vars.correct + answer = request.vars.answer + source = request.vars.source + db.parsons_answers.insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=answer, + source=source, + correct=correct, + course_name=course, + ) elif event == "codelensq" and auth.user: # if db((db.codelens_answers.sid == sid) & # (db.codelens_answers.div_id == div_id) & # (db.codelens_answers.course_name == auth.user.course_name) & # (db.codelens_answers.correct == 'T')).count() == 0: - correct = request.vars.correct - answer = request.vars.answer - source = request.vars.source - db.codelens_answers.insert(sid=sid, timestamp=ts, div_id=div_id, answer=answer, source=source, correct=correct, course_name=course) + correct = request.vars.correct + answer = request.vars.answer + source = request.vars.source + db.codelens_answers.insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=answer, + source=source, + correct=correct, + course_name=course, + ) elif event == "shortanswer" and auth.user: # for shortanswers just keep the latest?? -- the history will be in useinfo - db.shortanswer_answers.update_or_insert((db.shortanswer_answers.sid == sid) & (db.shortanswer_answers.div_id == div_id) & (db.shortanswer_answers.course_name == course), - sid=sid, answer=act, div_id=div_id, timestamp=ts, course_name=course) + db.shortanswer_answers.update_or_insert( + (db.shortanswer_answers.sid == sid) + & (db.shortanswer_answers.div_id == div_id) + & (db.shortanswer_answers.course_name == course), + sid=sid, + answer=act, + div_id=div_id, + timestamp=ts, + course_name=course, + ) elif event == "lp_build" and auth.user: - ret, new_fields = db.lp_answers._validate_fields(dict( - sid=sid, timestamp=ts, div_id=div_id, course_name=course - )) + ret, new_fields = db.lp_answers._validate_fields( + dict(sid=sid, timestamp=ts, div_id=div_id, course_name=course) + ) if not ret.errors: do_server_feedback, feedback = is_server_feedback(div_id, course) if do_server_feedback: try: - code_snippets = json.loads(request.vars.answer)['code_snippets'] + code_snippets = json.loads(request.vars.answer)["code_snippets"] except: code_snippets = [] result = lp_feedback(code_snippets, feedback) @@ -167,37 +260,44 @@ def hsblog(): res.update(result) # Record the results in the database. - correct = result.get('correct') - answer = result.get('answer', {}) - answer['code_snippets'] = code_snippets - ret = db.lp_answers.validate_and_insert(sid=sid, timestamp=ts, div_id=div_id, - answer=json.dumps(answer), correct=correct, course_name=course) + correct = result.get("correct") + answer = result.get("answer", {}) + answer["code_snippets"] = code_snippets + ret = db.lp_answers.validate_and_insert( + sid=sid, + timestamp=ts, + div_id=div_id, + answer=json.dumps(answer), + correct=correct, + course_name=course, + ) if ret.errors: - res.setdefault('errors', []).append(ret.errors.as_dict()) + res.setdefault("errors", []).append(ret.errors.as_dict()) else: - res['errors'] = ['No feedback provided.'] + res["errors"] = ["No feedback provided."] else: - res.setdefault('errors', []).append(ret.errors.as_dict()) + res.setdefault("errors", []).append(ret.errors.as_dict()) - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" if setCookie: - response.cookies['ipuser'] = sid - response.cookies['ipuser']['expires'] = 24*3600*90 - response.cookies['ipuser']['path'] = '/' + response.cookies["ipuser"] = sid + response.cookies["ipuser"]["expires"] = 24 * 3600 * 90 + response.cookies["ipuser"]["path"] = "/" return json.dumps(res) -def runlog(): # Log errors and runs with code + +def runlog(): # Log errors and runs with code # response.headers['content-type'] = 'application/json' setCookie = False if auth.user: sid = auth.user.username setCookie = True else: - if 'ipuser' in request.cookies: - sid = request.cookies['ipuser'].value + if "ipuser" in request.cookies: + sid = request.cookies["ipuser"].value setCookie = True else: - sid = str(uuid.uuid1().int)+"@"+request.client + sid = str(uuid.uuid1().int) + "@" + request.client setCookie = True div_id = request.vars.div_id course = request.vars.course @@ -206,23 +306,34 @@ def runlog(): # Log errors and runs with code error_info = request.vars.errinfo pre = request.vars.prefix if request.vars.prefix else "" post = request.vars.suffix if request.vars.suffix else "" - if error_info != 'success': - event = 'ac_error' + if error_info != "success": + event = "ac_error" act = str(error_info)[:512] else: - act = 'run' + act = "run" if request.vars.event: event = request.vars.event else: - event = 'activecode' + event = "activecode" num_tries = 3 done = False while num_tries > 0 and not done: try: - db.useinfo.insert(sid=sid, act=act, div_id=div_id, event=event, timestamp=ts, course_id=course) + db.useinfo.insert( + sid=sid, + act=act, + div_id=div_id, + event=event, + timestamp=ts, + course_id=course, + ) done = True except Exception as e: - logger.error("probable Too Long problem trying to insert sid={} act={} div_id={} event={} timestamp={} course_id={} exception={}".format(sid, act, div_id, event, ts, course, e)) + logger.error( + "probable Too Long problem trying to insert sid={} act={} div_id={} event={} timestamp={} course_id={} exception={}".format( + sid, act, div_id, event, ts, course, e + ) + ) num_tries -= 1 if num_tries == 0: raise Exception("Runlog Failed to insert into useinfo") @@ -231,12 +342,14 @@ def runlog(): # Log errors and runs with code done = False while num_tries > 0 and not done: try: - dbid = db.acerror_log.insert(sid=sid, - div_id=div_id, - timestamp=ts, - course_id=course, - code=pre+code+post, - emessage=error_info) + dbid = db.acerror_log.insert( + sid=sid, + div_id=div_id, + timestamp=ts, + course_id=course, + code=pre + code + post, + emessage=error_info, + ) done = True except: logger.error("INSERT into acerror_log FAILED retrying") @@ -244,33 +357,43 @@ def runlog(): # Log errors and runs with code if num_tries == 0: raise Exception("Runlog Failed to insert into acerror_log") - #lintAfterSave(dbid, code, div_id, sid) + # lintAfterSave(dbid, code, div_id, sid) if auth.user: - if 'to_save' in request.vars and (request.vars.to_save == "True" or request.vars.to_save == "true"): + if "to_save" in request.vars and ( + request.vars.to_save == "True" or request.vars.to_save == "true" + ): num_tries = 3 done = False dbcourse = db(db.courses.course_name == course).select().first() while num_tries > 0 and not done: try: - db.code.insert(sid=sid, + db.code.insert( + sid=sid, acid=div_id, code=code, emessage=error_info, timestamp=ts, course_id=dbcourse, - language=request.vars.lang) + language=request.vars.lang, + ) if request.vars.partner: if _same_class(sid, request.vars.partner): - newcode = "# This code was shared by {}\n\n".format(sid) + code - db.code.insert(sid=request.vars.partner, + newcode = ( + "# This code was shared by {}\n\n".format(sid) + code + ) + db.code.insert( + sid=request.vars.partner, acid=div_id, code=newcode, emessage=error_info, timestamp=ts, course_id=dbcourse, - language=request.vars.lang) + language=request.vars.lang, + ) else: - res = {'message': 'You must be enrolled in the same class as your partner'} + res = { + "message": "You must be enrolled in the same class as your partner" + } return json.dumps(res) done = True except: @@ -279,13 +402,14 @@ def runlog(): # Log errors and runs with code if num_tries == 0: raise Exception("Runlog Failed to insert into code") - res = {'log':True} + res = {"log": True} if setCookie: - response.cookies['ipuser'] = sid - response.cookies['ipuser']['expires'] = 24*3600*90 - response.cookies['ipuser']['path'] = '/' + response.cookies["ipuser"] = sid + response.cookies["ipuser"]["expires"] = 24 * 3600 * 90 + response.cookies["ipuser"]["path"] = "/" return json.dumps(res) + # Ajax Handlers for saving and restoring active code blocks @@ -318,15 +442,20 @@ def gethist(): res = {} if sid: - query = ((codetbl.sid == sid) & (codetbl.acid == acid) & (codetbl.course_id == course_id) & (codetbl.timestamp != None)) - res['acid'] = acid - res['sid'] = sid + query = ( + (codetbl.sid == sid) + & (codetbl.acid == acid) + & (codetbl.course_id == course_id) + & (codetbl.timestamp != None) + ) + res["acid"] = acid + res["sid"] = sid # get the code they saved in chronological order; id order gets that for us r = db(query).select(orderby=codetbl.id) - res['history'] = [row.code for row in r] - res['timestamps'] = [row.timestamp.isoformat() for row in r] + res["history"] = [row.code for row in r] + res["timestamps"] = [row.timestamp.isoformat() for row in r] - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" return json.dumps(res) @@ -344,63 +473,76 @@ def getprog(): sid = request.vars.sid if sid: - query = ((codetbl.sid == sid) & (codetbl.acid == acid) & (codetbl.timestamp != None)) + query = ( + (codetbl.sid == sid) & (codetbl.acid == acid) & (codetbl.timestamp != None) + ) else: if auth.user: - query = ((codetbl.sid == auth.user.username) & (codetbl.acid == acid) & (codetbl.timestamp != None)) + query = ( + (codetbl.sid == auth.user.username) + & (codetbl.acid == acid) + & (codetbl.timestamp != None) + ) else: query = None res = {} if query: result = db(query) - res['acid'] = acid + res["acid"] = acid if not result.isempty(): # get the last code they saved; id order gets that for us r = result.select(orderby=codetbl.id).last().code - res['source'] = r + res["source"] = r if sid: - res['sid'] = sid + res["sid"] = sid else: - logger.debug("Did not find anything to load for %s"%sid) - response.headers['content-type'] = 'application/json' + logger.debug("Did not find anything to load for %s" % sid) + response.headers["content-type"] = "application/json" return json.dumps([res]) - -#@auth.requires_login() +# @auth.requires_login() # This function is deprecated as of June 2019 # We need to keep it in place as long as we continue to serve books # from runestone/static/ When that period is over we can eliminate def getuser(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" if auth.user: try: # return the list of courses that auth.user is registered for to keep them from # accidentally wandering into courses they are not registered for. - cres = db( (db.user_courses.user_id == auth.user.id) & - (db.user_courses.course_id == db.courses.id)).select(db.courses.course_name) + cres = db( + (db.user_courses.user_id == auth.user.id) + & (db.user_courses.course_id == db.courses.id) + ).select(db.courses.course_name) clist = [] for row in cres: clist.append(row.course_name) - res = {'email': auth.user.email, - 'nick': auth.user.username, - 'donated': auth.user.donated, - 'isInstructor': verifyInstructorStatus(auth.user.course_name, auth.user.id), - 'course_list': clist - } + res = { + "email": auth.user.email, + "nick": auth.user.username, + "donated": auth.user.donated, + "isInstructor": verifyInstructorStatus( + auth.user.course_name, auth.user.id + ), + "course_list": clist, + } session.timezoneoffset = request.vars.timezoneoffset - logger.debug("setting timezone offset in session %s hours" % session.timezoneoffset) + logger.debug( + "setting timezone offset in session %s hours" % session.timezoneoffset + ) except: res = dict(redirect=auth.settings.login_url) # ?_next=.... else: - res = dict(redirect=auth.settings.login_url) #?_next=.... + res = dict(redirect=auth.settings.login_url) # ?_next=.... if session.readings: - res['readings'] = session.readings + res["readings"] = session.readings logger.debug("returning login info: %s" % res) return json.dumps([res]) + def set_tz_offset(): session.timezoneoffset = request.vars.timezoneoffset logger.debug("setting timezone offset in session %s hours" % session.timezoneoffset) @@ -408,7 +550,7 @@ def set_tz_offset(): def getnumonline(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" try: query = """select count(distinct sid) from useinfo where timestamp > current_timestamp - interval '5 minutes' """ @@ -416,15 +558,15 @@ def getnumonline(): except: rows = [[21]] - res = {'online':rows[0][0]} + res = {"online": rows[0][0]} return json.dumps([res]) def getnumusers(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" query = """select count(*) from (select distinct(sid) from useinfo) as X """ - numusers = 'more than 850,000' + numusers = "more than 850,000" # try: # numusers = cache.disk('numusers', lambda: db.executesql(query)[0][0], time_expire=21600) @@ -432,19 +574,26 @@ def getnumusers(): # # sometimes the DB query takes too long and is timed out - return something anyway # numusers = 'more than 250,000' - res = {'numusers':numusers} + res = {"numusers": numusers} return json.dumps([res]) # I was not sure if it's okay to import it from `assignmnets.py`. # Only questions that are marked for practice are eligible for the spaced practice. def _get_qualified_questions(base_course, chapter_label, sub_chapter_label): - return db((db.questions.base_course == base_course) & - ((db.questions.topic == "{}/{}".format(chapter_label, sub_chapter_label)) | - ((db.questions.chapter == chapter_label) & - (db.questions.topic == None) & - (db.questions.subchapter == sub_chapter_label))) & - (db.questions.practice == True)).select() + return db( + (db.questions.base_course == base_course) + & ( + (db.questions.topic == "{}/{}".format(chapter_label, sub_chapter_label)) + | ( + (db.questions.chapter == chapter_label) + & (db.questions.topic == None) + & (db.questions.subchapter == sub_chapter_label) + ) + ) + & (db.questions.practice == True) + ).select() + # # Ajax Handlers to update and retrieve the last position of the user in the course @@ -453,7 +602,7 @@ def updatelastpage(): lastPageUrl = request.vars.lastPageUrl lastPageScrollLocation = request.vars.lastPageScrollLocation if lastPageUrl is None: - return # todo: log request.vars, request.args and request.env.path_info + return # todo: log request.vars, request.args and request.env.path_info course = request.vars.course completionFlag = request.vars.completionFlag lastPageChapter = lastPageUrl.split("/")[-2] @@ -463,13 +612,16 @@ def updatelastpage(): num_tries = 3 while not done and num_tries > 0: try: - db((db.user_state.user_id == auth.user.id) & - (db.user_state.course_id == course)).update( - last_page_url=lastPageUrl, - last_page_chapter=lastPageChapter, - last_page_subchapter=lastPageSubchapter, - last_page_scroll_location=lastPageScrollLocation, - last_page_accessed_on=datetime.datetime.utcnow()) + db( + (db.user_state.user_id == auth.user.id) + & (db.user_state.course_id == course) + ).update( + last_page_url=lastPageUrl, + last_page_chapter=lastPageChapter, + last_page_subchapter=lastPageSubchapter, + last_page_scroll_location=lastPageScrollLocation, + last_page_accessed_on=datetime.datetime.utcnow(), + ) done = True except: num_tries -= 1 @@ -480,11 +632,14 @@ def updatelastpage(): num_tries = 3 while not done and num_tries > 0: try: - db((db.user_sub_chapter_progress.user_id == auth.user.id) & - (db.user_sub_chapter_progress.chapter_id == lastPageChapter) & - (db.user_sub_chapter_progress.sub_chapter_id == lastPageSubchapter)).update( - status=completionFlag, - end_date=datetime.datetime.utcnow()) + db( + (db.user_sub_chapter_progress.user_id == auth.user.id) + & (db.user_sub_chapter_progress.chapter_id == lastPageChapter) + & ( + db.user_sub_chapter_progress.sub_chapter_id + == lastPageSubchapter + ) + ).update(status=completionFlag, end_date=datetime.datetime.utcnow()) done = True except: num_tries -= 1 @@ -492,26 +647,33 @@ def updatelastpage(): raise Exception("Failed to save sub chapter progress in update_last_page") practice_settings = db(db.course_practice.course_name == auth.user.course_name) - if (practice_settings.count() != 0 and - practice_settings.select().first().flashcard_creation_method == 0): + if ( + practice_settings.count() != 0 + and practice_settings.select().first().flashcard_creation_method == 0 + ): # Since each authenticated user has only one active course, we retrieve the course this way. course = db(db.courses.id == auth.user.course_id).select().first() # We only retrieve questions to be used in flashcards if they are marked for practice purpose. - questions = _get_qualified_questions(course.base_course, - lastPageChapter, - lastPageSubchapter) + questions = _get_qualified_questions( + course.base_course, lastPageChapter, lastPageSubchapter + ) if len(questions) > 0: now = datetime.datetime.utcnow() - now_local = now - datetime.timedelta(hours=float(session.timezoneoffset) if 'timezoneoffset' in session else 0) - existing_flashcards = db((db.user_topic_practice.user_id == auth.user.id) & - (db.user_topic_practice.course_name == auth.user.course_name) & - (db.user_topic_practice.chapter_label == lastPageChapter) & - (db.user_topic_practice.sub_chapter_label == lastPageSubchapter) & - (db.user_topic_practice.question_name == questions[0].name) - ) + now_local = now - datetime.timedelta( + hours=float(session.timezoneoffset) + if "timezoneoffset" in session + else 0 + ) + existing_flashcards = db( + (db.user_topic_practice.user_id == auth.user.id) + & (db.user_topic_practice.course_name == auth.user.course_name) + & (db.user_topic_practice.chapter_label == lastPageChapter) + & (db.user_topic_practice.sub_chapter_label == lastPageSubchapter) + & (db.user_topic_practice.question_name == questions[0].name) + ) # There is at least one qualified question in this subchapter, so insert a flashcard for the subchapter. - if completionFlag == '1' and existing_flashcards.isempty(): + if completionFlag == "1" and existing_flashcards.isempty(): db.user_topic_practice.insert( user_id=auth.user.id, course_name=auth.user.course_name, @@ -526,9 +688,11 @@ def updatelastpage(): last_presented=now - datetime.timedelta(1), last_completed=now - datetime.timedelta(1), creation_time=now, - timezoneoffset=float(session.timezoneoffset) if 'timezoneoffset' in session else 0 + timezoneoffset=float(session.timezoneoffset) + if "timezoneoffset" in session + else 0, ) - if completionFlag == '0' and not existing_flashcards.isempty(): + if completionFlag == "0" and not existing_flashcards.isempty(): existing_flashcards.delete() @@ -537,15 +701,17 @@ def getCompletionStatus(): lastPageUrl = request.vars.lastPageUrl lastPageChapter = lastPageUrl.split("/")[-2] lastPageSubchapter = ".".join(lastPageUrl.split("/")[-1].split(".")[:-1]) - result = db((db.user_sub_chapter_progress.user_id == auth.user.id) & - (db.user_sub_chapter_progress.chapter_id == lastPageChapter) & - (db.user_sub_chapter_progress.sub_chapter_id == lastPageSubchapter)).select(db.user_sub_chapter_progress.status) + result = db( + (db.user_sub_chapter_progress.user_id == auth.user.id) + & (db.user_sub_chapter_progress.chapter_id == lastPageChapter) + & (db.user_sub_chapter_progress.sub_chapter_id == lastPageSubchapter) + ).select(db.user_sub_chapter_progress.status) rowarray_list = [] if result: for row in result: - res = {'completionStatus': row.status} + res = {"completionStatus": row.status} rowarray_list.append(res) - #question: since the javascript in user-highlights.js is going to look only at the first row, shouldn't + # question: since the javascript in user-highlights.js is going to look only at the first row, shouldn't # we be returning just the *last* status? Or is there no history of status kept anyway? return json.dumps(rowarray_list) else: @@ -553,32 +719,47 @@ def getCompletionStatus(): # make the insertions into the DB as necessary # we know the subchapter doesn't exist - db.user_sub_chapter_progress.insert(user_id=auth.user.id, - chapter_id = lastPageChapter, - sub_chapter_id = lastPageSubchapter, - status = -1, start_date=datetime.datetime.utcnow()) + db.user_sub_chapter_progress.insert( + user_id=auth.user.id, + chapter_id=lastPageChapter, + sub_chapter_id=lastPageSubchapter, + status=-1, + start_date=datetime.datetime.utcnow(), + ) # the chapter might exist without the subchapter - result = db((db.user_chapter_progress.user_id == auth.user.id) & (db.user_chapter_progress.chapter_id == lastPageChapter)).select() + result = db( + (db.user_chapter_progress.user_id == auth.user.id) + & (db.user_chapter_progress.chapter_id == lastPageChapter) + ).select() if not result: - db.user_chapter_progress.insert(user_id = auth.user.id, - chapter_id = lastPageChapter, - status = -1) - return json.dumps([{'completionStatus': -1}]) + db.user_chapter_progress.insert( + user_id=auth.user.id, chapter_id=lastPageChapter, status=-1 + ) + return json.dumps([{"completionStatus": -1}]) + def getAllCompletionStatus(): if auth.user: - result = db((db.user_sub_chapter_progress.user_id == auth.user.id)).select(db.user_sub_chapter_progress.chapter_id, db.user_sub_chapter_progress.sub_chapter_id, db.user_sub_chapter_progress.status, db.user_sub_chapter_progress.status, db.user_sub_chapter_progress.end_date) + result = db((db.user_sub_chapter_progress.user_id == auth.user.id)).select( + db.user_sub_chapter_progress.chapter_id, + db.user_sub_chapter_progress.sub_chapter_id, + db.user_sub_chapter_progress.status, + db.user_sub_chapter_progress.status, + db.user_sub_chapter_progress.end_date, + ) rowarray_list = [] if result: for row in result: if row.end_date == None: endDate = 0 else: - endDate = row.end_date.strftime('%d %b, %Y') - res = {'chapterName': row.chapter_id, - 'subChapterName': row.sub_chapter_id, - 'completionStatus': row.status, - 'endDate': endDate} + endDate = row.end_date.strftime("%d %b, %Y") + res = { + "chapterName": row.chapter_id, + "subChapterName": row.sub_chapter_id, + "completionStatus": row.status, + "endDate": endDate, + } rowarray_list.append(res) return json.dumps(rowarray_list) @@ -588,31 +769,37 @@ def getlastpage(): course = request.vars.course course = db(db.courses.course_name == course).select().first() - result = db((db.user_state.user_id == auth.user.id) & - (db.user_state.course_id == course.course_name) & - (db.chapters.course_id == course.base_course) & - (db.user_state.last_page_chapter == db.chapters.chapter_label) & - (db.sub_chapters.chapter_id == db.chapters.id) & - (db.user_state.last_page_subchapter == db.sub_chapters.sub_chapter_label) - ).select(db.user_state.last_page_url, db.user_state.last_page_hash, - db.chapters.chapter_name, - db.user_state.last_page_scroll_location, - db.sub_chapters.sub_chapter_name) + result = db( + (db.user_state.user_id == auth.user.id) + & (db.user_state.course_id == course.course_name) + & (db.chapters.course_id == course.base_course) + & (db.user_state.last_page_chapter == db.chapters.chapter_label) + & (db.sub_chapters.chapter_id == db.chapters.id) + & (db.user_state.last_page_subchapter == db.sub_chapters.sub_chapter_label) + ).select( + db.user_state.last_page_url, + db.user_state.last_page_hash, + db.chapters.chapter_name, + db.user_state.last_page_scroll_location, + db.sub_chapters.sub_chapter_name, + ) rowarray_list = [] if result: for row in result: - res = {'lastPageUrl': row.user_state.last_page_url, - 'lastPageHash': row.user_state.last_page_hash, - 'lastPageChapter': row.chapters.chapter_name, - 'lastPageSubchapter': row.sub_chapters.sub_chapter_name, - 'lastPageScrollLocation': row.user_state.last_page_scroll_location} + res = { + "lastPageUrl": row.user_state.last_page_url, + "lastPageHash": row.user_state.last_page_hash, + "lastPageChapter": row.chapters.chapter_name, + "lastPageSubchapter": row.sub_chapters.sub_chapter_name, + "lastPageScrollLocation": row.user_state.last_page_scroll_location, + } rowarray_list.append(res) return json.dumps(rowarray_list) else: db.user_state.insert(user_id=auth.user.id, course_id=course.course_name) -def _getCorrectStats(miscdata,event): +def _getCorrectStats(miscdata, event): # TODO: update this to use the xxx_answer table # select and count grouping by the correct column # this version can suffer from division by zero error @@ -622,15 +809,17 @@ def _getCorrectStats(miscdata,event): if auth.user: sid = auth.user.username else: - if 'ipuser' in request.cookies: - sid = request.cookies['ipuser'].value + if "ipuser" in request.cookies: + sid = request.cookies["ipuser"].value if sid: - course = db(db.courses.course_name == miscdata['course']).select().first() + course = db(db.courses.course_name == miscdata["course"]).select().first() tbl = db[dbtable] count_expr = tbl.correct.count() - rows = db((tbl.sid == sid) & (tbl.timestamp > course.term_start_date)).select(tbl.correct, count_expr, groupby=tbl.correct) + rows = db((tbl.sid == sid) & (tbl.timestamp > course.term_start_date)).select( + tbl.correct, count_expr, groupby=tbl.correct + ) total = 0 correct = 0 for row in rows: @@ -641,11 +830,11 @@ def _getCorrectStats(miscdata,event): if total > 0: pctcorr = round(float(correct) / total * 100) else: - pctcorr = 'unavailable' + pctcorr = "unavailable" else: - pctcorr = 'unavailable' + pctcorr = "unavailable" - miscdata['yourpct'] = pctcorr + miscdata["yourpct"] = pctcorr def _getStudentResults(question): @@ -654,13 +843,22 @@ def _getStudentResults(question): """ cc = db(db.courses.id == auth.user.course_id).select().first() course = cc.course_name - qst = db((db.questions.name == question) & (db.questions.base_course == cc.base_course )).select().first() + qst = ( + db( + (db.questions.name == question) + & (db.questions.base_course == cc.base_course) + ) + .select() + .first() + ) tbl_name = EVENT_TABLE[qst.question_type] tbl = db[tbl_name] - res = db( (tbl.div_id == question) & - (tbl.course_name == cc.course_name) & - (tbl.timestamp >= cc.term_start_date)).select(tbl.sid, tbl.answer, orderby=tbl.sid) + res = db( + (tbl.div_id == question) + & (tbl.course_name == cc.course_name) + & (tbl.timestamp >= cc.term_start_date) + ).select(tbl.sid, tbl.answer, orderby=tbl.sid) resultList = [] if len(res) > 0: @@ -691,25 +889,33 @@ def getaggregateresults(): course = request.vars.course question = request.vars.div_id # select act, count(*) from useinfo where div_id = 'question4_2_1' group by act; - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" if not auth.user: - return json.dumps([dict(answerDict={}, misc={}, emess='You must be logged in')]) + return json.dumps([dict(answerDict={}, misc={}, emess="You must be logged in")]) - is_instructor = verifyInstructorStatus(course,auth.user.id) + is_instructor = verifyInstructorStatus(course, auth.user.id) # Yes, these two things could be done as a join. but this **may** be better for performance - if course == 'thinkcspy' or course == 'pythonds': + if course == "thinkcspy" or course == "pythonds": start_date = datetime.datetime.utcnow() - datetime.timedelta(days=90) else: - start_date = db(db.courses.course_name == course).select(db.courses.term_start_date).first().term_start_date + start_date = ( + db(db.courses.course_name == course) + .select(db.courses.term_start_date) + .first() + .term_start_date + ) count = db.useinfo.id.count() try: - result = db((db.useinfo.div_id == question) & - (db.useinfo.course_id == course) & - (db.useinfo.timestamp >= start_date) - ).select(db.useinfo.act, count, groupby=db.useinfo.act) + result = db( + (db.useinfo.div_id == question) + & (db.useinfo.course_id == course) + & (db.useinfo.timestamp >= start_date) + ).select(db.useinfo.act, count, groupby=db.useinfo.act) except: - return json.dumps([dict(answerDict={}, misc={}, emess='Sorry, the request timed out')]) + return json.dumps( + [dict(answerDict={}, misc={}, emess="Sorry, the request timed out")] + ) tdata = {} tot = 0 @@ -723,10 +929,10 @@ def getaggregateresults(): correct = "" if tot > 0: for key in tdata: - l = key.split(':') + l = key.split(":") try: answer = l[1] - if 'correct' in key: + if "correct" in key: correct = answer count = int(tdata[key]) if answer in rdata: @@ -736,18 +942,18 @@ def getaggregateresults(): if answer != "undefined" and answer != "": rdata[answer] = pct except: - logger.debug("Bad data for %s data is %s " % (question,key)) + logger.debug("Bad data for %s data is %s " % (question, key)) - miscdata['correct'] = correct - miscdata['course'] = course + miscdata["correct"] = correct + miscdata["course"] = course - _getCorrectStats(miscdata, 'mChoice') + _getCorrectStats(miscdata, "mChoice") returnDict = dict(answerDict=rdata, misc=miscdata) if auth.user and is_instructor: resultList = _getStudentResults(question) - returnDict['reslist'] = resultList + returnDict["reslist"] = resultList return json.dumps([returnDict]) @@ -756,17 +962,17 @@ def getpollresults(): course = request.vars.course div_id = request.vars.div_id - response.headers['content-type'] = 'application/json' - + response.headers["content-type"] = "application/json" - query = '''select act from useinfo + query = """select act from useinfo join (select sid, max(id) mid from useinfo where event='poll' and div_id = '{}' and course_id = '{}' group by sid) as T - on id = T.mid'''.format(div_id, course) + on id = T.mid""".format( + div_id, course + ) rows = db.executesql(query) - result_list = [] for row in rows: val = row[0].split(":")[0] @@ -788,9 +994,15 @@ def getpollresults(): user_res = None if auth.user: - user_res = db((db.useinfo.sid == auth.user.username) & - (db.useinfo.course_id == course) & - (db.useinfo.div_id == div_id)).select(db.useinfo.act, orderby=~db.useinfo.id).first() + user_res = ( + db( + (db.useinfo.sid == auth.user.username) + & (db.useinfo.course_id == course) + & (db.useinfo.div_id == div_id) + ) + .select(db.useinfo.act, orderby=~db.useinfo.id) + .first() + ) if user_res: my_vote = user_res.act @@ -803,73 +1015,95 @@ def getpollresults(): def gettop10Answers(): course = request.vars.course question = request.vars.div_id - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" rows = [] try: dbcourse = db(db.courses.course_name == course).select().first() count_expr = db.fitb_answers.answer.count() - rows = db((db.fitb_answers.div_id == question) & - (db.fitb_answers.course_name == course) & - (db.fitb_answers.timestamp > dbcourse.term_start_date)).select(db.fitb_answers.answer, count_expr, - groupby=db.fitb_answers.answer, orderby=~count_expr, limitby=(0, 10)) - res = [{'answer':clean(row.fitb_answers.answer), 'count':row[count_expr]} for row in rows] + rows = db( + (db.fitb_answers.div_id == question) + & (db.fitb_answers.course_name == course) + & (db.fitb_answers.timestamp > dbcourse.term_start_date) + ).select( + db.fitb_answers.answer, + count_expr, + groupby=db.fitb_answers.answer, + orderby=~count_expr, + limitby=(0, 10), + ) + res = [ + {"answer": clean(row.fitb_answers.answer), "count": row[count_expr]} + for row in rows + ] except Exception as e: logger.debug(e) - res = 'error in query' + res = "error in query" - miscdata = {'course': course} - _getCorrectStats(miscdata,'fillb') # TODO: rewrite _getCorrectStats to use xxx_answers + miscdata = {"course": course} + _getCorrectStats( + miscdata, "fillb" + ) # TODO: rewrite _getCorrectStats to use xxx_answers if auth.user and verifyInstructorStatus(course, auth.user.id): resultList = _getStudentResults(question) - miscdata['reslist'] = resultList + miscdata["reslist"] = resultList - return json.dumps([res,miscdata]) + return json.dumps([res, miscdata]) def getassignmentgrade(): - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" if not auth.user: return json.dumps([dict(message="not logged in")]) divid = request.vars.div_id ret = { - 'grade':"Not graded yet", - 'comment': "No Comments", - 'avg': 'None', - 'count': 'None', + "grade": "Not graded yet", + "comment": "No Comments", + "avg": "None", + "count": "None", } # check that the assignment is released # - a_q = db( - (db.assignments.released == True) & - (db.assignments.course == auth.user.course_id) & - (db.assignment_questions.assignment_id == db.assignments.id) & - (db.assignment_questions.question_id == db.questions.id) & - (db.questions.name == divid) - ).select(db.assignments.released, db.assignments.id, db.assignment_questions.points).first() + a_q = ( + db( + (db.assignments.released == True) + & (db.assignments.course == auth.user.course_id) + & (db.assignment_questions.assignment_id == db.assignments.id) + & (db.assignment_questions.question_id == db.questions.id) + & (db.questions.name == divid) + ) + .select( + db.assignments.released, db.assignments.id, db.assignment_questions.points + ) + .first() + ) logger.debug(a_q) if not a_q: return json.dumps([ret]) # try new way that we store scores and comments # divid is a question; find question_grades row - result = db( - (db.question_grades.sid == auth.user.username) & - (db.question_grades.course_name == auth.user.course_name) & - (db.question_grades.div_id == divid) - ).select(db.question_grades.score, db.question_grades.comment).first() + result = ( + db( + (db.question_grades.sid == auth.user.username) + & (db.question_grades.course_name == auth.user.course_name) + & (db.question_grades.div_id == divid) + ) + .select(db.question_grades.score, db.question_grades.comment) + .first() + ) logger.debug(result) if result: # say that we're sending back result styles in new version, so they can be processed differently without affecting old way during transition. - ret['version'] = 2 - ret['grade'] = result.score - ret['max'] = a_q.assignment_questions.points + ret["version"] = 2 + ret["grade"] = result.score + ret["max"] = a_q.assignment_questions.points if result.comment: - ret['comment'] = result.comment + ret["comment"] = result.comment return json.dumps([ret]) @@ -882,148 +1116,288 @@ def getAssessResults(): course = request.vars.course div_id = request.vars.div_id event = request.vars.event - if request.vars.sid: # retrieving results for grader + if request.vars.sid: # retrieving results for grader sid = request.vars.sid else: sid = auth.user.username - response.headers['content-type'] = 'application/json' + response.headers["content-type"] = "application/json" # Identify the correct event and query the database so we can load it from the server if event == "fillb": - rows = db((db.fitb_answers.div_id == div_id) & (db.fitb_answers.course_name == course) & (db.fitb_answers.sid == sid)).select(db.fitb_answers.answer, db.fitb_answers.timestamp, orderby=~db.fitb_answers.id).first() + rows = ( + db( + (db.fitb_answers.div_id == div_id) + & (db.fitb_answers.course_name == course) + & (db.fitb_answers.sid == sid) + ) + .select( + db.fitb_answers.answer, + db.fitb_answers.timestamp, + orderby=~db.fitb_answers.id, + ) + .first() + ) if not rows: - return "" # server doesn't have it so we load from local storage instead + return "" # server doesn't have it so we load from local storage instead # - res = { - 'answer': rows.answer, - 'timestamp': str(rows.timestamp) - } + res = {"answer": rows.answer, "timestamp": str(rows.timestamp)} do_server_feedback, feedback = is_server_feedback(div_id, course) if do_server_feedback: correct, res_update = fitb_feedback(rows.answer, feedback) res.update(res_update) return json.dumps(res) elif event == "mChoice": - rows = db((db.mchoice_answers.div_id == div_id) & (db.mchoice_answers.course_name == course) & (db.mchoice_answers.sid == sid)).select(db.mchoice_answers.answer, db.mchoice_answers.timestamp, db.mchoice_answers.correct, orderby=~db.mchoice_answers.id).first() + rows = ( + db( + (db.mchoice_answers.div_id == div_id) + & (db.mchoice_answers.course_name == course) + & (db.mchoice_answers.sid == sid) + ) + .select( + db.mchoice_answers.answer, + db.mchoice_answers.timestamp, + db.mchoice_answers.correct, + orderby=~db.mchoice_answers.id, + ) + .first() + ) if not rows: return "" - res = {'answer': rows.answer, 'timestamp': str(rows.timestamp), 'correct': rows.correct} + res = { + "answer": rows.answer, + "timestamp": str(rows.timestamp), + "correct": rows.correct, + } return json.dumps(res) elif event == "dragNdrop": - rows = db((db.dragndrop_answers.div_id == div_id) & (db.dragndrop_answers.course_name == course) & (db.dragndrop_answers.sid == sid)).select(db.dragndrop_answers.answer, db.dragndrop_answers.timestamp, db.dragndrop_answers.correct, db.dragndrop_answers.minHeight, orderby=~db.dragndrop_answers.id).first() + rows = ( + db( + (db.dragndrop_answers.div_id == div_id) + & (db.dragndrop_answers.course_name == course) + & (db.dragndrop_answers.sid == sid) + ) + .select( + db.dragndrop_answers.answer, + db.dragndrop_answers.timestamp, + db.dragndrop_answers.correct, + db.dragndrop_answers.minHeight, + orderby=~db.dragndrop_answers.id, + ) + .first() + ) if not rows: return "" - res = {'answer': rows.answer, 'timestamp': str(rows.timestamp), 'correct': rows.correct, 'minHeight': str(rows.minHeight)} + res = { + "answer": rows.answer, + "timestamp": str(rows.timestamp), + "correct": rows.correct, + "minHeight": str(rows.minHeight), + } return json.dumps(res) elif event == "clickableArea": - rows = db((db.clickablearea_answers.div_id == div_id) & (db.clickablearea_answers.course_name == course) & (db.clickablearea_answers.sid == sid)).select(db.clickablearea_answers.answer, db.clickablearea_answers.timestamp, db.clickablearea_answers.correct, orderby=~db.clickablearea_answers.id).first() + rows = ( + db( + (db.clickablearea_answers.div_id == div_id) + & (db.clickablearea_answers.course_name == course) + & (db.clickablearea_answers.sid == sid) + ) + .select( + db.clickablearea_answers.answer, + db.clickablearea_answers.timestamp, + db.clickablearea_answers.correct, + orderby=~db.clickablearea_answers.id, + ) + .first() + ) if not rows: return "" - res = {'answer': rows.answer, 'timestamp': str(rows.timestamp), 'correct': rows.correct} + res = { + "answer": rows.answer, + "timestamp": str(rows.timestamp), + "correct": rows.correct, + } return json.dumps(res) elif event == "timedExam": - rows = db((db.timed_exam.reset == None) & (db.timed_exam.div_id == div_id) & (db.timed_exam.course_name == course) & (db.timed_exam.sid == sid)).select(db.timed_exam.correct, db.timed_exam.incorrect, db.timed_exam.skipped, db.timed_exam.time_taken, db.timed_exam.timestamp, db.timed_exam.reset, orderby=~db.timed_exam.id).first() + rows = ( + db( + (db.timed_exam.reset == None) + & (db.timed_exam.div_id == div_id) + & (db.timed_exam.course_name == course) + & (db.timed_exam.sid == sid) + ) + .select( + db.timed_exam.correct, + db.timed_exam.incorrect, + db.timed_exam.skipped, + db.timed_exam.time_taken, + db.timed_exam.timestamp, + db.timed_exam.reset, + orderby=~db.timed_exam.id, + ) + .first() + ) if not rows: return "" - res = {'correct': rows.correct, 'incorrect': rows.incorrect, 'skipped': str(rows.skipped), 'timeTaken': str(rows.time_taken), 'timestamp': str(rows.timestamp), 'reset': str(rows.reset)} + res = { + "correct": rows.correct, + "incorrect": rows.incorrect, + "skipped": str(rows.skipped), + "timeTaken": str(rows.time_taken), + "timestamp": str(rows.timestamp), + "reset": str(rows.reset), + } return json.dumps(res) elif event == "parsons": - rows = db((db.parsons_answers.div_id == div_id) & (db.parsons_answers.course_name == course) & (db.parsons_answers.sid == sid)).select(db.parsons_answers.answer, db.parsons_answers.source, db.parsons_answers.timestamp, orderby=~db.parsons_answers.id).first() + rows = ( + db( + (db.parsons_answers.div_id == div_id) + & (db.parsons_answers.course_name == course) + & (db.parsons_answers.sid == sid) + ) + .select( + db.parsons_answers.answer, + db.parsons_answers.source, + db.parsons_answers.timestamp, + orderby=~db.parsons_answers.id, + ) + .first() + ) if not rows: return "" - res = {'answer': rows.answer, 'source': rows.source, 'timestamp': str(rows.timestamp)} + res = { + "answer": rows.answer, + "source": rows.source, + "timestamp": str(rows.timestamp), + } return json.dumps(res) elif event == "shortanswer": - row = db((db.shortanswer_answers.sid == sid) & (db.shortanswer_answers.div_id == div_id) & (db.shortanswer_answers.course_name == course)).select().first() + row = ( + db( + (db.shortanswer_answers.sid == sid) + & (db.shortanswer_answers.div_id == div_id) + & (db.shortanswer_answers.course_name == course) + ) + .select() + .first() + ) if not row: return "" - res = {'answer': row.answer, 'timestamp': str(row.timestamp)} + res = {"answer": row.answer, "timestamp": str(row.timestamp)} return json.dumps(res) elif event == "lp_build": - rows = db( - (db.lp_answers.div_id == div_id) & - (db.lp_answers.course_name == course) & - (db.lp_answers.sid == sid) - ).select(db.lp_answers.answer, db.lp_answers.timestamp, db.lp_answers.correct, orderby=~db.lp_answers.id).first() + rows = ( + db( + (db.lp_answers.div_id == div_id) + & (db.lp_answers.course_name == course) + & (db.lp_answers.sid == sid) + ) + .select( + db.lp_answers.answer, + db.lp_answers.timestamp, + db.lp_answers.correct, + orderby=~db.lp_answers.id, + ) + .first() + ) if not rows: - return "" # server doesn't have it so we load from local storage instead + return "" # server doesn't have it so we load from local storage instead answer = json.loads(rows.answer) correct = rows.correct - return json.dumps({ - 'answer': answer, - 'timestamp': str(rows.timestamp), - 'correct': correct - }) - + return json.dumps( + {"answer": answer, "timestamp": str(rows.timestamp), "correct": correct} + ) def checkTimedReset(): if auth.user: user = auth.user.username else: - return json.dumps({"canReset":False}) + return json.dumps({"canReset": False}) divId = request.vars.div_id course = request.vars.course - rows = db((db.timed_exam.div_id == divId) & (db.timed_exam.sid == user) & (db.timed_exam.course_name == course)).select(orderby=~db.timed_exam.id).first() - - if rows: # If there was a scored exam + rows = ( + db( + (db.timed_exam.div_id == divId) + & (db.timed_exam.sid == user) + & (db.timed_exam.course_name == course) + ) + .select(orderby=~db.timed_exam.id) + .first() + ) + + if rows: # If there was a scored exam if rows.reset == True: - return json.dumps({"canReset":True}) + return json.dumps({"canReset": True}) else: - return json.dumps({"canReset":False}) + return json.dumps({"canReset": False}) else: - return json.dumps({"canReset":True}) + return json.dumps({"canReset": True}) # The request variable ``code`` must contain JSON-encoded RST to be rendered by Runestone. Only the HTML containing the actual Runestone component will be returned. def preview_question(): try: code = json.loads(request.vars.code) - with open("applications/{}/build/preview/_sources/index.rst".format(request.application), "w", encoding="utf-8") as ixf: + with open( + "applications/{}/build/preview/_sources/index.rst".format( + request.application + ), + "w", + encoding="utf-8", + ) as ixf: ixf.write(code) # Note that ``os.environ`` isn't a dict, it's an object whose setter modifies environment variables. So, modifications of a copy/deepcopy still `modify the original environment `_. Therefore, convert it to a dict, where modifications will not affect the environment. env = dict(os.environ) # Prevent any changes to the database when building a preview question. - del env['DBURL'] + del env["DBURL"] # Run a runestone build. # We would like to use sys.executable But when we run web2py # in uwsgi then sys.executable is uwsgi which doesn't work. # Why not just run runestone? popen_obj = subprocess.Popen( - [settings.python_interpreter, '-m', 'runestone', 'build'], + [settings.python_interpreter, "-m", "runestone", "build"], # The build must be run from the directory containing a ``conf.py`` and all the needed support files. - cwd='applications/{}/build/preview'.format(request.application), + cwd="applications/{}/build/preview".format(request.application), # Capture the build output as text in case of an error. - stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True, # Pass the modified environment which doesn't contain ``DBURL``. - env=env) + env=env, + ) stdout, stderr = popen_obj.communicate() # If there was an error, return stdout and stderr from the build. if popen_obj.returncode != 0: - return json.dumps('Error: Runestone build failed:\n\n' + - stdout + '\n' + stderr) - - with open('applications/{}/build/preview/build/preview/index.html'.format(request.application), 'r', encoding='utf-8') as ixf: + return json.dumps( + "Error: Runestone build failed:\n\n" + stdout + "\n" + stderr + ) + + with open( + "applications/{}/build/preview/build/preview/index.html".format( + request.application + ), + "r", + encoding="utf-8", + ) as ixf: src = ixf.read() tree = html.fromstring(src) component = tree.cssselect(".runestone") if len(component) > 0: - ctext = html.tostring(component[0]).decode('utf-8') + ctext = html.tostring(component[0]).decode("utf-8") else: component = tree.cssselect(".system-message") if len(component) > 0: - ctext = html.tostring(component[0]).decode('utf-8') + ctext = html.tostring(component[0]).decode("utf-8") logger.debug("error - ", ctext) else: ctext = "Error: Runestone content missing." return json.dumps(ctext) except Exception as ex: - return json.dumps('Error: {}'.format(ex)) + return json.dumps("Error: {}".format(ex)) def save_donate(): @@ -1033,7 +1407,9 @@ def save_donate(): def did_donate(): if auth.user: - d_status = db(db.auth_user.id == auth.user.id).select(db.auth_user.donated).first() + d_status = ( + db(db.auth_user.id == auth.user.id).select(db.auth_user.donated).first() + ) return json.dumps(dict(donate=d_status.donated)) return json.dumps(dict(donate=False)) @@ -1047,10 +1423,17 @@ def get_datafile(): course = request.vars.course_id # the course name the_course = db(db.courses.course_name == course).select().first() acid = request.vars.acid - file_contents = db((db.source_code.acid == acid) & - ((db.source_code.course_id == the_course.base_course) | - (db.source_code.course_id == course)) - ).select(db.source_code.main_code).first() + file_contents = ( + db( + (db.source_code.acid == acid) + & ( + (db.source_code.course_id == the_course.base_course) + | (db.source_code.course_id == course) + ) + ) + .select(db.source_code.main_code) + .first() + ) if file_contents: file_contents = file_contents.main_code @@ -1061,7 +1444,11 @@ def get_datafile(): def _same_class(user1, user2): - user1_course = db(db.auth_user.username == user1).select(db.auth_user.course_id).first() - user2_course = db(db.auth_user.username == user2).select(db.auth_user.course_id).first() + user1_course = ( + db(db.auth_user.username == user1).select(db.auth_user.course_id).first() + ) + user2_course = ( + db(db.auth_user.username == user2).select(db.auth_user.course_id).first() + ) return user1_course == user2_course diff --git a/controllers/appadmin.py b/controllers/appadmin.py index 005285a5a..e67b09ac2 100644 --- a/controllers/appadmin.py +++ b/controllers/appadmin.py @@ -17,53 +17,75 @@ # ## critical --- make a copy of the environment global_env = copy.copy(globals()) -global_env['datetime'] = datetime +global_env["datetime"] = datetime -http_host = request.env.http_host.split(':')[0] +http_host = request.env.http_host.split(":")[0] remote_addr = request.env.remote_addr try: - hosts = (http_host, socket.gethostname(), - socket.gethostbyname(http_host), - '::1', '127.0.0.1', '::ffff:127.0.0.1') + hosts = ( + http_host, + socket.gethostname(), + socket.gethostbyname(http_host), + "::1", + "127.0.0.1", + "::ffff:127.0.0.1", + ) except: - hosts = (http_host, ) + hosts = (http_host,) if request.is_https: session.secure() -elif (remote_addr not in hosts) and (remote_addr != "127.0.0.1") and \ - (request.function != 'manage'): - raise HTTP(200, T('appadmin is disabled because insecure channel')) - -if request.function == 'manage': - if not 'auth' in globals() or not request.args: - redirect(URL(request.controller, 'index')) +elif ( + (remote_addr not in hosts) + and (remote_addr != "127.0.0.1") + and (request.function != "manage") +): + raise HTTP(200, T("appadmin is disabled because insecure channel")) + +if request.function == "manage": + if not "auth" in globals() or not request.args: + redirect(URL(request.controller, "index")) manager_action = auth.settings.manager_actions.get(request.args(0), None) - if manager_action is None and request.args(0) == 'auth': - manager_action = dict(role=auth.settings.auth_manager_role, - heading=T('Manage Access Control'), - tables=[auth.table_user(), - auth.table_group(), - auth.table_permission()]) - manager_role = manager_action.get('role', None) if manager_action else None - if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)): + if manager_action is None and request.args(0) == "auth": + manager_action = dict( + role=auth.settings.auth_manager_role, + heading=T("Manage Access Control"), + tables=[auth.table_user(), auth.table_group(), auth.table_permission()], + ) + manager_role = manager_action.get("role", None) if manager_action else None + if not ( + gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role) + ): raise HTTP(403, "Not authorized") menu = False -elif (request.application == 'admin' and not session.authorized) or \ - (request.application != 'admin' and not gluon.fileutils.check_credentials(request)): - redirect(URL('admin', 'default', 'index', - vars=dict(send=URL(args=request.args, vars=request.vars)))) +elif (request.application == "admin" and not session.authorized) or ( + request.application != "admin" and not gluon.fileutils.check_credentials(request) +): + redirect( + URL( + "admin", + "default", + "index", + vars=dict(send=URL(args=request.args, vars=request.vars)), + ) + ) else: - response.subtitle = T('Database Administration (appadmin)') + response.subtitle = T("Database Administration (appadmin)") menu = True ignore_rw = True -response.view = 'appadmin.html' +response.view = "appadmin.html" if menu: - response.menu = [[T('design'), False, URL('admin', 'default', 'design', - args=[request.application])], [T('db'), False, - URL('index')], [T('state'), False, - URL('state')], [T('cache'), False, - URL('ccache')]] + response.menu = [ + [ + T("design"), + False, + URL("admin", "default", "design", args=[request.application]), + ], + [T("db"), False, URL("index")], + [T("state"), False, URL("state")], + [T("cache"), False, URL("ccache")], + ] # ########################################################## # ## auxiliary functions @@ -71,9 +93,11 @@ if False and request.tickets_db: from gluon.restricted import TicketStorage + ts = TicketStorage() ts._get_table(request.tickets_db, ts.tablename, request.application) + def get_databases(request): dbs = {} for (key, value) in global_env.items(): @@ -85,27 +109,30 @@ def get_databases(request): dbs[key] = value return dbs + databases = get_databases(None) + def eval_in_global_env(text): - exec ('_ret=%s' % text, {}, global_env) - return global_env['_ret'] + exec("_ret=%s" % text, {}, global_env) + return global_env["_ret"] def get_database(request): if request.args and request.args[0] in databases: return eval_in_global_env(request.args[0]) else: - session.flash = T('invalid request') - redirect(URL('index')) + session.flash = T("invalid request") + redirect(URL("index")) + def get_table(request): db = get_database(request) if len(request.args) > 1 and request.args[1] in db.tables: return (db, request.args[1]) else: - session.flash = T('invalid request') - redirect(URL('index')) + session.flash = T("invalid request") + redirect(URL("index")) def get_query(request): @@ -116,16 +143,15 @@ def get_query(request): def query_by_table_type(tablename, db, request=request): - keyed = hasattr(db[tablename], '_primarykey') + keyed = hasattr(db[tablename], "_primarykey") if keyed: firstkey = db[tablename][db[tablename]._primarykey[0]] - cond = '>0' - if firstkey.type in ['string', 'text']: + cond = ">0" + if firstkey.type in ["string", "text"]: cond = '!=""' - qry = '%s.%s.%s%s' % ( - request.args[0], request.args[1], firstkey.name, cond) + qry = "%s.%s.%s%s" % (request.args[0], request.args[1], firstkey.name, cond) else: - qry = '%s.%s.id>0' % tuple(request.args[:2]) + qry = "%s.%s.id>0" % tuple(request.args[:2]) return qry @@ -145,7 +171,7 @@ def insert(): (db, table) = get_table(request) form = SQLFORM(db[table], ignore_rw=ignore_rw) if form.accepts(request.vars, session): - response.flash = T('new record inserted') + response.flash = T("new record inserted") return dict(form=form, table=db[table]) @@ -156,20 +182,22 @@ def insert(): def download(): import os + db = get_database(request) return response.download(request, db) def csv(): import gluon.contenttype - response.headers['Content-Type'] = \ - gluon.contenttype.contenttype('.csv') + + response.headers["Content-Type"] = gluon.contenttype.contenttype(".csv") db = get_database(request) query = get_query(request) if not query: return None - response.headers['Content-disposition'] = 'attachment; filename=%s_%s.csv'\ - % tuple(request.vars.query.split('.')[:2]) + response.headers["Content-disposition"] = "attachment; filename=%s_%s.csv" % tuple( + request.vars.query.split(".")[:2] + ) return str(db(query, ignore_common_filters=True).select()) @@ -179,21 +207,25 @@ def import_csv(table, file): def select(): import re + db = get_database(request) dbname = request.args[0] try: is_imap = db._uri.startswith("imap://") except (KeyError, AttributeError, TypeError): is_imap = False - regex = re.compile(r'(?P\w+)\.(?P\w+)=(?P\d+)') - if len(request.args) > 1 and hasattr(db[request.args[1]], '_primarykey'): - regex = re.compile(r'(?P
\w+)\.(?P\w+)=(?P.+)') + regex = re.compile(r"(?P
\w+)\.(?P\w+)=(?P\d+)") + if len(request.args) > 1 and hasattr(db[request.args[1]], "_primarykey"): + regex = re.compile(r"(?P
\w+)\.(?P\w+)=(?P.+)") if request.vars.query: match = regex.match(request.vars.query) if match: - request.vars.query = '%s.%s.%s==%s' % (request.args[0], - match.group('table'), match.group('field'), - match.group('value')) + request.vars.query = "%s.%s.%s==%s" % ( + request.args[0], + match.group("table"), + match.group("field"), + match.group("value"), + ) else: request.vars.query = session.last_query query = get_query(request) @@ -215,75 +247,107 @@ def select(): rows = [] orderby = request.vars.orderby if orderby: - orderby = dbname + '.' + orderby + orderby = dbname + "." + orderby if orderby == session.last_orderby: - if orderby[0] == '~': + if orderby[0] == "~": orderby = orderby[1:] else: - orderby = '~' + orderby + orderby = "~" + orderby session.last_orderby = orderby session.last_query = request.vars.query - form = FORM(TABLE(TR(T('Query:'), '', INPUT(_style='width:400px', - _name='query', _value=request.vars.query or '', _class="form-control", - requires=IS_NOT_EMPTY( - error_message=T("Cannot be empty")))), TR(T('Update:'), - INPUT(_name='update_check', _type='checkbox', - value=False), INPUT(_style='width:400px', - _name='update_fields', _value=request.vars.update_fields - or '', _class="form-control")), TR(T('Delete:'), INPUT(_name='delete_check', - _class='delete', _type='checkbox', value=False), ''), - TR('', '', INPUT(_type='submit', _value=T('submit'), _class="btn btn-primary"))), - _action=URL(r=request, args=request.args)) + form = FORM( + TABLE( + TR( + T("Query:"), + "", + INPUT( + _style="width:400px", + _name="query", + _value=request.vars.query or "", + _class="form-control", + requires=IS_NOT_EMPTY(error_message=T("Cannot be empty")), + ), + ), + TR( + T("Update:"), + INPUT(_name="update_check", _type="checkbox", value=False), + INPUT( + _style="width:400px", + _name="update_fields", + _value=request.vars.update_fields or "", + _class="form-control", + ), + ), + TR( + T("Delete:"), + INPUT( + _name="delete_check", _class="delete", _type="checkbox", value=False + ), + "", + ), + TR( + "", + "", + INPUT(_type="submit", _value=T("submit"), _class="btn btn-primary"), + ), + ), + _action=URL(r=request, args=request.args), + ) tb = None if form.accepts(request.vars, formname=None): - regex = re.compile(request.args[0] + r'\.(?P
\w+)\..+') + regex = re.compile(request.args[0] + r"\.(?P
\w+)\..+") match = regex.match(form.vars.query.strip()) if match: - table = match.group('table') + table = match.group("table") try: nrows = db(query, ignore_common_filters=True).count() if form.vars.update_check and form.vars.update_fields: db(query, ignore_common_filters=True).update( - **eval_in_global_env('dict(%s)' % form.vars.update_fields)) - response.flash = T('%s %%{row} updated', nrows) + **eval_in_global_env("dict(%s)" % form.vars.update_fields) + ) + response.flash = T("%s %%{row} updated", nrows) elif form.vars.delete_check: db(query, ignore_common_filters=True).delete() - response.flash = T('%s %%{row} deleted', nrows) + response.flash = T("%s %%{row} deleted", nrows) nrows = db(query, ignore_common_filters=True).count() if is_imap: - fields = [db[table][name] for name in - ("id", "uid", "created", "to", - "sender", "subject")] + fields = [ + db[table][name] + for name in ("id", "uid", "created", "to", "sender", "subject") + ] if orderby: rows = db(query, ignore_common_filters=True).select( - *fields, limitby=(start, stop), - orderby=eval_in_global_env(orderby)) + *fields, limitby=(start, stop), orderby=eval_in_global_env(orderby) + ) else: rows = db(query, ignore_common_filters=True).select( - *fields, limitby=(start, stop)) + *fields, limitby=(start, stop) + ) except Exception as e: import traceback + tb = traceback.format_exc() (rows, nrows) = ([], 0) - response.flash = DIV(T('Invalid Query'), PRE(str(e))) + response.flash = DIV(T("Invalid Query"), PRE(str(e))) # begin handle upload csv csv_table = table or request.vars.table if csv_table: - formcsv = FORM(str(T('or import from csv file')) + " ", - INPUT(_type='file', _name='csvfile'), - INPUT(_type='hidden', _value=csv_table, _name='table'), - INPUT(_type='submit', _value=T('import'), _class="btn btn-primary")) + formcsv = FORM( + str(T("or import from csv file")) + " ", + INPUT(_type="file", _name="csvfile"), + INPUT(_type="hidden", _value=csv_table, _name="table"), + INPUT(_type="submit", _value=T("import"), _class="btn btn-primary"), + ) else: formcsv = None if formcsv and formcsv.process().accepted: try: - import_csv(db[request.vars.table], - request.vars.csvfile.file) - response.flash = T('data uploaded') + import_csv(db[request.vars.table], request.vars.csvfile.file) + response.flash = T("data uploaded") except Exception as e: - response.flash = DIV(T('unable to parse csv file'), PRE(str(e))) + response.flash = DIV(T("unable to parse csv file"), PRE(str(e))) # end handle upload csv return dict( @@ -296,7 +360,7 @@ def select(): rows=rows, query=request.vars.query, formcsv=formcsv, - tb=tb + tb=tb, ) @@ -307,40 +371,39 @@ def select(): def update(): (db, table) = get_table(request) - keyed = hasattr(db[table], '_primarykey') + keyed = hasattr(db[table], "_primarykey") record = None db[table]._common_filter = None if keyed: key = [f for f in request.vars if f in db[table]._primarykey] if key: - record = db(db[table][key[0]] == request.vars[key[ - 0]]).select().first() + record = db(db[table][key[0]] == request.vars[key[0]]).select().first() else: - record = db(db[table].id == request.args( - 2)).select().first() + record = db(db[table].id == request.args(2)).select().first() if not record: qry = query_by_table_type(table, db) - session.flash = T('record does not exist') - redirect(URL('select', args=request.args[:1], - vars=dict(query=qry))) + session.flash = T("record does not exist") + redirect(URL("select", args=request.args[:1], vars=dict(query=qry))) if keyed: for k in db[table]._primarykey: db[table][k].writable = False form = SQLFORM( - db[table], record, deletable=True, delete_label=T('Check to delete'), + db[table], + record, + deletable=True, + delete_label=T("Check to delete"), ignore_rw=ignore_rw and not keyed, - linkto=URL('select', - args=request.args[:1]), upload=URL(r=request, - f='download', args=request.args[:1])) + linkto=URL("select", args=request.args[:1]), + upload=URL(r=request, f="download", args=request.args[:1]), + ) if form.accepts(request.vars, session): - session.flash = T('done!') + session.flash = T("done!") qry = query_by_table_type(table, db) - redirect(URL('select', args=request.args[:1], - vars=dict(query=qry))) + redirect(URL("select", args=request.args[:1], vars=dict(query=qry))) return dict(form=form, table=db[table]) @@ -356,18 +419,16 @@ def state(): def ccache(): if is_gae: form = FORM( - P(TAG.BUTTON(T("Clear CACHE?"), _type="submit", _name="yes", _value="yes"))) + P(TAG.BUTTON(T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")) + ) else: cache.ram.initialize() cache.disk.initialize() form = FORM( - P(TAG.BUTTON( - T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")), - P(TAG.BUTTON( - T("Clear RAM"), _type="submit", _name="ram", _value="ram")), - P(TAG.BUTTON( - T("Clear DISK"), _type="submit", _name="disk", _value="disk")), + P(TAG.BUTTON(T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")), + P(TAG.BUTTON(T("Clear RAM"), _type="submit", _name="ram", _value="ram")), + P(TAG.BUTTON(T("Clear DISK"), _type="submit", _name="disk", _value="disk")), ) if form.accepts(request.vars, session): @@ -406,20 +467,20 @@ def ccache(): from pydal.contrib import portalocker ram = { - 'entries': 0, - 'bytes': 0, - 'objects': 0, - 'hits': 0, - 'misses': 0, - 'ratio': 0, - 'oldest': time.time(), - 'keys': [] + "entries": 0, + "bytes": 0, + "objects": 0, + "hits": 0, + "misses": 0, + "ratio": 0, + "oldest": time.time(), + "keys": [], } disk = copy.copy(ram) total = copy.copy(ram) - disk['keys'] = [] - total['keys'] = [] + disk["keys"] = [] + total["keys"] = [] def GetInHMS(seconds): hours = math.floor(seconds / 3600) @@ -433,84 +494,90 @@ def GetInHMS(seconds): if is_gae: gae_stats = cache.ram.client.get_stats() try: - gae_stats['ratio'] = ((gae_stats['hits'] * 100) / - (gae_stats['hits'] + gae_stats['misses'])) + gae_stats["ratio"] = (gae_stats["hits"] * 100) / ( + gae_stats["hits"] + gae_stats["misses"] + ) except ZeroDivisionError: - gae_stats['ratio'] = T("?") - gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age']) + gae_stats["ratio"] = T("?") + gae_stats["oldest"] = GetInHMS(time.time() - gae_stats["oldest_item_age"]) total.update(gae_stats) else: # get ram stats directly from the cache object ram_stats = cache.ram.stats[request.application] - ram['hits'] = ram_stats['hit_total'] - ram_stats['misses'] - ram['misses'] = ram_stats['misses'] + ram["hits"] = ram_stats["hit_total"] - ram_stats["misses"] + ram["misses"] = ram_stats["misses"] try: - ram['ratio'] = ram['hits'] * 100 / ram_stats['hit_total'] + ram["ratio"] = ram["hits"] * 100 / ram_stats["hit_total"] except (KeyError, ZeroDivisionError): - ram['ratio'] = 0 + ram["ratio"] = 0 for key, value in iteritems(cache.ram.storage): if asizeof: - ram['bytes'] += asizeof(value[1]) - ram['objects'] += 1 - ram['entries'] += 1 - if value[0] < ram['oldest']: - ram['oldest'] = value[0] - ram['keys'].append((key, GetInHMS(time.time() - value[0]))) + ram["bytes"] += asizeof(value[1]) + ram["objects"] += 1 + ram["entries"] += 1 + if value[0] < ram["oldest"]: + ram["oldest"] = value[0] + ram["keys"].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: value = cache.disk.storage[key] - if key == 'web2py_cache_statistics' and isinstance(value[1], dict): - disk['hits'] = value[1]['hit_total'] - value[1]['misses'] - disk['misses'] = value[1]['misses'] + if key == "web2py_cache_statistics" and isinstance(value[1], dict): + disk["hits"] = value[1]["hit_total"] - value[1]["misses"] + disk["misses"] = value[1]["misses"] try: - disk['ratio'] = disk['hits'] * 100 / value[1]['hit_total'] + disk["ratio"] = disk["hits"] * 100 / value[1]["hit_total"] except (KeyError, ZeroDivisionError): - disk['ratio'] = 0 + disk["ratio"] = 0 else: if asizeof: - disk['bytes'] += asizeof(value[1]) - disk['objects'] += 1 - disk['entries'] += 1 - if value[0] < disk['oldest']: - disk['oldest'] = value[0] - disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - - ram_keys = list(ram) # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] - ram_keys.remove('ratio') - ram_keys.remove('oldest') + disk["bytes"] += asizeof(value[1]) + disk["objects"] += 1 + disk["entries"] += 1 + if value[0] < disk["oldest"]: + disk["oldest"] = value[0] + disk["keys"].append((key, GetInHMS(time.time() - value[0]))) + + ram_keys = list( + ram + ) # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys.remove("ratio") + ram_keys.remove("oldest") for key in ram_keys: total[key] = ram[key] + disk[key] try: - total['ratio'] = total['hits'] * 100 / (total['hits'] + - total['misses']) + total["ratio"] = total["hits"] * 100 / (total["hits"] + total["misses"]) except (KeyError, ZeroDivisionError): - total['ratio'] = 0 + total["ratio"] = 0 - if disk['oldest'] < ram['oldest']: - total['oldest'] = disk['oldest'] + if disk["oldest"] < ram["oldest"]: + total["oldest"] = disk["oldest"] else: - total['oldest'] = ram['oldest'] + total["oldest"] = ram["oldest"] - ram['oldest'] = GetInHMS(time.time() - ram['oldest']) - disk['oldest'] = GetInHMS(time.time() - disk['oldest']) - total['oldest'] = GetInHMS(time.time() - total['oldest']) + ram["oldest"] = GetInHMS(time.time() - ram["oldest"]) + disk["oldest"] = GetInHMS(time.time() - disk["oldest"]) + total["oldest"] = GetInHMS(time.time() - total["oldest"]) def key_table(keys): return TABLE( - TR(TD(B(T('Key'))), TD(B(T('Time in Cache (h:m:s)')))), - *[TR(TD(k[0]), TD('%02d:%02d:%02d' % k[1])) for k in keys], - **dict(_class='cache-keys', - _style="border-collapse: separate; border-spacing: .5em;")) + TR(TD(B(T("Key"))), TD(B(T("Time in Cache (h:m:s)")))), + *[TR(TD(k[0]), TD("%02d:%02d:%02d" % k[1])) for k in keys], + **dict( + _class="cache-keys", + _style="border-collapse: separate; border-spacing: .5em;", + ) + ) if not is_gae: - ram['keys'] = key_table(ram['keys']) - disk['keys'] = key_table(disk['keys']) - total['keys'] = key_table(total['keys']) + ram["keys"] = key_table(ram["keys"]) + disk["keys"] = key_table(disk["keys"]) + total["keys"] = key_table(total["keys"]) - return dict(form=form, total=total, - ram=ram, disk=disk, object_stats=asizeof != False) + return dict( + form=form, total=total, ram=ram, disk=disk, object_stats=asizeof != False + ) def table_template(table): @@ -521,17 +588,16 @@ def FONT(*args, **kwargs): def types(field): f_type = field.type - if not isinstance(f_type,str): - return ' ' - elif f_type == 'string': + if not isinstance(f_type, str): + return " " + elif f_type == "string": return field.length - elif f_type == 'id': - return B('pk') - elif f_type.startswith('reference') or \ - f_type.startswith('list:reference'): - return B('fk') + elif f_type == "id": + return B("pk") + elif f_type.startswith("reference") or f_type.startswith("list:reference"): + return B("fk") else: - return ' ' + return " " # This is horribe HTML but the only one graphiz understands rows = [] @@ -542,69 +608,113 @@ def types(field): face_bold = "Helvetica Bold" border = 0 - rows.append(TR(TD(FONT(table, _face=face_bold, _color=bgcolor), - _colspan=3, _cellpadding=cellpadding, - _align="center", _bgcolor=color))) + rows.append( + TR( + TD( + FONT(table, _face=face_bold, _color=bgcolor), + _colspan=3, + _cellpadding=cellpadding, + _align="center", + _bgcolor=color, + ) + ) + ) for row in db[table]: - rows.append(TR(TD(FONT(row.name, _color=color, _face=face_bold), - _align="left", _cellpadding=cellpadding, - _border=border), - TD(FONT(row.type, _color=color, _face=face), - _align="left", _cellpadding=cellpadding, - _border=border), - TD(FONT(types(row), _color=color, _face=face), - _align="center", _cellpadding=cellpadding, - _border=border))) - return "< %s >" % TABLE(*rows, **dict(_bgcolor=bgcolor, _border=1, - _cellborder=0, _cellspacing=0) - ).xml() + rows.append( + TR( + TD( + FONT(row.name, _color=color, _face=face_bold), + _align="left", + _cellpadding=cellpadding, + _border=border, + ), + TD( + FONT(row.type, _color=color, _face=face), + _align="left", + _cellpadding=cellpadding, + _border=border, + ), + TD( + FONT(types(row), _color=color, _face=face), + _align="center", + _cellpadding=cellpadding, + _border=border, + ), + ) + ) + return ( + "< %s >" + % TABLE( + *rows, **dict(_bgcolor=bgcolor, _border=1, _cellborder=0, _cellspacing=0) + ).xml() + ) + def manage(): - tables = manager_action['tables'] + tables = manager_action["tables"] if isinstance(tables[0], str): - db = manager_action.get('db', auth.db) + db = manager_action.get("db", auth.db) db = globals()[db] if isinstance(db, str) else db tables = [db[table] for table in tables] - if request.args(0) == 'auth': - auth.table_user()._plural = T('Users') - auth.table_group()._plural = T('Roles') - auth.table_membership()._plural = T('Memberships') - auth.table_permission()._plural = T('Permissions') - if request.extension != 'load': - return dict(heading=manager_action.get('heading', - T('Manage %(action)s') % dict(action=request.args(0).replace('_', ' ').title())), - tablenames=[table._tablename for table in tables], - labels=[table._plural.title() for table in tables]) + if request.args(0) == "auth": + auth.table_user()._plural = T("Users") + auth.table_group()._plural = T("Roles") + auth.table_membership()._plural = T("Memberships") + auth.table_permission()._plural = T("Permissions") + if request.extension != "load": + return dict( + heading=manager_action.get( + "heading", + T("Manage %(action)s") + % dict(action=request.args(0).replace("_", " ").title()), + ), + tablenames=[table._tablename for table in tables], + labels=[table._plural.title() for table in tables], + ) table = tables[request.args(1, cast=int)] - formname = '%s_grid' % table._tablename + formname = "%s_grid" % table._tablename linked_tables = orderby = None - if request.args(0) == 'auth': - auth.table_group()._id.readable = \ - auth.table_membership()._id.readable = \ - auth.table_permission()._id.readable = False - auth.table_membership().user_id.label = T('User') - auth.table_membership().group_id.label = T('Role') - auth.table_permission().group_id.label = T('Role') - auth.table_permission().name.label = T('Permission') + if request.args(0) == "auth": + auth.table_group()._id.readable = ( + auth.table_membership()._id.readable + ) = auth.table_permission()._id.readable = False + auth.table_membership().user_id.label = T("User") + auth.table_membership().group_id.label = T("Role") + auth.table_permission().group_id.label = T("Role") + auth.table_permission().name.label = T("Permission") if table == auth.table_user(): linked_tables = [auth.settings.table_membership_name] elif table == auth.table_group(): - orderby = 'role' if not request.args(3) or '.group_id' not in request.args(3) else None + orderby = ( + "role" + if not request.args(3) or ".group_id" not in request.args(3) + else None + ) elif table == auth.table_permission(): - orderby = 'group_id' - kwargs = dict(user_signature=True, maxtextlength=1000, - orderby=orderby, linked_tables=linked_tables) - smartgrid_args = manager_action.get('smartgrid_args', {}) - kwargs.update(**smartgrid_args.get('DEFAULT', {})) + orderby = "group_id" + kwargs = dict( + user_signature=True, + maxtextlength=1000, + orderby=orderby, + linked_tables=linked_tables, + ) + smartgrid_args = manager_action.get("smartgrid_args", {}) + kwargs.update(**smartgrid_args.get("DEFAULT", {})) kwargs.update(**smartgrid_args.get(table._tablename, {})) grid = SQLFORM.smartgrid(table, args=request.args[:2], formname=formname, **kwargs) return grid + def hooks(): import functools import inspect - list_op = ['_%s_%s' %(h,m) for h in ['before', 'after'] for m in ['insert','update','delete']] + + list_op = [ + "_%s_%s" % (h, m) + for h in ["before", "after"] + for m in ["insert", "update", "delete"] + ] tables = [] with_build_it = False for db_str in sorted(databases): @@ -614,33 +724,65 @@ def hooks(): for op in list_op: functions = [] for f in getattr(db[t], op): - if hasattr(f, '__call__'): + if hasattr(f, "__call__"): try: if isinstance(f, (functools.partial)): f = f.func filename = inspect.getsourcefile(f) - details = {'funcname':f.__name__, - 'filename':filename[len(request.folder):] if request.folder in filename else None, - 'lineno': inspect.getsourcelines(f)[1]} - if details['filename']: # Built in functions as delete_uploaded_files are not editable - details['url'] = URL(a='admin',c='default',f='edit', args=[request['application'], details['filename']],vars={'lineno':details['lineno']}) - if details['filename'] or with_build_it: + details = { + "funcname": f.__name__, + "filename": filename[len(request.folder) :] + if request.folder in filename + else None, + "lineno": inspect.getsourcelines(f)[1], + } + if details[ + "filename" + ]: # Built in functions as delete_uploaded_files are not editable + details["url"] = URL( + a="admin", + c="default", + f="edit", + args=[request["application"], details["filename"]], + vars={"lineno": details["lineno"]}, + ) + if details["filename"] or with_build_it: functions.append(details) # compiled app and windows build don't support code inspection except: pass if len(functions): - method_hooks.append({'name': op, 'functions':functions}) + method_hooks.append({"name": op, "functions": functions}) if len(method_hooks): - tables.append({'name': "%s.%s" % (db_str, t), 'slug': IS_SLUG()("%s.%s" % (db_str,t))[0], 'method_hooks':method_hooks}) + tables.append( + { + "name": "%s.%s" % (db_str, t), + "slug": IS_SLUG()("%s.%s" % (db_str, t))[0], + "method_hooks": method_hooks, + } + ) # Render - ul_main = UL(_class='nav nav-list') + ul_main = UL(_class="nav nav-list") for t in tables: - ul_main.append(A(t['name'], _onclick="collapse('a_%s')" % t['slug'])) - ul_t = UL(_class='nav nav-list', _id="a_%s" % t['slug'], _style='display:none') - for op in t['method_hooks']: - ul_t.append(LI(op['name'])) - ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']])) + ul_main.append(A(t["name"], _onclick="collapse('a_%s')" % t["slug"])) + ul_t = UL(_class="nav nav-list", _id="a_%s" % t["slug"], _style="display:none") + for op in t["method_hooks"]: + ul_t.append(LI(op["name"])) + ul_t.append( + UL( + [ + LI( + A( + f["funcname"], + _class="editor_filelink", + _href=f["url"] if "url" in f else None, + **{"_data-lineno": f["lineno"] - 1} + ) + ) + for f in op["functions"] + ] + ) + ) ul_main.append(ul_t) return ul_main @@ -649,6 +791,7 @@ def hooks(): # d3 based model visualizations # ########################################################### + def d3_graph_model(): """ See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha and also the app_admin bg_graph_model function @@ -666,28 +809,30 @@ def d3_graph_model(): for field in db[tablename]: f_type = field.type if not isinstance(f_type, str): - disp = ' ' - elif f_type == 'string': + disp = " " + elif f_type == "string": disp = field.length - elif f_type == 'id': + elif f_type == "id": disp = "PK" - elif f_type.startswith('reference') or \ - f_type.startswith('list:reference'): + elif f_type.startswith("reference") or f_type.startswith( + "list:reference" + ): disp = "FK" else: - disp = ' ' + disp = " " fields.append(dict(name=field.name, type=field.type, disp=disp)) if isinstance(f_type, str) and ( - f_type.startswith('reference') or - f_type.startswith('list:reference')): - referenced_table = f_type.split()[1].split('.')[0] + f_type.startswith("reference") + or f_type.startswith("list:reference") + ): + referenced_table = f_type.split()[1].split(".")[0] - links.append(dict(source=tablename, target = referenced_table)) + links.append(dict(source=tablename, target=referenced_table)) - nodes.append(dict(name=tablename, type="table", fields = fields)) + nodes.append(dict(name=tablename, type="table", fields=fields)) # d3 v4 allows individual modules to be specified. The complete d3 library is included below. - response.files.append(URL('admin','static','js/d3.min.js')) - response.files.append(URL('admin','static','js/d3_graph.js')) + response.files.append(URL("admin", "static", "js/d3.min.js")) + response.files.append(URL("admin", "static", "js/d3_graph.js")) return dict(databases=databases, nodes=nodes, links=links) diff --git a/controllers/assignments.py b/controllers/assignments.py index b9e06a620..7fb843b03 100644 --- a/controllers/assignments.py +++ b/controllers/assignments.py @@ -23,7 +23,14 @@ # Local application imports # ------------------------- -from rs_grading import do_autograde, do_calculate_totals, do_check_answer, send_lti_grade, _get_lti_record, _try_to_send_lti_grade +from rs_grading import ( + do_autograde, + do_calculate_totals, + do_check_answer, + send_lti_grade, + _get_lti_record, + _try_to_send_lti_grade, +) from db_dashboard import DashboardDataAnalyzer logger = logging.getLogger(settings.logger) @@ -34,79 +41,109 @@ def index(): if not auth.user: session.flash = "Please Login" - return redirect(URL('default', 'index')) - if 'sid' not in request.vars: - #return redirect(URL('assignments','index') + '?sid=%s' % (auth.user.username)) + return redirect(URL("default", "index")) + if "sid" not in request.vars: + # return redirect(URL('assignments','index') + '?sid=%s' % (auth.user.username)) request.vars.sid = auth.user.username - student = db(db.auth_user.username == request.vars.sid).select( - db.auth_user.id, - db.auth_user.username, - db.auth_user.first_name, - db.auth_user.last_name, - db.auth_user.email, - ).first() + student = ( + db(db.auth_user.username == request.vars.sid) + .select( + db.auth_user.id, + db.auth_user.username, + db.auth_user.first_name, + db.auth_user.last_name, + db.auth_user.email, + ) + .first() + ) if not student: - return redirect(URL('assignments', 'index')) - - if auth.user.course_name in ['thinkcspy', 'pythonds', 'JavaReview', 'webfundamentals', 'StudentCSP', 'apcsareview']: + return redirect(URL("assignments", "index")) + + if auth.user.course_name in [ + "thinkcspy", + "pythonds", + "JavaReview", + "webfundamentals", + "StudentCSP", + "apcsareview", + ]: session.flash = "{} is not a graded course".format(auth.user.course_name) - return redirect(URL('default', 'user')) + return redirect(URL("default", "user")) data_analyzer = DashboardDataAnalyzer(auth.user.course_id) data_analyzer.load_user_metrics(request.vars.sid) data_analyzer.load_assignment_metrics(request.vars.sid, studentView=True) chapters = [] - for chapter_label, chapter in six.iteritems(data_analyzer.chapter_progress.chapters): - chapters.append({ - "label": chapter.chapter_label, - "status": chapter.status_text(), - "subchapters": chapter.get_sub_chapter_progress() - }) + for chapter_label, chapter in six.iteritems( + data_analyzer.chapter_progress.chapters + ): + chapters.append( + { + "label": chapter.chapter_label, + "status": chapter.status_text(), + "subchapters": chapter.get_sub_chapter_progress(), + } + ) activity = data_analyzer.formatted_activity.activities - (now, - now_local, - message1, - message2, - practice_graded, - spacing, - interleaving, - practice_completion_count, - remaining_days, - max_days, - max_questions, - day_points, - question_points, - presentable_flashcards, - available_flashcards_num, - practiced_today_count, - questions_to_complete_day, - practice_today_left, - points_received, - total_possible_points, - flashcard_creation_method) = _get_practice_data(auth.user, - float(session.timezoneoffset) if 'timezoneoffset' in session else 0) - - return dict(student=student, course_id=auth.user.course_id, course_name=auth.user.course_name, - user=data_analyzer.user, chapters=chapters, activity=activity, assignments=data_analyzer.grades, - practice_message1=message1, practice_message2=message2, - practice_graded=practice_graded, flashcard_count=available_flashcards_num, - # The number of days the student has completed their practice. - practice_completion_count=practice_completion_count, - remaining_days=remaining_days, max_questions=max_questions, max_days=max_days, - total_today_count=min(practice_today_left + practiced_today_count, questions_to_complete_day), - # The number of times remaining to practice today to get the completion point. - practice_today_left=practice_today_left, - # The number of times this user has submitted their practice from the beginning of today (12:00 am) - # till now. - practiced_today_count=practiced_today_count, - points_received=points_received, - total_possible_points=total_possible_points, - spacing=spacing, - interleaving=interleaving - ) + ( + now, + now_local, + message1, + message2, + practice_graded, + spacing, + interleaving, + practice_completion_count, + remaining_days, + max_days, + max_questions, + day_points, + question_points, + presentable_flashcards, + available_flashcards_num, + practiced_today_count, + questions_to_complete_day, + practice_today_left, + points_received, + total_possible_points, + flashcard_creation_method, + ) = _get_practice_data( + auth.user, float(session.timezoneoffset) if "timezoneoffset" in session else 0 + ) + + return dict( + student=student, + course_id=auth.user.course_id, + course_name=auth.user.course_name, + user=data_analyzer.user, + chapters=chapters, + activity=activity, + assignments=data_analyzer.grades, + practice_message1=message1, + practice_message2=message2, + practice_graded=practice_graded, + flashcard_count=available_flashcards_num, + # The number of days the student has completed their practice. + practice_completion_count=practice_completion_count, + remaining_days=remaining_days, + max_questions=max_questions, + max_days=max_days, + total_today_count=min( + practice_today_left + practiced_today_count, questions_to_complete_day + ), + # The number of times remaining to practice today to get the completion point. + practice_today_left=practice_today_left, + # The number of times this user has submitted their practice from the beginning of today (12:00 am) + # till now. + practiced_today_count=practiced_today_count, + points_received=points_received, + total_possible_points=total_possible_points, + spacing=spacing, + interleaving=interleaving, + ) # Get practice data for this student and create flashcards for them is they are newcomers. @@ -138,7 +175,10 @@ def _get_practice_data(user, timezoneoffset): course = db(db.courses.id == user.course_id).select().first() practice_settings = db(db.course_practice.course_name == user.course_name) - if practice_settings.isempty() or practice_settings.select().first().end_date is None: + if ( + practice_settings.isempty() + or practice_settings.select().first().end_date is None + ): practice_message1 = "Practice tool is not set up for this course yet." practice_message2 = "Please ask your instructor to set it up." else: @@ -159,42 +199,72 @@ def _get_practice_data(user, timezoneoffset): if practice_start_date > now_local.date(): days_to_start = (practice_start_date - now_local.date()).days - practice_message1 = "Practice period will start in this course on " + str(practice_start_date) + "." - practice_message2 = ("Please return in " + str(days_to_start) + " day" + - ("." if days_to_start == 1 else "s.")) + practice_message1 = ( + "Practice period will start in this course on " + + str(practice_start_date) + + "." + ) + practice_message2 = ( + "Please return in " + + str(days_to_start) + + " day" + + ("." if days_to_start == 1 else "s.") + ) else: # Check whether flashcards are created for this user in the current course. - flashcards = db((db.user_topic_practice.course_name == user.course_name) & - (db.user_topic_practice.user_id == user.id)) + flashcards = db( + (db.user_topic_practice.course_name == user.course_name) + & (db.user_topic_practice.user_id == user.id) + ) if flashcards.isempty(): if flashcard_creation_method == 0: - practice_message1 = ("Only pages that you mark as complete, at the bottom of the page, are the" + - " ones that are eligible for practice.") - practice_message2 = ("You've not marked any pages as complete yet. Please mark some pages first" + - " to practice them.") + practice_message1 = ( + "Only pages that you mark as complete, at the bottom of the page, are the" + + " ones that are eligible for practice." + ) + practice_message2 = ( + "You've not marked any pages as complete yet. Please mark some pages first" + + " to practice them." + ) else: # new student; create flashcards # We only create flashcards for those sections that are marked by the instructor as taught. - subchaptersTaught = db((db.sub_chapter_taught.course_name == user.course_name) & - (db.sub_chapter_taught.chapter_label == db.chapters.chapter_label) & - (db.sub_chapter_taught.sub_chapter_label == db.sub_chapters.sub_chapter_label) & - (db.chapters.course_id == user.course_name) & - (db.sub_chapters.chapter_id == db.chapters.id)) + subchaptersTaught = db( + (db.sub_chapter_taught.course_name == user.course_name) + & ( + db.sub_chapter_taught.chapter_label + == db.chapters.chapter_label + ) + & ( + db.sub_chapter_taught.sub_chapter_label + == db.sub_chapters.sub_chapter_label + ) + & (db.chapters.course_id == user.course_name) + & (db.sub_chapters.chapter_id == db.chapters.id) + ) if subchaptersTaught.isempty(): - practice_message1 = ("The practice period is already started, but your instructor has not" + - " added topics of your course to practice.") - practice_message2 = "Please ask your instructor to add topics to practice." + practice_message1 = ( + "The practice period is already started, but your instructor has not" + + " added topics of your course to practice." + ) + practice_message2 = ( + "Please ask your instructor to add topics to practice." + ) else: - subchaptersTaught = subchaptersTaught.select(db.chapters.chapter_label, - db.chapters.chapter_name, - db.sub_chapters.sub_chapter_label, - orderby=db.chapters.id | db.sub_chapters.id) + subchaptersTaught = subchaptersTaught.select( + db.chapters.chapter_label, + db.chapters.chapter_name, + db.sub_chapters.sub_chapter_label, + orderby=db.chapters.id | db.sub_chapters.id, + ) for subchapterTaught in subchaptersTaught: # We only retrieve questions to be used in flashcards if they are marked for practice # purpose. - questions = _get_qualified_questions(course.base_course, - subchapterTaught.chapters.chapter_label, - subchapterTaught.sub_chapters.sub_chapter_label) + questions = _get_qualified_questions( + course.base_course, + subchapterTaught.chapters.chapter_label, + subchapterTaught.sub_chapters.sub_chapter_label, + ) if len(questions) > 0: # There is at least one qualified question in this subchapter, so insert a flashcard for # the subchapter. @@ -213,39 +283,59 @@ def _get_practice_data(user, timezoneoffset): last_presented=now - datetime.timedelta(1), last_completed=now - datetime.timedelta(1), creation_time=now, - timezoneoffset=timezoneoffset + timezoneoffset=timezoneoffset, ) # Retrieve all the flashcards created for this user in the current course and order them by their order of # creation. - flashcards = db((db.user_topic_practice.course_name == user.course_name) & - (db.user_topic_practice.user_id == user.id)).select(orderby=db.user_topic_practice.id) + flashcards = db( + (db.user_topic_practice.course_name == user.course_name) + & (db.user_topic_practice.user_id == user.id) + ).select(orderby=db.user_topic_practice.id) # We need the following `for` loop to make sure the number of repetitions for both blocking and interleaving # groups are the same. for f in flashcards: - f_logs = db((db.user_topic_practice_log.course_name == user.course_name) & - (db.user_topic_practice_log.user_id == user.id) & - (db.user_topic_practice_log.chapter_label == f.chapter_label) & - (db.user_topic_practice_log.sub_chapter_label == f.sub_chapter_label) - ).select(orderby=db.user_topic_practice_log.end_practice) + f_logs = db( + (db.user_topic_practice_log.course_name == user.course_name) + & (db.user_topic_practice_log.user_id == user.id) + & (db.user_topic_practice_log.chapter_label == f.chapter_label) + & ( + db.user_topic_practice_log.sub_chapter_label + == f.sub_chapter_label + ) + ).select(orderby=db.user_topic_practice_log.end_practice) f["blocking_eligible_date"] = f.next_eligible_date if len(f_logs) > 0: days_to_add = sum([f_log.i_interval for f_log in f_logs[0:-1]]) - days_to_add -= (f_logs[-1].end_practice - f_logs[0].end_practice).days + days_to_add -= ( + f_logs[-1].end_practice - f_logs[0].end_practice + ).days if days_to_add > 0: - f["blocking_eligible_date"] += datetime.timedelta(days=days_to_add) + f["blocking_eligible_date"] += datetime.timedelta( + days=days_to_add + ) if interleaving == 1: # Select only those where enough time has passed since last presentation. - presentable_flashcards = [f for f in flashcards if now_local.date() >= f.next_eligible_date] + presentable_flashcards = [ + f for f in flashcards if now_local.date() >= f.next_eligible_date + ] available_flashcards_num = len(presentable_flashcards) else: # Select only those that are not mastered yet. - presentable_flashcards = [f for f in flashcards - if (f.q * f.e_factor < 12.5 and - f.blocking_eligible_date < practice_settings.end_date and - (f.q != -1 or (f.next_eligible_date - now_local.date()).days != 1))] + presentable_flashcards = [ + f + for f in flashcards + if ( + f.q * f.e_factor < 12.5 + and f.blocking_eligible_date < practice_settings.end_date + and ( + f.q != -1 + or (f.next_eligible_date - now_local.date()).days != 1 + ) + ) + ] available_flashcards_num = len(presentable_flashcards) if len(presentable_flashcards) > 0: # It's okay to continue with the next chapter if there is no more question in the current chapter @@ -253,116 +343,214 @@ def _get_practice_data(user, timezoneoffset): # blocking, because a postponed question from the current chapter could be asked tomorrow, after # some questions from the next chapter that are asked today. presentable_chapter = presentable_flashcards[0].chapter_label - presentable_flashcards = [f for f in presentable_flashcards if f.chapter_label == presentable_chapter] + presentable_flashcards = [ + f + for f in presentable_flashcards + if f.chapter_label == presentable_chapter + ] shuffle(presentable_flashcards) # How many times has this user submitted their practice from the beginning of today (12:00 am) till now? - practiced_log = db((db.user_topic_practice_log.course_name == user.course_name) & - (db.user_topic_practice_log.user_id == user.id) & - (db.user_topic_practice_log.q != 0) & - (db.user_topic_practice_log.q != -1)).select() + practiced_log = db( + (db.user_topic_practice_log.course_name == user.course_name) + & (db.user_topic_practice_log.user_id == user.id) + & (db.user_topic_practice_log.q != 0) + & (db.user_topic_practice_log.q != -1) + ).select() practiced_today_count = 0 for pr in practiced_log: - if (pr.end_practice - datetime.timedelta(hours=pr.timezoneoffset) >= - datetime.datetime(now_local.year, now_local.month, now_local.day, 0, 0, 0, 0)): + if pr.end_practice - datetime.timedelta( + hours=pr.timezoneoffset + ) >= datetime.datetime( + now_local.year, now_local.month, now_local.day, 0, 0, 0, 0 + ): practiced_today_count += 1 - practice_completion_count = _get_practice_completion(user.id, user.course_name, spacing) + practice_completion_count = _get_practice_completion( + user.id, user.course_name, spacing + ) if practice_graded == 1: if spacing == 1: total_possible_points = practice_settings.day_points * max_days points_received = day_points * practice_completion_count else: - total_possible_points = practice_settings.question_points * max_questions + total_possible_points = ( + practice_settings.question_points * max_questions + ) points_received = question_points * practice_completion_count # Calculate the number of questions left for the student to practice today to get the completion point. if spacing == 1: - practice_today_left = min(available_flashcards_num, max(0, questions_to_complete_day - - practiced_today_count)) + practice_today_left = min( + available_flashcards_num, + max(0, questions_to_complete_day - practiced_today_count), + ) else: practice_today_left = available_flashcards_num - return (now, - now_local, - practice_message1, - practice_message2, - practice_graded, - spacing, - interleaving, - practice_completion_count, - remaining_days, - max_days, - max_questions, - day_points, - question_points, - presentable_flashcards, - available_flashcards_num, - practiced_today_count, - questions_to_complete_day, - practice_today_left, - points_received, - total_possible_points, - flashcard_creation_method) - - -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + return ( + now, + now_local, + practice_message1, + practice_message2, + practice_graded, + spacing, + interleaving, + practice_completion_count, + remaining_days, + max_days, + max_questions, + day_points, + question_points, + presentable_flashcards, + available_flashcards_num, + practiced_today_count, + questions_to_complete_day, + practice_today_left, + points_received, + total_possible_points, + flashcard_creation_method, + ) + + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def record_assignment_score(): - score = request.vars.get('score', None) + score = request.vars.get("score", None) assignment_name = request.vars.assignment - assignment = db( - (db.assignments.name == assignment_name) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.name == assignment_name) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) if assignment: assignment_id = assignment.id else: - return json.dumps({'success': False, 'message': "Select an assignment before trying to calculate totals."}) + return json.dumps( + { + "success": False, + "message": "Select an assignment before trying to calculate totals.", + } + ) if score: # Write the score to the grades table # grades table expects row ids for auth_user and assignment - sname = request.vars.get('sid', None) + sname = request.vars.get("sid", None) sid = db((db.auth_user.username == sname)).select(db.auth_user.id).first().id db.grades.update_or_insert( - ((db.grades.auth_user == sid) & - (db.grades.assignment == assignment_id)), + ((db.grades.auth_user == sid) & (db.grades.assignment == assignment_id)), auth_user=sid, assignment=assignment_id, score=score, - manual_total=True + manual_total=True, ) -def _calculate_totals(sid=None, student_rownum=None, assignment_name = None, assignment_id = None): + +def _calculate_totals( + sid=None, student_rownum=None, assignment_name=None, assignment_id=None +): if assignment_id: - assignment = db( - (db.assignments.id == assignment_id) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.id == assignment_id) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) else: - assignment = db( - (db.assignments.name == assignment_name) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.name == assignment_name) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) if assignment: - return do_calculate_totals(assignment, auth.user.course_id, auth.user.course_name, sid, student_rownum, db, settings) + return do_calculate_totals( + assignment, + auth.user.course_id, + auth.user.course_name, + sid, + student_rownum, + db, + settings, + ) else: - return {'success': False, 'message': "Select an assignment before trying to calculate totals."} + return { + "success": False, + "message": "Select an assignment before trying to calculate totals.", + } -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def calculate_totals(): assignment_name = request.vars.assignment - sid = request.vars.get('sid', None) + sid = request.vars.get("sid", None) return json.dumps(_calculate_totals(sid=sid, assignment_name=assignment_name)) -def _autograde(sid=None, student_rownum=None, question_name=None, enforce_deadline=False, assignment_name=None, assignment_id=None, timezoneoffset=None): + +def _autograde( + sid=None, + student_rownum=None, + question_name=None, + enforce_deadline=False, + assignment_name=None, + assignment_id=None, + timezoneoffset=None, +): if assignment_id: - assignment = db( - (db.assignments.id == assignment_id) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.id == assignment_id) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) else: - assignment = db( - (db.assignments.name == assignment_name) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.name == assignment_name) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) if assignment: - count = do_autograde(assignment, auth.user.course_id, auth.user.course_name, sid, student_rownum, question_name, - enforce_deadline, timezoneoffset, db, settings) - return {'success': True, 'message': "autograded {} items".format(count), 'count':count} + count = do_autograde( + assignment, + auth.user.course_id, + auth.user.course_name, + sid, + student_rownum, + question_name, + enforce_deadline, + timezoneoffset, + db, + settings, + ) + return { + "success": True, + "message": "autograded {} items".format(count), + "count": count, + } else: - return {'success': False, 'message': "Select an assignment before trying to autograde."} + return { + "success": False, + "message": "Select an assignment before trying to autograde.", + } @auth.requires_login() @@ -374,118 +562,159 @@ def student_autograde(): sent via LTI (if LTI is configured). """ assignment_id = request.vars.assignment_id - timezoneoffset = session.timezoneoffset if 'timezoneoffset' in session else None + timezoneoffset = session.timezoneoffset if "timezoneoffset" in session else None + res = _autograde( + student_rownum=auth.user.id, + assignment_id=assignment_id, + timezoneoffset=timezoneoffset, + ) - res = _autograde(student_rownum=auth.user.id, - assignment_id=assignment_id, - timezoneoffset=timezoneoffset) - - if not res['success']: - session.flash = "Failed to autograde questions for user id {} for assignment {}".format(auth.user.id, assignment_id) - res = {'success':False} + if not res["success"]: + session.flash = "Failed to autograde questions for user id {} for assignment {}".format( + auth.user.id, assignment_id + ) + res = {"success": False} else: if settings.coursera_mode: - res2 = _calculate_totals(student_rownum=auth.user.id, assignment_id=assignment_id) - if not res2['success']: - session.flash = "Failed to compute totals for user id {} for assignment {}".format(auth.user.id, assignment_id) - res = {'success':False} + res2 = _calculate_totals( + student_rownum=auth.user.id, assignment_id=assignment_id + ) + if not res2["success"]: + session.flash = "Failed to compute totals for user id {} for assignment {}".format( + auth.user.id, assignment_id + ) + res = {"success": False} else: _try_to_send_lti_grade(auth.user.id, assignment_id) return json.dumps(res) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def autograde(): ### This endpoint is hit to autograde one or all students or questions for an assignment - sid = request.vars.get('sid', None) - question_name = request.vars.get('question', None) - enforce_deadline = request.vars.get('enforceDeadline', None) + sid = request.vars.get("sid", None) + question_name = request.vars.get("question", None) + enforce_deadline = request.vars.get("enforceDeadline", None) assignment_name = request.vars.assignment - timezoneoffset = session.timezoneoffset if 'timezoneoffset' in session else None + timezoneoffset = session.timezoneoffset if "timezoneoffset" in session else None + + return json.dumps( + _autograde( + sid=sid, + question_name=question_name, + enforce_deadline=enforce_deadline, + assignment_name=assignment_name, + timezoneoffset=timezoneoffset, + ) + ) - return json.dumps(_autograde(sid=sid, - question_name=question_name, - enforce_deadline=enforce_deadline, - assignment_name=assignment_name, - timezoneoffset=timezoneoffset)) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def send_assignment_score_via_LTI(): assignment_name = request.vars.assignment - sid = request.vars.get('sid', None) - assignment = db( - (db.assignments.name == assignment_name) & (db.assignments.course == auth.user.course_id)).select().first() + sid = request.vars.get("sid", None) + assignment = ( + db( + (db.assignments.name == assignment_name) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) student_row = db((db.auth_user.username == sid)).select(db.auth_user.id).first() _try_to_send_lti_grade(student_row.id, assignment.id) - return json.dumps({'success': True }) - + return json.dumps({"success": True}) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def record_grade(): """ Called from the grading interface when the instructor manually records a grade. """ - if 'acid' not in request.vars or 'sid' not in request.vars: - return json.dumps({'success': False, 'message': "Need problem and user."}) + if "acid" not in request.vars or "sid" not in request.vars: + return json.dumps({"success": False, "message": "Need problem and user."}) - score_str = request.vars.get('grade', 0) + score_str = request.vars.get("grade", 0) if score_str == "": score = 0 else: score = float(score_str) - comment = request.vars.get('comment', None) - if score_str != "" or ('comment' in request.vars and comment != ""): + comment = request.vars.get("comment", None) + if score_str != "" or ("comment" in request.vars and comment != ""): try: - db.question_grades.update_or_insert(( - (db.question_grades.sid == request.vars['sid']) - & (db.question_grades.div_id == request.vars['acid']) - & (db.question_grades.course_name == auth.user.course_name) + db.question_grades.update_or_insert( + ( + (db.question_grades.sid == request.vars["sid"]) + & (db.question_grades.div_id == request.vars["acid"]) + & (db.question_grades.course_name == auth.user.course_name) ), - sid=request.vars['sid'], - div_id=request.vars['acid'], + sid=request.vars["sid"], + div_id=request.vars["acid"], course_name=auth.user.course_name, score=score, - comment=comment) + comment=comment, + ) except IntegrityError: logger.error( - "IntegrityError {} {} {}".format(request.vars['sid'], request.vars['acid'], auth.user.course_name)) - return json.dumps({'response': 'not replaced'}) - return json.dumps({'response': 'replaced'}) + "IntegrityError {} {} {}".format( + request.vars["sid"], request.vars["acid"], auth.user.course_name + ) + ) + return json.dumps({"response": "not replaced"}) + return json.dumps({"response": "replaced"}) else: - return json.dumps({'response': 'not replaced'}) + return json.dumps({"response": "not replaced"}) # create a unique index: question_grades_sid_course_name_div_id_idx" UNIQUE, btree (sid, course_name, div_id) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def get_problem(): """ Called from the instructors grading interface """ - if 'acid' not in request.vars or 'sid' not in request.vars: - return json.dumps({'success': False, 'message': "Need problem and user."}) + if "acid" not in request.vars or "sid" not in request.vars: + return json.dumps({"success": False, "message": "Need problem and user."}) user = db(db.auth_user.username == request.vars.sid).select().first() if not user: - return json.dumps({'success': False, 'message': "User does not exist. Sorry!"}) + return json.dumps({"success": False, "message": "User does not exist. Sorry!"}) res = { - 'id': "%s-%d" % (request.vars.acid, user.id), - 'acid': request.vars.acid, - 'sid': user.id, - 'username': user.username, - 'name': "%s %s" % (user.first_name, user.last_name), - 'code': "" + "id": "%s-%d" % (request.vars.acid, user.id), + "acid": request.vars.acid, + "sid": user.id, + "username": user.username, + "name": "%s %s" % (user.first_name, user.last_name), + "code": "", } # get the deadline associated with the assignment assignment_name = request.vars.assignment if assignment_name and auth.user.course_id: - assignment = db( - (db.assignments.name == assignment_name) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.name == assignment_name) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) deadline = assignment.duedate else: deadline = None @@ -495,21 +724,25 @@ def get_problem(): offset = datetime.timedelta(hours=float(session.timezoneoffset)) logger.debug("setting offset %s %s", offset, deadline + offset) - query = (db.code.acid == request.vars.acid) & (db.code.sid == request.vars.sid) & ( - db.code.course_id == auth.user.course_id) + query = ( + (db.code.acid == request.vars.acid) + & (db.code.sid == request.vars.sid) + & (db.code.course_id == auth.user.course_id) + ) if request.vars.enforceDeadline == "true" and deadline: query = query & (db.code.timestamp < deadline + offset) logger.debug("DEADLINE QUERY = %s", query) c = db(query).select(orderby=db.code.id).last() if c: - res['code'] = c.code + res["code"] = c.code # add prefixes, suffix_code and files that are available # retrieve the db record source = db.source_code(acid=request.vars.acid, course_id=auth.user.course_name) if source and c and c.code: + def get_source(acid): r = db.source_code(acid=acid) if r: @@ -519,19 +752,22 @@ def get_source(acid): if source.includes: # strip off "data-include" - txt = source.includes[len("data-include="):] - included_divs = [x.strip() for x in txt.split(',') if x != ''] + txt = source.includes[len("data-include=") :] + included_divs = [x.strip() for x in txt.split(",") if x != ""] # join together code for each of the includes - res['includes'] = '\n'.join([get_source(acid) for acid in included_divs]) + res["includes"] = "\n".join([get_source(acid) for acid in included_divs]) # logger.debug(res['includes']) if source.suffix_code: - res['suffix_code'] = source.suffix_code + res["suffix_code"] = source.suffix_code # logger.debug(source.suffix_code) - file_divs = [x.strip() for x in source.available_files.split(',') if x != ''] - res['file_includes'] = [{'acid': acid, 'contents': get_source(acid)} for acid in file_divs] + file_divs = [x.strip() for x in source.available_files.split(",") if x != ""] + res["file_includes"] = [ + {"acid": acid, "contents": get_source(acid)} for acid in file_divs + ] return json.dumps(res) + @auth.requires_login() def doAssignment(): @@ -543,46 +779,72 @@ def doAssignment(): return redirect(URL("assignments", "chooseAssignment")) logger.debug("COURSE = %s assignment %s", course, assignment_id) - assignment = db( - (db.assignments.id == assignment_id) & (db.assignments.course == auth.user.course_id)).select().first() + assignment = ( + db( + (db.assignments.id == assignment_id) + & (db.assignments.course == auth.user.course_id) + ) + .select() + .first() + ) if not assignment: - logger.error("NO ASSIGNMENT assign_id = %s course = %s user = %s", assignment_id, course, auth.user.username) + logger.error( + "NO ASSIGNMENT assign_id = %s course = %s user = %s", + assignment_id, + course, + auth.user.username, + ) session.flash = "Could not find login and try again." - return redirect(URL('default', 'index')) + return redirect(URL("default", "index")) - if assignment.visible == 'F' or assignment.visible == None: + if assignment.visible == "F" or assignment.visible == None: if verifyInstructorStatus(auth.user.course_name, auth.user) == False: session.flash = "That assignment is no longer available" - return redirect(URL('assignments', 'chooseAssignment')) + return redirect(URL("assignments", "chooseAssignment")) if assignment.points is None: assignment.points = 0 - questions = db((db.assignment_questions.assignment_id == assignment.id) & \ - (db.assignment_questions.question_id == db.questions.id) & \ - (db.chapters.chapter_label == db.questions.chapter) & \ - ((db.chapters.course_id == course.course_name) | (db.chapters.course_id == course.base_course)) & \ - (db.sub_chapters.chapter_id == db.chapters.id) & \ - (db.sub_chapters.sub_chapter_label == db.questions.subchapter)) \ - .select(db.questions.name, - db.questions.htmlsrc, - db.questions.id, - db.questions.chapter, - db.questions.subchapter, - db.assignment_questions.points, - db.assignment_questions.activities_required, - db.assignment_questions.reading_assignment, - db.chapters.chapter_name, - db.sub_chapters.sub_chapter_name, - orderby=db.assignment_questions.sorting_priority) + questions = db( + (db.assignment_questions.assignment_id == assignment.id) + & (db.assignment_questions.question_id == db.questions.id) + & (db.chapters.chapter_label == db.questions.chapter) + & ( + (db.chapters.course_id == course.course_name) + | (db.chapters.course_id == course.base_course) + ) + & (db.sub_chapters.chapter_id == db.chapters.id) + & (db.sub_chapters.sub_chapter_label == db.questions.subchapter) + ).select( + db.questions.name, + db.questions.htmlsrc, + db.questions.id, + db.questions.chapter, + db.questions.subchapter, + db.assignment_questions.points, + db.assignment_questions.activities_required, + db.assignment_questions.reading_assignment, + db.chapters.chapter_name, + db.sub_chapters.sub_chapter_name, + orderby=db.assignment_questions.sorting_priority, + ) try: - db.useinfo.insert(sid=auth.user.username,act='viewassignment',div_id=assignment.name, - event='page', - timestamp=datetime.datetime.utcnow(),course_id=course.course_name) + db.useinfo.insert( + sid=auth.user.username, + act="viewassignment", + div_id=assignment.name, + event="page", + timestamp=datetime.datetime.utcnow(), + course_id=course.course_name, + ) except: - logger.debug('failed to insert log record for {} in {} : doAssignment '.format(auth.user.username, course.course_name)) + logger.debug( + "failed to insert log record for {} in {} : doAssignment ".format( + auth.user.username, course.course_name + ) + ) questionslist = [] questions_score = 0 @@ -598,23 +860,29 @@ def doAssignment(): if six.PY3: bts = q.questions.htmlsrc else: - bts = bytes(q.questions.htmlsrc).decode('utf8') + bts = bytes(q.questions.htmlsrc).decode("utf8") - htmlsrc = bts.replace('src="../_static/', - 'src="' + get_course_url('_static/')) - htmlsrc = htmlsrc.replace("../_images/", - get_course_url('_images/')) + htmlsrc = bts.replace( + 'src="../_static/', 'src="' + get_course_url("_static/") + ) + htmlsrc = htmlsrc.replace("../_images/", get_course_url("_images/")) else: htmlsrc = None # get score and comment - grade = db((db.question_grades.sid == auth.user.username) & - (db.question_grades.course_name == auth.user.course_name) & - (db.question_grades.div_id == q.questions.name)).select().first() + grade = ( + db( + (db.question_grades.sid == auth.user.username) + & (db.question_grades.course_name == auth.user.course_name) + & (db.question_grades.div_id == q.questions.name) + ) + .select() + .first() + ) if grade: score, comment = grade.score, grade.comment else: - score, comment = 0, 'ungraded' + score, comment = 0, "ungraded" info = dict( htmlsrc=htmlsrc, @@ -626,97 +894,131 @@ def doAssignment(): chapter_name=q.chapters.chapter_name, subchapter_name=q.sub_chapters.sub_chapter_name, name=q.questions.name, - activities_required=q.assignment_questions.activities_required + activities_required=q.assignment_questions.activities_required, ) if q.assignment_questions.reading_assignment: # add to readings ch_name = q.chapters.chapter_name if ch_name not in readings: # add chapter info - completion = db((db.user_chapter_progress.user_id == auth.user.id) & \ - (db.user_chapter_progress.chapter_id == ch_name)).select().first() + completion = ( + db( + (db.user_chapter_progress.user_id == auth.user.id) + & (db.user_chapter_progress.chapter_id == ch_name) + ) + .select() + .first() + ) if not completion: - status = 'notstarted' + status = "notstarted" elif completion.status == 1: - status = 'completed' + status = "completed" elif completion.status == 0: - status = 'started' + status = "started" else: - status = 'notstarted' + status = "notstarted" readings[ch_name] = dict(status=status, subchapters=[]) # add subchapter info # add completion status to info - subch_completion = db((db.user_sub_chapter_progress.user_id == auth.user.id) & \ - ( - db.user_sub_chapter_progress.sub_chapter_id == q.questions.subchapter)).select().first() + subch_completion = ( + db( + (db.user_sub_chapter_progress.user_id == auth.user.id) + & ( + db.user_sub_chapter_progress.sub_chapter_id + == q.questions.subchapter + ) + ) + .select() + .first() + ) if not subch_completion: - status = 'notstarted' + status = "notstarted" elif subch_completion.status == 1: - status = 'completed' + status = "completed" elif subch_completion.status == 0: - status = 'started' + status = "started" else: - status = 'notstarted' - info['status'] = status + status = "notstarted" + info["status"] = status # Make sure we don't create duplicate entries for older courses. New style # courses only have the base course in the database, but old will have both - if info not in readings[ch_name]['subchapters']: - readings[ch_name]['subchapters'].append(info) - readings_score += info['score'] + if info not in readings[ch_name]["subchapters"]: + readings[ch_name]["subchapters"].append(info) + readings_score += info["score"] else: - if info not in questionslist:# add to questions + if info not in questionslist: # add to questions questionslist.append(info) - questions_score += info['score'] + questions_score += info["score"] # put readings into a session variable, to enable next/prev button readings_names = [] for chapname in readings: - readings_names = readings_names + ["{}/{}.html".format(d['chapter'], d['subchapter']) for d in readings[chapname]['subchapters']] + readings_names = readings_names + [ + "{}/{}.html".format(d["chapter"], d["subchapter"]) + for d in readings[chapname]["subchapters"] + ] session.readings = readings_names - return dict(course=course, - course_name=auth.user.course_name, - assignment=assignment, - questioninfo=questionslist, - course_id=auth.user.course_name, - readings=readings, - questions_score=questions_score, - readings_score=readings_score, - # gradeRecordingUrl=URL('assignments', 'record_grade'), - # calcTotalsURL=URL('assignments', 'calculate_totals'), - student_id=auth.user.username, - released=assignment['released']) + return dict( + course=course, + course_name=auth.user.course_name, + assignment=assignment, + questioninfo=questionslist, + course_id=auth.user.course_name, + readings=readings, + questions_score=questions_score, + readings_score=readings_score, + # gradeRecordingUrl=URL('assignments', 'record_grade'), + # calcTotalsURL=URL('assignments', 'calculate_totals'), + student_id=auth.user.username, + released=assignment["released"], + ) + @auth.requires_login() def chooseAssignment(): course = db(db.courses.id == auth.user.course_id).select().first() - assignments = db((db.assignments.course == course.id) & (db.assignments.visible == 'T')).select( - orderby=db.assignments.duedate) - return (dict(assignments=assignments)) + assignments = db( + (db.assignments.course == course.id) & (db.assignments.visible == "T") + ).select(orderby=db.assignments.duedate) + return dict(assignments=assignments) # The rest of the file is about the the spaced practice: + def _get_course_practice_record(course_name): return db(db.course_practice.course_name == course_name).select().first() + def _get_student_practice_grade(sid, course_name): - return db((db.practice_grades.auth_user==sid) & - (db.practice_grades.course_name==course_name)).select().first() + return ( + db( + (db.practice_grades.auth_user == sid) + & (db.practice_grades.course_name == course_name) + ) + .select() + .first() + ) def _get_practice_completion(user_id, course_name, spacing): if spacing == 1: - return db((db.user_topic_practice_Completion.course_name == course_name) & - (db.user_topic_practice_Completion.user_id == user_id)).count() - return db((db.user_topic_practice_log.course_name == course_name) & - (db.user_topic_practice_log.user_id == user_id) & - (db.user_topic_practice_log.q != 0) & - (db.user_topic_practice_log.q != -1)).count() + return db( + (db.user_topic_practice_Completion.course_name == course_name) + & (db.user_topic_practice_Completion.user_id == user_id) + ).count() + return db( + (db.user_topic_practice_log.course_name == course_name) + & (db.user_topic_practice_log.user_id == user_id) + & (db.user_topic_practice_log.q != 0) + & (db.user_topic_practice_log.q != -1) + ).count() + # Called when user clicks "I'm done" button. @auth.requires_login() @@ -725,51 +1027,68 @@ def checkanswer(): sid = auth.user.id course_name = auth.user.course_name # Retrieve the question id from the request object. - qid = request.vars.get('QID', None) + qid = request.vars.get("QID", None) username = auth.user.username # Retrieve the q (quality of answer) from the request object. - q = request.vars.get('q', None) + q = request.vars.get("q", None) # If the question id exists: if request.vars.QID: now = datetime.datetime.utcnow() # Use the autograding function to update the flashcard's e-factor and i-interval. - do_check_answer(sid, - course_name, - qid, - username, - q, - db, - settings, - now, - float(session.timezoneoffset) if 'timezoneoffset' in session else 0) + do_check_answer( + sid, + course_name, + qid, + username, + q, + db, + settings, + now, + float(session.timezoneoffset) if "timezoneoffset" in session else 0, + ) # Since the user wants to continue practicing, continue with the practice action. - redirect(URL('practice')) - session.flash = "Sorry, your score was not saved. Please try submitting your answer again." - redirect(URL('practice')) + redirect(URL("practice")) + session.flash = ( + "Sorry, your score was not saved. Please try submitting your answer again." + ) + redirect(URL("practice")) # Only questions that are marked for practice are eligible for the spaced practice. def _get_qualified_questions(base_course, chapter_label, sub_chapter_label): - return db((db.questions.base_course == base_course) & - ((db.questions.topic == "{}/{}".format(chapter_label, sub_chapter_label)) | - ((db.questions.chapter == chapter_label) & - (db.questions.topic == None) & - (db.questions.subchapter == sub_chapter_label))) & - (db.questions.practice == True)).select() + return db( + (db.questions.base_course == base_course) + & ( + (db.questions.topic == "{}/{}".format(chapter_label, sub_chapter_label)) + | ( + (db.questions.chapter == chapter_label) + & (db.questions.topic == None) + & (db.questions.subchapter == sub_chapter_label) + ) + ) + & (db.questions.practice == True) + ).select() # Gets invoked from lti to set timezone and then redirect to practice() def settz_then_practice(): - return dict(course=get_course_row(), course_name=request.vars.get('course_name', settings.default_course)) + return dict( + course=get_course_row(), + course_name=request.vars.get("course_name", settings.default_course), + ) # Gets invoked from practice if there is no record in course_practice for this course or the practice is not started. @auth.requires_login() def practiceNotStartedYet(): - return dict(course=get_course_row(db.courses.ALL), course_id=auth.user.course_name, - message1=bleach.clean(request.vars.message1 or ''), message2=bleach.clean(request.vars.message2 or '')) + return dict( + course=get_course_row(db.courses.ALL), + course_id=auth.user.course_name, + message1=bleach.clean(request.vars.message1 or ""), + message2=bleach.clean(request.vars.message2 or ""), + ) # Gets invoked when the student requests practicing topics. @@ -778,59 +1097,73 @@ def practice(): if not session.timezoneoffset: session.timezoneoffset = 0 - feedback_saved = request.vars.get('feedback_saved', None) + feedback_saved = request.vars.get("feedback_saved", None) if feedback_saved is None: feedback_saved = "" - (now, - now_local, - message1, - message2, - practice_graded, - spacing, - interleaving, - practice_completion_count, - remaining_days, - max_days, - max_questions, - day_points, - question_points, - presentable_flashcards, - available_flashcards_num, - practiced_today_count, - questions_to_complete_day, - practice_today_left, - points_received, - total_possible_points, - flashcard_creation_method) = _get_practice_data(auth.user, - float(session.timezoneoffset) if 'timezoneoffset' in session else 0) + ( + now, + now_local, + message1, + message2, + practice_graded, + spacing, + interleaving, + practice_completion_count, + remaining_days, + max_days, + max_questions, + day_points, + question_points, + presentable_flashcards, + available_flashcards_num, + practiced_today_count, + questions_to_complete_day, + practice_today_left, + points_received, + total_possible_points, + flashcard_creation_method, + ) = _get_practice_data( + auth.user, float(session.timezoneoffset) if "timezoneoffset" in session else 0 + ) if message1 != "": # session.flash = message1 + " " + message2 - return redirect(URL('practiceNotStartedYet', - vars=dict(message1=message1, - message2=message2))) + return redirect( + URL( + "practiceNotStartedYet", vars=dict(message1=message1, message2=message2) + ) + ) # Since each authenticated user has only one active course, we retrieve the course this way. course = db(db.courses.id == auth.user.course_id).select().first() - all_flashcards = db((db.user_topic_practice.course_name == auth.user.course_name) & - (db.user_topic_practice.user_id == auth.user.id) & - (db.user_topic_practice.chapter_label == db.chapters.chapter_label) & - (db.user_topic_practice.sub_chapter_label == db.sub_chapters.sub_chapter_label) & - (db.chapters.course_id == course.base_course) & - (db.sub_chapters.chapter_id == db.chapters.id)) \ - .select(db.chapters.chapter_name, - db.sub_chapters.sub_chapter_name, - db.user_topic_practice.i_interval, - db.user_topic_practice.next_eligible_date, - db.user_topic_practice.e_factor, - db.user_topic_practice.q, - db.user_topic_practice.last_completed, - orderby=db.user_topic_practice.id) + all_flashcards = db( + (db.user_topic_practice.course_name == auth.user.course_name) + & (db.user_topic_practice.user_id == auth.user.id) + & (db.user_topic_practice.chapter_label == db.chapters.chapter_label) + & ( + db.user_topic_practice.sub_chapter_label + == db.sub_chapters.sub_chapter_label + ) + & (db.chapters.course_id == course.base_course) + & (db.sub_chapters.chapter_id == db.chapters.id) + ).select( + db.chapters.chapter_name, + db.sub_chapters.sub_chapter_name, + db.user_topic_practice.i_interval, + db.user_topic_practice.next_eligible_date, + db.user_topic_practice.e_factor, + db.user_topic_practice.q, + db.user_topic_practice.last_completed, + orderby=db.user_topic_practice.id, + ) for f_card in all_flashcards: if interleaving == 1: - f_card["remaining_days"] = max(0, (f_card.user_topic_practice.next_eligible_date - now_local.date()).days) + f_card["remaining_days"] = max( + 0, + (f_card.user_topic_practice.next_eligible_date - now_local.date()).days, + ) # f_card["mastery_percent"] = int(100 * f_card["remaining_days"] // 55) f_card["mastery_percent"] = int(f_card["remaining_days"]) else: @@ -838,8 +1171,12 @@ def practice(): # I learned that when students under the blocking condition answer something wrong multiple times, # it becomes too difficult for them to pass it and the system asks them the same question many times # (because most subchapters have only one question). To solve this issue, I changed the blocking formula. - f_card["mastery_percent"] = int(100 * f_card.user_topic_practice.e_factor * - f_card.user_topic_practice.q / 12.5) + f_card["mastery_percent"] = int( + 100 + * f_card.user_topic_practice.e_factor + * f_card.user_topic_practice.q + / 12.5 + ) if f_card["mastery_percent"] > 100: f_card["mastery_percent"] = 100 @@ -858,16 +1195,20 @@ def practice(): # Present the first one. flashcard = presentable_flashcards[0] # Get eligible questions. - questions = _get_qualified_questions(course.base_course, - flashcard.chapter_label, - flashcard.sub_chapter_label) + questions = _get_qualified_questions( + course.base_course, flashcard.chapter_label, flashcard.sub_chapter_label + ) # If the student has any flashcards to practice and has not practiced enough to get their points for today or they # have intrinsic motivation to practice beyond what they are expected to do. - if (available_flashcards_num > 0 and - len(questions) > 0 and - (practiced_today_count != questions_to_complete_day or - request.vars.willing_to_continue or - spacing == 0)): + if ( + available_flashcards_num > 0 + and len(questions) > 0 + and ( + practiced_today_count != questions_to_complete_day + or request.vars.willing_to_continue + or spacing == 0 + ) + ): # Find index of the last question asked. question_names = [q.name for q in questions] @@ -880,18 +1221,27 @@ def practice(): question = questions[(qIndex + 1) % len(questions)] # This replacement is to render images - question.htmlsrc = question.htmlsrc.replace('src="../_static/', - 'src="../static/' + course[ - 'course_name'] + '/_static/') - question.htmlsrc = question.htmlsrc.replace("../_images", - "/{}/static/{}/_images".format(request.application, - course.course_name)) + question.htmlsrc = question.htmlsrc.replace( + 'src="../_static/', 'src="../static/' + course["course_name"] + "/_static/" + ) + question.htmlsrc = question.htmlsrc.replace( + "../_images", + "/{}/static/{}/_images".format(request.application, course.course_name), + ) autogradable = 1 # If it is possible to autograde it: - if ((question.autograde is not None) or - (question.question_type is not None and question.question_type in - ['mchoice', 'parsonsprob', 'fillintheblank', 'clickablearea', 'dragndrop'])): + if (question.autograde is not None) or ( + question.question_type is not None + and question.question_type + in [ + "mchoice", + "parsonsprob", + "fillintheblank", + "clickablearea", + "dragndrop", + ] + ): autogradable = 2 questioninfo = [question.htmlsrc, question.name, question.id, autogradable] @@ -900,29 +1250,38 @@ def practice(): flashcard.question_name = question.name # This is required to only check answers after this timestamp in do_check_answer(). flashcard.last_presented = now - flashcard.timezoneoffset = float(session.timezoneoffset) if 'timezoneoffset' in session else 0 + flashcard.timezoneoffset = ( + float(session.timezoneoffset) if "timezoneoffset" in session else 0 + ) flashcard.update_record() else: questioninfo = None # Add a practice completion record for today, if there isn't one already. - practice_completion_today = db((db.user_topic_practice_Completion.course_name == auth.user.course_name) & - (db.user_topic_practice_Completion.user_id == auth.user.id) & - (db.user_topic_practice_Completion.practice_completion_date == now_local.date())) + practice_completion_today = db( + (db.user_topic_practice_Completion.course_name == auth.user.course_name) + & (db.user_topic_practice_Completion.user_id == auth.user.id) + & ( + db.user_topic_practice_Completion.practice_completion_date + == now_local.date() + ) + ) if practice_completion_today.isempty(): db.user_topic_practice_Completion.insert( user_id=auth.user.id, course_name=auth.user.course_name, - practice_completion_date=now_local.date() + practice_completion_date=now_local.date(), + ) + practice_completion_count = _get_practice_completion( + auth.user.id, auth.user.course_name, spacing ) - practice_completion_count = _get_practice_completion(auth.user.id, - auth.user.course_name, - spacing) if practice_graded == 1: # send practice grade via lti, if setup for that lti_record = _get_lti_record(session.oauth_consumer_key) - practice_grade = _get_student_practice_grade(auth.user.id, auth.user.course_name) + practice_grade = _get_student_practice_grade( + auth.user.id, auth.user.course_name + ) course_settings = _get_course_practice_record(auth.user.course_name) if spacing == 1: @@ -932,45 +1291,59 @@ def practice(): total_possible_points = question_points * max_questions points_received = question_points * practice_completion_count - if lti_record and \ - practice_grade and \ - practice_grade.lis_outcome_url and \ - practice_grade.lis_result_sourcedid and \ - course_settings: + if ( + lti_record + and practice_grade + and practice_grade.lis_outcome_url + and practice_grade.lis_result_sourcedid + and course_settings + ): if spacing == 1: - send_lti_grade(assignment_points=max_days, - score=practice_completion_count, - consumer=lti_record.consumer, - secret=lti_record.secret, - outcome_url=practice_grade.lis_outcome_url, - result_sourcedid=practice_grade.lis_result_sourcedid) + send_lti_grade( + assignment_points=max_days, + score=practice_completion_count, + consumer=lti_record.consumer, + secret=lti_record.secret, + outcome_url=practice_grade.lis_outcome_url, + result_sourcedid=practice_grade.lis_result_sourcedid, + ) else: - send_lti_grade(assignment_points=max_questions, - score=practice_completion_count, - consumer=lti_record.consumer, - secret=lti_record.secret, - outcome_url=practice_grade.lis_outcome_url, - result_sourcedid=practice_grade.lis_result_sourcedid) - - return dict(course=course, - q=questioninfo, all_flashcards=all_flashcards, - flashcard_count=available_flashcards_num, - # The number of days the student has completed their practice. - practice_completion_count=practice_completion_count, - remaining_days=remaining_days, max_questions=max_questions, max_days=max_days, - # The number of times remaining to practice today to get the completion point. - practice_today_left=practice_today_left, - # The number of times this user has submitted their practice from the beginning of today (12:00 am) - # till now. - practiced_today_count=practiced_today_count, - total_today_count=min(practice_today_left + practiced_today_count, questions_to_complete_day), - questions_to_complete_day=questions_to_complete_day, - points_received=points_received, - total_possible_points=total_possible_points, - practice_graded=practice_graded, - spacing=spacing, interleaving=interleaving, - flashcard_creation_method=flashcard_creation_method, - feedback_saved=feedback_saved) + send_lti_grade( + assignment_points=max_questions, + score=practice_completion_count, + consumer=lti_record.consumer, + secret=lti_record.secret, + outcome_url=practice_grade.lis_outcome_url, + result_sourcedid=practice_grade.lis_result_sourcedid, + ) + + return dict( + course=course, + q=questioninfo, + all_flashcards=all_flashcards, + flashcard_count=available_flashcards_num, + # The number of days the student has completed their practice. + practice_completion_count=practice_completion_count, + remaining_days=remaining_days, + max_questions=max_questions, + max_days=max_days, + # The number of times remaining to practice today to get the completion point. + practice_today_left=practice_today_left, + # The number of times this user has submitted their practice from the beginning of today (12:00 am) + # till now. + practiced_today_count=practiced_today_count, + total_today_count=min( + practice_today_left + practiced_today_count, questions_to_complete_day + ), + questions_to_complete_day=questions_to_complete_day, + points_received=points_received, + total_possible_points=total_possible_points, + practice_graded=practice_graded, + spacing=spacing, + interleaving=interleaving, + flashcard_creation_method=flashcard_creation_method, + feedback_saved=feedback_saved, + ) # Called when user clicks like or dislike icons. @@ -979,7 +1352,7 @@ def like_dislike(): sid = auth.user.id course_name = auth.user.course_name - likeVal = request.vars.get('likeVal', None) + likeVal = request.vars.get("likeVal", None) if likeVal: db.user_topic_practice_survey.insert( @@ -987,11 +1360,13 @@ def like_dislike(): course_name=course_name, like_practice=likeVal, response_time=datetime.datetime.utcnow(), - timezoneoffset=float(session.timezoneoffset) if 'timezoneoffset' in session else 0 + timezoneoffset=float(session.timezoneoffset) + if "timezoneoffset" in session + else 0, ) - redirect(URL('practice')) + redirect(URL("practice")) session.flash = "Sorry, your request was not saved. Please login and try again." - redirect(URL('practice')) + redirect(URL("practice")) # Called when user submits their feedback at the end of practicing. @@ -1000,7 +1375,7 @@ def practice_feedback(): sid = auth.user.id course_name = auth.user.course_name - feedback = request.vars.get('Feed', None) + feedback = request.vars.get("Feed", None) if feedback: db.user_topic_practice_feedback.insert( @@ -1008,8 +1383,10 @@ def practice_feedback(): course_name=course_name, feedback=feedback, response_time=datetime.datetime.utcnow(), - timezoneoffset=float(session.timezoneoffset) if 'timezoneoffset' in session else 0 + timezoneoffset=float(session.timezoneoffset) + if "timezoneoffset" in session + else 0, ) - redirect(URL('practice', vars=dict(feedback_saved=1))) + redirect(URL("practice", vars=dict(feedback_saved=1))) session.flash = "Sorry, your request was not saved. Please login and try again." - redirect(URL('practice')) + redirect(URL("practice")) diff --git a/controllers/books.py b/controllers/books.py index 88f66e5bf..1025a3e5c 100644 --- a/controllers/books.py +++ b/controllers/books.py @@ -48,37 +48,55 @@ def _route_book(is_published=True): # See `caching selects `_. cache_kwargs = dict(cache=(cache.ram, 3600), cacheable=True) - allow_pairs = 'false' + allow_pairs = "false" # Find the course to access. if auth.user: # Given a logged-in user, use ``auth.user.course_id``. - course = db(db.courses.id == auth.user.course_id).select( - db.courses.course_name, - db.courses.base_course, - db.courses.allow_pairs, - **cache_kwargs).first() + course = ( + db(db.courses.id == auth.user.course_id) + .select( + db.courses.course_name, + db.courses.base_course, + db.courses.allow_pairs, + **cache_kwargs + ) + .first() + ) # Ensure the base course in the URL agrees with the base course in ``course``. If not, ask the user to select a course. if not course or course.base_course != base_course: - session.flash = "{} is not the course your are currently in, switch to or add it to go there".format(base_course) - redirect(URL(c='default', f='courses')) + session.flash = "{} is not the course your are currently in, switch to or add it to go there".format( + base_course + ) + redirect(URL(c="default", f="courses")) - allow_pairs = 'true' if course.allow_pairs else 'false' + allow_pairs = "true" if course.allow_pairs else "false" # Ensure the user has access to this book. - if is_published and not db( - (db.user_courses.user_id == auth.user.id) & - (db.user_courses.course_id == auth.user.course_id) - ).select(db.user_courses.id, **cache_kwargs).first(): + if ( + is_published + and not db( + (db.user_courses.user_id == auth.user.id) + & (db.user_courses.course_id == auth.user.course_id) + ) + .select(db.user_courses.id, **cache_kwargs) + .first() + ): session.flash = "Sorry you are not registered for this course. You can view most Open courses if you log out" - redirect(URL(c='default', f='courses')) + redirect(URL(c="default", f="courses")) else: # Get the base course from the URL. - course = db(db.courses.course_name == base_course).select( - db.courses.course_name, db.courses.base_course, - db.courses.login_required, **cache_kwargs - ).first() + course = ( + db(db.courses.course_name == base_course) + .select( + db.courses.course_name, + db.courses.base_course, + db.courses.login_required, + **cache_kwargs + ) + .first() + ) if not course: # This course doesn't exist. @@ -96,17 +114,24 @@ def dummy(): assert False # Make this an absolute path. - book_path = safe_join(os.path.join(request.folder, 'books', base_course, - 'published' if is_published else 'build', base_course), - *request.args[1:]) + book_path = safe_join( + os.path.join( + request.folder, + "books", + base_course, + "published" if is_published else "build", + base_course, + ), + *request.args[1:] + ) if not book_path: logger.error("No Safe Path for {}".format(request.args[1:])) raise HTTP(404) # See if this is static content. By default, the Sphinx static directory names are ``_static`` and ``_images``. - if request.args(1) in ['_static', '_images']: + if request.args(1) in ["_static", "_images"]: # See the `response `_. Warning: this is slow. Configure a production server to serve this statically. - return response.stream(book_path, 2**20, request=request) + return response.stream(book_path, 2 ** 20, request=request) # It's HTML -- use the file as a template. # @@ -121,77 +146,105 @@ def dummy(): if auth.user: user_id = auth.user.username email = auth.user.email - is_logged_in = 'true' + is_logged_in = "true" # Get the necessary information to update subchapter progress on the page - page_divids = db((db.questions.subchapter == subchapter) & - (db.questions.chapter == chapter) & - (db.questions.from_source == True) & - (db.questions.base_course == base_course)).select(db.questions.name) - div_counts = {q.name:0 for q in page_divids} - sid_counts = db((db.questions.subchapter == subchapter) & - (db.questions.chapter == chapter) & - (db.questions.base_course == base_course) & - (db.questions.from_source == True) & - (db.questions.name == db.useinfo.div_id) & - (db.useinfo.course_id == auth.user.course_name) & - (db.useinfo.sid == auth.user.username)).select(db.useinfo.div_id, distinct=True) + page_divids = db( + (db.questions.subchapter == subchapter) + & (db.questions.chapter == chapter) + & (db.questions.from_source == True) + & (db.questions.base_course == base_course) + ).select(db.questions.name) + div_counts = {q.name: 0 for q in page_divids} + sid_counts = db( + (db.questions.subchapter == subchapter) + & (db.questions.chapter == chapter) + & (db.questions.base_course == base_course) + & (db.questions.from_source == True) + & (db.questions.name == db.useinfo.div_id) + & (db.useinfo.course_id == auth.user.course_name) + & (db.useinfo.sid == auth.user.username) + ).select(db.useinfo.div_id, distinct=True) for row in sid_counts: div_counts[row.div_id] = 1 else: - user_id = 'Anonymous' - email = '' - is_logged_in = 'false' + user_id = "Anonymous" + email = "" + is_logged_in = "false" if session.readings: reading_list = session.readings else: - reading_list = 'null' + reading_list = "null" # TODO: - Add log entry for page view try: - db.useinfo.insert(sid=user_id, act='view', div_id=book_path, - event='page', timestamp=datetime.datetime.utcnow(), - course_id=course.course_name) + db.useinfo.insert( + sid=user_id, + act="view", + div_id=book_path, + event="page", + timestamp=datetime.datetime.utcnow(), + course_id=course.course_name, + ) except: - logger.debug('failed to insert log record for {} in {} : {} {} {}'.format(user_id, course.course_name, book_path, 'page', 'view')) - - user_is_instructor = 'true' if auth.user and verifyInstructorStatus(auth.user.course_name, auth.user) else 'false' - - return dict(course_name=course.course_name, - base_course=base_course, - is_logged_in=is_logged_in, - user_id=user_id, - user_email=email, - is_instructor=user_is_instructor, - allow_pairs=allow_pairs, - readings=XML(reading_list), - activity_info=json.dumps(div_counts), - subchapter_list=_subchaptoc(base_course, chapter)) + logger.debug( + "failed to insert log record for {} in {} : {} {} {}".format( + user_id, course.course_name, book_path, "page", "view" + ) + ) + + user_is_instructor = ( + "true" + if auth.user and verifyInstructorStatus(auth.user.course_name, auth.user) + else "false" + ) + + return dict( + course_name=course.course_name, + base_course=base_course, + is_logged_in=is_logged_in, + user_id=user_id, + user_email=email, + is_instructor=user_is_instructor, + allow_pairs=allow_pairs, + readings=XML(reading_list), + activity_info=json.dumps(div_counts), + subchapter_list=_subchaptoc(base_course, chapter), + ) def _subchaptoc(course, chap): - res = db( (db.chapters.id == db.sub_chapters.chapter_id) & - (db.chapters.course_id == course ) & - (db.chapters.chapter_label == chap) ).select(db.chapters.chapter_num, - db.sub_chapters.sub_chapter_num, - db.chapters.chapter_label, - db.sub_chapters.sub_chapter_label, - db.sub_chapters.sub_chapter_name, orderby=db.sub_chapters.sub_chapter_num, - cache=(cache.ram, 3600), cacheable=True) + res = db( + (db.chapters.id == db.sub_chapters.chapter_id) + & (db.chapters.course_id == course) + & (db.chapters.chapter_label == chap) + ).select( + db.chapters.chapter_num, + db.sub_chapters.sub_chapter_num, + db.chapters.chapter_label, + db.sub_chapters.sub_chapter_label, + db.sub_chapters.sub_chapter_name, + orderby=db.sub_chapters.sub_chapter_num, + cache=(cache.ram, 3600), + cacheable=True, + ) toclist = [] for row in res: sc_url = "{}.html".format(row.sub_chapters.sub_chapter_label) - title = "{}.{} {}".format(row.chapters.chapter_num, - row.sub_chapters.sub_chapter_num, - row.sub_chapters.sub_chapter_name) + title = "{}.{} {}".format( + row.chapters.chapter_num, + row.sub_chapters.sub_chapter_num, + row.sub_chapters.sub_chapter_name, + ) toclist.append(dict(subchap_uri=sc_url, title=title)) return toclist # This is copied verbatim from https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L30. -_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] - if sep not in (None, '/')) +_os_alt_seps = list( + sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, "/") +) # This is copied verbatim from https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L216. @@ -203,14 +256,12 @@ def safe_join(directory, *pathnames): """ parts = [directory] for filename in pathnames: - if filename != '': + if filename != "": filename = posixpath.normpath(filename) for sep in _os_alt_seps: if sep in filename: return None - if os.path.isabs(filename) or \ - filename == '..' or \ - filename.startswith('../'): + if os.path.isabs(filename) or filename == ".." or filename.startswith("../"): return None parts.append(filename) return posixpath.join(*parts) @@ -219,7 +270,10 @@ def safe_join(directory, *pathnames): # Endpoints # ========= # This serves pages directly from the book's build directory. Therefore, restrict access. -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def draft(): return _route_book(False) @@ -230,6 +284,7 @@ def published(): return index() return _route_book() + def index(): """ Called by default (and by published if no args) @@ -238,28 +293,32 @@ def index(): """ - book_list = os.listdir('applications/{}/books'.format(request.application)) - book_list = [book for book in book_list if '.git' not in book] + book_list = os.listdir("applications/{}/books".format(request.application)) + book_list = [book for book in book_list if ".git" not in book] res = [] for book in sorted(book_list): try: # WARNING: This imports from ``applications..books.``. Since ``runestone/books/`` lacks an ``__init__.py``, it will be treated as a `namespace package `_. Therefore, odd things will happen if there are other modules named ``applications..books.`` in the Python path. - config = importlib.import_module('applications.{}.books.{}.conf'.format(request.application, book)) + config = importlib.import_module( + "applications.{}.books.{}.conf".format(request.application, book) + ) except: continue book_info = {} - if hasattr(config, 'navbar_title'): - book_info['title'] = config.navbar_title - elif hasattr(config, 'html_title'): - book_info['title'] = config.html_title - elif hasattr(config, 'html_short_title'): - book_info['title'] = config.html_short_title + if hasattr(config, "navbar_title"): + book_info["title"] = config.navbar_title + elif hasattr(config, "html_title"): + book_info["title"] = config.html_title + elif hasattr(config, "html_short_title"): + book_info["title"] = config.html_short_title else: - book_info['title'] = 'Runestone Book' + book_info["title"] = "Runestone Book" - book_info['url'] = '/{}/books/published/{}/index.html'.format(request.application, book) - book_info['regname'] = book + book_info["url"] = "/{}/books/published/{}/index.html".format( + request.application, book + ) + book_info["regname"] = book res.append(book_info) diff --git a/controllers/dashboard.py b/controllers/dashboard.py index 4c17cd4d9..e136d4469 100644 --- a/controllers/dashboard.py +++ b/controllers/dashboard.py @@ -23,55 +23,63 @@ # select acid, sid from code as T where timestamp = (select max(timestamp) from code where sid=T.sid and acid=T.acid); + class ChapterGet: -# chapnum_map={} -# sub_chapters={} -# subchap_map={} -# subchapnum_map={} -# subchapNum_map={} - def __init__(self,chapters): - - self.Cmap={} - self.Smap={} #dictionary organized by chapter and section labels - self.SAmap={} #organized just by section label + # chapnum_map={} + # sub_chapters={} + # subchap_map={} + # subchapnum_map={} + # subchapNum_map={} + def __init__(self, chapters): + + self.Cmap = {} + self.Smap = {} # dictionary organized by chapter and section labels + self.SAmap = {} # organized just by section label for chapter in chapters: - label=chapter.chapter_label - self.Cmap[label]=chapter - sub_chapters=db(db.sub_chapters.chapter_id==chapter.id).select(db.sub_chapters.ALL) #FIX: get right course_id, too - #NOTE: sub_chapters table doesn't have a course name column in it, kind of a problem - self.Smap[label]={} + label = chapter.chapter_label + self.Cmap[label] = chapter + sub_chapters = db(db.sub_chapters.chapter_id == chapter.id).select( + db.sub_chapters.ALL + ) # FIX: get right course_id, too + # NOTE: sub_chapters table doesn't have a course name column in it, kind of a problem + self.Smap[label] = {} for sub_chapter in sub_chapters: - self.Smap[label][sub_chapter.sub_chapter_label]=sub_chapter - self.SAmap[sub_chapter.sub_chapter_label]=sub_chapter - def ChapterNumber(self,label): + self.Smap[label][sub_chapter.sub_chapter_label] = sub_chapter + self.SAmap[sub_chapter.sub_chapter_label] = sub_chapter + + def ChapterNumber(self, label): """Given the label of a chapter, return its number""" try: return self.Cmap[label].chapter_num except KeyError: return "" - def ChapterName(self,label): + + def ChapterName(self, label): try: return self.Cmap[label].chapter_name except KeyError: return label - def SectionName(self,chapter,section): + + def SectionName(self, chapter, section): try: return self.Smap[chapter][section].sub_chapter_name except KeyError: return section - def SectionNumber(self,chapter,section=None): + + def SectionNumber(self, chapter, section=None): try: - if section==None: - lookup=self.SAmap - section=chapter + if section == None: + lookup = self.SAmap + section = chapter else: - lookup=self.Smap[chapter] + lookup = self.Smap[chapter] return lookup[section].sub_chapter_num except KeyError: return 999 + @auth.requires_login() def index(): selected_chapter = None @@ -79,23 +87,37 @@ def index(): sections = [] if settings.academy_mode and not settings.docker_institution_mode: - if auth.user.course_name in ['thinkcspy','pythonds','JavaReview','JavaReview-RU', 'StudentCSP']: - session.flash = "Student Progress page not available for {}".format(auth.user.course_name) - return redirect(URL('admin','admin')) + if auth.user.course_name in [ + "thinkcspy", + "pythonds", + "JavaReview", + "JavaReview-RU", + "StudentCSP", + ]: + session.flash = "Student Progress page not available for {}".format( + auth.user.course_name + ) + return redirect(URL("admin", "admin")) course = db(db.courses.id == auth.user.course_id).select().first() - assignments = db(db.assignments.course == course.id).select(db.assignments.ALL, orderby=db.assignments.name) - chapters = db(db.chapters.course_id == course.base_course).select(orderby=db.chapters.chapter_num) + assignments = db(db.assignments.course == course.id).select( + db.assignments.ALL, orderby=db.assignments.name + ) + chapters = db(db.chapters.course_id == course.base_course).select( + orderby=db.chapters.chapter_num + ) logger.debug("getting chapters for {}".format(auth.user.course_name)) chapget = ChapterGet(chapters) - for chapter in chapters.find(lambda chapter: chapter.chapter_label==request.vars['chapter']): + for chapter in chapters.find( + lambda chapter: chapter.chapter_label == request.vars["chapter"] + ): selected_chapter = chapter if selected_chapter is None: selected_chapter = chapters.first() logger.debug("making an analyzer") - data_analyzer = DashboardDataAnalyzer(auth.user.course_id,selected_chapter) + data_analyzer = DashboardDataAnalyzer(auth.user.course_id, selected_chapter) logger.debug("loading chapter metrics for course {}".format(auth.user.course_name)) data_analyzer.load_chapter_metrics(selected_chapter) logger.debug("loading problem metrics") @@ -117,14 +139,14 @@ def index(): "chapter_title": chapget.ChapterName(chtmp), "chapter_number": chapget.ChapterNumber(chtmp), "sub_chapter": schtmp, - "sub_chapter_number": chapget.SectionNumber(chtmp,schtmp), - "sub_chapter_title": chapget.SectionName(chtmp,schtmp), + "sub_chapter_number": chapget.SectionNumber(chtmp, schtmp), + "sub_chapter_title": chapget.SectionName(chtmp, schtmp), "correct": stats[2], "correct_mult_attempt": stats[3], "incomplete": stats[1], "not_attempted": stats[0], - "attemptedBy": stats[1] + stats[2] + stats[3] - } + "attemptedBy": stats[1] + stats[2] + stats[3], + } else: entry = { "id": problem_id, @@ -132,35 +154,39 @@ def index(): "chapter": "unknown", "sub_chapter": "unknown", "sub_chapter_number": 0, - "sub_chapter_title":"unknown", + "sub_chapter_title": "unknown", "chapter_title": "unknown", "correct": stats[2], "correct_mult_attempt": stats[3], "incomplete": stats[1], "not_attempted": stats[0], - "attemptedBy": stats[1] + stats[2] + stats[3] - } + "attemptedBy": stats[1] + stats[2] + stats[3], + } questions.append(entry) logger.debug("ADDING QUESTION %s ", entry["chapter"]) logger.debug("getting questions") try: - questions = sorted(questions, key=itemgetter("chapter","sub_chapter_number")) + questions = sorted(questions, key=itemgetter("chapter", "sub_chapter_number")) except: logger.error("FAILED TO SORT {}".format(questions)) logger.debug("starting sub_chapter loop") for sub_chapter, metric in six.iteritems(progress_metrics.sub_chapters): - sections.append({ - "id": metric.sub_chapter_label, - "text": metric.sub_chapter_text, - "name": metric.sub_chapter_name, - "number": chapget.SectionNumber(selected_chapter.chapter_label,metric.sub_chapter_label), - #FIX: Using selected_chapter here might be a kludge - #Better if metric contained chapter numbers associated with sub_chapters - "readPercent": metric.get_completed_percent(), - "startedPercent": metric.get_started_percent(), - "unreadPercent": metric.get_not_started_percent() - }) + sections.append( + { + "id": metric.sub_chapter_label, + "text": metric.sub_chapter_text, + "name": metric.sub_chapter_name, + "number": chapget.SectionNumber( + selected_chapter.chapter_label, metric.sub_chapter_label + ), + # FIX: Using selected_chapter here might be a kludge + # Better if metric contained chapter numbers associated with sub_chapters + "readPercent": metric.get_completed_percent(), + "startedPercent": metric.get_started_percent(), + "unreadPercent": metric.get_not_started_percent(), + } + ) read_data = [] recent_data = [] @@ -168,74 +194,91 @@ def index(): user_activity = data_analyzer.user_activity for user, activity in six.iteritems(user_activity.user_activities): - read_data.append({ - "student":activity.name, # causes username instead of full name to show in the report, but it works ?? how to display the name but use the username on click?? - "sid":activity.username, - "count":activity.get_page_views() - }) - - recent_data.append({ - "student":activity.name, - "sid":activity.username, - "count":activity.get_recent_page_views() - }) + read_data.append( + { + "student": activity.name, # causes username instead of full name to show in the report, but it works ?? how to display the name but use the username on click?? + "sid": activity.username, + "count": activity.get_page_views(), + } + ) + + recent_data.append( + { + "student": activity.name, + "sid": activity.username, + "count": activity.get_recent_page_views(), + } + ) logger.debug("finishing") - studentactivity = [{ - "data":read_data, - "name":"Sections Read" - },{ - "data":read_data, - "name":"Exercises Correct" - },{ - "data":read_data, - "name":"Exercises Missed" - }] - - recentactivity = [{ - "data":recent_data, - "name":"Sections Read" - },{ - "data":recent_data, - "name":"Exercises Correct" - },{ - "data":recent_data, - "name":"Exercises Missed" - }] - - return dict(assignments=assignments, course=course, questions=questions, sections=sections, chapters=chapters, selected_chapter=selected_chapter, studentactivity=studentactivity, recentactivity=recentactivity) + studentactivity = [ + {"data": read_data, "name": "Sections Read"}, + {"data": read_data, "name": "Exercises Correct"}, + {"data": read_data, "name": "Exercises Missed"}, + ] + + recentactivity = [ + {"data": recent_data, "name": "Sections Read"}, + {"data": recent_data, "name": "Exercises Correct"}, + {"data": recent_data, "name": "Exercises Missed"}, + ] + + return dict( + assignments=assignments, + course=course, + questions=questions, + sections=sections, + chapters=chapters, + selected_chapter=selected_chapter, + studentactivity=studentactivity, + recentactivity=recentactivity, + ) + @auth.requires_login() def studentreport(): data_analyzer = DashboardDataAnalyzer(auth.user.course_id) - #todo: Test to see if vars.id is there -- if its not then load_user_metrics will crash - #todo: This seems redundant with assignments/index -- should use this one... id should be text sid + # todo: Test to see if vars.id is there -- if its not then load_user_metrics will crash + # todo: This seems redundant with assignments/index -- should use this one... id should be text sid data_analyzer.load_user_metrics(request.vars.id) data_analyzer.load_assignment_metrics(request.vars.id) chapters = [] - for chapter_label, chapter in six.iteritems(data_analyzer.chapter_progress.chapters): - chapters.append({ - "label": chapter.chapter_label, - "status": chapter.status_text(), - "subchapters": chapter.get_sub_chapter_progress() - }) + for chapter_label, chapter in six.iteritems( + data_analyzer.chapter_progress.chapters + ): + chapters.append( + { + "label": chapter.chapter_label, + "status": chapter.status_text(), + "subchapters": chapter.get_sub_chapter_progress(), + } + ) activity = data_analyzer.formatted_activity.activities - logger.debug("GRADES = %s",data_analyzer.grades) - return dict(course=get_course_row(db.courses.ALL), user=data_analyzer.user, chapters=chapters, activity=activity, assignments=data_analyzer.grades) + logger.debug("GRADES = %s", data_analyzer.grades) + return dict( + course=get_course_row(db.courses.ALL), + user=data_analyzer.user, + chapters=chapters, + activity=activity, + assignments=data_analyzer.grades, + ) + @auth.requires_login() def studentprogress(): return dict(course_name=auth.user.course_name) + @auth.requires_login() def grades(): response.title = "Gradebook" course = db(db.courses.id == auth.user.course_id).select().first() - assignments = db(db.assignments.course == course.id).select(db.assignments.ALL, - orderby=(db.assignments.duedate, db.assignments.id)) + assignments = db(db.assignments.course == course.id).select( + db.assignments.ALL, orderby=(db.assignments.duedate, db.assignments.id) + ) # recalculate total points for each assignment in case the stored # total is out of sync. @@ -243,12 +286,17 @@ def grades(): assign.points = update_total_points(assign.id) students = db( - (db.user_courses.course_id == auth.user.course_id) & - (db.auth_user.id == db.user_courses.user_id) - ).select(db.auth_user.username, db.auth_user.first_name, db.auth_user.last_name, - db.auth_user.id, db.auth_user.email, db.auth_user.course_name, - orderby=(db.auth_user.last_name, db.auth_user.first_name)) - + (db.user_courses.course_id == auth.user.course_id) + & (db.auth_user.id == db.user_courses.user_id) + ).select( + db.auth_user.username, + db.auth_user.first_name, + db.auth_user.last_name, + db.auth_user.id, + db.auth_user.email, + db.auth_user.course_name, + orderby=(db.auth_user.last_name, db.auth_user.first_name), + ) query = """select score, points, assignments.id, auth_user.id from auth_user join grades on (auth_user.id = grades.auth_user) @@ -256,52 +304,72 @@ def grades(): where points is not null and assignments.course = %s and auth_user.id in (select user_id from user_courses where course_id = %s) order by last_name, first_name, assignments.duedate, assignments.id;""" - rows = db.executesql(query, [course['id'], course['id']]) + rows = db.executesql(query, [course["id"], course["id"]]) studentinfo = {} - practice_setting = db(db.course_practice.course_name == auth.user.course_name).select().first() + practice_setting = ( + db(db.course_practice.course_name == auth.user.course_name).select().first() + ) practice_average = 0 total_possible_points = 0 for s in students: practice_grade = 0 if practice_setting: if practice_setting.spacing == 1: - practice_completion_count = db((db.user_topic_practice_Completion.course_name == s.course_name) & - (db.user_topic_practice_Completion.user_id == s.id)).count() - total_possible_points = practice_setting.day_points * practice_setting.max_practice_days - points_received = practice_setting.day_points * practice_completion_count + practice_completion_count = db( + (db.user_topic_practice_Completion.course_name == s.course_name) + & (db.user_topic_practice_Completion.user_id == s.id) + ).count() + total_possible_points = ( + practice_setting.day_points * practice_setting.max_practice_days + ) + points_received = ( + practice_setting.day_points * practice_completion_count + ) else: - practice_completion_count = db((db.user_topic_practice_log.course_name == s.course_name) & - (db.user_topic_practice_log.user_id == s.id) & - (db.user_topic_practice_log.q != 0) & - (db.user_topic_practice_log.q != -1)).count() - total_possible_points = practice_setting.question_points * practice_setting.max_practice_questions - points_received = practice_setting.question_points * practice_completion_count + practice_completion_count = db( + (db.user_topic_practice_log.course_name == s.course_name) + & (db.user_topic_practice_log.user_id == s.id) + & (db.user_topic_practice_log.q != 0) + & (db.user_topic_practice_log.q != -1) + ).count() + total_possible_points = ( + practice_setting.question_points + * practice_setting.max_practice_questions + ) + points_received = ( + practice_setting.question_points * practice_completion_count + ) if total_possible_points > 0: practice_average += 100 * points_received / total_possible_points - studentinfo[s.id] = {'last_name': s.last_name, - 'first_name': s.first_name, - 'username': s.username, - 'email': s.email, - 'practice': '{0:.2f}'.format((100 * points_received/total_possible_points) - ) if total_possible_points > 0 else 'n/a'} + studentinfo[s.id] = { + "last_name": s.last_name, + "first_name": s.first_name, + "username": s.username, + "email": s.email, + "practice": "{0:.2f}".format( + (100 * points_received / total_possible_points) + ) + if total_possible_points > 0 + else "n/a", + } practice_average /= len(students) - practice_average = '{0:.2f}'.format(practice_average) + practice_average = "{0:.2f}".format(practice_average) # create a matrix indexed by user.id and assignment.id gradebook = OrderedDict((sid.id, OrderedDict()) for sid in students) - avgs = OrderedDict((assign.id, {'total':0, 'count':0}) for assign in assignments) + avgs = OrderedDict((assign.id, {"total": 0, "count": 0}) for assign in assignments) for k in gradebook: - gradebook[k] = OrderedDict((assign.id,'n/a') for assign in assignments) + gradebook[k] = OrderedDict((assign.id, "n/a") for assign in assignments) for score, points, assignments_id, auth_user_id in rows: if (score is not None) and (points > 0): percent_grade = 100 * score / points - gradebook_entry = '{0:.2f}'.format(percent_grade) - avgs[assignments_id]['total'] += percent_grade - avgs[assignments_id]['count'] += 1 + gradebook_entry = "{0:.2f}".format(percent_grade) + avgs[assignments_id]["total"] += percent_grade + avgs[assignments_id]["count"] += 1 else: - gradebook_entry = 'n/a' + gradebook_entry = "n/a" gradebook[auth_user_id][assignments_id] = gradebook_entry logger.debug("GRADEBOOK = {}".format(gradebook)) @@ -312,34 +380,39 @@ def grades(): for k in gradebook: studentrow = [] - studentrow.append(studentinfo[k]['first_name']) - studentrow.append(studentinfo[k]['last_name']) - studentrow.append(studentinfo[k]['username']) - studentrow.append(studentinfo[k]['email']) - studentrow.append(studentinfo[k]['practice']) + studentrow.append(studentinfo[k]["first_name"]) + studentrow.append(studentinfo[k]["last_name"]) + studentrow.append(studentinfo[k]["username"]) + studentrow.append(studentinfo[k]["email"]) + studentrow.append(studentinfo[k]["practice"]) for assignment in gradebook[k]: studentrow.append(gradebook[k][assignment]) gradetable.append(studentrow) - #Then build the average row for the table + # Then build the average row for the table for g in avgs: - if avgs[g]['count'] > 0: - averagerow.append('{0:.2f}'.format(avgs[g]['total']/avgs[g]['count'])) + if avgs[g]["count"] > 0: + averagerow.append("{0:.2f}".format(avgs[g]["total"] / avgs[g]["count"])) else: - averagerow.append('n/a') - + averagerow.append("n/a") + + return dict( + course=course, + assignments=assignments, + students=students, + gradetable=gradetable, + averagerow=averagerow, + practice_average=practice_average, + ) - return dict(course=course, - assignments=assignments, students=students, gradetable=gradetable, - averagerow=averagerow, practice_average=practice_average) # This is meant to be called from a form submission, not as a bare controller endpoint @auth.requires_login() def questiongrades(): - if 'sid' not in request.vars: + if "sid" not in request.vars: logger.error("It Appears questiongrades was called without any request vars") session.flash = "Cannot call questiongrades directly" - redirect(URL('dashboard','index')) + redirect(URL("dashboard", "index")) course = db(db.courses.id == auth.user.course_id).select().first() @@ -347,48 +420,82 @@ def questiongrades(): assignment_id = request.vars.assignment_id update_total_points(assignment_id) - assignment = db((db.assignments.id == request.vars.assignment_id) & (db.assignments.course == course.id)).select().first() + assignment = ( + db( + (db.assignments.id == request.vars.assignment_id) + & (db.assignments.course == course.id) + ) + .select() + .first() + ) sid = request.vars.sid - student = db(db.auth_user.username == sid).select(db.auth_user.first_name, db.auth_user.last_name) + student = db(db.auth_user.username == sid).select( + db.auth_user.first_name, db.auth_user.last_name + ) - query = ("""select questions.name, score, points + query = """select questions.name, score, points from questions join assignment_questions on (questions.id = assignment_questions.question_id) left outer join question_grades on (questions.name = question_grades.div_id and sid = %s and question_grades.course_name = %s) - where assignment_id = %s ;""") - rows = db.executesql(query, [sid, course.course_name, assignment['id']]) + where assignment_id = %s ;""" + rows = db.executesql(query, [sid, course.course_name, assignment["id"]]) if not student or not rows: - session.flash = "Student {} not found for course {}".format(sid, course.course_name) - return redirect(URL('dashboard','grades')) + session.flash = "Student {} not found for course {}".format( + sid, course.course_name + ) + return redirect(URL("dashboard", "grades")) + + return dict( + assignment=assignment, student=student, rows=rows, total=0, course=course + ) - return dict(assignment=assignment, student=student, rows=rows, total=0, course=course) def update_total_points(assignment_id): sum_op = db.assignment_questions.points.sum() - total = db(db.assignment_questions.assignment_id == assignment_id).select(sum_op).first()[sum_op] - db(db.assignments.id == assignment_id).update( - points=total + total = ( + db(db.assignment_questions.assignment_id == assignment_id) + .select(sum_op) + .first()[sum_op] ) + db(db.assignments.id == assignment_id).update(points=total) return total + # Note this is meant to be called from a form submission not as a bare endpoint @auth.requires_login() def exercisemetrics(): - if 'chapter' not in request.vars: + if "chapter" not in request.vars: logger.error("It Appears exercisemetrics was called without any request vars") session.flash = "Cannot call exercisemetrics directly" - redirect(URL('dashboard','index')) - chapter = request.vars['chapter'] - base_course = db(db.courses.course_name == auth.user.course_name).select().first().base_course - chapter = db(((db.chapters.course_id == auth.user.course_name) | (db.chapters.course_id == base_course)) & - (db.chapters.chapter_label == chapter)).select().first() + redirect(URL("dashboard", "index")) + chapter = request.vars["chapter"] + base_course = ( + db(db.courses.course_name == auth.user.course_name).select().first().base_course + ) + chapter = ( + db( + ( + (db.chapters.course_id == auth.user.course_name) + | (db.chapters.course_id == base_course) + ) + & (db.chapters.chapter_label == chapter) + ) + .select() + .first() + ) if not chapter: - logger.error("Error -- No Chapter information for {} and {}".format(auth.user.course_name, request.vars['chapter'])) - session.flash = "No Chapter information for {} and {}".format(auth.user.course_name, request.vars['chapter']) - redirect(URL('dashboard','index')) + logger.error( + "Error -- No Chapter information for {} and {}".format( + auth.user.course_name, request.vars["chapter"] + ) + ) + session.flash = "No Chapter information for {} and {}".format( + auth.user.course_name, request.vars["chapter"] + ) + redirect(URL("dashboard", "index")) # TODO: When all old style courses were gone this can be just a base course - data_analyzer = DashboardDataAnalyzer(auth.user.course_id,chapter) + data_analyzer = DashboardDataAnalyzer(auth.user.course_id, chapter) data_analyzer.load_exercise_metrics(request.vars["id"]) problem_metrics = data_analyzer.problem_metrics @@ -401,20 +508,25 @@ def exercisemetrics(): for username, user_responses in six.iteritems(problem_metric.user_responses): responses = user_responses.responses[:4] - responses += [''] * (4 - len(responses)) - answers.append({ - "user":user_responses.user, - "username": user_responses.username, - "answers":responses - }) + responses += [""] * (4 - len(responses)) + answers.append( + { + "user": user_responses.user, + "username": user_responses.username, + "answers": responses, + } + ) for attempts, count in six.iteritems(problem_metric.user_number_responses()): - attempt_histogram.append({ - "attempts": attempts, - "frequency": count - }) - - return dict(course=get_course_row(db.courses.ALL), answers=answers, response_frequency=response_frequency, attempt_histogram=attempt_histogram, exercise_label=problem_metric.problem_text) + attempt_histogram.append({"attempts": attempts, "frequency": count}) + + return dict( + course=get_course_row(db.courses.ALL), + answers=answers, + response_frequency=response_frequency, + attempt_histogram=attempt_histogram, + exercise_label=problem_metric.problem_text, + ) @auth.requires_login() @@ -425,23 +537,28 @@ def subchapoverview(): is_instructor = verifyInstructorStatus(course, auth.user.id) if not is_instructor: session.flash = "Not Authorized for this page" - return redirect(URL('default','user')) + return redirect(URL("default", "user")) - data = pd.read_sql_query(""" + data = pd.read_sql_query( + """ select sid, useinfo.timestamp, div_id, chapter, subchapter from useinfo join questions on div_id = name - where course_id = '{}'""".format(course), settings.database_uri) - data = data[~data.sid.str.contains('@')] - if 'tablekind' not in request.vars: - request.vars.tablekind = 'sccount' + where course_id = '{}'""".format( + course + ), + settings.database_uri, + ) + data = data[~data.sid.str.contains("@")] + if "tablekind" not in request.vars: + request.vars.tablekind = "sccount" values = "timestamp" - idxlist = ['chapter', 'subchapter', 'div_id'] + idxlist = ["chapter", "subchapter", "div_id"] if request.vars.tablekind == "sccount": values = "div_id" afunc = "nunique" - idxlist = ['chapter', 'subchapter'] + idxlist = ["chapter", "subchapter"] elif request.vars.tablekind == "dividmin": afunc = "min" elif request.vars.tablekind == "dividmax": @@ -449,7 +566,7 @@ def subchapoverview(): else: afunc = "count" - pt = data.pivot_table(index=idxlist, values=values, columns='sid', aggfunc=afunc) + pt = data.pivot_table(index=idxlist, values=values, columns="sid", aggfunc=afunc) # TODO: debug tests so these can be live # if pt.empty: @@ -457,23 +574,42 @@ def subchapoverview(): # session.flash = "Error: Not enough data" # return redirect(URL('dashboard','index')) - cmap = pd.read_sql_query("""select chapter_num, sub_chapter_num, chapter_label, sub_chapter_label + cmap = pd.read_sql_query( + """select chapter_num, sub_chapter_num, chapter_label, sub_chapter_label from sub_chapters join chapters on chapters.id = sub_chapters.chapter_id where chapters.course_id = '{}' order by chapter_num, sub_chapter_num; - """.format(thecourse.base_course), settings.database_uri ) + """.format( + thecourse.base_course + ), + settings.database_uri, + ) if request.vars.tablekind != "sccount": pt = pt.reset_index(2) - l = pt.merge(cmap, left_index=True, right_on=['chapter_label', 'sub_chapter_label'], how='outer') - l = l.set_index(['chapter_num','sub_chapter_num']).sort_index() - + l = pt.merge( + cmap, + left_index=True, + right_on=["chapter_label", "sub_chapter_label"], + how="outer", + ) + l = l.set_index(["chapter_num", "sub_chapter_num"]).sort_index() if request.vars.action == "tocsv": - response.headers['Content-Type']='application/vnd.ms-excel' - response.headers['Content-Disposition']= 'attachment; filename=data_for_{}.csv'.format(auth.user.course_name) + response.headers["Content-Type"] = "application/vnd.ms-excel" + response.headers[ + "Content-Disposition" + ] = "attachment; filename=data_for_{}.csv".format(auth.user.course_name) return l.to_csv(na_rep=" ") else: - return dict(course_name=auth.user.course_name, course_id=auth.user.course_name, course=thecourse, - summary=l.to_html(classes="table table-striped table-bordered table-lg", na_rep=" ", table_id="scsummary").replace("NaT","")) + return dict( + course_name=auth.user.course_name, + course_id=auth.user.course_name, + course=thecourse, + summary=l.to_html( + classes="table table-striped table-bordered table-lg", + na_rep=" ", + table_id="scsummary", + ).replace("NaT", ""), + ) diff --git a/controllers/default.py b/controllers/default.py index 95e1b19f5..ccaf12db0 100644 --- a/controllers/default.py +++ b/controllers/default.py @@ -17,22 +17,22 @@ def user(): # this is kinda hacky but it's the only way I can figure out how to pre-populate # the course_id field - if 'everyday' in request.env.http_host: - redirect('http://interactivepython.org/runestone/everyday') + if "everyday" in request.env.http_host: + redirect("http://interactivepython.org/runestone/everyday") if not request.args(0): - redirect(URL('default', 'user/login')) + redirect(URL("default", "user/login")) - if 'register' in request.args(0): + if "register" in request.args(0): # If we can't pre-populate, just set it to blank. # This will force the user to choose a valid course name - db.auth_user.course_id.default = '' + db.auth_user.course_id.default = "" # Otherwise, use the referer URL to try to pre-populate ref = request.env.http_referer if ref: ref = unquote(ref) - if '_next' in ref: + if "_next" in ref: ref = ref.split("_next") url_parts = ref[1].split("/") else: @@ -51,30 +51,54 @@ def user(): # through db.auth_user._after_insert.append(some_function) form = auth() except HTTPError: - session.flash = "Sorry, that service failed. Try a different service or file a bug" - redirect(URL('default', 'index')) + session.flash = ( + "Sorry, that service failed. Try a different service or file a bug" + ) + redirect(URL("default", "index")) - if 'profile' in request.args(0): + if "profile" in request.args(0): try: - sect = db(db.section_users.auth_user == auth.user.id).select(db.section_users.section).first().section + sect = ( + db(db.section_users.auth_user == auth.user.id) + .select(db.section_users.section) + .first() + .section + ) sectname = db(db.sections.id == sect).select(db.sections.name).first() except: sectname = None if sectname: sectname = sectname.name else: - sectname = 'default' + sectname = "default" # Add the section. Taken from ``gluon.tools.addrow`` where ``style='table3cols'``. - form[0].insert(-1, + form[0].insert( + -1, TR( - TD(LABEL('Section Name: ', _for='auth_user_section', _id='auth_user_section__label'), _class='w2p_fl'), - TD(INPUT(_name='section', _type='text', _value=sectname, _id='auth_user_section', _class='string'), _class='w2p_fw'), - TD('', _class='w2p_fc'), - _id='auth_user_section__row', - ) + TD( + LABEL( + "Section Name: ", + _for="auth_user_section", + _id="auth_user_section__label", + ), + _class="w2p_fl", + ), + TD( + INPUT( + _name="section", + _type="text", + _value=sectname, + _id="auth_user_section", + _class="string", + ), + _class="w2p_fw", + ), + TD("", _class="w2p_fc"), + _id="auth_user_section__row", + ), ) # Make the username read-only. - form.element('#auth_user_username')['_readonly'] = True + form.element("#auth_user_username")["_readonly"] = True form.vars.course_id = auth.user.course_name if form.validate(): @@ -84,19 +108,34 @@ def user(): # auth.user session object doesn't automatically update when the DB gets updated auth.user.update(form.vars) # TODO: Why is this necessary? - auth.user.course_name = db(db.auth_user.id == auth.user.id).select()[0].course_name + auth.user.course_name = ( + db(db.auth_user.id == auth.user.id).select()[0].course_name + ) # Add user to default section for course. - sect = db((db.sections.course_id == auth.user.course_id) & - (db.sections.name == form.vars.section)).select(db.sections.id).first() + sect = ( + db( + (db.sections.course_id == auth.user.course_id) + & (db.sections.name == form.vars.section) + ) + .select(db.sections.id) + .first() + ) if sect: - x = db.section_users.update_or_insert(auth_user=auth.user.id, section=sect) - db((auth.user.id == db.section_users.auth_user) & ( - (db.section_users.section != sect) | (db.section_users.section is None))).delete() + x = db.section_users.update_or_insert( + auth_user=auth.user.id, section=sect + ) + db( + (auth.user.id == db.section_users.auth_user) + & ( + (db.section_users.section != sect) + | (db.section_users.section is None) + ) + ).delete() # select from sections where course_id = auth_user.course_id and section.name = 'default' # add a row to section_users for this user with the section selected. - redirect(URL('default', 'index')) + redirect(URL("default", "index")) - if 'register' in request.args(0): + if "register" in request.args(0): # The validation function ``IS_COURSE_ID`` in ``models/db.py`` changes the course name supplied to a course ID. If the overall form doesn't validate, the value when the form is re-displayed with errors will contain the ID instead of the course name. Change it back to the course name. Note: if the user enters a course for the course name, it will be displayed as the corresponding course name after a failed validation. I don't think this case is important enough to fix. try: course_id = int(form.vars.course_id) @@ -108,8 +147,11 @@ def user(): # this looks horrible but it seems to be the only way to add a CSS class to the submit button try: - form.element(_id='submit_record__row')[1][0]['_class'] = 'btn btn-default' - except (AttributeError, TypeError): # not all auth methods actually have a submit button (e.g. user/not_authorized) + form.element(_id="submit_record__row")[1][0]["_class"] = "btn btn-default" + except ( + AttributeError, + TypeError, + ): # not all auth methods actually have a submit button (e.g. user/not_authorized) pass return dict(form=form) @@ -118,22 +160,33 @@ def user(): # to add a custom function to deal with donation and/or creating a course # May have to disable auto_login ?? -def download(): return response.download(request, db) + +def download(): + return response.download(request, db) -def call(): return service() +def call(): + return service() # Determine the student price for a given ``course_id``. Returns a value in cents, where 0 cents indicates a free book. def _course_price(course_id): # Look for a student price for this course. - course = db(db.courses.id == course_id).select(db.courses.student_price, db.courses.base_course).first() + course = ( + db(db.courses.id == course_id) + .select(db.courses.student_price, db.courses.base_course) + .first() + ) assert course price = course.student_price # Only look deeper if a price isn't set (even a price of 0). if price is None: # See if the base course has a student price. - base_course = db(db.courses.course_name == course.base_course).select(db.courses.student_price).first() + base_course = ( + db(db.courses.course_name == course.base_course) + .select(db.courses.student_price) + .first() + ) # If this is already a base course, we're done. if base_course: price = base_course.student_price @@ -144,18 +197,26 @@ def _course_price(course_id): @auth.requires_login() def payment(): # The payment will be made for ``auth.user.course_id``. Get the corresponding course name. - course = db(db.courses.id == auth.user.course_id).select(db.courses.course_name).first() + course = ( + db(db.courses.id == auth.user.course_id).select(db.courses.course_name).first() + ) assert course.course_name form = StripeForm( pk=settings.STRIPE_PUBLISHABLE_KEY, sk=settings.STRIPE_SECRET_KEY, amount=_course_price(auth.user.course_id), - description="Access to the {} textbook from {}".format(course.course_name, settings.title) + description="Access to the {} textbook from {}".format( + course.course_name, settings.title + ), ).process() if form.accepted: # Save the payment info, then redirect to the index. - user_courses_id = db.user_courses.insert(user_id=auth.user.id, course_id=auth.user.course_id) - db.payments.insert(user_courses_id=user_courses_id, charge_id=form.response['id']) + user_courses_id = db.user_courses.insert( + user_id=auth.user.id, course_id=auth.user.course_id + ) + db.payments.insert( + user_courses_id=user_courses_id, charge_id=form.response["id"] + ) db.commit() return dict(request=request, course=course, payment_success=True) elif form.errors: @@ -165,20 +226,28 @@ def payment(): return dict(html=html, payment_success=None) - @auth.requires_login() def index(): -# print("REFERER = ", request.env.http_referer) + # print("REFERER = ", request.env.http_referer) - course = db(db.courses.id == auth.user.course_id).select(db.courses.course_name, db.courses.base_course).first() + course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.course_name, db.courses.base_course) + .first() + ) - if not course or 'boguscourse' in course.course_name: + if not course or "boguscourse" in course.course_name: # if login was handled by Janrain, user didn't have a chance to choose the course_id; # redirect them to the profile page to choose one - redirect('/%s/default/user/profile?_next=/%s/default/index' % (request.application, request.application)) + redirect( + "/%s/default/user/profile?_next=/%s/default/index" + % (request.application, request.application) + ) else: in_db = db( - (db.user_courses.user_id == auth.user.id) & (db.user_courses.course_id == auth.user.course_id)).select() + (db.user_courses.user_id == auth.user.id) + & (db.user_courses.course_id == auth.user.course_id) + ).select() db_check = [] for row in in_db: db_check.append(row) @@ -187,32 +256,50 @@ def index(): price = _course_price(auth.user.course_id) # If the price is non-zero, then require a payment. Otherwise, ask for a donation. if price > 0: - redirect(URL('payment')) + redirect(URL("payment")) else: session.request_donation = True db.user_courses.insert(user_id=auth.user.id, course_id=auth.user.course_id) try: logger.debug("INDEX - checking for progress table") - chapter_label = db(db.chapters.course_id == course.base_course).select().first().chapter_label - logger.debug("LABEL = %s user_id = %s course_name = %s", chapter_label, auth.user.id, auth.user.course_name) - if db((db.user_sub_chapter_progress.user_id == auth.user.id) & - (db.user_sub_chapter_progress.chapter_id == chapter_label)).count() == 0: - db.executesql(''' + chapter_label = ( + db(db.chapters.course_id == course.base_course) + .select() + .first() + .chapter_label + ) + logger.debug( + "LABEL = %s user_id = %s course_name = %s", + chapter_label, + auth.user.id, + auth.user.course_name, + ) + if ( + db( + (db.user_sub_chapter_progress.user_id == auth.user.id) + & (db.user_sub_chapter_progress.chapter_id == chapter_label) + ).count() + == 0 + ): + db.executesql( + """ INSERT INTO user_sub_chapter_progress(user_id, chapter_id,sub_chapter_id, status, start_date) SELECT %s, chapters.chapter_label, sub_chapters.sub_chapter_label, -1, now() FROM chapters, sub_chapters where sub_chapters.chapter_id = chapters.id and chapters.course_id = '%s'; - ''' % (auth.user.id, course.base_course)) + """ + % (auth.user.id, course.base_course) + ) except: session.flash = "Your course is not set up to track your progress" # todo: check course.course_name make sure it is valid if not then redirect to a nicer page. if session.request_donation: del session.request_donation - redirect(URL(c='default', f='donate')) + redirect(URL(c="default", f="donate")) if session.build_course: del session.build_course - redirect(URL(c='designer', f='index')) + redirect(URL(c="designer", f="index")) # See if we need to do a redirect from LTI. if session.lti_url_next: @@ -223,14 +310,14 @@ def index(): # check number of classes, if more than 1, send to course selection, if only 1, send to book num_courses = db(db.user_courses.user_id == auth.user.id).count() # Don't redirect when there's only one course for testing. Since the static files don't exist, this produces a server error ``invalid file``. - if num_courses == 1 and os.environ.get('WEB2PY_CONFIG') != 'test': - redirect(get_course_url('index.html')) - redirect(URL(c='default', f='courses')) + if num_courses == 1 and os.environ.get("WEB2PY_CONFIG") != "test": + redirect(get_course_url("index.html")) + redirect(URL(c="default", f="courses")) def error(): # As recommended in http://web2py.com/books/default/chapter/29/04/the-core#Routes-on-error, pass on the error code that brought us here. TODO: This actually returns a 500 (Internal server error). ??? - #response.status = request.vars.code + # response.status = request.vars.code return dict() @@ -246,48 +333,65 @@ def ack(): def bio(): existing_record = db(db.user_biography.user_id == auth.user.id).select().first() db.user_biography.laptop_type.widget = SQLFORM.widgets.radio.widget - form = SQLFORM(db.user_biography, existing_record, - showid=False, - fields=['prefered_name', 'interesting_fact', 'programming_experience', 'laptop_type', 'image'], - keepvalues=True, - upload=URL('download'), - formstyle='table3cols', - col3={ - 'prefered_name': "Name you would like to be called by in class. Pronunciation hints are also welcome!", - 'interesting_fact': "Tell me something interesting about your outside activities that you wouldn't mind my mentioning in class. For example, are you the goalie for the UM soccer team? An officer in a club or fraternity? An expert on South American insects? Going into the Peace Corps after graduation? Have a company that you started last summer? Have an unusual favorite color?", - 'programming_experience': "Have you ever done any programming before? If so, please describe briefly. (Note: no prior programming experience is required for this course. I just like to know whether you have programmed before.)", - 'image': 'I use a flashcard app to help me learn student names. Please provide a recent photo. (Optional. If you have religious or privacy or other objections to providing a photo, feel free to skip this.)', - 'laptop_type': "Do you have a laptop you can bring to class? If so, what kind?", - 'confidence': "On a 1-5 scale, how confident are you that you can learn to program?" - } - ) + form = SQLFORM( + db.user_biography, + existing_record, + showid=False, + fields=[ + "prefered_name", + "interesting_fact", + "programming_experience", + "laptop_type", + "image", + ], + keepvalues=True, + upload=URL("download"), + formstyle="table3cols", + col3={ + "prefered_name": "Name you would like to be called by in class. Pronunciation hints are also welcome!", + "interesting_fact": "Tell me something interesting about your outside activities that you wouldn't mind my mentioning in class. For example, are you the goalie for the UM soccer team? An officer in a club or fraternity? An expert on South American insects? Going into the Peace Corps after graduation? Have a company that you started last summer? Have an unusual favorite color?", + "programming_experience": "Have you ever done any programming before? If so, please describe briefly. (Note: no prior programming experience is required for this course. I just like to know whether you have programmed before.)", + "image": "I use a flashcard app to help me learn student names. Please provide a recent photo. (Optional. If you have religious or privacy or other objections to providing a photo, feel free to skip this.)", + "laptop_type": "Do you have a laptop you can bring to class? If so, what kind?", + "confidence": "On a 1-5 scale, how confident are you that you can learn to program?", + }, + ) form.vars.user_id = auth.user.id if form.process().accepted: - session.flash = 'form accepted' - redirect(URL('default', 'bio')) + session.flash = "form accepted" + redirect(URL("default", "bio")) elif form.errors: - response.flash = 'form has errors' + response.flash = "form has errors" return dict(form=form) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def bios(): # go to /default/bios and then click on TSV (not CSV) to export properly with First and Last names showing # instead of id get only the people in the course you are instructor for - q = (db.user_biography.user_id == db.auth_user.id) & (db.auth_user.course_id == auth.user.course_id) - fields = [db.user_biography.image, - db.user_biography.prefered_name, - db.user_biography.user_id, - db.user_biography.interesting_fact, - db.user_biography.programming_experience, - db.user_biography.laptop_type, - db.auth_user.email] + q = (db.user_biography.user_id == db.auth_user.id) & ( + db.auth_user.course_id == auth.user.course_id + ) + fields = [ + db.user_biography.image, + db.user_biography.prefered_name, + db.user_biography.user_id, + db.user_biography.interesting_fact, + db.user_biography.programming_experience, + db.user_biography.laptop_type, + db.auth_user.email, + ] # headers that make it easy to import into Flashcards Deluxe - headers = {'user_biography.image': 'Picture 1', - 'user_biography.prefered_name': 'Text 2', - 'user_biography.user_id': 'Text 3', - 'user_biography.interesting_fact': 'Text 4', - 'user_biography.programming_experience': 'Text 5'} + headers = { + "user_biography.image": "Picture 1", + "user_biography.prefered_name": "Text 2", + "user_biography.user_id": "Text 3", + "user_biography.interesting_fact": "Text 4", + "user_biography.programming_experience": "Text 5", + } bios_form = SQLFORM.grid(q, fields=fields, headers=headers) return dict(bios=bios_form) @@ -317,9 +421,13 @@ def remove(): @auth.requires_login() def coursechooser(): if not request.args(0): - redirect(URL('default', 'courses')) + redirect(URL("default", "courses")) - res = db(db.courses.course_name == request.args[0]).select(db.courses.id, db.courses.base_course).first() + res = ( + db(db.courses.course_name == request.args[0]) + .select(db.courses.id, db.courses.base_course) + .first() + ) if res: db(db.auth_user.id == auth.user.id).update(course_id=res.id) @@ -330,53 +438,74 @@ def coursechooser(): logger.debug("COURSECHOOSER checking for progress table %s ", res) if res1.count() > 0: chapter_label = res1.select().first().chapter_label - if db((db.user_sub_chapter_progress.user_id == auth.user.id) & - (db.user_sub_chapter_progress.chapter_id == chapter_label)).count() == 0: - logger.debug("SETTING UP PROGRESS for %s %s", auth.user.username, auth.user.course_name) - db.executesql(''' + if ( + db( + (db.user_sub_chapter_progress.user_id == auth.user.id) + & (db.user_sub_chapter_progress.chapter_id == chapter_label) + ).count() + == 0 + ): + logger.debug( + "SETTING UP PROGRESS for %s %s", + auth.user.username, + auth.user.course_name, + ) + db.executesql( + """ INSERT INTO user_sub_chapter_progress(user_id, chapter_id,sub_chapter_id, status) SELECT %s, chapters.chapter_label, sub_chapters.sub_chapter_label, -1 FROM chapters, sub_chapters where sub_chapters.chapter_id = chapters.id and chapters.course_id = %s; - ''', (auth.user.id, auth.user.course_name)) + """, + (auth.user.id, auth.user.course_name), + ) - redirect(get_course_url('index.html')) + redirect(get_course_url("index.html")) else: - redirect('/%s/default/user/profile?_next=/%s/default/index' % (request.application, request.application)) + redirect( + "/%s/default/user/profile?_next=/%s/default/index" + % (request.application, request.application) + ) @auth.requires_login() def removecourse(): if not request.args(0): - redirect(URL('default', 'courses')) + redirect(URL("default", "courses")) if settings.academy_mode: - course_id_query = db(db.courses.course_name == request.args[0]).select(db.courses.id) - # todo: properly encode course_names to handle courses with special characters - # Check if they're about to remove their currently active course - auth_query = db(db.auth_user.id == auth.user.id).select() - for row in auth_query: - if row.course_name == request.args[0] and course_id_query: - session.flash = T("Sorry, you cannot remove your current active course.") - else: - db((db.user_courses.user_id == auth.user.id) & - (db.user_courses.course_id == course_id_query[0].id)).delete() + course_id_query = db(db.courses.course_name == request.args[0]).select( + db.courses.id + ) + # todo: properly encode course_names to handle courses with special characters + # Check if they're about to remove their currently active course + auth_query = db(db.auth_user.id == auth.user.id).select() + for row in auth_query: + if row.course_name == request.args[0] and course_id_query: + session.flash = T( + "Sorry, you cannot remove your current active course." + ) + else: + db( + (db.user_courses.user_id == auth.user.id) + & (db.user_courses.course_id == course_id_query[0].id) + ).delete() - redirect('/%s/default/courses' % request.application) + redirect("/%s/default/courses" % request.application) def reportabug(): - path = os.path.join(request.folder, 'errors') - course = request.vars['course'] - uri = request.vars['page'] - username = 'anonymous' - email = 'anonymous' + path = os.path.join(request.folder, "errors") + course = request.vars["course"] + uri = request.vars["page"] + username = "anonymous" + email = "anonymous" code = None ticket = None registered_user = False if request.vars.code: code = request.vars.code - ticket = request.vars.ticket.split('/')[1] + ticket = request.vars.ticket.split("/")[1] uri = request.vars.requested_uri error = RestrictedError() error.load(request, request.application, os.path.join(path, ticket)) @@ -388,52 +517,91 @@ def reportabug(): course = auth.user.course_name registered_user = True - return dict(course=course, uri=uri, username=username, email=email, code=code, ticket=ticket, - registered_user=registered_user) + return dict( + course=course, + uri=uri, + username=username, + email=email, + code=code, + ticket=ticket, + registered_user=registered_user, + ) @auth.requires_login() def sendreport(): if settings.academy_mode: - if request.vars['bookerror'] == 'on': - basecourse = db(db.courses.course_name == request.vars['coursename']).select().first().base_course + if request.vars["bookerror"] == "on": + basecourse = ( + db(db.courses.course_name == request.vars["coursename"]) + .select() + .first() + .base_course + ) if basecourse is None: - url = 'https://api.github.com/repos/RunestoneInteractive/%s/issues' % request.vars['coursename'] + url = ( + "https://api.github.com/repos/RunestoneInteractive/%s/issues" + % request.vars["coursename"] + ) else: - url = 'https://api.github.com/repos/RunestoneInteractive/%s/issues' % basecourse + url = ( + "https://api.github.com/repos/RunestoneInteractive/%s/issues" + % basecourse + ) else: - url = 'https://api.github.com/repos/RunestoneInteractive/RunestoneComponents/issues' + url = "https://api.github.com/repos/RunestoneInteractive/RunestoneComponents/issues" reqsession = requests.Session() - reqsession.auth = ('token', settings.github_token) - coursename = request.vars['coursename'] if request.vars['coursename'] else "None Provided" - pagename = request.vars['pagename'] if request.vars['pagename'] else "None Provided" - details = request.vars['bugdetails'] if request.vars['bugdetails'] else "None Provided" - uname = request.vars['username'] if request.vars['username'] else "anonymous" - uemail = request.vars['useremail'] if request.vars['useremail'] else "no_email" - userinfo = uname + ' ' + uemail - - body = 'Error reported in course ' + coursename + ' on page ' + pagename + ' by user ' + userinfo + '\n' + details - issue = {'title': request.vars['bugtitle'], - 'body': body} + reqsession.auth = ("token", settings.github_token) + coursename = ( + request.vars["coursename"] + if request.vars["coursename"] + else "None Provided" + ) + pagename = ( + request.vars["pagename"] if request.vars["pagename"] else "None Provided" + ) + details = ( + request.vars["bugdetails"] + if request.vars["bugdetails"] + else "None Provided" + ) + uname = request.vars["username"] if request.vars["username"] else "anonymous" + uemail = request.vars["useremail"] if request.vars["useremail"] else "no_email" + userinfo = uname + " " + uemail + + body = ( + "Error reported in course " + + coursename + + " on page " + + pagename + + " by user " + + userinfo + + "\n" + + details + ) + issue = {"title": request.vars["bugtitle"], "body": body} logger.debug("POSTING ISSUE %s ", issue) r = reqsession.post(url, json.dumps(issue)) if r.status_code == 201: - session.flash = 'Successfully created Issue "%s"' % request.vars['bugtitle'] + session.flash = 'Successfully created Issue "%s"' % request.vars["bugtitle"] else: - session.flash = 'Could not create Issue "%s"' % request.vars['bugtitle'] + session.flash = 'Could not create Issue "%s"' % request.vars["bugtitle"] logger.debug("POST STATUS = %s", r.status_code) course_check = 0 if auth.user: course_check = db(db.user_courses.user_id == auth.user.id).count() - if course_check == 1 and request.vars['coursename']: - redirect('/%s/static/%s/index.html' % (request.application, request.vars['coursename'])) + if course_check == 1 and request.vars["coursename"]: + redirect( + "/%s/static/%s/index.html" + % (request.application, request.vars["coursename"]) + ) elif course_check > 1: - redirect('/%s/default/courses' % request.application) + redirect("/%s/default/courses" % request.application) else: - redirect('/%s/default/' % request.application) - redirect('/%s/default/' % request.application) + redirect("/%s/default/" % request.application) + redirect("/%s/default/" % request.application) def terms(): @@ -443,9 +611,11 @@ def terms(): def privacy(): return dict(private={}) + def ct_addendum(): return dict(private={}) + def donate(): if request.vars.donate: amt = request.vars.donate @@ -455,18 +625,30 @@ def donate(): amt = None return dict(donate=amt) + @auth.requires_login() def delete(): - if request.vars['deleteaccount']: - logger.error("deleting account {} for {}".format(auth.user.id, auth.user.username)) + if request.vars["deleteaccount"]: + logger.error( + "deleting account {} for {}".format(auth.user.id, auth.user.username) + ) session.flash = "Account Deleted" db(db.auth_user.id == auth.user.id).delete() db(db.useinfo.sid == auth.user.username).delete() db(db.code.sid == auth.user.username).delete() db(db.acerror_log.sid == auth.user.username).delete() - for t in ['clickablearea','codelens','dragndrop','fitb','lp','mchoice','parsons','shortanswer']: - db(db['{}_answers'.format(t)].sid == auth.user.username).delete() - - auth.logout() #logout user and redirect to home page + for t in [ + "clickablearea", + "codelens", + "dragndrop", + "fitb", + "lp", + "mchoice", + "parsons", + "shortanswer", + ]: + db(db["{}_answers".format(t)].sid == auth.user.username).delete() + + auth.logout() # logout user and redirect to home page else: - redirect(URL('default','user/profile')) + redirect(URL("default", "user/profile")) diff --git a/controllers/designer.py b/controllers/designer.py index 2f1a4884c..c50b73676 100644 --- a/controllers/designer.py +++ b/controllers/designer.py @@ -13,6 +13,7 @@ ## - call exposes all registered services (none by default) ######################################################################### + @auth.requires_login() def index(): basicvalues = {} @@ -21,41 +22,51 @@ def index(): example action using the internationalization operator T and flash rendered by views/default/index.html or views/generic.html """ - #response.flash = "Welcome to CourseWare Manager!" - - basicvalues["message"]=T('Build a Custom Course') - basicvalues["descr"]=T('''This page allows you to select a book for your own class. You will have access to all student activities in your course. - To begin, enter a project name below.''') - #return dict(message=T('Welcome to CourseWare Manager')) + # response.flash = "Welcome to CourseWare Manager!" + + basicvalues["message"] = T("Build a Custom Course") + basicvalues["descr"] = T( + """This page allows you to select a book for your own class. You will have access to all student activities in your course. + To begin, enter a project name below.""" + ) + # return dict(message=T('Welcome to CourseWare Manager')) return basicvalues + def build(): buildvalues = {} if settings.academy_mode: - buildvalues['pname']=request.vars.projectname - buildvalues['pdescr']=request.vars.projectdescription + buildvalues["pname"] = request.vars.projectname + buildvalues["pdescr"] = request.vars.projectdescription - existing_course = db(db.courses.course_name == request.vars.projectname).select().first() + existing_course = ( + db(db.courses.course_name == request.vars.projectname).select().first() + ) if existing_course: - return dict(mess='That name has already been used.', building=False) - + return dict(mess="That name has already been used.", building=False) # if make instructor add row to auth_membership - if 'instructor' in request.vars: - gid = db(db.auth_group.role == 'instructor').select(db.auth_group.id).first() - db.auth_membership.insert(user_id=auth.user.id,group_id=gid) + if "instructor" in request.vars: + gid = ( + db(db.auth_group.role == "instructor").select(db.auth_group.id).first() + ) + db.auth_membership.insert(user_id=auth.user.id, group_id=gid) # todo: Here we can add some processing to check for an A/B testing course - if path.exists(path.join(request.folder,'books',request.vars.coursetype+"_A")): - base_course = request.vars.coursetype + "_" + random.sample("AB",1)[0] + if path.exists( + path.join(request.folder, "books", request.vars.coursetype + "_A") + ): + base_course = request.vars.coursetype + "_" + random.sample("AB", 1)[0] else: base_course = request.vars.coursetype - if request.vars.startdate == '': + if request.vars.startdate == "": request.vars.startdate = datetime.date.today() else: - date = request.vars.startdate.split('/') - request.vars.startdate = datetime.date(int(date[2]), int(date[0]), int(date[1])) + date = request.vars.startdate.split("/") + request.vars.startdate = datetime.date( + int(date[2]), int(date[0]), int(date[1]) + ) if not request.vars.institution: institution = "Not Provided" @@ -63,40 +74,50 @@ def build(): institution = request.vars.institution if not request.vars.python3: - python3 = 'false' + python3 = "false" else: - python3 = 'true' + python3 = "true" if not request.vars.loginreq: - login_required = 'false' + login_required = "false" else: - login_required = 'true' - - cid = db.courses.update_or_insert(course_name=request.vars.projectname, - term_start_date=request.vars.startdate, - institution=institution, - base_course=base_course, - login_required = login_required, - python3=python3) + login_required = "true" + cid = db.courses.update_or_insert( + course_name=request.vars.projectname, + term_start_date=request.vars.startdate, + institution=institution, + base_course=base_course, + login_required=login_required, + python3=python3, + ) # enrol the user in their new course - db(db.auth_user.id == auth.user.id).update(course_id = cid) + db(db.auth_user.id == auth.user.id).update(course_id=cid) db.course_instructor.insert(instructor=auth.user.id, course=cid) - auth.user.update(course_name=request.vars.projectname) # also updates session info + auth.user.update( + course_name=request.vars.projectname + ) # also updates session info auth.user.update(course_id=cid) - db.executesql(''' + db.executesql( + """ INSERT INTO user_courses(user_id, course_id) SELECT %s, %s - ''' % (auth.user.id, cid)) + """ + % (auth.user.id, cid) + ) # Create a default section for this course and add the instructor. - sectid = db.sections.update_or_insert(name='default',course_id=cid) - db.section_users.update_or_insert(auth_user=auth.user.id,section=sectid) + sectid = db.sections.update_or_insert(name="default", course_id=cid) + db.section_users.update_or_insert(auth_user=auth.user.id, section=sectid) - course_url=path.join('/',request.application,"static",request.vars.projectname,"index.html") + course_url = path.join( + "/", request.application, "static", request.vars.projectname, "index.html" + ) session.flash = "Course Created Successfully" - redirect(URL('books', 'published', args=[request.vars.projectname, 'index.html'])) + redirect( + URL("books", "published", args=[request.vars.projectname, "index.html"]) + ) return buildvalues diff --git a/controllers/everyday.py b/controllers/everyday.py index 3a7bc105f..136362c50 100644 --- a/controllers/everyday.py +++ b/controllers/everyday.py @@ -1,13 +1,22 @@ import datetime import os.path + def index(): try: - f = open(os.path.join(request.folder,'everyday/latest.txt')) + f = open(os.path.join(request.folder, "everyday/latest.txt")) latest = f.readline()[:-1] f.close() - path = "http://%s/%s/static/everyday/%s" % (request.env.http_host,request.application,latest) + path = "http://%s/%s/static/everyday/%s" % ( + request.env.http_host, + request.application, + latest, + ) except: - path = "http://%s/%s/static/everyday/%s" % (request.env.http_host,request.application,'index.html') + path = "http://%s/%s/static/everyday/%s" % ( + request.env.http_host, + request.application, + "index.html", + ) redirect(path) diff --git a/controllers/feed.py b/controllers/feed.py index 574db0e04..f0e76a68c 100644 --- a/controllers/feed.py +++ b/controllers/feed.py @@ -1,6 +1,7 @@ import os, os.path import time + def everyday(): def get_posts(args, dname, flist): print(dname) @@ -8,27 +9,34 @@ def get_posts(args, dname, flist): for f in l: if "~" in f: flist.remove(f) - lname = dname.replace('everyday','static/everyday') - lname = lname.replace('applications','') + lname = dname.replace("everyday", "static/everyday") + lname = lname.replace("applications", "") for f in flist: if ".rst" in f: - efile = open("%s/%s"%(dname,f)) + efile = open("%s/%s" % (dname, f)) ttext = efile.readline()[:-1] efile.close() - stime = os.path.getmtime("%s/%s"%(dname,f)) + stime = os.path.getmtime("%s/%s" % (dname, f)) mtime = time.ctime(stime) - f = f.replace(".rst",".html") - args.append(dict(title=ttext, - link="http://interactivepython.org%s/%s" %(lname,f), - description="", - created_on=mtime, - sort_time=stime - )) + f = f.replace(".rst", ".html") + args.append( + dict( + title=ttext, + link="http://interactivepython.org%s/%s" % (lname, f), + description="", + created_on=mtime, + sort_time=stime, + ) + ) + entry_list = [] - os.walk("applications/%s/everyday/2013"%request.application,get_posts,entry_list) - entry_list.sort(key=lambda x: x['sort_time']) - return dict(title="Everyday Python", - link = "http://interactivepython.org/courselib/feed/everyday.rss", - description="Everyday Python, Lessons in Python programming", - entries=entry_list - ) + os.walk( + "applications/%s/everyday/2013" % request.application, get_posts, entry_list + ) + entry_list.sort(key=lambda x: x["sort_time"]) + return dict( + title="Everyday Python", + link="http://interactivepython.org/courselib/feed/everyday.rss", + description="Everyday Python, Lessons in Python programming", + entries=entry_list, + ) diff --git a/controllers/lti.py b/controllers/lti.py index 8f643f544..21dae062a 100644 --- a/controllers/lti.py +++ b/controllers/lti.py @@ -17,50 +17,76 @@ def index(): masterapp = None userinfo = None - user_id = request.vars.get('user_id', None) - last_name = request.vars.get('lis_person_name_family', None) - first_name = request.vars.get('lis_person_name_given', None) - full_name = request.vars.get('lis_person_name_full', None) + user_id = request.vars.get("user_id", None) + last_name = request.vars.get("lis_person_name_family", None) + first_name = request.vars.get("lis_person_name_given", None) + full_name = request.vars.get("lis_person_name_full", None) if full_name and not last_name: names = full_name.strip().split() last_name = names[-1] - first_name = ' '.join(names[:-1]) - email = request.vars.get('lis_person_contact_email_primary', None) - instructor = ("Instructor" in request.vars.get('roles', [])) or \ - ("TeachingAssistant" in request.vars.get('roles', [])) - result_source_did=request.vars.get('lis_result_sourcedid', None) - outcome_url=request.vars.get('lis_outcome_service_url', None) - assignment_id = _param_converter(request.vars.get('assignment_id', None)) - practice = request.vars.get('practice', None) - - if user_id is None : - return dict(logged_in=False, lti_errors=["user_id is required for this tool to function", request.vars], masterapp=masterapp) - elif first_name is None : - return dict(logged_in=False, lti_errors=["First Name is required for this tool to function", request.vars], masterapp=masterapp) - elif last_name is None : - return dict(logged_in=False, lti_errors=["Last Name is required for this tool to function", request.vars], masterapp=masterapp) - elif email is None : - return dict(logged_in=False, lti_errors=["Email is required for this tool to function", request.vars], masterapp=masterapp) - else : + first_name = " ".join(names[:-1]) + email = request.vars.get("lis_person_contact_email_primary", None) + instructor = ("Instructor" in request.vars.get("roles", [])) or ( + "TeachingAssistant" in request.vars.get("roles", []) + ) + result_source_did = request.vars.get("lis_result_sourcedid", None) + outcome_url = request.vars.get("lis_outcome_service_url", None) + assignment_id = _param_converter(request.vars.get("assignment_id", None)) + practice = request.vars.get("practice", None) + + if user_id is None: + return dict( + logged_in=False, + lti_errors=["user_id is required for this tool to function", request.vars], + masterapp=masterapp, + ) + elif first_name is None: + return dict( + logged_in=False, + lti_errors=[ + "First Name is required for this tool to function", + request.vars, + ], + masterapp=masterapp, + ) + elif last_name is None: + return dict( + logged_in=False, + lti_errors=[ + "Last Name is required for this tool to function", + request.vars, + ], + masterapp=masterapp, + ) + elif email is None: + return dict( + logged_in=False, + lti_errors=["Email is required for this tool to function", request.vars], + masterapp=masterapp, + ) + else: userinfo = dict() - userinfo['first_name'] = first_name - userinfo['last_name'] = last_name + userinfo["first_name"] = first_name + userinfo["last_name"] = last_name # In the `Canvas Student View `_ as of 7-Jan-2019, the ``lis_person_contact_email_primary`` is an empty string. In this case, use the userid instead. - email = email or (user_id + '@junk.com') - userinfo['email'] = email + email = email or (user_id + "@junk.com") + userinfo["email"] = email - key = request.vars.get('oauth_consumer_key', None) + key = request.vars.get("oauth_consumer_key", None) if key is not None: - myrecord = db(db.lti_keys.consumer==key).select().first() - if myrecord is None : - return dict(logged_in=False, lti_errors=["Could not find oauth_consumer_key", request.vars], - masterapp=masterapp) + myrecord = db(db.lti_keys.consumer == key).select().first() + if myrecord is None: + return dict( + logged_in=False, + lti_errors=["Could not find oauth_consumer_key", request.vars], + masterapp=masterapp, + ) else: session.oauth_consumer_key = key - if myrecord is not None : + if myrecord is not None: masterapp = myrecord.application - if len(masterapp) < 1 : - masterapp = 'welcome' + if len(masterapp) < 1: + masterapp = "welcome" session.connect(request, response, masterapp=masterapp, db=db) oauth_server = oauth2.Server() @@ -68,111 +94,172 @@ def index(): oauth_server.add_signature_method(oauth2.SignatureMethod_HMAC_SHA1()) # Use ``setting.lti_uri`` if it's defined; otherwise, use the current URI (which must be built from its components). Don't include query parameters, which causes a failure in OAuth security validation. - full_uri = settings.get('lti_uri', '{}://{}{}'.format( - request.env.wsgi_url_scheme, request.env.http_host, request.url - )) + full_uri = settings.get( + "lti_uri", + "{}://{}{}".format( + request.env.wsgi_url_scheme, request.env.http_host, request.url + ), + ) oauth_request = oauth2.Request.from_request( - 'POST', full_uri, None, dict(request.vars), - query_string=request.env.query_string + "POST", + full_uri, + None, + dict(request.vars), + query_string=request.env.query_string, ) # Fix encoding -- the signed keys are in bytes, but the oauth2 Request constructor translates everything to a string. Therefore, they never compare as equal. ??? - if isinstance(oauth_request.get('oauth_signature'), six.string_types): - oauth_request['oauth_signature'] = oauth_request['oauth_signature'].encode('utf-8') + if isinstance(oauth_request.get("oauth_signature"), six.string_types): + oauth_request["oauth_signature"] = oauth_request["oauth_signature"].encode( + "utf-8" + ) consumer = oauth2.Consumer(myrecord.consumer, myrecord.secret) try: oauth_server.verify_request(oauth_request, consumer, None) except oauth2.Error as err: - return dict(logged_in=False, lti_errors=["OAuth Security Validation failed:"+err.message, request.vars], - masterapp=masterapp) + return dict( + logged_in=False, + lti_errors=[ + "OAuth Security Validation failed:" + err.message, + request.vars, + ], + masterapp=masterapp, + ) consumer = None # Time to create / update / login the user if userinfo and (consumer is not None): - userinfo['username'] = email + userinfo["username"] = email # Only assign a password if we're creating the user. The # ``get_or_create_user`` method checks for an existing user using both # the username and the email. - update_fields = ['email', 'first_name', 'last_name'] - if not db( - (db.auth_user.username == userinfo['username']) | - (db.auth_user.email == userinfo['email']) - ).select(db.auth_user.id).first(): + update_fields = ["email", "first_name", "last_name"] + if ( + not db( + (db.auth_user.username == userinfo["username"]) + | (db.auth_user.email == userinfo["email"]) + ) + .select(db.auth_user.id) + .first() + ): pw = db.auth_user.password.validate(str(uuid.uuid4()))[0] - userinfo['password'] = pw - update_fields.append('password') + userinfo["password"] = pw + update_fields.append("password") user = auth.get_or_create_user(userinfo, update_fields=update_fields) if user is None: - return dict(logged_in=False, lti_errors=["Unable to create user record", request.vars], - masterapp=masterapp) + return dict( + logged_in=False, + lti_errors=["Unable to create user record", request.vars], + masterapp=masterapp, + ) # user exists; make sure course name and id are set based on custom parameters passed, if this is for runestone. As noted for ``assignment_id``, parameters are passed as a two-element list. - course_id = _param_converter(request.vars.get('custom_course_id', None)) - section_id = _param_converter(request.vars.get('custom_section_id', None)) + course_id = _param_converter(request.vars.get("custom_course_id", None)) + section_id = _param_converter(request.vars.get("custom_section_id", None)) if course_id: - user['course_id'] = course_id - user['course_name'] = getCourseNameFromId(course_id) # need to set course_name because calls to verifyInstructor use it - user['section'] = section_id + user["course_id"] = course_id + user["course_name"] = getCourseNameFromId( + course_id + ) # need to set course_name because calls to verifyInstructor use it + user["section"] = section_id user.update_record() # Update instructor status. if instructor: # Give the instructor free access to the book. db.user_courses.update_or_insert(user_id=user.id, course_id=course_id) - db.course_instructor.update_or_insert(instructor=user.id, course=course_id) + db.course_instructor.update_or_insert( + instructor=user.id, course=course_id + ) else: - db((db.course_instructor.instructor == user.id) & - (db.course_instructor.course == course_id)).delete() + db( + (db.course_instructor.instructor == user.id) + & (db.course_instructor.course == course_id) + ).delete() # Before creating a new user_courses record, present payment or donation options. - if not db((db.user_courses.user_id==user.id) & - (db.user_courses.course_id==course_id)).select().first(): + if ( + not db( + (db.user_courses.user_id == user.id) + & (db.user_courses.course_id == course_id) + ) + .select() + .first() + ): # Store the current URL, so this request can be completed after creating the user. session.lti_url_next = full_uri auth.login_user(user) - redirect(URL(c='default')) + redirect(URL(c="default")) if section_id: # set the section in the section_users table # test this - db.section_users.update_or_insert(db.section_users.auth_user == user['id'], auth_user=user['id'], section = section_id) + db.section_users.update_or_insert( + db.section_users.auth_user == user["id"], + auth_user=user["id"], + section=section_id, + ) auth.login_user(user) if assignment_id: # If the assignment is released, but this is the first time a student has visited the assignment, auto-upload the grade. - assignment = db(db.assignments.id == assignment_id).select( - db.assignments.released).first() - grade = db( - (db.grades.auth_user == user.id) & - (db.grades.assignment == assignment_id) - ).select(db.grades.lis_result_sourcedid, db.grades.lis_outcome_url).first() - send_grade = (assignment and assignment.released and grade and - not grade.lis_result_sourcedid and - not grade.lis_outcome_url) + assignment = ( + db(db.assignments.id == assignment_id) + .select(db.assignments.released) + .first() + ) + grade = ( + db( + (db.grades.auth_user == user.id) + & (db.grades.assignment == assignment_id) + ) + .select(db.grades.lis_result_sourcedid, db.grades.lis_outcome_url) + .first() + ) + send_grade = ( + assignment + and assignment.released + and grade + and not grade.lis_result_sourcedid + and not grade.lis_outcome_url + ) # save the guid and url for reporting back the grade - db.grades.update_or_insert((db.grades.auth_user == user.id) & (db.grades.assignment == assignment_id), - auth_user=user.id, - assignment=assignment_id, - lis_result_sourcedid=result_source_did, - lis_outcome_url=outcome_url) + db.grades.update_or_insert( + (db.grades.auth_user == user.id) & (db.grades.assignment == assignment_id), + auth_user=user.id, + assignment=assignment_id, + lis_result_sourcedid=result_source_did, + lis_outcome_url=outcome_url, + ) if send_grade: _try_to_send_lti_grade(user.id, assignment_id) - redirect(URL('assignments', 'doAssignment', vars={'assignment_id':assignment_id})) + redirect( + URL("assignments", "doAssignment", vars={"assignment_id": assignment_id}) + ) elif practice: if outcome_url and result_source_did: - db.practice_grades.update_or_insert((db.practice_grades.auth_user == user.id), - auth_user=user.id, - lis_result_sourcedid=result_source_did, - lis_outcome_url=outcome_url, - course_name=getCourseNameFromId(course_id)) - else: # don't overwrite outcome_url and result_source_did - db.practice_grades.update_or_insert((db.practice_grades.auth_user == user.id), - auth_user=user.id, - course_name=getCourseNameFromId(course_id)) - redirect(URL('assignments', 'settz_then_practice', vars={'course_name':user['course_name']})) - - redirect(get_course_url('index.html')) + db.practice_grades.update_or_insert( + (db.practice_grades.auth_user == user.id), + auth_user=user.id, + lis_result_sourcedid=result_source_did, + lis_outcome_url=outcome_url, + course_name=getCourseNameFromId(course_id), + ) + else: # don't overwrite outcome_url and result_source_did + db.practice_grades.update_or_insert( + (db.practice_grades.auth_user == user.id), + auth_user=user.id, + course_name=getCourseNameFromId(course_id), + ) + redirect( + URL( + "assignments", + "settz_then_practice", + vars={"course_name": user["course_name"]}, + ) + ) + redirect(get_course_url("index.html")) diff --git a/controllers/oauth.py b/controllers/oauth.py index a8e0f8410..868310b68 100644 --- a/controllers/oauth.py +++ b/controllers/oauth.py @@ -1,4 +1,4 @@ # This page provides an endpoint for getting an oauth redirect after an oauth verification process def index(): full_url = URL(args=request.args, vars=request.get_vars, host=True) - return {'url': full_url} \ No newline at end of file + return {"url": full_url} diff --git a/controllers/proxy.py b/controllers/proxy.py index 0c5108cf0..77114252a 100644 --- a/controllers/proxy.py +++ b/controllers/proxy.py @@ -1,62 +1,67 @@ import requests as rq import logging import json + logger = logging.getLogger(settings.logger) logger.setLevel(settings.log_level) -response.headers["Access-Control-Allow-Headers"] = "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" -response.headers["Access-Control-Allow-Methods"] = 'GET, PUT, POST, HEAD, OPTIONS' +response.headers[ + "Access-Control-Allow-Headers" +] = "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" +response.headers["Access-Control-Allow-Methods"] = "GET, PUT, POST, HEAD, OPTIONS" + def jobeRun(): req = rq.Session() logger.debug("got a jobe request %s", request.vars.run_spec) - req.headers['Content-type'] = 'application/json; charset=utf-8' - req.headers['Accept'] = 'application/json' - req.headers['X-API-KEY'] = settings.jobe_key - - uri = '/jobe/index.php/restapi/runs/' + req.headers["Content-type"] = "application/json; charset=utf-8" + req.headers["Accept"] = "application/json" + req.headers["X-API-KEY"] = settings.jobe_key + + uri = "/jobe/index.php/restapi/runs/" url = settings.jobe_server + uri - rs = {'run_spec': request.vars.run_spec} + rs = {"run_spec": request.vars.run_spec} resp = req.post(url, json=rs) logger.debug("Got response from JOBE %s ", resp.status_code) return resp.content + def jobePushFile(): req = rq.Session() logger.debug("got a jobe request %s", request.vars.run_spec) - req.headers['Content-type'] = 'application/json; charset=utf-8' - req.headers['Accept'] = 'application/json' - req.headers['X-API-KEY'] = settings.jobe_key + req.headers["Content-type"] = "application/json; charset=utf-8" + req.headers["Accept"] = "application/json" + req.headers["X-API-KEY"] = settings.jobe_key - uri = '/jobe/index.php/restapi/files/'+request.args[0] + uri = "/jobe/index.php/restapi/files/" + request.args[0] url = settings.jobe_server + uri - rs = {'file_contents': request.vars.file_contents} + rs = {"file_contents": request.vars.file_contents} resp = req.put(url, json=rs) logger.debug("Got response from JOBE %s ", resp.status_code) - + response.status = resp.status_code return resp.content + def jobeCheckFile(): req = rq.Session() logger.debug("got a jobe request %s", request.vars.run_spec) - req.headers['Content-type'] = 'application/json; charset=utf-8' - req.headers['Accept'] = 'application/json' - req.headers['X-API-KEY'] = settings.jobe_key - uri = '/jobe/index.php/restapi/files/'+request.args[0] + req.headers["Content-type"] = "application/json; charset=utf-8" + req.headers["Accept"] = "application/json" + req.headers["X-API-KEY"] = settings.jobe_key + uri = "/jobe/index.php/restapi/files/" + request.args[0] url = settings.jobe_server + uri - rs = {'file_contents': request.vars.file_contents} + rs = {"file_contents": request.vars.file_contents} resp = req.head(url) logger.debug("Got response from JOBE %s ", resp.status_code) response.status = resp.status_code if resp.status_code == 404: response.status = 208 - - return resp.content + return resp.content diff --git a/controllers/sections.py b/controllers/sections.py index 89aa307c3..1bb62abe5 100644 --- a/controllers/sections.py +++ b/controllers/sections.py @@ -4,96 +4,113 @@ logger = logging.getLogger(settings.logger) logger.setLevel(settings.log_level) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def index(): course = db(db.courses.id == auth.user.course_id).select().first() sections = db(db.sections.course_id == course.id).select() # get all sections - for course, list number of users in each section - return dict( - course = course, - sections = sections - ) + return dict(course=course, sections=sections) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def create(): course = db(db.courses.id == auth.user.course_id).select().first() form = FORM( DIV( LABEL("Section Name", _for="section_name"), - INPUT(_id="section_name" ,_name="name", requires=IS_NOT_EMPTY(),_class="form-control"), - _class="form-group" + INPUT( + _id="section_name", + _name="name", + requires=IS_NOT_EMPTY(), + _class="form-control", ), + _class="form-group", + ), INPUT(_type="Submit", _value="Create Section", _class="btn"), - ) - if form.accepts(request,session): + ) + if form.accepts(request, session): section = db.sections.update_or_insert(name=form.vars.name, course_id=course.id) session.flash = "Section Created" - return redirect(URL('sections','update')+'?id=%d' % (section.id)) - return dict( - form = form, - ) + return redirect(URL("sections", "update") + "?id=%d" % (section.id)) + return dict(form=form) + -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def delete(): course = db(db.courses.id == auth.user.course_id).select().first() section = db(db.sections.id == request.vars.id).select().first() if not section or section.course_id != course.id: - return redirect(URL('admin','sections_list')) + return redirect(URL("admin", "sections_list")) section.clear_users() session.flash = "Deleted Section: %s" % (section.name) db(db.sections.id == section.id).delete() - return redirect(URL('sections','index')) + return redirect(URL("sections", "index")) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def update(): course = db(db.courses.id == auth.user.course_id).select().first() section = db(db.sections.id == request.vars.id).select().first() if not section or section.course_id != course.id: - redirect(URL('admin','sections_list')) + redirect(URL("admin", "sections_list")) bulk_email_form = FORM( DIV( - TEXTAREA(_name="emails_csv", - requires=IS_NOT_EMPTY(), - _class="form-control", - ), - _class="form-group", + TEXTAREA( + _name="emails_csv", requires=IS_NOT_EMPTY(), _class="form-control" ), + _class="form-group", + ), LABEL( INPUT(_name="overwrite", _type="Checkbox"), "Overwrite Users In Section", _class="checkbox", - ), - INPUT(_type='Submit', _class="btn", _value="Update Section"), - ) - if bulk_email_form.accepts(request,session): + ), + INPUT(_type="Submit", _class="btn", _value="Update Section"), + ) + if bulk_email_form.accepts(request, session): if bulk_email_form.vars.overwrite: section.clear_users() users_added_count = 0 - for email_address in bulk_email_form.vars.emails_csv.split(','): + for email_address in bulk_email_form.vars.emails_csv.split(","): user = db(db.auth_user.email == email_address.lower()).select().first() if user: if section.add_user(user): users_added_count += 1 session.flash = "%d Emails Added" % (users_added_count) - return redirect(URL('sections','update')+'?id=%d' % (section.id)) + return redirect(URL("sections", "update") + "?id=%d" % (section.id)) elif bulk_email_form.errors: response.flash = "Error Processing Request" return dict( - section = section, - users = section.get_users(), - bulk_email_form = bulk_email_form, - ) - + section=section, users=section.get_users(), bulk_email_form=bulk_email_form + ) - -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def students(): - sectionName = request.args[0].replace('_',' ') + sectionName = request.args[0].replace("_", " ") course = auth.user.course_id logger.debug("Getting Students for %s", sectionName) - sectionIdQuery = db((db.sections.name == sectionName) & - (db.sections.course_id == course)).select().first() + sectionIdQuery = ( + db((db.sections.name == sectionName) & (db.sections.course_id == course)) + .select() + .first() + ) if not sectionIdQuery: return "" @@ -106,48 +123,75 @@ def students(): studentName = db(db.auth_user.id == student.auth_user).select().first() firstName = studentName.first_name lastName = studentName.last_name - studentList.append((firstName,lastName)) + studentList.append((firstName, lastName)) return json.dumps(studentList) - -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def addSection(): - return redirect(URL('admin','sections_create')) + return redirect(URL("admin", "sections_create")) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def remove(): course = db(db.courses.id == auth.user.course_id).select().first() form = FORM( DIV( LABEL("Section Name", _for="section_name"), - INPUT(_id="section_name" ,_name="name", requires=IS_NOT_EMPTY(),_class="form-control"), - _class="form-group" + INPUT( + _id="section_name", + _name="name", + requires=IS_NOT_EMPTY(), + _class="form-control", ), + _class="form-group", + ), INPUT(_type="Submit", _value="Remove Section", _class="btn"), - ) - if form.accepts(request,session): + ) + if form.accepts(request, session): session.flash = T("Section Removed") - section = db((db.sections.name == form.vars.name) & (db.sections.course_id==course.id)).select().first() + section = ( + db( + (db.sections.name == form.vars.name) + & (db.sections.course_id == course.id) + ) + .select() + .first() + ) if section: db(db.sections.id == section.id).delete() - return redirect(URL('admin','admin')) - return dict( - form = form, - ) + return redirect(URL("admin", "admin")) + return dict(form=form) -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) + +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def changeDate(): - return redirect(URL('admin','startdate')) + return redirect(URL("admin", "startdate")) + -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) def getDate(): - sectionName = request.args[0].replace('_',' ') + sectionName = request.args[0].replace("_", " ") dateQuery = db(db.courses.course_name == auth.user.course_name).select() date = dateQuery[0].term_start_date return date -@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True) -def newCourse(): - return redirect(URL('','designer/index.html')) +@auth.requires( + lambda: verifyInstructorStatus(auth.user.course_name, auth.user), + requires_login=True, +) +def newCourse(): + return redirect(URL("", "designer/index.html")) diff --git a/models/0.py b/models/0.py index 783890414..02d6badb6 100644 --- a/models/0.py +++ b/models/0.py @@ -3,7 +3,7 @@ # of code in the ``modules/`` subdirectory. This is helpful when doing # development; otherwise, the web2py server must be restarted to reload any # changes made to ``modules/``. -#from gluon.custom_import import track_changes; track_changes(True) +# from gluon.custom_import import track_changes; track_changes(True) from gluon.storage import Storage import logging @@ -13,21 +13,21 @@ settings = Storage() settings.migrate = True -settings.migprefix = 'runestonebeta_' -settings.title = 'Runestone Interactive' -settings.subtitle = 'eBooks for Python' -settings.author = 'Brad Miller' -settings.author_email = 'info@interactivepython.org' -settings.keywords = '' -settings.description = '' -settings.layout_theme = 'Default' -settings.security_key = '0b734ebc-7a50-4167-99b1-2df09062fde8' -settings.email_server = 'smtp.webfaction.com' -settings.email_sender = 'info@interactivepython.org' -settings.email_login = 'sendmail_bnmnetp@web407.webfaction.com:password' -settings.login_method = 'local' -settings.login_config = '' -settings.course_id = 'devcourse' +settings.migprefix = "runestonebeta_" +settings.title = "Runestone Interactive" +settings.subtitle = "eBooks for Python" +settings.author = "Brad Miller" +settings.author_email = "info@interactivepython.org" +settings.keywords = "" +settings.description = "" +settings.layout_theme = "Default" +settings.security_key = "0b734ebc-7a50-4167-99b1-2df09062fde8" +settings.email_server = "smtp.webfaction.com" +settings.email_sender = "info@interactivepython.org" +settings.email_login = "sendmail_bnmnetp@web407.webfaction.com:password" +settings.login_method = "local" +settings.login_config = "" +settings.course_id = "devcourse" settings.plugins = [] settings.server_type = "http://" settings.academy_mode = True @@ -40,26 +40,28 @@ if config == "production": settings.database_uri = environ["DBURL"] # Set these - settings.STRIPE_PUBLISHABLE_KEY = environ.get('STRIPE_PUBLISHABLE_KEY') - settings.STRIPE_SECRET_KEY = environ.get('STRIPE_SECRET_KEY') + settings.STRIPE_PUBLISHABLE_KEY = environ.get("STRIPE_PUBLISHABLE_KEY") + settings.STRIPE_SECRET_KEY = environ.get("STRIPE_SECRET_KEY") elif config == "development": settings.database_uri = environ.get("DEV_DBURL") - settings.STRIPE_PUBLISHABLE_KEY = environ.get('STRIPE_DEV_PUBLISHABLE_KEY') - settings.STRIPE_SECRET_KEY = environ.get('STRIPE_DEV_SECRET_KEY') + settings.STRIPE_PUBLISHABLE_KEY = environ.get("STRIPE_DEV_PUBLISHABLE_KEY") + settings.STRIPE_SECRET_KEY = environ.get("STRIPE_DEV_SECRET_KEY") elif config == "test": settings.database_uri = environ.get("TEST_DBURL") - settings.STRIPE_PUBLISHABLE_KEY = environ.get('STRIPE_TEST_PUBLISHABLE_KEY') - settings.STRIPE_SECRET_KEY = environ.get('STRIPE_TEST_SECRET_KEY') + settings.STRIPE_PUBLISHABLE_KEY = environ.get("STRIPE_TEST_PUBLISHABLE_KEY") + settings.STRIPE_SECRET_KEY = environ.get("STRIPE_TEST_SECRET_KEY") else: print("To configure web2py you should set up both WEB2PY_CONFIG and") print("XXX_DBURL values in your environment -- See README for more detail") raise ValueError("unknown value for WEB2PY_CONFIG") # Just for compatibility -- many things use postgresql but web2py removes the ql -settings.database_uri = settings.database_uri.replace('postgresql://', 'postgres://') +settings.database_uri = settings.database_uri.replace("postgresql://", "postgres://") settings.logger = "web2py.app.runestone" -settings.sched_logger = settings.logger # works for production where sending log to syslog but not for dev. +settings.sched_logger = ( + settings.logger +) # works for production where sending log to syslog but not for dev. settings.log_level = logging.DEBUG -settings.python_interpreter = sys.executable \ No newline at end of file +settings.python_interpreter = sys.executable diff --git a/models/db.py b/models/db.py index 4e67e16c3..16ebc1593 100644 --- a/models/db.py +++ b/models/db.py @@ -14,26 +14,43 @@ ## be redirected to HTTPS, uncomment the line below: # request.requires_htps() -table_migrate_prefix = 'runestone_' -table_migrate_prefix_test = '' +table_migrate_prefix = "runestone_" +table_migrate_prefix_test = "" if not request.env.web2py_runtime_gae: ## if NOT running on Google App Engine use SQLite or other DB - if os.environ.get("WEB2PY_CONFIG","") == 'test': - db = DAL(settings.database_uri, migrate=False, pool_size=5, - adapter_args=dict(logfile='test_runestone_migrate.log')) - table_migrate_prefix = 'test_runestone_' + if os.environ.get("WEB2PY_CONFIG", "") == "test": + db = DAL( + settings.database_uri, + migrate=False, + pool_size=5, + adapter_args=dict(logfile="test_runestone_migrate.log"), + ) + table_migrate_prefix = "test_runestone_" table_migrate_prefix_test = table_migrate_prefix else: # WEB2PY_MIGRATE is either "Yes", "No", "Fake", or missing - db = DAL(settings.database_uri, pool_size=30, fake_migrate_all=(os.environ.get("WEB2PY_MIGRATE", "Yes") == 'Fake'), - migrate=False, migrate_enabled=(os.environ.get("WEB2PY_MIGRATE", "Yes") in ['Yes', 'Fake'])) - session.connect(request, response, db, masterapp=None, migrate=table_migrate_prefix + 'web2py_sessions.table') + db = DAL( + settings.database_uri, + pool_size=30, + fake_migrate_all=(os.environ.get("WEB2PY_MIGRATE", "Yes") == "Fake"), + migrate=False, + migrate_enabled=( + os.environ.get("WEB2PY_MIGRATE", "Yes") in ["Yes", "Fake"] + ), + ) + session.connect( + request, + response, + db, + masterapp=None, + migrate=table_migrate_prefix + "web2py_sessions.table", + ) else: ## connect to Google BigTable (optional 'google:datastore://namespace') - db = DAL('google:datastore') + db = DAL("google:datastore") ## store sessions and tickets there - session.connect(request, response, db = db) + session.connect(request, response, db=db) ## or store session in Memcache, Redis, etc. ## from gluon.contrib.memdb import MEMDB ## from google.appengine.api.memcache import Client @@ -41,7 +58,7 @@ ## by default give a view/generic.extension to all actions from localhost ## none otherwise. a pattern can be 'controller/function.extension' -response.generic_patterns = ['*'] if request.is_local else [] +response.generic_patterns = ["*"] if request.is_local else [] ## (optional) optimize handling of static files # response.optimize_css = 'concat,minify,inline' # response.optimize_js = 'concat,minify,inline' @@ -57,6 +74,7 @@ ######################################################################### from gluon.tools import Auth, Crud, Service, PluginManager, prettydate + auth = Auth(db, hmac_key=Auth.get_or_create_key()) crud, service, plugins = Crud(db), Service(), PluginManager() @@ -68,20 +86,23 @@ if settings.enable_captchas: ## Enable captcha's :-( from gluon.tools import Recaptcha - auth.settings.captcha = Recaptcha(request, - '6Lfb_t4SAAAAAB9pG_o1CwrMB40YPsdBsD8GsvlD', - '6Lfb_t4SAAAAAGvAHwmkahQ6s44478AL5Cf-fI-x', - options="theme:'blackglass'") + + auth.settings.captcha = Recaptcha( + request, + "6Lfb_t4SAAAAAB9pG_o1CwrMB40YPsdBsD8GsvlD", + "6Lfb_t4SAAAAAGvAHwmkahQ6s44478AL5Cf-fI-x", + options="theme:'blackglass'", + ) auth.settings.login_captcha = False -auth.settings.retrieve_password_captcha = False -auth.settings.retrieve_username_captcha = False +auth.settings.retrieve_password_captcha = False +auth.settings.retrieve_username_captcha = False # Set up for `two-factor authentication `_. -#auth.settings.auth_two_factor_enabled = True -#auth.settings.two_factor_methods = [lambda user, auth_two_factor: 'password_here'] +# auth.settings.auth_two_factor_enabled = True +# auth.settings.two_factor_methods = [lambda user, auth_two_factor: 'password_here'] -if os.environ.get("WEB2PY_CONFIG","") == 'production': +if os.environ.get("WEB2PY_CONFIG", "") == "production": SELECT_CACHE = dict(cache=(cache.ram, 3600), cacheable=True) COUNT_CACHE = dict(cache=(cache.ram, 3600)) else: @@ -89,16 +110,17 @@ COUNT_CACHE = {} ## create all tables needed by auth if not custom tables -db.define_table('courses', - Field('course_name', 'string', unique=True), - Field('term_start_date', 'date'), - Field('institution', 'string'), - Field('base_course', 'string'), - Field('python3', type='boolean', default=True), - Field('login_required', type='boolean', default=True), - Field('allow_pairs', type='boolean', default=False), - Field('student_price', type='integer'), - migrate=table_migrate_prefix + 'courses.table' +db.define_table( + "courses", + Field("course_name", "string", unique=True), + Field("term_start_date", "date"), + Field("institution", "string"), + Field("base_course", "string"), + Field("python3", type="boolean", default=True), + Field("login_required", type="boolean", default=True), + Field("allow_pairs", type="boolean", default=False), + Field("student_price", type="integer"), + migrate=table_migrate_prefix + "courses.table", ) @@ -112,23 +134,28 @@ def get_course_row(*args, **kwargs): # Provide the correct URL to a book, based on if it's statically or dynamically served. This function return URL(*args) and provides the correct controller/function based on the type of the current course (static vs dynamic). def get_course_url(*args): # Redirect to old-style statically-served books if it exists; otherwise, use the dynamically-served controller. - if os.path.exists(os.path.join(request.folder, 'static', auth.user.course_name)): - return URL('static', '/'.join( (auth.user.course_name, ) + args)) + if os.path.exists(os.path.join(request.folder, "static", auth.user.course_name)): + return URL("static", "/".join((auth.user.course_name,) + args)) else: - course = db(db.courses.id == auth.user.course_id).select(db.courses.base_course).first() + course = ( + db(db.courses.id == auth.user.course_id) + .select(db.courses.base_course) + .first() + ) if course: - return URL(c='books', f='published', args=(course.base_course, ) + args) + return URL(c="books", f="published", args=(course.base_course,) + args) else: - return URL(c='default') + return URL(c="default") ######################################## + def getCourseNameFromId(courseid): - ''' used to compute auth.user.course_name field ''' + """ used to compute auth.user.course_name field """ q = db.courses.id == courseid row = db(q).select().first() - return row.course_name if row else '' + return row.course_name if row else "" def verifyInstructorStatus(course, instructor): @@ -137,16 +164,28 @@ def verifyInstructorStatus(course, instructor): given course. """ if type(course) == str: - course = db(db.courses.course_name == course).select(db.courses.id, **SELECT_CACHE).first() + course = ( + db(db.courses.course_name == course) + .select(db.courses.id, **SELECT_CACHE) + .first() + ) + + return ( + db( + (db.course_instructor.course == course) + & (db.course_instructor.instructor == instructor) + ).count(**COUNT_CACHE) + > 0 + ) - return db((db.course_instructor.course == course) & - (db.course_instructor.instructor == instructor) - ).count(**COUNT_CACHE) > 0 class IS_COURSE_ID: - ''' used to validate that a course name entered (e.g. devcourse) corresponds to a - valid course ID (i.e. db.courses.id) ''' - def __init__(self, error_message='Unknown course name. Please see your instructor.'): + """ used to validate that a course name entered (e.g. devcourse) corresponds to a + valid course ID (i.e. db.courses.id) """ + + def __init__( + self, error_message="Unknown course name. Please see your instructor." + ): self.e = error_message def __call__(self, value): @@ -154,84 +193,116 @@ def __call__(self, value): return (db(db.courses.course_name == value).select()[0].id, None) return (value, self.e) + class HAS_NO_DOTS: - def __init__(self, error_message='Your username may not contain a \' or space or any other special characters just letters and numbers'): + def __init__( + self, + error_message="Your username may not contain a ' or space or any other special characters just letters and numbers", + ): self.e = error_message + def __call__(self, value): if "'" not in value and " " not in value: return (value, None) return (value, self.e) + def formatter(self, value): return value -db.define_table('auth_user', - Field('username', type='string', - label=T('Username')), - Field('first_name', type='string', - label=T('First Name')), - Field('last_name', type='string', - label=T('Last Name')), - Field('email', type='string', - requires=IS_EMAIL(banned='^.*shoeonlineblog\\.com$'), - label=T('Email')), - Field('password', type='password', - readable=False, - label=T('Password')), - Field('created_on','datetime',default=request.now, - label=T('Created On'),writable=False,readable=False), - Field('modified_on','datetime',default=request.now, - label=T('Modified On'),writable=False,readable=False, - update=request.now), - Field('registration_key',default='', - writable=False,readable=False), - Field('reset_password_key',default='', - writable=False,readable=False), - Field('registration_id',default='', - writable=False,readable=False), - Field('course_id','reference courses',label=T('Course Name'), - required=True, - default=1), - Field('course_name',compute=lambda row: getCourseNameFromId(row.course_id),readable=False, writable=False), - Field('accept_tcp', required=True, type='boolean', default=True, label=T('I Accept')), - Field('active',type='boolean',writable=False,readable=False,default=True), - Field('donated', type='boolean', writable=False, readable=False, default=False), -# format='%(username)s', - format=lambda u: (u.first_name or "") + " " + (u.last_name or ''), - migrate=table_migrate_prefix + 'auth_user.table') + +db.define_table( + "auth_user", + Field("username", type="string", label=T("Username")), + Field("first_name", type="string", label=T("First Name")), + Field("last_name", type="string", label=T("Last Name")), + Field( + "email", + type="string", + requires=IS_EMAIL(banned="^.*shoeonlineblog\\.com$"), + label=T("Email"), + ), + Field("password", type="password", readable=False, label=T("Password")), + Field( + "created_on", + "datetime", + default=request.now, + label=T("Created On"), + writable=False, + readable=False, + ), + Field( + "modified_on", + "datetime", + default=request.now, + label=T("Modified On"), + writable=False, + readable=False, + update=request.now, + ), + Field("registration_key", default="", writable=False, readable=False), + Field("reset_password_key", default="", writable=False, readable=False), + Field("registration_id", default="", writable=False, readable=False), + Field( + "course_id", + "reference courses", + label=T("Course Name"), + required=True, + default=1, + ), + Field( + "course_name", + compute=lambda row: getCourseNameFromId(row.course_id), + readable=False, + writable=False, + ), + Field( + "accept_tcp", required=True, type="boolean", default=True, label=T("I Accept") + ), + Field("active", type="boolean", writable=False, readable=False, default=True), + Field("donated", type="boolean", writable=False, readable=False, default=False), + # format='%(username)s', + format=lambda u: (u.first_name or "") + " " + (u.last_name or ""), + migrate=table_migrate_prefix + "auth_user.table", +) db.auth_user.first_name.requires = IS_NOT_EMPTY(error_message=auth.messages.is_empty) db.auth_user.last_name.requires = IS_NOT_EMPTY(error_message=auth.messages.is_empty) db.auth_user.password.requires = CRYPT(key=auth.settings.hmac_key) -db.auth_user.username.requires = (HAS_NO_DOTS(), IS_NOT_IN_DB(db, db.auth_user.username)) +db.auth_user.username.requires = ( + HAS_NO_DOTS(), + IS_NOT_IN_DB(db, db.auth_user.username), +) db.auth_user.registration_id.requires = IS_NOT_IN_DB(db, db.auth_user.registration_id) -db.auth_user.email.requires = (IS_EMAIL(error_message=auth.messages.invalid_email), - IS_NOT_IN_DB(db, db.auth_user.email)) +db.auth_user.email.requires = ( + IS_EMAIL(error_message=auth.messages.invalid_email), + IS_NOT_IN_DB(db, db.auth_user.email), +) db.auth_user.course_id.requires = IS_COURSE_ID() -auth.define_tables(username=True, signature=False, migrate=table_migrate_prefix + '') +auth.define_tables(username=True, signature=False, migrate=table_migrate_prefix + "") ## configure email -mail=auth.settings.mailer -mail.settings.server = 'logging' or 'smtp.gmail.com:587' -mail.settings.sender = 'you@gmail.com' -mail.settings.login = 'username:password' +mail = auth.settings.mailer +mail.settings.server = "logging" or "smtp.gmail.com:587" +mail.settings.sender = "you@gmail.com" +mail.settings.login = "username:password" ## configure auth policy auth.settings.registration_requires_verification = False auth.settings.registration_requires_approval = False auth.settings.reset_password_requires_verification = True -auth.settings.register_next = URL('default', 'index') +auth.settings.register_next = URL("default", "index") # change default session login time from 1 hour to 24 hours -auth.settings.expiration = 3600*24 +auth.settings.expiration = 3600 * 24 ## if you need to use OpenID, Facebook, MySpace, Twitter, Linkedin, etc. ## register with janrain.com, write your domain:api_key in private/janrain.key -#from gluon.contrib.login_methods.rpx_account import use_janrain -#use_janrain(auth,filename='private/janrain.key') +# from gluon.contrib.login_methods.rpx_account import use_janrain +# use_janrain(auth,filename='private/janrain.key') try: from gluon.contrib.login_methods.janrain_account import RPXAccount except: @@ -239,15 +310,19 @@ def formatter(self, value): from gluon.contrib.login_methods.rpx_account import RPXAccount from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm -janrain_url = 'http://%s/%s/default/user/login' % (request.env.http_host, - request.application) +janrain_url = "http://%s/%s/default/user/login" % ( + request.env.http_host, + request.application, +) -db.define_table('user_courses', - Field('user_id', db.auth_user, ondelete='CASCADE'), - Field('course_id', db.courses, ondelete='CASCADE'), - Field('user_id', db.auth_user), - Field('course_id', db.courses), - migrate=table_migrate_prefix + 'user_courses.table') +db.define_table( + "user_courses", + Field("user_id", db.auth_user, ondelete="CASCADE"), + Field("course_id", db.courses, ondelete="CASCADE"), + Field("user_id", db.auth_user), + Field("course_id", db.courses), + migrate=table_migrate_prefix + "user_courses.table", +) # For whatever reason the automatic migration of this table failed. Need the following manual statements # alter table user_courses alter column user_id type integer using user_id::integer; # alter table user_courses alter column course_id type integer using course_id::integer; @@ -275,7 +350,7 @@ def formatter(self, value): mail.settings.login = settings.email_login # Make sure the latest version of admin is always loaded. -adminjs = os.path.join('applications',request.application,'static','js','admin.js') +adminjs = os.path.join("applications", request.application, "static", "js", "admin.js") try: mtime = int(os.path.getmtime(adminjs)) except: @@ -283,12 +358,14 @@ def formatter(self, value): request.admin_mtime = str(mtime) -def check_for_donate_or_build(field_dict,id_of_insert): - if 'donate' in request.vars: + +def check_for_donate_or_build(field_dict, id_of_insert): + if "donate" in request.vars: session.donate = request.vars.donate - if 'ccn_checkbox' in request.vars: + if "ccn_checkbox" in request.vars: session.build_course = True -if 'auth_user' in db: + +if "auth_user" in db: db.auth_user._after_insert.append(check_for_donate_or_build) diff --git a/models/db_ebook.py b/models/db_ebook.py index 39352a2c7..84a080350 100644 --- a/models/db_ebook.py +++ b/models/db_ebook.py @@ -1,179 +1,202 @@ # Files in the model directory are loaded in alphabetical order. This one needs to be loaded after db.py -db.define_table('useinfo', - Field('timestamp','datetime'), - Field('sid','string'), - Field('event','string'), - Field('act','string'), - Field('div_id','string'), - Field('course_id','string'), - migrate=table_migrate_prefix + 'useinfo.table' +db.define_table( + "useinfo", + Field("timestamp", "datetime"), + Field("sid", "string"), + Field("event", "string"), + Field("act", "string"), + Field("div_id", "string"), + Field("course_id", "string"), + migrate=table_migrate_prefix + "useinfo.table", ) # stores student's saved code and, unfortunately, comments and grades, which really should be their own table linked to this -db.define_table('code', - Field('acid','string'), - Field('code','text'), - Field('emessage','text'), - Field('course_id','integer'), - Field('grade','double'), - Field('sid','string'), - Field('timestamp','datetime'), - Field('comment','text'), - Field('language','text', default='python'), - migrate=table_migrate_prefix + 'code.table' +db.define_table( + "code", + Field("acid", "string"), + Field("code", "text"), + Field("emessage", "text"), + Field("course_id", "integer"), + Field("grade", "double"), + Field("sid", "string"), + Field("timestamp", "datetime"), + Field("comment", "text"), + Field("language", "text", default="python"), + migrate=table_migrate_prefix + "code.table", ) # Stores the source code for activecodes, including prefix and suffix code, so that prefixes and suffixes can be run when grading # Contents of this table are filled when processing activecode directives, in activecod.py -db.define_table('source_code', - Field('acid','string', required=True), - Field('course_id', 'string'), - Field('includes', 'string'), # comma-separated string of acid main_codes to include when running this source_code - Field('available_files', 'string'), # comma-separated string of file_names to make available as divs when running this source_code - Field('main_code','text'), - Field('suffix_code', 'text'), # hidden suffix code - migrate=table_migrate_prefix + 'source_code.table' +db.define_table( + "source_code", + Field("acid", "string", required=True), + Field("course_id", "string"), + Field( + "includes", "string" + ), # comma-separated string of acid main_codes to include when running this source_code + Field( + "available_files", "string" + ), # comma-separated string of file_names to make available as divs when running this source_code + Field("main_code", "text"), + Field("suffix_code", "text"), # hidden suffix code + migrate=table_migrate_prefix + "source_code.table", ) -db.define_table('acerror_log', - Field('timestamp','datetime'), - Field('sid','string'), - Field('div_id','string'), - Field('course_id','string'), - Field('code','text'), - Field('emessage','text'), - migrate=table_migrate_prefix + 'acerror_log.table' - ) +db.define_table( + "acerror_log", + Field("timestamp", "datetime"), + Field("sid", "string"), + Field("div_id", "string"), + Field("course_id", "string"), + Field("code", "text"), + Field("emessage", "text"), + migrate=table_migrate_prefix + "acerror_log.table", +) ##table to store the last position of the user. 1 row per user, per course -db.define_table('user_state', - Field('user_id','integer'), - Field('course_id','string'), - Field('last_page_url','string'), - Field('last_page_hash','string'), - Field('last_page_chapter','string'), - Field('last_page_subchapter','string'), - Field('last_page_scroll_location','string'), - Field('last_page_accessed_on','datetime'), - migrate=table_migrate_prefix + 'user_state.table' +db.define_table( + "user_state", + Field("user_id", "integer"), + Field("course_id", "string"), + Field("last_page_url", "string"), + Field("last_page_hash", "string"), + Field("last_page_chapter", "string"), + Field("last_page_subchapter", "string"), + Field("last_page_scroll_location", "string"), + Field("last_page_accessed_on", "datetime"), + migrate=table_migrate_prefix + "user_state.table", ) # Table to match instructor(s) to their course(s) -db.define_table('course_instructor', - Field('course', db.courses ), - Field('instructor', db.auth_user), - Field('verified', 'boolean'), # some features we want to take the extra step of verifying an instructor - such as instructor guide - Field('paid', 'boolean'), # in the future some instructor features will be paid - migrate=table_migrate_prefix + 'course_instructor.table' +db.define_table( + "course_instructor", + Field("course", db.courses), + Field("instructor", db.auth_user), + Field( + "verified", "boolean" + ), # some features we want to take the extra step of verifying an instructor - such as instructor guide + Field("paid", "boolean"), # in the future some instructor features will be paid + migrate=table_migrate_prefix + "course_instructor.table", ) -db.define_table('coach_hints', - Field('category','string'), - Field('symbol','string'), - Field('msg_id','string'), - Field('line','integer'), - Field('col','integer'), - Field('obj','string'), - Field('msg','string'), - Field('source',db.acerror_log), - migrate=table_migrate_prefix + 'coach_hints.table' - ) +db.define_table( + "coach_hints", + Field("category", "string"), + Field("symbol", "string"), + Field("msg_id", "string"), + Field("line", "integer"), + Field("col", "integer"), + Field("obj", "string"), + Field("msg", "string"), + Field("source", db.acerror_log), + migrate=table_migrate_prefix + "coach_hints.table", +) -db.define_table('timed_exam', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('correct','integer'), - Field('incorrect','integer'), - Field('skipped','integer'), - Field('time_taken','integer'), - Field('reset','boolean'), - migrate=table_migrate_prefix + 'timed_exam.table' - ) +db.define_table( + "timed_exam", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("correct", "integer"), + Field("incorrect", "integer"), + Field("skipped", "integer"), + Field("time_taken", "integer"), + Field("reset", "boolean"), + migrate=table_migrate_prefix + "timed_exam.table", +) -db.define_table('mchoice_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','string', length=50), - Field('correct','boolean'), - migrate=table_migrate_prefix + 'mchoice_answers.table' - ) +db.define_table( + "mchoice_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "string", length=50), + Field("correct", "boolean"), + migrate=table_migrate_prefix + "mchoice_answers.table", +) -db.define_table('fitb_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','string'), - Field('correct','boolean'), - migrate=table_migrate_prefix + 'fitb_answers.table' - ) -db.define_table('dragndrop_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','string'), - Field('correct','boolean'), - Field('minHeight','string'), - migrate=table_migrate_prefix + 'dragndrop_answers.table' - ) -db.define_table('clickablearea_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','string'), - Field('correct','boolean'), - migrate=table_migrate_prefix + 'clickablearea_answers.table' - ) -db.define_table('parsons_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','string'), - Field('source','string'), - Field('correct','boolean'), - migrate=table_migrate_prefix + 'parsons_answers.table' - ) -db.define_table('codelens_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','string'), - Field('source','string'), - Field('correct','boolean'), - migrate=table_migrate_prefix + 'codelens_answers.table' - ) +db.define_table( + "fitb_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "string"), + Field("correct", "boolean"), + migrate=table_migrate_prefix + "fitb_answers.table", +) +db.define_table( + "dragndrop_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "string"), + Field("correct", "boolean"), + Field("minHeight", "string"), + migrate=table_migrate_prefix + "dragndrop_answers.table", +) +db.define_table( + "clickablearea_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "string"), + Field("correct", "boolean"), + migrate=table_migrate_prefix + "clickablearea_answers.table", +) +db.define_table( + "parsons_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "string"), + Field("source", "string"), + Field("correct", "boolean"), + migrate=table_migrate_prefix + "parsons_answers.table", +) +db.define_table( + "codelens_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "string"), + Field("source", "string"), + Field("correct", "boolean"), + migrate=table_migrate_prefix + "codelens_answers.table", +) -db.define_table('shortanswer_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','text'), - migrate=table_migrate_prefix + 'shortanswer_answers.table' - ) +db.define_table( + "shortanswer_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "text"), + migrate=table_migrate_prefix + "shortanswer_answers.table", +) -db.define_table('payments', - Field('user_courses_id', db.user_courses, required=True), +db.define_table( + "payments", + Field("user_courses_id", db.user_courses, required=True), # A `Stripe charge ID `_. Per the `Stripe docs `_, this is always 255 characters or less. - Field('charge_id', 'string', length=255, required=True), - migrate=table_migrate_prefix + 'payments.table' - ) + Field("charge_id", "string", length=255, required=True), + migrate=table_migrate_prefix + "payments.table", +) -db.define_table('lp_answers', - Field('timestamp','datetime'), - Field('div_id','string'), - Field('sid','string'), - Field('course_name','string'), - Field('answer','text'), - Field('correct','double'), - migrate=table_migrate_prefix + 'lp_answers.table' - ) +db.define_table( + "lp_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "text"), + Field("correct", "double"), + migrate=table_migrate_prefix + "lp_answers.table", +) diff --git a/models/db_ebook_chapters.py b/models/db_ebook_chapters.py index e5ac70296..18fca7cd3 100644 --- a/models/db_ebook_chapters.py +++ b/models/db_ebook_chapters.py @@ -1,50 +1,59 @@ import datetime # table of all book chapters -db.define_table('chapters', - Field('chapter_name','string'), # can have spaces in it, for human consumption - Field('course_id','string'), # references courses(course_name) - Field('chapter_label','string'), #no spaces, actual filename path - Field('chapter_num', 'integer'), # optional but nice to have for books that are numbered - migrate=table_migrate_prefix + 'chapters.table' +db.define_table( + "chapters", + Field("chapter_name", "string"), # can have spaces in it, for human consumption + Field("course_id", "string"), # references courses(course_name) + Field("chapter_label", "string"), # no spaces, actual filename path + Field( + "chapter_num", "integer" + ), # optional but nice to have for books that are numbered + migrate=table_migrate_prefix + "chapters.table", ) # table of sub chapters -db.define_table('sub_chapters', - Field('sub_chapter_name','string'), # can have spaces in it, for human consumption - Field('chapter_id','reference chapters'), - Field('sub_chapter_length','integer'), - Field('sub_chapter_label','string'), # no spaces, actual filename path - Field('skipreading', 'boolean'), # If true do not include this subchapter in the readings picker - Field('sub_chapter_num', 'integer'), - migrate=table_migrate_prefix + 'sub_chapters.table' +db.define_table( + "sub_chapters", + Field("sub_chapter_name", "string"), # can have spaces in it, for human consumption + Field("chapter_id", "reference chapters"), + Field("sub_chapter_length", "integer"), + Field("sub_chapter_label", "string"), # no spaces, actual filename path + Field( + "skipreading", "boolean" + ), # If true do not include this subchapter in the readings picker + Field("sub_chapter_num", "integer"), + migrate=table_migrate_prefix + "sub_chapters.table", ) -db.define_table('user_chapter_progress', - Field('user_id'), - Field('chapter_id','string'), - Field('start_date','datetime', default=datetime.datetime.utcnow()), - Field('end_date','datetime'), - Field('status','integer'), #-1 - not started. 0 - active. 1 - completed - migrate=table_migrate_prefix + 'user_chapter_progress.table' +db.define_table( + "user_chapter_progress", + Field("user_id"), + Field("chapter_id", "string"), + Field("start_date", "datetime", default=datetime.datetime.utcnow()), + Field("end_date", "datetime"), + Field("status", "integer"), # -1 - not started. 0 - active. 1 - completed + migrate=table_migrate_prefix + "user_chapter_progress.table", ) -db.define_table('user_sub_chapter_progress', - Field('user_id', 'reference auth_user'), - Field('chapter_id','string'), - Field('sub_chapter_id','string'), - Field('start_date','datetime', default=datetime.datetime.utcnow()), - Field('end_date','datetime'), - Field('status','integer'), #-1 - not started. 0 - active. 1 - completed - migrate=table_migrate_prefix + 'user_sub_chapter_progress.table' +db.define_table( + "user_sub_chapter_progress", + Field("user_id", "reference auth_user"), + Field("chapter_id", "string"), + Field("sub_chapter_id", "string"), + Field("start_date", "datetime", default=datetime.datetime.utcnow()), + Field("end_date", "datetime"), + Field("status", "integer"), # -1 - not started. 0 - active. 1 - completed + migrate=table_migrate_prefix + "user_sub_chapter_progress.table", ) -db.define_table('sub_chapter_taught', - Field('course_name', 'string'), - Field('chapter_label', 'string'), - Field('sub_chapter_label', 'string'), - Field('teaching_date', 'date', default=datetime.datetime.utcnow()), - migrate=table_migrate_prefix + 'sub_chapter_taught.table' +db.define_table( + "sub_chapter_taught", + Field("course_name", "string"), + Field("chapter_label", "string"), + Field("sub_chapter_label", "string"), + Field("teaching_date", "date", default=datetime.datetime.utcnow()), + migrate=table_migrate_prefix + "sub_chapter_taught.table", ) # @@ -52,18 +61,29 @@ # user_sub_chapter_progress table. One for each section/subsection # This is like a trigger, but will work across all databases. # -def make_progress_entries(field_dict,id_of_insert): - cname = db(db.courses.id == field_dict['course_id']).select(db.courses.course_name).first()['course_name'] - db.executesql(''' +def make_progress_entries(field_dict, id_of_insert): + cname = ( + db(db.courses.id == field_dict["course_id"]) + .select(db.courses.course_name) + .first()["course_name"] + ) + db.executesql( + """ INSERT INTO user_chapter_progress(user_id, chapter_id, status) SELECT %s, chapters.chapter_label, -1 FROM chapters where chapters.course_id = '%s'; - ''' % (id_of_insert,cname)) - db.executesql(''' + """ + % (id_of_insert, cname) + ) + db.executesql( + """ INSERT INTO user_sub_chapter_progress(user_id, chapter_id,sub_chapter_id, status) SELECT %s, chapters.chapter_label, sub_chapters.sub_chapter_label, -1 FROM chapters, sub_chapters where sub_chapters.chapter_id = chapters.id and chapters.course_id = '%s'; - ''' % (id_of_insert,cname)) + """ + % (id_of_insert, cname) + ) -if 'auth_user' in db: + +if "auth_user" in db: db.auth_user._after_insert.append(make_progress_entries) diff --git a/models/db_sections.py b/models/db_sections.py index 152035e8d..fe9c822fa 100644 --- a/models/db_sections.py +++ b/models/db_sections.py @@ -1,55 +1,76 @@ -db.define_table('sections', - Field('name', - type='string', - label=T('Name') - ), - Field('course_id', - db.courses, - label=('Course ID'), - required=True - ), - migrate=table_migrate_prefix + 'sections.table' - ) +db.define_table( + "sections", + Field("name", type="string", label=T("Name")), + Field("course_id", db.courses, label=("Course ID"), required=True), + migrate=table_migrate_prefix + "sections.table", +) + + class ExtendedSection(object): - def get_users(self): - def users(): - return section_users(db.sections.id == self.sections.id).select(db.auth_user.ALL) - return users - def add_user(self): - section = self.sections - def user_func(user): - for sec in db(db.sections.course_id == self.sections.course_id).select(db.sections.ALL): - db((db.section_users.section == sec.id) & (db.section_users.auth_user == user.id)).delete() - db.section_users.insert(section=section.id, auth_user=user) - return True - return user_func - def clear_users(self): - def clear(): - db(db.section_users.section == self.sections.id).delete() - return True - return clear + def get_users(self): + def users(): + return section_users(db.sections.id == self.sections.id).select( + db.auth_user.ALL + ) + + return users + + def add_user(self): + section = self.sections + + def user_func(user): + for sec in db(db.sections.course_id == self.sections.course_id).select( + db.sections.ALL + ): + db( + (db.section_users.section == sec.id) + & (db.section_users.auth_user == user.id) + ).delete() + db.section_users.insert(section=section.id, auth_user=user) + return True + + return user_func + + def clear_users(self): + def clear(): + db(db.section_users.section == self.sections.id).delete() + return True + + return clear + + db.sections.virtualfields.append(ExtendedSection()) -db.define_table('section_users', - Field('auth_user',db.auth_user, required=True), - Field('section',db.sections, label="Section ID", required=True), - migrate=table_migrate_prefix + 'section_users.table', - ) +db.define_table( + "section_users", + Field("auth_user", db.auth_user, required=True), + Field("section", db.sections, label="Section ID", required=True), + migrate=table_migrate_prefix + "section_users.table", +) -section_users = db((db.sections.id==db.section_users.section) & (db.auth_user.id==db.section_users.auth_user)) +section_users = db( + (db.sections.id == db.section_users.section) + & (db.auth_user.id == db.section_users.auth_user) +) # # when a user registers for a course, add them to the default section. # -def make_section_entries(field_dict,id_of_insert): +def make_section_entries(field_dict, id_of_insert): # Add user to default section for course. - sect = db((db.sections.course_id == field_dict['course_id']) & (db.sections.name == 'default')).select( - db.sections.id).first() + sect = ( + db( + (db.sections.course_id == field_dict["course_id"]) + & (db.sections.name == "default") + ) + .select(db.sections.id) + .first() + ) x = db.section_users.update_or_insert(auth_user=id_of_insert, section=sect) # select from sections where course_id = auth_user.course_id and section.name = 'default' # add a row to section_users for this user with the section selected. -if 'auth_user' in db: - db.auth_user._after_insert.append(make_section_entries) +if "auth_user" in db: + db.auth_user._after_insert.append(make_section_entries) diff --git a/models/grouped_assignments.py b/models/grouped_assignments.py index 6e107fb52..6ec4641ef 100644 --- a/models/grouped_assignments.py +++ b/models/grouped_assignments.py @@ -1,56 +1,66 @@ -db.define_table('assignments', - Field('course', db.courses), - Field('name', 'string'), - Field('points', 'integer', default=0), # max possible points on the assignment, cached sum of assignment_question points - Field('threshold_pct', 'float'), # threshold required to qualify for maximum points on the assignment; null means use actual points - Field('released', 'boolean'), - Field('allow_self_autograde', 'boolean'), # if True, when student clicks to autograde assignment, it calculates totals; otherwise it only scores individual questions but doesn't calculate score for the assignment - Field('description', 'text'), - Field('duedate','datetime'), - Field('visible','boolean'), - format='%(name)s', - migrate=table_migrate_prefix + 'assignments.table' - ) +db.define_table( + "assignments", + Field("course", db.courses), + Field("name", "string"), + Field( + "points", "integer", default=0 + ), # max possible points on the assignment, cached sum of assignment_question points + Field( + "threshold_pct", "float" + ), # threshold required to qualify for maximum points on the assignment; null means use actual points + Field("released", "boolean"), + Field( + "allow_self_autograde", "boolean" + ), # if True, when student clicks to autograde assignment, it calculates totals; otherwise it only scores individual questions but doesn't calculate score for the assignment + Field("description", "text"), + Field("duedate", "datetime"), + Field("visible", "boolean"), + format="%(name)s", + migrate=table_migrate_prefix + "assignments.table", +) -db.define_table('grades', +db.define_table( + "grades", # This table records grades on whole assignments, not individual questions - Field('auth_user', db.auth_user), - Field('assignment', db.assignments), - Field('score', 'double'), - Field('manual_total', 'boolean'), - Field('projected', 'double'), + Field("auth_user", db.auth_user), + Field("assignment", db.assignments), + Field("score", "double"), + Field("manual_total", "boolean"), + Field("projected", "double"), # guid for the student x assignment cell in the external gradebook # # Guessing that the ``lis_outcome_url`` length is actually inteded for this field, use that as its maximum length. - Field('lis_result_sourcedid', 'string', length=1024), + Field("lis_result_sourcedid", "string", length=1024), # web service endpoint where you send signed xml messages to insert into gradebook; guid above will be one parameter you send in that xml; the actual grade and comment will be others # # Per the ``LTI spec v1.1.1 `_ in section 6, the maximum length of the ``lis_outcome_url`` field is 1023 characters. - Field('lis_outcome_url', 'string', length=1024), - migrate=table_migrate_prefix + 'grades.table', - ) + Field("lis_outcome_url", "string", length=1024), + migrate=table_migrate_prefix + "grades.table", +) -db.define_table('practice_grades', - Field('auth_user', db.auth_user), - Field('course_name', 'string'), - Field('score', 'double'), - Field('lis_result_sourcedid', 'string'), - # guid for the student x assignment cell in the external gradebook - Field('lis_outcome_url', 'string'), - # web service endpoint where you send signed xml messages to insert into gradebook; guid above will be one parameter you send in that xml; the actual grade and comment will be others - migrate=table_migrate_prefix + 'practice_grades.table', - ) +db.define_table( + "practice_grades", + Field("auth_user", db.auth_user), + Field("course_name", "string"), + Field("score", "double"), + Field("lis_result_sourcedid", "string"), + # guid for the student x assignment cell in the external gradebook + Field("lis_outcome_url", "string"), + # web service endpoint where you send signed xml messages to insert into gradebook; guid above will be one parameter you send in that xml; the actual grade and comment will be others + migrate=table_migrate_prefix + "practice_grades.table", +) -db.define_table('question_grades', +db.define_table( + "question_grades", # This table records grades on individual gradeable items - Field('sid', type='string', notnull=True), - Field('course_name',type='string', notnull=True), - Field('div_id', type = 'string', notnull=True), - Field('useinfo_id', db.useinfo), # the particular useinfo run that was graded - Field('deadline', 'datetime'), - Field('score', type='double'), - Field('comment', type ='text'), - migrate=table_migrate_prefix + 'question_grades.table', - ) + Field("sid", type="string", notnull=True), + Field("course_name", type="string", notnull=True), + Field("div_id", type="string", notnull=True), + Field("useinfo_id", db.useinfo), # the particular useinfo run that was graded + Field("deadline", "datetime"), + Field("score", type="double"), + Field("comment", type="text"), + migrate=table_migrate_prefix + "question_grades.table", +) diff --git a/models/lti.py b/models/lti.py index 6c6af82ea..7c395499c 100644 --- a/models/lti.py +++ b/models/lti.py @@ -1,9 +1,10 @@ -db.define_table('lti_keys', - Field('consumer'), - Field('secret'), - Field('application'), - migrate=table_migrate_prefix + 'lti_keys.table' - ) +db.define_table( + "lti_keys", + Field("consumer"), + Field("secret"), + Field("application"), + migrate=table_migrate_prefix + "lti_keys.table", +) # insert the initial lti_keys; get the values from 1.py diff --git a/models/menu.py b/models/menu.py index 2aaa7785a..b34352980 100644 --- a/models/menu.py +++ b/models/menu.py @@ -1,8 +1,8 @@ response.title = settings.title response.subtitle = settings.subtitle -response.meta.author = '%(author)s <%(author_email)s>' % settings +response.meta.author = "%(author)s <%(author_email)s>" % settings response.meta.keywords = settings.keywords response.meta.description = settings.description response.menu = [ -(T('Index'),URL('default','index')==URL(),URL('default','index'),[]), -] \ No newline at end of file + (T("Index"), URL("default", "index") == URL(), URL("default", "index"), []) +] diff --git a/models/practice.py b/models/practice.py index b4f007973..e83e9cf60 100644 --- a/models/practice.py +++ b/models/practice.py @@ -1,83 +1,121 @@ -db.define_table('course_practice', - Field('auth_user_id', 'reference auth_user', label=T('Instructor Name'), required=True, default=1), - Field('course_name', 'string'), - Field('start_date', type='date'), - Field('end_date', type='date'), - Field('max_practice_days', type='integer'), - Field('max_practice_questions', type='integer'), - Field('day_points', type='double'), - Field('question_points', type='double'), - Field('questions_to_complete_day', type='integer'), - Field('graded', type='integer'), - Field('spacing', type='integer'), - Field('interleaving', type='integer'), - # A value of 0 indicates self-paced (when student marks a page complete). - # A value of 1 indicates whenever a page is assigned in any reading assignment and the reading - # assignment deadline passes. - # A value of 2 indicates manually by the instructor, as it is implemented currently. - Field('flashcard_creation_method', type='integer', default=0), - migrate=table_migrate_prefix + 'course_practice.table') +db.define_table( + "course_practice", + Field( + "auth_user_id", + "reference auth_user", + label=T("Instructor Name"), + required=True, + default=1, + ), + Field("course_name", "string"), + Field("start_date", type="date"), + Field("end_date", type="date"), + Field("max_practice_days", type="integer"), + Field("max_practice_questions", type="integer"), + Field("day_points", type="double"), + Field("question_points", type="double"), + Field("questions_to_complete_day", type="integer"), + Field("graded", type="integer"), + Field("spacing", type="integer"), + Field("interleaving", type="integer"), + # A value of 0 indicates self-paced (when student marks a page complete). + # A value of 1 indicates whenever a page is assigned in any reading assignment and the reading + # assignment deadline passes. + # A value of 2 indicates manually by the instructor, as it is implemented currently. + Field("flashcard_creation_method", type="integer", default=0), + migrate=table_migrate_prefix + "course_practice.table", +) -db.define_table('user_topic_practice', - Field('user_id', db.auth_user), - Field('course_name', 'string'), - Field('chapter_label', 'string'), - Field('sub_chapter_label', 'string'), - Field('question_name', 'string'), - Field('i_interval', type='integer', notnull=True), - Field('e_factor', type='double', notnull=True), - Field('q', type='integer', notnull=True, default=0), - Field('last_presented', type='datetime'), - Field('last_completed', type='datetime'), - Field('next_eligible_date', type='date'), - Field('creation_time', type='datetime'), - Field('timezoneoffset', type='integer', default=0), - migrate=table_migrate_prefix + 'spacing.table') +db.define_table( + "user_topic_practice", + Field("user_id", db.auth_user), + Field("course_name", "string"), + Field("chapter_label", "string"), + Field("sub_chapter_label", "string"), + Field("question_name", "string"), + Field("i_interval", type="integer", notnull=True), + Field("e_factor", type="double", notnull=True), + Field("q", type="integer", notnull=True, default=0), + Field("last_presented", type="datetime"), + Field("last_completed", type="datetime"), + Field("next_eligible_date", type="date"), + Field("creation_time", type="datetime"), + Field("timezoneoffset", type="integer", default=0), + migrate=table_migrate_prefix + "spacing.table", +) -db.define_table('user_topic_practice_log', - Field('user_id', db.auth_user), - Field('course_name', 'string'), - Field('chapter_label', 'string'), - Field('sub_chapter_label', 'string'), - Field('question_name', 'string'), - Field('i_interval', type='integer', notnull=True), - Field('e_factor', type='double', notnull=True), - Field('q', type='integer', notnull=True, default=-1), - Field('trials_num', type='integer', notnull=True), - Field('available_flashcards', type='integer', notnull=True, default=-1), - Field('start_practice', type='datetime'), - Field('end_practice', type='datetime'), - Field('timezoneoffset', type='integer', default=0), - Field('next_eligible_date', type='date'), - migrate=table_migrate_prefix + 'spacing_log.table') +db.define_table( + "user_topic_practice_log", + Field("user_id", db.auth_user), + Field("course_name", "string"), + Field("chapter_label", "string"), + Field("sub_chapter_label", "string"), + Field("question_name", "string"), + Field("i_interval", type="integer", notnull=True), + Field("e_factor", type="double", notnull=True), + Field("q", type="integer", notnull=True, default=-1), + Field("trials_num", type="integer", notnull=True), + Field("available_flashcards", type="integer", notnull=True, default=-1), + Field("start_practice", type="datetime"), + Field("end_practice", type="datetime"), + Field("timezoneoffset", type="integer", default=0), + Field("next_eligible_date", type="date"), + migrate=table_migrate_prefix + "spacing_log.table", +) -db.define_table('user_topic_practice_Completion', - Field('user_id', db.auth_user), - Field('course_name', 'string'), - Field('practice_completion_date', type='date'), - migrate=table_migrate_prefix + 'user_topic_practice_Completion.table') +db.define_table( + "user_topic_practice_Completion", + Field("user_id", db.auth_user), + Field("course_name", "string"), + Field("practice_completion_date", type="date"), + migrate=table_migrate_prefix + "user_topic_practice_Completion.table", +) -db.define_table('user_topic_practice_survey', - Field('user_id', db.auth_user, - default=auth.user_id, update=auth.user_id, writable=False), - Field('course_name', 'string'), - Field('like_practice', requires=IS_IN_SET(['Like', 'Dislike'])), - Field('response_time', type='datetime', - default=request.now, update=request.now, writable=False), - Field('timezoneoffset', type='integer', default=0), - migrate=table_migrate_prefix + 'user_topic_practice_survey.table') +db.define_table( + "user_topic_practice_survey", + Field( + "user_id", + db.auth_user, + default=auth.user_id, + update=auth.user_id, + writable=False, + ), + Field("course_name", "string"), + Field("like_practice", requires=IS_IN_SET(["Like", "Dislike"])), + Field( + "response_time", + type="datetime", + default=request.now, + update=request.now, + writable=False, + ), + Field("timezoneoffset", type="integer", default=0), + migrate=table_migrate_prefix + "user_topic_practice_survey.table", +) -db.define_table('user_topic_practice_feedback', - Field('user_id', db.auth_user, - default=auth.user_id, update=auth.user_id, writable=False), - Field('course_name', 'string'), - Field('feedback', 'string'), - Field('response_time', type='datetime', - default=request.now, update=request.now, writable=False), - Field('timezoneoffset', type='integer', default=0), - migrate=table_migrate_prefix + 'user_topic_practice_feedback.table') +db.define_table( + "user_topic_practice_feedback", + Field( + "user_id", + db.auth_user, + default=auth.user_id, + update=auth.user_id, + writable=False, + ), + Field("course_name", "string"), + Field("feedback", "string"), + Field( + "response_time", + type="datetime", + default=request.now, + update=request.now, + writable=False, + ), + Field("timezoneoffset", type="integer", default=0), + migrate=table_migrate_prefix + "user_topic_practice_feedback.table", +) diff --git a/models/questions.py b/models/questions.py index c446b5ab7..9bbba9c41 100644 --- a/models/questions.py +++ b/models/questions.py @@ -1,44 +1,62 @@ -db.define_table('questions', - Field('base_course', type='string', notnull=True), - Field('name', type='string', notnull=True), - Field('chapter', type='string'), # matches chapter_label, not name - Field('subchapter', type='string'), # matches sub_chapter_label, not name - Field('author', type='string'), - Field('difficulty', type='integer'), - Field('question', type='text'), - Field('timestamp',type='datetime'), - Field('question_type',type='string'), - Field('is_private', type='boolean'), - Field('htmlsrc', type='text'), - Field('practice', type='boolean'), - Field('autograde', type='string'), - Field('topic', type='string'), - Field('feedback', type='text'), - Field('from_source', type='boolean'), - migrate=table_migrate_prefix + 'questions.table') +db.define_table( + "questions", + Field("base_course", type="string", notnull=True), + Field("name", type="string", notnull=True), + Field("chapter", type="string"), # matches chapter_label, not name + Field("subchapter", type="string"), # matches sub_chapter_label, not name + Field("author", type="string"), + Field("difficulty", type="integer"), + Field("question", type="text"), + Field("timestamp", type="datetime"), + Field("question_type", type="string"), + Field("is_private", type="boolean"), + Field("htmlsrc", type="text"), + Field("practice", type="boolean"), + Field("autograde", type="string"), + Field("topic", type="string"), + Field("feedback", type="text"), + Field("from_source", type="boolean"), + migrate=table_migrate_prefix + "questions.table", +) -db.define_table('tags', - Field('tag_name', type='string', unique=True), - migrate=table_migrate_prefix + 'tags.table') +db.define_table( + "tags", + Field("tag_name", type="string", unique=True), + migrate=table_migrate_prefix + "tags.table", +) -db.define_table('question_tags', - Field('question_id', db.questions), - Field('tag_id', db.tags), - migrate=table_migrate_prefix + 'question_tags.table') +db.define_table( + "question_tags", + Field("question_id", db.questions), + Field("tag_id", db.tags), + migrate=table_migrate_prefix + "question_tags.table", +) ## assignment <--> questions is a many-to-many relation. This table associates them ## points and how it's autograded are properties of a particular use of a question in an assignment, ## so that different instructors (assignments) can have a different way of doing it. -db.define_table('assignment_questions', - Field('assignment_id', db.assignments), - Field('question_id', db.questions), - Field('points', type='integer'), - Field('timed', type='boolean'), #deprecated; should be a property of the assignment - Field('autograde', type='string'), # oneof: null, all_or_nothing, pct_correct - Field('which_to_grade', type='string'), # oneof: first_answer, last_answer, last_answer_before_deadline, or best_answer - Field('reading_assignment', type='boolean'), # so we can differentiate reading part of an assignment from the questions to be embedded on the assignment page - # Also use this when it's an mchoice or parsons that's within a subchapter, not to be embeddedon the assignment page - Field('sorting_priority', type='integer'), #determines sort order of questions when displaying - Field('activities_required', type='integer'), # specifies how many activities in a sub chapter a student must perform in order to receive credit - migrate=table_migrate_prefix + 'assignment_questions.table') +db.define_table( + "assignment_questions", + Field("assignment_id", db.assignments), + Field("question_id", db.questions), + Field("points", type="integer"), + Field( + "timed", type="boolean" + ), # deprecated; should be a property of the assignment + Field("autograde", type="string"), # oneof: null, all_or_nothing, pct_correct + Field( + "which_to_grade", type="string" + ), # oneof: first_answer, last_answer, last_answer_before_deadline, or best_answer + Field( + "reading_assignment", type="boolean" + ), # so we can differentiate reading part of an assignment from the questions to be embedded on the assignment page + # Also use this when it's an mchoice or parsons that's within a subchapter, not to be embeddedon the assignment page + Field( + "sorting_priority", type="integer" + ), # determines sort order of questions when displaying + Field( + "activities_required", type="integer" + ), # specifies how many activities in a sub chapter a student must perform in order to receive credit + migrate=table_migrate_prefix + "assignment_questions.table", +) diff --git a/models/scheduler.py b/models/scheduler.py index 4f94753aa..05d909302 100644 --- a/models/scheduler.py +++ b/models/scheduler.py @@ -5,4 +5,4 @@ if settings.academy_mode: scheduler = Scheduler(db, migrate=table_migrate_prefix, heartbeat=1) - current.scheduler = scheduler \ No newline at end of file + current.scheduler = scheduler diff --git a/models/user_biography.py b/models/user_biography.py index d261c246b..89e8c21cb 100644 --- a/models/user_biography.py +++ b/models/user_biography.py @@ -1,10 +1,17 @@ -db.define_table('user_biography', - Field('user_id', 'reference auth_user'), - Field('prefered_name', 'text'), - # Field('pronounced_name', 'string'), - Field('interesting_fact', 'text'), - Field('programming_experience', 'text'), - Field('laptop_type', requires=IS_IN_SET(['Windows', 'Mac', 'Chromebook', 'Unix/Linux', 'Other', 'None'])), - Field('image', 'upload'), - Field('confidence', 'text'), - migrate=table_migrate_prefix + 'user_biography.table') +db.define_table( + "user_biography", + Field("user_id", "reference auth_user"), + Field("prefered_name", "text"), + # Field('pronounced_name', 'string'), + Field("interesting_fact", "text"), + Field("programming_experience", "text"), + Field( + "laptop_type", + requires=IS_IN_SET( + ["Windows", "Mac", "Chromebook", "Unix/Linux", "Other", "None"] + ), + ), + Field("image", "upload"), + Field("confidence", "text"), + migrate=table_migrate_prefix + "user_biography.table", +) diff --git a/modules/db_dashboard.py b/modules/db_dashboard.py index aead6be8c..414756e67 100644 --- a/modules/db_dashboard.py +++ b/modules/db_dashboard.py @@ -7,7 +7,7 @@ rslogger = logging.getLogger(current.settings.logger) rslogger.setLevel(current.settings.log_level) -#current.db.define_table('dash_problem_answers', +# current.db.define_table('dash_problem_answers', # Field('timestamp','datetime'), # Field('sid','string'), # Field('event','string'), @@ -15,10 +15,10 @@ # Field('div_id','string'), # Field('course_id','string'), # migrate=table_migrate_prefix + 'useinfo.table' -#) +# ) -#current.db.define_table('dash_problem_user_metrics', +# current.db.define_table('dash_problem_user_metrics', # Field('timestamp','datetime'), # Field('sid','string'), # Field('event','string'), @@ -26,7 +26,7 @@ # Field('div_id','string'), # Field('course_id','string'), # migrate=table_migrate_prefix + 'useinfo.table' -#) +# ) # it would be good at some point to save these to a table and # periodicly update them with new log entries instead of having @@ -38,9 +38,9 @@ def __init__(self, course_id, problem_id, users): self.course_id = course_id self.problem_id = problem_id self.problem_text = IdConverter.problem_id_to_text(problem_id) - #total responses by answer choice, eg. A: 5, B: 3, C: 13 + # total responses by answer choice, eg. A: 5, B: 3, C: 13 self.aggregate_responses = {} - #responses keyed by user + # responses keyed by user self.user_responses = {} for user in users: @@ -82,9 +82,10 @@ def user_number_responses(self): attempts = len(user_response.responses) if attempts >= 5: attempts = "5+" - histogram[attempts] = histogram.get(attempts,0) + 1 + histogram[attempts] = histogram.get(attempts, 0) + 1 return histogram + class UserResponse(object): NOT_ATTEMPTED = 0 INCOMPLETE = 1 @@ -95,11 +96,11 @@ def __init__(self, user): self.status = UserResponse.NOT_ATTEMPTED self.correct = False self.username = user.username - self.user = '{0} {1}'.format(user.first_name, user.last_name) + self.user = "{0} {1}".format(user.first_name, user.last_name) self.responses = [] def add_response(self, response, correct): - if not self.correct: #ignore if the person already answered it correctly. + if not self.correct: # ignore if the person already answered it correctly. self.responses.append(response) if correct: @@ -111,6 +112,7 @@ def add_response(self, response, correct): else: self.status = UserResponse.INCOMPLETE + class CourseProblemMetrics(object): def __init__(self, course_id, users, chapter): self.course_id = course_id @@ -119,44 +121,55 @@ def __init__(self, course_id, users, chapter): self.chapter = chapter def update_metrics(self, course_name): - rslogger.debug("Updating CourseProblemMetrics for {} of {}".format(self.chapter, course_name)) + rslogger.debug( + "Updating CourseProblemMetrics for {} of {}".format( + self.chapter, course_name + ) + ) rslogger.debug("doing chapter {}".format(self.chapter)) # todo: Join this with questions so that we can limit the questions to the selected chapter - mcans = current.db((current.db.mchoice_answers.course_name==course_name) & - (current.db.mchoice_answers.div_id == current.db.questions.name) & - (current.db.questions.chapter == self.chapter.chapter_label) - ).select(orderby=current.db.mchoice_answers.timestamp) + mcans = current.db( + (current.db.mchoice_answers.course_name == course_name) + & (current.db.mchoice_answers.div_id == current.db.questions.name) + & (current.db.questions.chapter == self.chapter.chapter_label) + ).select(orderby=current.db.mchoice_answers.timestamp) rslogger.debug("Found {} exercises") - fbans = current.db((current.db.fitb_answers.course_name==course_name) & - (current.db.fitb_answers.div_id == current.db.questions.name) & - (current.db.questions.chapter == self.chapter.chapter_label) - ).select(orderby=current.db.fitb_answers.timestamp) - psans = current.db((current.db.parsons_answers.course_name==course_name) & - (current.db.parsons_answers.div_id == current.db.questions.name) & - (current.db.questions.chapter == self.chapter.chapter_label) - ).select(orderby=current.db.parsons_answers.timestamp) + fbans = current.db( + (current.db.fitb_answers.course_name == course_name) + & (current.db.fitb_answers.div_id == current.db.questions.name) + & (current.db.questions.chapter == self.chapter.chapter_label) + ).select(orderby=current.db.fitb_answers.timestamp) + psans = current.db( + (current.db.parsons_answers.course_name == course_name) + & (current.db.parsons_answers.div_id == current.db.questions.name) + & (current.db.questions.chapter == self.chapter.chapter_label) + ).select(orderby=current.db.parsons_answers.timestamp) # convert the numeric answer to letter answers to match the questions easier. to_letter = dict(zip("0123456789", "ABCDEFGHIJ")) for row in mcans: - mc = row['mchoice_answers'] + mc = row["mchoice_answers"] mc.answer = to_letter.get(mc.answer, mc.answer) - def add_problems(result_set,tbl): + def add_problems(result_set, tbl): for srow in result_set: row = srow[tbl] rslogger.debug("UPDATE_METRICS {}".format(row)) if not row.div_id in self.problems: - self.problems[row.div_id] = ProblemMetrics(self.course_id, row.div_id, self.users) + self.problems[row.div_id] = ProblemMetrics( + self.course_id, row.div_id, self.users + ) self.problems[row.div_id].add_data_point(row) - add_problems(mcans, 'mchoice_answers') - add_problems(fbans, 'fitb_answers') - add_problems(psans, 'parsons_answers') + + add_problems(mcans, "mchoice_answers") + add_problems(fbans, "fitb_answers") + add_problems(psans, "parsons_answers") def retrieve_chapter_problems(self): return self + class UserActivityMetrics(object): def __init__(self, course_id, users): self.course_id = course_id @@ -169,9 +182,10 @@ def update_metrics(self, logs): if row.sid in self.user_activities: self.user_activities[row.sid].add_activity(row) + class UserActivity(object): def __init__(self, user): - self.name = "{0} {1}".format(user.first_name,user.last_name) + self.name = "{0} {1}".format(user.first_name, user.last_name) self.username = user.username self.rows = [] self.page_views = [] @@ -188,26 +202,31 @@ def get_recent_page_views(self): # returns page views for the last 7 days recentViewCount = 0 current = len(self.rows) - 1 - while current >= 0 and self.rows[current]['timestamp'] >= datetime.datetime.utcnow() - datetime.timedelta(days=7): + while current >= 0 and self.rows[current][ + "timestamp" + ] >= datetime.datetime.utcnow() - datetime.timedelta(days=7): recentViewCount += 1 current = current - 1 return recentViewCount - def get_activity_stats(self): return self + class UserActivityChapterProgress(object): def __init__(self, chapters, sub_chapter_progress): self.chapters = OrderedDict() for chapter in chapters: - self.chapters[chapter.chapter_label] = UserActivitySubChapterProgress(chapter) + self.chapters[chapter.chapter_label] = UserActivitySubChapterProgress( + chapter + ) for sub_chapter in sub_chapter_progress: try: self.chapters[sub_chapter.chapter_id].add_progress(sub_chapter) except KeyError: rslogger.debug("Key Error for {}".format(sub_chapter.chapter_id)) + class UserActivitySubChapterProgress(object): def __init__(self, chapter): self.chapter_label = chapter.chapter_name @@ -225,13 +244,23 @@ def add_progress(self, progress): def get_sub_chapter_progress(self): subchapters = [] - subchapter_res = current.db(current.db.sub_chapters.chapter_id == self.chapter_id).select() - sub_chapter_label_to_text = {sc.sub_chapter_label : sc.sub_chapter_name for sc in subchapter_res} + subchapter_res = current.db( + current.db.sub_chapters.chapter_id == self.chapter_id + ).select() + sub_chapter_label_to_text = { + sc.sub_chapter_label: sc.sub_chapter_name for sc in subchapter_res + } for subchapter_label, status in six.iteritems(self.sub_chapters): - subchapters.append({ - "label": sub_chapter_label_to_text.get(subchapter_label,subchapter_label), - "status": UserActivitySubChapterProgress.completion_status_to_text(status) - }) + subchapters.append( + { + "label": sub_chapter_label_to_text.get( + subchapter_label, subchapter_label + ), + "status": UserActivitySubChapterProgress.completion_status_to_text( + status + ), + } + ) return subchapters def status_text(self): @@ -254,20 +283,29 @@ def completion_status_to_text(status): return "notstarted" return status + class ProgressMetrics(object): def __init__(self, course_id, sub_chapters, users): self.sub_chapters = OrderedDict() for sub_chapter in sub_chapters: rslogger.debug(sub_chapter) - self.sub_chapters[sub_chapter.sub_chapter_label] = SubChapterActivity(sub_chapter, len(users)) + self.sub_chapters[sub_chapter.sub_chapter_label] = SubChapterActivity( + sub_chapter, len(users) + ) def update_metrics(self, logs, chapter_progress): for row in chapter_progress: try: - self.sub_chapters[row.user_sub_chapter_progress.sub_chapter_id].add_activity(row) - except KeyError as e: - rslogger.debug("Key error for {} user is {}".format(row.user_sub_chapter_progress.sub_chapter_id, current.auth.user.username)) - + self.sub_chapters[ + row.user_sub_chapter_progress.sub_chapter_id + ].add_activity(row) + except KeyError as e: + rslogger.debug( + "Key error for {} user is {}".format( + row.user_sub_chapter_progress.sub_chapter_id, + current.auth.user.username, + ) + ) class SubChapterActivity(object): @@ -298,41 +336,47 @@ def get_not_started_percent(self): def get_completed_percent(self): return "{0:.2f}%".format(float(self.completed) / self.total_users * 100) + class UserLogCategorizer(object): def __init__(self, logs): self.activities = [] for log in logs: - self.activities.append({ - "time": log.timestamp, - "event": UserLogCategorizer.format_event(log.event, log.act, log.div_id) - }) + self.activities.append( + { + "time": log.timestamp, + "event": UserLogCategorizer.format_event( + log.event, log.act, log.div_id + ), + } + ) @staticmethod def format_event(event, action, div_id): short_div_id = div_id if len(div_id) > 25: short_div_id = "...{0}".format(div_id[-25:]) - if (event == 'page') & (action == 'view'): + if (event == "page") & (action == "view"): return "{0} {1}".format("Viewed", short_div_id) - elif (event == 'timedExam') & (action =='start'): + elif (event == "timedExam") & (action == "start"): return "{0} {1}".format("Started Timed Exam", div_id) - elif (event == 'timedExam') & (action =='finish'): + elif (event == "timedExam") & (action == "finish"): return "{0} {1}".format("Finished Timed Exam", div_id) - elif (event == 'highlight'): + elif event == "highlight": return "{0} {1}".format("Highlighted", short_div_id) - elif (event == 'activecode') & (action == 'run'): + elif (event == "activecode") & (action == "run"): return "{0} {1}".format("Ran Activecode", div_id) - elif (event == 'parsons') & (action == 'yes'): + elif (event == "parsons") & (action == "yes"): return "{0} {1}".format("Solved Parsons", div_id) - elif (event == 'parsons') & (action != 'yes'): + elif (event == "parsons") & (action != "yes"): return "{0} {1}".format("Attempted Parsons", div_id) - elif (event == 'mChoice') | (event == 'fillb'): - answer = action.split(':') - if action.count(':') == 2 and answer[2] == 'correct': + elif (event == "mChoice") | (event == "fillb"): + answer = action.split(":") + if action.count(":") == 2 and answer[2] == "correct": return "{0} {1}".format("Solved", div_id) return "{0} {1}".format("Attempted", div_id) return "{0} {1}".format(event, div_id) + class DashboardDataAnalyzer(object): def __init__(self, course_id, chapter=None): self.course_id = course_id @@ -348,112 +392,238 @@ def load_chapter_metrics(self, chapter): return self.db_chapter = chapter - #go get all the course data... in the future the post processing - #should probably be stored and only new data appended. - self.course = current.db(current.db.courses.id == self.course_id).select().first() + # go get all the course data... in the future the post processing + # should probably be stored and only new data appended. + self.course = ( + current.db(current.db.courses.id == self.course_id).select().first() + ) rslogger.debug("COURSE QUERY GOT %s", self.course) - self.users = current.db((current.db.auth_user.course_id == current.auth.user.course_id) & (current.db.auth_user.active == 'T') ).select(current.db.auth_user.username, current.db.auth_user.first_name,current.db.auth_user.last_name, current.db.auth_user.id) - self.instructors = current.db((current.db.course_instructor.course == current.auth.user.course_id)).select(current.db.course_instructor.instructor) + self.users = current.db( + (current.db.auth_user.course_id == current.auth.user.course_id) + & (current.db.auth_user.active == "T") + ).select( + current.db.auth_user.username, + current.db.auth_user.first_name, + current.db.auth_user.last_name, + current.db.auth_user.id, + ) + self.instructors = current.db( + (current.db.course_instructor.course == current.auth.user.course_id) + ).select(current.db.course_instructor.instructor) inums = [x.instructor for x in self.instructors] self.users.exclude(lambda x: x.id in inums) - self.logs = current.db((current.db.useinfo.course_id==self.course.course_name) & (current.db.useinfo.timestamp >= self.course.term_start_date)).select(current.db.useinfo.timestamp,current.db.useinfo.sid, current.db.useinfo.event,current.db.useinfo.act,current.db.useinfo.div_id, orderby=current.db.useinfo.timestamp) + self.logs = current.db( + (current.db.useinfo.course_id == self.course.course_name) + & (current.db.useinfo.timestamp >= self.course.term_start_date) + ).select( + current.db.useinfo.timestamp, + current.db.useinfo.sid, + current.db.useinfo.event, + current.db.useinfo.act, + current.db.useinfo.div_id, + orderby=current.db.useinfo.timestamp, + ) # todo: Yikes! Loading all of the log data for a large or even medium class is a LOT - self.db_chapter_progress = current.db((current.db.user_sub_chapter_progress.user_id == current.db.auth_user.id) & - (current.db.auth_user.course_id == current.auth.user.course_id) & # todo: missing link from course_id to chapter/sub_chapter progress - (current.db.user_sub_chapter_progress.chapter_id == chapter.chapter_label)).select(current.db.auth_user.username,current.db.user_sub_chapter_progress.chapter_id,current.db.user_sub_chapter_progress.sub_chapter_id,current.db.user_sub_chapter_progress.status,current.db.auth_user.id) + self.db_chapter_progress = current.db( + (current.db.user_sub_chapter_progress.user_id == current.db.auth_user.id) + & (current.db.auth_user.course_id == current.auth.user.course_id) + & ( # todo: missing link from course_id to chapter/sub_chapter progress + current.db.user_sub_chapter_progress.chapter_id == chapter.chapter_label + ) + ).select( + current.db.auth_user.username, + current.db.user_sub_chapter_progress.chapter_id, + current.db.user_sub_chapter_progress.sub_chapter_id, + current.db.user_sub_chapter_progress.status, + current.db.auth_user.id, + ) self.db_chapter_progress.exclude(lambda x: x.auth_user.id in inums) - self.db_sub_chapters = current.db((current.db.sub_chapters.chapter_id == chapter.id)).select(current.db.sub_chapters.ALL,orderby=current.db.sub_chapters.id) + self.db_sub_chapters = current.db( + (current.db.sub_chapters.chapter_id == chapter.id) + ).select(current.db.sub_chapters.ALL, orderby=current.db.sub_chapters.id) self.problem_metrics = CourseProblemMetrics(self.course_id, self.users, chapter) rslogger.debug("About to call update_metrics") self.problem_metrics.update_metrics(self.course.course_name) self.user_activity = UserActivityMetrics(self.course_id, self.users) self.user_activity.update_metrics(self.logs) - self.progress_metrics = ProgressMetrics(self.course_id, self.db_sub_chapters, self.users) + self.progress_metrics = ProgressMetrics( + self.course_id, self.db_sub_chapters, self.users + ) self.progress_metrics.update_metrics(self.logs, self.db_chapter_progress) self.questions = {} for i in self.problem_metrics.problems.keys(): - self.questions[i] = current.db((current.db.questions.name == i) & - (current.db.questions.base_course == self.course.base_course)).select(current.db.questions.chapter, current.db.questions.subchapter).first() + self.questions[i] = ( + current.db( + (current.db.questions.name == i) + & (current.db.questions.base_course == self.course.base_course) + ) + .select(current.db.questions.chapter, current.db.questions.subchapter) + .first() + ) def load_user_metrics(self, username): self.username = username - self.course = current.db(current.db.courses.id == self.course_id).select().first() + self.course = ( + current.db(current.db.courses.id == self.course_id).select().first() + ) if not self.course: rslogger.debug("ERROR - NO COURSE course_id = {}".format(self.course_id)) - self.chapters = current.db(current.db.chapters.course_id == current.auth.user.course_name).select() - self.user = current.db((current.db.auth_user.username == username) & - (current.db.auth_user.course_id == self.course_id)).select(current.db.auth_user.id, current.db.auth_user.first_name, current.db.auth_user.last_name, current.db.auth_user.email, current.db.auth_user.username).first() + self.chapters = current.db( + current.db.chapters.course_id == current.auth.user.course_name + ).select() + self.user = ( + current.db( + (current.db.auth_user.username == username) + & (current.db.auth_user.course_id == self.course_id) + ) + .select( + current.db.auth_user.id, + current.db.auth_user.first_name, + current.db.auth_user.last_name, + current.db.auth_user.email, + current.db.auth_user.username, + ) + .first() + ) if not self.user: - rslogger.debug("ERROR - NO USER username={} course_id={}".format(username, self.course_id)) - current.session.flash = 'Please make sure you are in the correct course' - redirect(URL('default', 'courses')) + rslogger.debug( + "ERROR - NO USER username={} course_id={}".format( + username, self.course_id + ) + ) + current.session.flash = "Please make sure you are in the correct course" + redirect(URL("default", "courses")) # TODO: calling redirect here is kind of a hacky way to handle this. - self.logs = current.db((current.db.useinfo.course_id==self.course.course_name) & - (current.db.useinfo.sid == username) & - (current.db.useinfo.timestamp >= self.course.term_start_date)).select(current.db.useinfo.timestamp,current.db.useinfo.sid, current.db.useinfo.event,current.db.useinfo.act,current.db.useinfo.div_id, orderby=~current.db.useinfo.timestamp) - self.db_chapter_progress = current.db((current.db.user_sub_chapter_progress.user_id == self.user.id)).select(current.db.user_sub_chapter_progress.chapter_id,current.db.user_sub_chapter_progress.sub_chapter_id,current.db.user_sub_chapter_progress.status) + self.logs = current.db( + (current.db.useinfo.course_id == self.course.course_name) + & (current.db.useinfo.sid == username) + & (current.db.useinfo.timestamp >= self.course.term_start_date) + ).select( + current.db.useinfo.timestamp, + current.db.useinfo.sid, + current.db.useinfo.event, + current.db.useinfo.act, + current.db.useinfo.div_id, + orderby=~current.db.useinfo.timestamp, + ) + self.db_chapter_progress = current.db( + (current.db.user_sub_chapter_progress.user_id == self.user.id) + ).select( + current.db.user_sub_chapter_progress.chapter_id, + current.db.user_sub_chapter_progress.sub_chapter_id, + current.db.user_sub_chapter_progress.status, + ) self.formatted_activity = UserLogCategorizer(self.logs) - self.chapter_progress = UserActivityChapterProgress(self.chapters, self.db_chapter_progress) + self.chapter_progress = UserActivityChapterProgress( + self.chapters, self.db_chapter_progress + ) def load_exercise_metrics(self, exercise): - self.course = current.db(current.db.courses.id == self.course_id).select().first() - self.users = current.db(current.db.auth_user.course_id == current.auth.user.course_id).select(current.db.auth_user.username, current.db.auth_user.first_name,current.db.auth_user.last_name) - self.logs = current.db((current.db.useinfo.course_id==self.course.course_name) & (current.db.useinfo.timestamp >= self.course.term_start_date)).select(current.db.useinfo.timestamp,current.db.useinfo.sid, current.db.useinfo.event,current.db.useinfo.act,current.db.useinfo.div_id, orderby=current.db.useinfo.timestamp) - self.problem_metrics = CourseProblemMetrics(self.course_id, self.users,self.db_chapter) + self.course = ( + current.db(current.db.courses.id == self.course_id).select().first() + ) + self.users = current.db( + current.db.auth_user.course_id == current.auth.user.course_id + ).select( + current.db.auth_user.username, + current.db.auth_user.first_name, + current.db.auth_user.last_name, + ) + self.logs = current.db( + (current.db.useinfo.course_id == self.course.course_name) + & (current.db.useinfo.timestamp >= self.course.term_start_date) + ).select( + current.db.useinfo.timestamp, + current.db.useinfo.sid, + current.db.useinfo.event, + current.db.useinfo.act, + current.db.useinfo.div_id, + orderby=current.db.useinfo.timestamp, + ) + self.problem_metrics = CourseProblemMetrics( + self.course_id, self.users, self.db_chapter + ) self.problem_metrics.update_metrics(self.course.course_name) def load_assignment_metrics(self, username, studentView=False): self.assignments = [] - res = current.db(current.db.assignments.course == self.course_id)\ - .select(current.db.assignments.id, current.db.assignments.name, current.db.assignments.points, current.db.assignments.duedate, current.db.assignments.released) - # ^ Get assignments from DB + res = current.db(current.db.assignments.course == self.course_id).select( + current.db.assignments.id, + current.db.assignments.name, + current.db.assignments.points, + current.db.assignments.duedate, + current.db.assignments.released, + ) + # ^ Get assignments from DB for aRow in res: self.assignments.append(aRow.as_dict()) self.grades = {} for assign in self.assignments: - rslogger.debug("Processing assignment %s",assign) - row = current.db((current.db.grades.assignment == assign["id"]) & (current.db.grades.auth_user == current.db.auth_user.id))\ - .select(current.db.auth_user.username, current.db.grades.auth_user, current.db.grades.score, current.db.grades.assignment) - # ^ Get grades for assignment - - if row.records: # If the row has a result - rl = row.as_list() # List of dictionaries + rslogger.debug("Processing assignment %s", assign) + row = current.db( + (current.db.grades.assignment == assign["id"]) + & (current.db.grades.auth_user == current.db.auth_user.id) + ).select( + current.db.auth_user.username, + current.db.grades.auth_user, + current.db.grades.score, + current.db.grades.assignment, + ) + # ^ Get grades for assignment + + if row.records: # If the row has a result + rl = row.as_list() # List of dictionaries rslogger.debug("RL = %s", rl) - if studentView and not assign['released']: # N/A should be shown to students if assignment grades are not released - self.grades[assign["name"]] = {"score":"N/A", - "class_average":"N/A", - "due_date":assign["duedate"].date().strftime("%m-%d-%Y")} + if ( + studentView and not assign["released"] + ): # N/A should be shown to students if assignment grades are not released + self.grades[assign["name"]] = { + "score": "N/A", + "class_average": "N/A", + "due_date": assign["duedate"].date().strftime("%m-%d-%Y"), + } else: s = 0.0 count = 0 self.grades[assign["name"]] = {} for userEntry in rl: - rslogger.debug("GETTING USER SCORES %s",userEntry) + rslogger.debug("GETTING USER SCORES %s", userEntry) this_score = userEntry["grades"]["score"] if this_score != None: - s += this_score # Calculating average + s += this_score # Calculating average count += 1 - if userEntry["auth_user"]["username"] == username: # If this is the student we are looking for + if ( + userEntry["auth_user"]["username"] == username + ): # If this is the student we are looking for self.grades[assign["name"]]["score"] = this_score - if 'score' not in self.grades[assign["name"]]: - self.grades[assign["name"]]["score"] = "N/A" # This is redundant as a failsafe + if "score" not in self.grades[assign["name"]]: + self.grades[assign["name"]][ + "score" + ] = "N/A" # This is redundant as a failsafe rslogger.debug("COUNT = %s", count) try: - average = s/count + average = s / count except: average = 0 - self.grades[assign["name"]]["class_average"] = "{:.02f}".format(average) - self.grades[assign["name"]]["due_date"] = assign["duedate"].date().strftime("%m-%d-%Y") + self.grades[assign["name"]]["class_average"] = "{:.02f}".format( + average + ) + self.grades[assign["name"]]["due_date"] = ( + assign["duedate"].date().strftime("%m-%d-%Y") + ) + + else: # The row has no result --> the query returned empty + self.grades[assign["name"]] = { + "score": "N/A", + "class_average": "N/A", + "due_date": assign["duedate"].date().strftime("%m-%d-%Y"), + } - else: # The row has no result --> the query returned empty - self.grades[assign["name"]] = {"score":"N/A", - "class_average":"N/A", - "due_date":assign["duedate"].date().strftime("%m-%d-%Y")} # This whole object is a workaround because these strings # are not generated and stored in the db. This needs automating @@ -465,22 +635,20 @@ def load_assignment_metrics(self, username, studentView=False): # way to identify it. We could use its chapter-subchapter-position... class IdConverter(object): problem_id_map = { - "pre_1":"Pretest-1: What will be the values in x, y, and z after the following lines of code execute?", - "pre_2":"Pretest-2: What is the output from the program below?", - "1_3_1_BMI_Q1":"1-3-1: Imagine that you are 5 foot 7 inches and weighed 140 pounds. What is your BMI?", - "1_4_1_String_Methods_Q1":"1-4-1: What would the following code print?", - "1_5_1_Turtle_Q1":"1-5-1: Which direction will alex move when the code below executes?", - "":"1-5-2: ", - "1_6_1_Image_Q1":"1-6-1: Which way does y increase on an image?", - - "3_2_1_Mult_fill":"3-2-1: What will be printed when you click on the Run button in the code below?", - "3_2_2_Div_fill":"3-2-2: What will be printed when you click on the Run button in the code below?", - "3_2_3_Mod_fill":"3-2-3: What will be printed when you click on the Run button in the code below?", - "4_1_2_noSpace":"4-1-2: What will be printed when the following executes?", - "4_2_2_Slice2":"4-2-2: What will be printed when the following executes?", - - "4_3_1_s1":"4-3-1: Given the following code segment, what is the value of the string s1 after these are executed?", - "4_3_2_s2":"4-3-2: What is the value of s1 after the following code executes?", + "pre_1": "Pretest-1: What will be the values in x, y, and z after the following lines of code execute?", + "pre_2": "Pretest-2: What is the output from the program below?", + "1_3_1_BMI_Q1": "1-3-1: Imagine that you are 5 foot 7 inches and weighed 140 pounds. What is your BMI?", + "1_4_1_String_Methods_Q1": "1-4-1: What would the following code print?", + "1_5_1_Turtle_Q1": "1-5-1: Which direction will alex move when the code below executes?", + "": "1-5-2: ", + "1_6_1_Image_Q1": "1-6-1: Which way does y increase on an image?", + "3_2_1_Mult_fill": "3-2-1: What will be printed when you click on the Run button in the code below?", + "3_2_2_Div_fill": "3-2-2: What will be printed when you click on the Run button in the code below?", + "3_2_3_Mod_fill": "3-2-3: What will be printed when you click on the Run button in the code below?", + "4_1_2_noSpace": "4-1-2: What will be printed when the following executes?", + "4_2_2_Slice2": "4-2-2: What will be printed when the following executes?", + "4_3_1_s1": "4-3-1: Given the following code segment, what is the value of the string s1 after these are executed?", + "4_3_2_s2": "4-3-2: What is the value of s1 after the following code executes?", } @staticmethod diff --git a/modules/feedback.py b/modules/feedback.py index 23e3900b7..624d397cc 100644 --- a/modules/feedback.py +++ b/modules/feedback.py @@ -24,8 +24,13 @@ # Third-party imports # ------------------- from gluon import current -from runestone.lp.lp_common_lib import STUDENT_SOURCE_PATH, \ - code_here_comment, read_sphinx_config, BUILD_SYSTEM_PATH, get_sim_str_sim30 +from runestone.lp.lp_common_lib import ( + STUDENT_SOURCE_PATH, + code_here_comment, + read_sphinx_config, + BUILD_SYSTEM_PATH, + get_sim_str_sim30, +) # Local imports # ------------- @@ -38,14 +43,15 @@ def is_server_feedback(div_id, course): # Get the information about this question. Per the `web2py docs # `_, # an assignment in ``models/db.py`` makes ``current.db`` available. - query_results = current.db( - (current.db.questions.name == div_id) & - (current.db.questions.base_course == current.db.courses.base_course) & - (current.db.courses.course_name == course) - ).select( - current.db.questions.feedback, - current.db.courses.login_required - ).first() + query_results = ( + current.db( + (current.db.questions.name == div_id) + & (current.db.questions.base_course == current.db.courses.base_course) + & (current.db.courses.course_name == course) + ) + .select(current.db.questions.feedback, current.db.courses.login_required) + .first() + ) # check for query_results if not query_results: @@ -71,7 +77,7 @@ def fitb_feedback(answer_json, feedback): # new format should always return an array. assert isinstance(answer, list) except: - answer = answer_json.split(',') + answer = answer_json.split(",") displayFeed = [] isCorrectArray = [] # The overall correctness of the entire problem. @@ -79,24 +85,25 @@ def fitb_feedback(answer_json, feedback): for blank, feedback_for_blank in zip(answer, feedback): if not blank: isCorrectArray.append(None) - displayFeed.append('No answer provided.') + displayFeed.append("No answer provided.") correct = False else: # The correctness of this problem depends on if the first item matches. is_first_item = True # Check everything but the last answer, which always matches. for fb in feedback_for_blank[:-1]: - if 'regex' in fb: - if re.search(fb['regex'], blank, - re.I if fb['regexFlags'] == 'i' else 0): + if "regex" in fb: + if re.search( + fb["regex"], blank, re.I if fb["regexFlags"] == "i" else 0 + ): isCorrectArray.append(is_first_item) if not is_first_item: correct = False - displayFeed.append(fb['feedback']) + displayFeed.append(fb["feedback"]) break else: - assert 'number' in fb - min_, max_ = fb['number'] + assert "number" in fb + min_, max_ = fb["number"] try: val = ast.literal_eval(blank) in_range = val >= min_ and val <= max_ @@ -107,51 +114,59 @@ def fitb_feedback(answer_json, feedback): isCorrectArray.append(is_first_item) if not is_first_item: correct = False - displayFeed.append(fb['feedback']) + displayFeed.append(fb["feedback"]) break is_first_item = False # Nothing matched. Use the last feedback. else: isCorrectArray.append(False) correct = False - displayFeed.append(feedback_for_blank[-1]['feedback']) + displayFeed.append(feedback_for_blank[-1]["feedback"]) # Return grading results to the client for a non-test scenario. - res = dict( - correct=correct, - displayFeed=displayFeed, - isCorrectArray=isCorrectArray) - return 'T' if correct else 'F', res + res = dict(correct=correct, displayFeed=displayFeed, isCorrectArray=isCorrectArray) + return "T" if correct else "F", res # lp feedback # =========== def lp_feedback(code_snippets, feedback_struct): db = current.db - base_course = db( - (db.courses.id == current.auth.user.course_id) - ).select(db.courses.base_course).first().base_course - sphinx_base_path = os.path.join(current.request.folder, 'books', base_course) - source_path = feedback_struct['source_path'] + base_course = ( + db((db.courses.id == current.auth.user.course_id)) + .select(db.courses.base_course) + .first() + .base_course + ) + sphinx_base_path = os.path.join(current.request.folder, "books", base_course) + source_path = feedback_struct["source_path"] # Read the Sphinx config file to find paths relative to this directory. sphinx_config = read_sphinx_config(sphinx_base_path) if not sphinx_config: return { - 'errors': ['Unable to load Sphinx configuration file from {}'.format(sphinx_base_path)] + "errors": [ + "Unable to load Sphinx configuration file from {}".format( + sphinx_base_path + ) + ] } - sphinx_source_path = sphinx_config['SPHINX_SOURCE_PATH'] - sphinx_out_path = sphinx_config['SPHINX_OUT_PATH'] + sphinx_source_path = sphinx_config["SPHINX_SOURCE_PATH"] + sphinx_out_path = sphinx_config["SPHINX_OUT_PATH"] # Next, read the student source in for the program the student is working on. try: # Find the path to the student source file. - abs_source_path = os.path.normpath(os.path.join(sphinx_base_path, - sphinx_out_path, STUDENT_SOURCE_PATH, source_path)) - with open(abs_source_path, encoding='utf-8') as f: + abs_source_path = os.path.normpath( + os.path.join( + sphinx_base_path, sphinx_out_path, STUDENT_SOURCE_PATH, source_path + ) + ) + with open(abs_source_path, encoding="utf-8") as f: source_str = f.read() except Exception as e: - return { 'errors': ['Cannot open source file {}: {}.' - .format(abs_source_path, e)] } + return { + "errors": ["Cannot open source file {}: {}.".format(abs_source_path, e)] + } # Create a snippet-replaced version of the source, by looking for "put code # here" comments and replacing them with the provided code. To do so, @@ -160,35 +175,44 @@ def lp_feedback(code_snippets, feedback_struct): # Sanity check! Source with n "put code here" comments splits into n+1 # items, into which the n student code snippets should be interleaved. if len(split_source) - 1 != len(code_snippets): - return { 'errors': ['Wrong number of snippets.'] } + return {"errors": ["Wrong number of snippets."]} # Interleave these with the student snippets. - interleaved_source = [None]*(2*len(split_source) - 1) + interleaved_source = [None] * (2 * len(split_source) - 1) interleaved_source[::2] = split_source try: - interleaved_source[1::2] = _platform_edit(feedback_struct['builder'], - code_snippets, source_path) + interleaved_source[1::2] = _platform_edit( + feedback_struct["builder"], code_snippets, source_path + ) except Exception as e: - return { 'errors': ['An exception occurred: {}'.format(e)] } + return {"errors": ["An exception occurred: {}".format(e)]} # Join them into a single string. Make sure newlines separate everything. - source_str = '\n'.join(interleaved_source) + source_str = "\n".join(interleaved_source) # Create a temporary directory, then write the source there. Horrible kluge # for Python 2.7. Much better: use tempfile.TemporaryDirectory instead. try: temp_path = tempfile.mkdtemp() temp_source_path = os.path.join(temp_path, os.path.basename(source_path)) - with open(temp_source_path, 'w', encoding='utf-8') as f: + with open(temp_source_path, "w", encoding="utf-8") as f: f.write(source_str) # Schedule the build. Omitting this commit causes tests to fail. ??? current.db.commit() - task = current.scheduler.queue_task(_scheduled_builder, pargs=[ - feedback_struct['builder'], temp_source_path, sphinx_base_path, - sphinx_source_path, sphinx_out_path, source_path], immediate=True) + task = current.scheduler.queue_task( + _scheduled_builder, + pargs=[ + feedback_struct["builder"], + temp_source_path, + sphinx_base_path, + sphinx_source_path, + sphinx_out_path, + source_path, + ], + immediate=True, + ) if task.errors: return { - 'errors': ['Error in scheduling build task: {}' - .format(task.errors)] + "errors": ["Error in scheduling build task: {}".format(task.errors)] } # In order to monitor the status of the scheduled task, commit it now. (web2py assumes that the current request wouldn't want to monitor its own scheduled task.) This allows the workers to see it and begin work. current.db.commit() @@ -196,35 +220,38 @@ def lp_feedback(code_snippets, feedback_struct): while True: time.sleep(0.5) task_status = current.scheduler.task_status(task.id, output=True) - if task_status.scheduler_task.status == 'EXPIRED': + if task_status.scheduler_task.status == "EXPIRED": # Remove the task entry, since it's no longer needed. del current.db.scheduler_task[task_status.scheduler_task.id] - return { 'errors': ['Build task expired.'] } - elif task_status.scheduler_task.status == 'TIMEOUT': + return {"errors": ["Build task expired."]} + elif task_status.scheduler_task.status == "TIMEOUT": del current.db.scheduler_task[task_status.scheduler_task.id] - return { 'errors': ['Build task timed out.'] } - elif task_status.scheduler_task.status == 'FAILED': + return {"errors": ["Build task timed out."]} + elif task_status.scheduler_task.status == "FAILED": # This also deletes the ``scheduler_run`` record. del current.db.scheduler_task[task_status.scheduler_task.id] return { - 'errors': ['Exception during build: {}'. - format(task_status.scheduler_run.traceback)] + "errors": [ + "Exception during build: {}".format( + task_status.scheduler_run.traceback + ) + ] } - elif task_status.scheduler_task.status == 'COMPLETED': + elif task_status.scheduler_task.status == "COMPLETED": # This also deletes the ``scheduler_run`` record. del current.db.scheduler_task[task_status.scheduler_task.id] output, is_correct = json.loads(task_status.scheduler_run.run_result) return { # The answer. - 'answer': { + "answer": { # Strip whitespace and return only the last 4K or data or so. # There's no need for more -- it's probably just a crashed or # confused program spewing output, so don't waste bandwidth or # storage space on it. - 'resultString': output.strip()[-4096:], + "resultString": output.strip()[-4096:] }, - 'correct': is_correct, + "correct": is_correct, } finally: shutil.rmtree(temp_path) @@ -239,7 +266,8 @@ def _platform_edit( # A list of code snippets submitted by the user. code_snippets, # The name of the source file into which these snippets will be inserted. - source_path): + source_path, +): # Prepend a line number directive to each snippet. I can't get this to work # in the assembler. I tried: @@ -264,25 +292,27 @@ def _platform_edit( # # Select what to prepend based on the language. ext = os.path.splitext(source_path)[1] - if ext == '.c': + if ext == ".c": # See https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html. fmt = '#line 1 "box {}"\n' - elif ext == '.s': - fmt = '' - elif ext == '.py': + elif ext == ".s": + fmt = "" + elif ext == ".py": # Python doesn't (easily) support `setting line numbers `_. - fmt = '' + fmt = "" else: # This is an unsupported language. It would be nice to report this as an error instead of raising an exception. - raise RuntimeError('Unsupported extension {}'.format(ext)) - return [fmt.format(index + 1) + code_snippets[index] - for index in range(len(code_snippets))] + raise RuntimeError("Unsupported extension {}".format(ext)) + return [ + fmt.format(index + 1) + code_snippets[index] + for index in range(len(code_snippets)) + ] # Transform the arguments to ``subprocess.run`` into a string showing what # command will be executed. def _subprocess_string(*args, **kwargs): - return kwargs.get('cwd', '') + '% ' + ' '.join(args[0]) + '\n' + return kwargs.get("cwd", "") + "% " + " ".join(args[0]) + "\n" # This function should run the provided code and report the results. It will @@ -302,45 +332,85 @@ def _scheduled_builder( sphinx_out_path, # A relative path to the source file from the ``sphinx_source_path``, based # on the submitting web page. - source_path): + source_path, +): - if builder == 'unsafe-python' and os.environ.get('WEB2PY_CONFIG') == 'test': + if builder == "unsafe-python" and os.environ.get("WEB2PY_CONFIG") == "test": # Run the test in Python. This is for testing only, and should never be used in production; instead, this should be run in a limited Docker container. For simplicity, it lacks a timeout. # # First, copy the test to the temp directory. Otherwise, running the test file from its book location means it will import the solution, which is in the same directory. cwd = os.path.dirname(file_path) - test_file_name = os.path.splitext(os.path.basename(file_path))[0] + '-test.py' + test_file_name = os.path.splitext(os.path.basename(file_path))[0] + "-test.py" dest_test_path = os.path.join(cwd, test_file_name) - shutil.copyfile(os.path.join(sphinx_base_path, sphinx_source_path, - os.path.dirname(source_path), test_file_name), - dest_test_path) + shutil.copyfile( + os.path.join( + sphinx_base_path, + sphinx_source_path, + os.path.dirname(source_path), + test_file_name, + ), + dest_test_path, + ) try: - str_out = subprocess.check_output([sys.executable, dest_test_path], - stderr=subprocess.STDOUT, universal_newlines=True, cwd=cwd) + str_out = subprocess.check_output( + [sys.executable, dest_test_path], + stderr=subprocess.STDOUT, + universal_newlines=True, + cwd=cwd, + ) return str_out, 100 except subprocess.CalledProcessError as e: - #from gluon.debug import dbg; dbg.set_trace() + # from gluon.debug import dbg; dbg.set_trace() return e.output, 0 - elif builder != 'pic24-xc16-bullylib': - raise RuntimeError('Unknown builder {}'.format(builder)) + elif builder != "pic24-xc16-bullylib": + raise RuntimeError("Unknown builder {}".format(builder)) # Assemble or compile the source. We assume that the binaries are already in the path. - xc16_path = '' + xc16_path = "" # Compile in the temporary directory, in which ``file_path`` resides. sp_args = dict( stderr=subprocess.STDOUT, universal_newlines=True, cwd=os.path.dirname(file_path), ) - o_path = file_path + '.o' + o_path = file_path + ".o" extension = os.path.splitext(file_path)[1] - if extension == '.s': - args = [os.path.join(xc16_path, 'xc16-as'), '-omf=elf', '-g', - '--processor=33EP128GP502', file_path, '-o' + o_path] - elif extension == '.c': - args = [os.path.join(xc16_path, 'xc16-gcc'), '-mcpu=33EP128GP502', '-omf=elf', '-g', '-O0', '-msmart-io=1', '-Wall', '-Wextra', '-Wdeclaration-after-statement', '-I' + os.path.join(sphinx_base_path, sphinx_source_path, 'lib/include'), '-I' + os.path.join(sphinx_base_path, sphinx_source_path, 'tests'), '-I' + os.path.join(sphinx_base_path, sphinx_source_path, 'tests/platform/Microchip_PIC24'), '-I' + os.path.join(sphinx_base_path, sphinx_source_path, os.path.dirname(source_path)), file_path, '-c', '-o' + o_path] + if extension == ".s": + args = [ + os.path.join(xc16_path, "xc16-as"), + "-omf=elf", + "-g", + "--processor=33EP128GP502", + file_path, + "-o" + o_path, + ] + elif extension == ".c": + args = [ + os.path.join(xc16_path, "xc16-gcc"), + "-mcpu=33EP128GP502", + "-omf=elf", + "-g", + "-O0", + "-msmart-io=1", + "-Wall", + "-Wextra", + "-Wdeclaration-after-statement", + "-I" + os.path.join(sphinx_base_path, sphinx_source_path, "lib/include"), + "-I" + os.path.join(sphinx_base_path, sphinx_source_path, "tests"), + "-I" + + os.path.join( + sphinx_base_path, sphinx_source_path, "tests/platform/Microchip_PIC24" + ), + "-I" + + os.path.join( + sphinx_base_path, sphinx_source_path, os.path.dirname(source_path) + ), + file_path, + "-c", + "-o" + o_path, + ] else: - raise RuntimeError('Unknown file extension in {}.'.format(file_path)) + raise RuntimeError("Unknown file extension in {}.".format(file_path)) out = _subprocess_string(args, **sp_args) try: out += subprocess.check_output(args, **sp_args) @@ -349,13 +419,38 @@ def _scheduled_builder( return out, 0 # Link. - elf_path = file_path + '.elf' - waf_root = os.path.normpath(os.path.join(sphinx_base_path, sphinx_out_path, - BUILD_SYSTEM_PATH, sphinx_source_path)) - test_object_path = os.path.join(waf_root, - os.path.splitext(source_path)[0] + '-test.c.1.o') - args = [os.path.join(xc16_path, 'xc16-gcc'), '-omf=elf', '-Wl,--heap=100,--stack=16,--check-sections,--data-init,--pack-data,--handles,--isr,--no-gc-sections,--fill-upper=0,--stackguard=16,--no-force-link,--smart-io', '-Wl,--script=' + os.path.join(sphinx_base_path, sphinx_source_path, 'lib/lkr/p33EP128GP502_bootldr.gld'), test_object_path, o_path, os.path.join(waf_root, 'lib/src/pic24_clockfreq.c.1.o'), os.path.join(waf_root, 'lib/src/pic24_configbits.c.1.o'), os.path.join(waf_root, 'lib/src/pic24_serial.c.1.o'), os.path.join(waf_root, 'lib/src/pic24_timer.c.1.o'), os.path.join(waf_root, 'lib/src/pic24_uart.c.1.o'), os.path.join(waf_root, 'lib/src/pic24_util.c.1.o'), os.path.join(waf_root, 'tests/test_utils.c.1.o'), os.path.join(waf_root, 'tests/test_assert.c.1.o'), '-o' + elf_path, '-Wl,-Bstatic', '-Wl,-Bdynamic'] - out += '\n' + _subprocess_string(args, **sp_args) + elf_path = file_path + ".elf" + waf_root = os.path.normpath( + os.path.join( + sphinx_base_path, sphinx_out_path, BUILD_SYSTEM_PATH, sphinx_source_path + ) + ) + test_object_path = os.path.join( + waf_root, os.path.splitext(source_path)[0] + "-test.c.1.o" + ) + args = [ + os.path.join(xc16_path, "xc16-gcc"), + "-omf=elf", + "-Wl,--heap=100,--stack=16,--check-sections,--data-init,--pack-data,--handles,--isr,--no-gc-sections,--fill-upper=0,--stackguard=16,--no-force-link,--smart-io", + "-Wl,--script=" + + os.path.join( + sphinx_base_path, sphinx_source_path, "lib/lkr/p33EP128GP502_bootldr.gld" + ), + test_object_path, + o_path, + os.path.join(waf_root, "lib/src/pic24_clockfreq.c.1.o"), + os.path.join(waf_root, "lib/src/pic24_configbits.c.1.o"), + os.path.join(waf_root, "lib/src/pic24_serial.c.1.o"), + os.path.join(waf_root, "lib/src/pic24_timer.c.1.o"), + os.path.join(waf_root, "lib/src/pic24_uart.c.1.o"), + os.path.join(waf_root, "lib/src/pic24_util.c.1.o"), + os.path.join(waf_root, "tests/test_utils.c.1.o"), + os.path.join(waf_root, "tests/test_assert.c.1.o"), + "-o" + elf_path, + "-Wl,-Bstatic", + "-Wl,-Bdynamic", + ] + out += "\n" + _subprocess_string(args, **sp_args) try: out += subprocess.check_output(args, **sp_args) except subprocess.CalledProcessError as e: @@ -363,22 +458,24 @@ def _scheduled_builder( return out, 0 # Simulate. Create the simulation commands. - simout_path = file_path + '.simout' - ss = get_sim_str_sim30('dspic33epsuper', elf_path, simout_path) + simout_path = file_path + ".simout" + ss = get_sim_str_sim30("dspic33epsuper", elf_path, simout_path) # Run the simulation. This is a re-coded version of ``wscript.sim_run`` -- I # couldn't find a way to re-use that code. sim_ret = 0 - args = [os.path.join(xc16_path, 'sim30')] - out += '\nTest results:\n' + _subprocess_string(args, **sp_args) + args = [os.path.join(xc16_path, "sim30")] + out += "\nTest results:\n" + _subprocess_string(args, **sp_args) timeout_list = [] try: - p = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, **sp_args) + p = subprocess.Popen( + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, **sp_args + ) # Horrible kludge: implement a crude timeout. Instead, use ``timeout`` # with Python 3. def on_timeout(msg_list): p.terminate() - msg_list += ['\n\nTimeout.'] + msg_list += ["\n\nTimeout."] + t = Timer(3, on_timeout, [timeout_list]) t.start() p.communicate(ss) @@ -387,8 +484,8 @@ def on_timeout(msg_list): sim_ret = 1 # Check the output. t.cancel() - with open(simout_path, encoding='utf-8') as f: + with open(simout_path, encoding="utf-8") as f: out += f.read().rstrip() # Put the timeout string at the end of all the simulator output. - out += ''.join(timeout_list) - return out, (100 if not sim_ret and out.endswith('Correct.') else 0) + out += "".join(timeout_list) + return out, (100 if not sim_ret and out.endswith("Correct.") else 0) diff --git a/modules/outcome_request.py b/modules/outcome_request.py index 4953e10eb..4f22aac13 100644 --- a/modules/outcome_request.py +++ b/modules/outcome_request.py @@ -1,4 +1,3 @@ - # Forked from - Wed Jun 21 09:01:59 EDT 2017 # https://github.com/tophatmonocle/ims_lti_py @@ -21,26 +20,26 @@ from outcome_response import OutcomeResponse from pytsugi_utils import InvalidLTIConfigError -REPLACE_REQUEST = 'replaceResult' -DELETE_REQUEST = 'deleteResult' -READ_REQUEST = 'readResult' +REPLACE_REQUEST = "replaceResult" +DELETE_REQUEST = "deleteResult" +READ_REQUEST = "readResult" accessors = [ - 'operation', - 'score', - 'result_data', - 'outcome_response', - 'message_identifier', - 'lis_outcome_service_url', - 'lis_result_sourcedid', - 'consumer_key', - 'consumer_secret', - 'post_request' + "operation", + "score", + "result_data", + "outcome_response", + "message_identifier", + "lis_outcome_service_url", + "lis_result_sourcedid", + "consumer_key", + "consumer_secret", + "post_request", ] -class OutcomeRequest(): - ''' +class OutcomeRequest: + """ Class for consuming & generating LTI Outcome Requests. Outcome Request documentation: @@ -49,7 +48,8 @@ class OutcomeRequest(): This class can be used both by Tool Providers and Tool Consumers, though they each use it differently. The TP will use it to POST an OAuth-signed request to the TC. A TC will use it to parse such a request from a TP. - ''' + """ + def __init__(self, opts=defaultdict(lambda: None)): # Initialize all our accessors to None for accessor in accessors: @@ -61,19 +61,19 @@ def __init__(self, opts=defaultdict(lambda: None)): @staticmethod def from_post_request(post_request): - ''' + """ Convenience method for creating a new OutcomeRequest from a request object. post_request is assumed to be a Django HttpRequest object - ''' + """ request = OutcomeRequest() request.post_request = post_request request.process_xml(post_request.data) return request def post_replace_result(self, score, result_data=None): - ''' + """ POSTs the given score to the Tool Consumer with a replaceResult. OPTIONAL: @@ -83,18 +83,22 @@ def post_replace_result(self, score, result_data=None): 'text' : str text 'url' : str url - ''' + """ self.operation = REPLACE_REQUEST self.score = score self.result_data = result_data if result_data is not None: if len(result_data) > 1: - error_msg = ('Dictionary result_data can only have one entry. ' - '{0} entries were found.'.format(len(result_data))) + error_msg = ( + "Dictionary result_data can only have one entry. " + "{0} entries were found.".format(len(result_data)) + ) raise InvalidLTIConfigError(error_msg) - elif 'text' not in result_data and 'url' not in result_data: - error_msg = ('Dictionary result_data can only have the key ' - '"text" or the key "url".') + elif "text" not in result_data and "url" not in result_data: + error_msg = ( + "Dictionary result_data can only have the key " + '"text" or the key "url".' + ) raise InvalidLTIConfigError(error_msg) else: return self.post_outcome_request() @@ -102,50 +106,50 @@ def post_replace_result(self, score, result_data=None): return self.post_outcome_request() def post_delete_result(self): - ''' + """ POSTs a deleteRequest to the Tool Consumer. - ''' + """ self.operation = DELETE_REQUEST return self.post_outcome_request() def post_read_result(self): - ''' + """ POSTS a readResult to the Tool Consumer. - ''' + """ self.operation = READ_REQUEST return self.post_outcome_request() def is_replace_request(self): - ''' + """ Check whether this request is a replaceResult request. - ''' + """ return self.operation == REPLACE_REQUEST def is_delete_request(self): - ''' + """ Check whether this request is a deleteResult request. - ''' + """ return self.operation == DELETE_REQUEST def is_read_request(self): - ''' + """ Check whether this request is a readResult request. - ''' + """ return self.operation == READ_REQUEST def was_outcome_post_successful(self): return self.outcome_response and self.outcome_response.is_success() def post_outcome_request(self): - ''' + """ POST an OAuth signed request to the Tool Consumer. - ''' + """ if not self.has_required_attributes(): raise InvalidLTIConfigError( - 'OutcomeRequest does not have all required attributes') + "OutcomeRequest does not have all required attributes" + ) - consumer = oauth2.Consumer(key=self.consumer_key, - secret=self.consumer_secret) + consumer = oauth2.Consumer(key=self.consumer_key, secret=self.consumer_secret) client = oauth2.Client(consumer) # monkey_patch_headers ensures that Authorization @@ -154,6 +158,7 @@ def post_outcome_request(self): monkey_patch_function = None if monkey_patch_headers: import httplib2 + http = httplib2.Http normalize = http._normalize_headers @@ -161,44 +166,44 @@ def post_outcome_request(self): def my_normalize(self, headers): # print("My Normalize", headers) ret = normalize(self, headers) - if 'authorization' in ret: - ret['Authorization'] = ret.pop('authorization') + if "authorization" in ret: + ret["Authorization"] = ret.pop("authorization") # print("My Normalize", ret) return ret + http._normalize_headers = my_normalize monkey_patch_function = normalize response, content = client.request( self.lis_outcome_service_url, - 'POST', + "POST", body=self.generate_request_xml(), - headers={'Content-Type': 'application/xml'}) + headers={"Content-Type": "application/xml"}, + ) if monkey_patch_headers and monkey_patch_function: import httplib2 + http = httplib2.Http http._normalize_headers = monkey_patch_function - self.outcome_response = OutcomeResponse.from_post_response(response, - content) + self.outcome_response = OutcomeResponse.from_post_response(response, content) return self.outcome_response def process_xml(self, xml): - ''' + """ Parse Outcome Request data from XML. - ''' + """ root = objectify.fromstring(xml) self.message_identifier = str( - root.imsx_POXHeader.imsx_POXRequestHeaderInfo. - imsx_messageIdentifier) + root.imsx_POXHeader.imsx_POXRequestHeaderInfo.imsx_messageIdentifier + ) try: result = root.imsx_POXBody.replaceResultRequest self.operation = REPLACE_REQUEST # Get result sourced id from resultRecord - self.lis_result_sourcedid = result.resultRecord.\ - sourcedGUID.sourcedId - self.score = str(result.resultRecord.result. - resultScore.textString) + self.lis_result_sourcedid = result.resultRecord.sourcedGUID.sourcedId + self.score = str(result.resultRecord.result.resultScore.textString) except: pass @@ -206,8 +211,7 @@ def process_xml(self, xml): result = root.imsx_POXBody.deleteResultRequest self.operation = DELETE_REQUEST # Get result sourced id from resultRecord - self.lis_result_sourcedid = result.resultRecord.\ - sourcedGUID.sourcedId + self.lis_result_sourcedid = result.resultRecord.sourcedGUID.sourcedId except: pass @@ -215,55 +219,55 @@ def process_xml(self, xml): result = root.imsx_POXBody.readResultRequest self.operation = READ_REQUEST # Get result sourced id from resultRecord - self.lis_result_sourcedid = result.resultRecord.\ - sourcedGUID.sourcedId + self.lis_result_sourcedid = result.resultRecord.sourcedGUID.sourcedId except: pass def has_required_attributes(self): - return self.consumer_key is not None\ - and self.consumer_secret is not None\ - and self.lis_outcome_service_url is not None\ - and self.lis_result_sourcedid is not None\ + return ( + self.consumer_key is not None + and self.consumer_secret is not None + and self.lis_outcome_service_url is not None + and self.lis_result_sourcedid is not None and self.operation is not None + ) def generate_request_xml(self): root = etree.Element( - 'imsx_POXEnvelopeRequest', - xmlns='http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0') - - header = etree.SubElement(root, 'imsx_POXHeader') - header_info = etree.SubElement(header, 'imsx_POXRequestHeaderInfo') - version = etree.SubElement(header_info, 'imsx_version') - version.text = 'V1.0' - message_identifier = etree.SubElement(header_info, - 'imsx_messageIdentifier') + "imsx_POXEnvelopeRequest", + xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0", + ) + + header = etree.SubElement(root, "imsx_POXHeader") + header_info = etree.SubElement(header, "imsx_POXRequestHeaderInfo") + version = etree.SubElement(header_info, "imsx_version") + version.text = "V1.0" + message_identifier = etree.SubElement(header_info, "imsx_messageIdentifier") message_identifier.text = self.message_identifier - body = etree.SubElement(root, 'imsx_POXBody') - request = etree.SubElement(body, '%s%s' % (self.operation, - 'Request')) - record = etree.SubElement(request, 'resultRecord') + body = etree.SubElement(root, "imsx_POXBody") + request = etree.SubElement(body, "%s%s" % (self.operation, "Request")) + record = etree.SubElement(request, "resultRecord") - guid = etree.SubElement(record, 'sourcedGUID') + guid = etree.SubElement(record, "sourcedGUID") - sourcedid = etree.SubElement(guid, 'sourcedId') + sourcedid = etree.SubElement(guid, "sourcedId") sourcedid.text = self.lis_result_sourcedid if self.score is not None: - result = etree.SubElement(record, 'result') - result_score = etree.SubElement(result, 'resultScore') - language = etree.SubElement(result_score, 'language') - language.text = 'en' - text_string = etree.SubElement(result_score, 'textString') + result = etree.SubElement(record, "result") + result_score = etree.SubElement(result, "resultScore") + language = etree.SubElement(result_score, "language") + language.text = "en" + text_string = etree.SubElement(result_score, "textString") text_string.text = self.score.__str__() if self.result_data: - resultData = etree.SubElement(result, 'resultData') - if 'text' in self.result_data: - resultDataText = etree.SubElement(resultData, 'text') - resultDataText.text = self.result_data['text'] - elif 'url' in self.result_data: - resultDataURL = etree.SubElement(resultData, 'url') - resultDataURL.text = self.result_data['url'] - - return etree.tostring(root, xml_declaration=True, encoding='utf-8') + resultData = etree.SubElement(result, "resultData") + if "text" in self.result_data: + resultDataText = etree.SubElement(resultData, "text") + resultDataText.text = self.result_data["text"] + elif "url" in self.result_data: + resultDataURL = etree.SubElement(resultData, "url") + resultDataURL.text = self.result_data["url"] + + return etree.tostring(root, xml_declaration=True, encoding="utf-8") diff --git a/modules/outcome_response.py b/modules/outcome_response.py index cd732adac..047e22623 100644 --- a/modules/outcome_response.py +++ b/modules/outcome_response.py @@ -1,4 +1,3 @@ - # Forked from - Wed Jun 21 09:01:59 EDT 2017 # https://github.com/tophatmonocle/ims_lti_py @@ -14,35 +13,26 @@ from lxml import etree, objectify import six -CODE_MAJOR_CODES = [ - 'success', - 'processing', - 'failure', - 'unsupported' -] +CODE_MAJOR_CODES = ["success", "processing", "failure", "unsupported"] -SEVERITY_CODES = [ - 'status', - 'warning', - 'error' -] +SEVERITY_CODES = ["status", "warning", "error"] accessors = [ - 'request_type', - 'score', - 'message_identifier', - 'response_code', - 'post_response', - 'code_major', - 'severity', - 'description', - 'operation', - 'message_ref_identifier' + "request_type", + "score", + "message_identifier", + "response_code", + "post_response", + "code_major", + "severity", + "description", + "operation", + "message_ref_identifier", ] -class OutcomeResponse(): - ''' +class OutcomeResponse: + """ This class consumes & generates LTI Outcome Responses. Response documentation: @@ -55,7 +45,8 @@ class OutcomeResponse(): each will use it differently. TPs will use it to partse the result of an OutcomeRequest to the TC. A TC will use it to generate proper response XML to send back to a TP. - ''' + """ + def __init__(self, **kwargs): # Initialize all class accessors to None for opt in accessors: @@ -67,10 +58,10 @@ def __init__(self, **kwargs): @staticmethod def from_post_response(post_response, content): - ''' + """ Convenience method for creating a new OutcomeResponse from a response object. - ''' + """ response = OutcomeResponse() response.post_response = post_response response.response_code = post_response.status @@ -78,50 +69,48 @@ def from_post_response(post_response, content): return response def is_success(self): - return self.code_major == 'success' + return self.code_major == "success" def is_processing(self): - return self.code_major == 'processing' + return self.code_major == "processing" def is_failure(self): - return self.code_major == 'failure' + return self.code_major == "failure" def is_unsupported(self): - return self.code_major == 'unsupported' + return self.code_major == "unsupported" def has_warning(self): - return self.severity == 'warning' + return self.severity == "warning" def has_error(self): - return self.severity == 'error' + return self.severity == "error" def process_xml(self, xml): - ''' + """ Parse OutcomeResponse data form XML. - ''' + """ try: root = objectify.fromstring(xml) # Get message idenifier from header info - self.message_identifier = root.imsx_POXHeader.\ - imsx_POXResponseHeaderInfo.\ - imsx_messageIdentifier + self.message_identifier = ( + root.imsx_POXHeader.imsx_POXResponseHeaderInfo.imsx_messageIdentifier + ) - status_node = root.imsx_POXHeader.\ - imsx_POXResponseHeaderInfo.\ - imsx_statusInfo + status_node = root.imsx_POXHeader.imsx_POXResponseHeaderInfo.imsx_statusInfo # Get status parameters from header info status self.code_major = status_node.imsx_codeMajor self.severity = status_node.imsx_severity self.description = status_node.imsx_description - self.message_ref_identifier = str( - status_node.imsx_messageRefIdentifier) + self.message_ref_identifier = str(status_node.imsx_messageRefIdentifier) self.operation = status_node.imsx_operationRefIdentifier try: # Try to get the score - self.score = str(root.imsx_POXBody.readResultResponse. - result.resultScore.textString) + self.score = str( + root.imsx_POXBody.readResultResponse.result.resultScore.textString + ) except AttributeError: # Not a readResult, just ignore! pass @@ -129,46 +118,45 @@ def process_xml(self, xml): pass def generate_response_xml(self): - ''' + """ Generate XML based on the current configuration. - ''' + """ root = etree.Element( - 'imsx_POXEnvelopeResponse', - xmlns='http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0') - - header = etree.SubElement(root, 'imsx_POXHeader') - header_info = etree.SubElement(header, 'imsx_POXResponseHeaderInfo') - version = etree.SubElement(header_info, 'imsx_version') - version.text = 'V1.0' - message_identifier = etree.SubElement(header_info, - 'imsx_messageIdentifier') + "imsx_POXEnvelopeResponse", + xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0", + ) + + header = etree.SubElement(root, "imsx_POXHeader") + header_info = etree.SubElement(header, "imsx_POXResponseHeaderInfo") + version = etree.SubElement(header_info, "imsx_version") + version.text = "V1.0" + message_identifier = etree.SubElement(header_info, "imsx_messageIdentifier") message_identifier.text = str(self.message_identifier) - status_info = etree.SubElement(header_info, 'imsx_statusInfo') - code_major = etree.SubElement(status_info, 'imsx_codeMajor') + status_info = etree.SubElement(header_info, "imsx_statusInfo") + code_major = etree.SubElement(status_info, "imsx_codeMajor") code_major.text = str(self.code_major) - severity = etree.SubElement(status_info, 'imsx_severity') + severity = etree.SubElement(status_info, "imsx_severity") severity.text = str(self.severity) - description = etree.SubElement(status_info, 'imsx_description') + description = etree.SubElement(status_info, "imsx_description") description.text = str(self.description) message_ref_identifier = etree.SubElement( - status_info, - 'imsx_messageRefIdentifier') + status_info, "imsx_messageRefIdentifier" + ) message_ref_identifier.text = str(self.message_ref_identifier) operation_ref_identifier = etree.SubElement( - status_info, - 'imsx_operationRefIdentifier') + status_info, "imsx_operationRefIdentifier" + ) operation_ref_identifier.text = str(self.operation) - body = etree.SubElement(root, 'imsx_POXBody') - response = etree.SubElement(body, '%s%s' % (self.operation, - 'Response')) + body = etree.SubElement(root, "imsx_POXBody") + response = etree.SubElement(body, "%s%s" % (self.operation, "Response")) if self.score: - result = etree.SubElement(response, 'result') - result_score = etree.SubElement(result, 'resultScore') - language = etree.SubElement(result_score, 'language') - language.text = 'en' - text_string = etree.SubElement(result_score, 'textString') + result = etree.SubElement(response, "result") + result_score = etree.SubElement(result, "resultScore") + language = etree.SubElement(result_score, "language") + language.text = "en" + text_string = etree.SubElement(result_score, "textString") text_string.text = str(self.score) return '' + etree.tostring(root) diff --git a/modules/pytsugi_utils.py b/modules/pytsugi_utils.py index 6d9734647..05fae92c4 100644 --- a/modules/pytsugi_utils.py +++ b/modules/pytsugi_utils.py @@ -12,17 +12,22 @@ from uuid import uuid1 + def generate_identifier(): return uuid1().__str__() + class InvalidLTIConfigError(Exception): def __init__(self, value): self.value = value + def __str__(self): return repr(self.value) + class InvalidLTIRequestError(Exception): def __init__(self, value): self.value = value + def __str__(self): - return repr(self.value) \ No newline at end of file + return repr(self.value) diff --git a/modules/rs_grading.py b/modules/rs_grading.py index 0d65ec866..b8bf5e2fd 100644 --- a/modules/rs_grading.py +++ b/modules/rs_grading.py @@ -22,20 +22,24 @@ logger = logging.getLogger(current.settings.logger) logger.setLevel(current.settings.log_level) + def _profile(start, msg): delta = datetime.datetime.now() - start print("{}: {}.{}".format(msg, delta.seconds, delta.microseconds)) -D1 = Decimal('1') + +D1 = Decimal("1") + + def _score_from_pct_correct(pct_correct, points, autograde): # ALL_AUTOGRADE_OPTIONS = ['all_or_nothing', 'pct_correct', 'interact'] - if autograde == 'interact' or autograde == 'visited': + if autograde == "interact" or autograde == "visited": return points - elif autograde == 'pct_correct': + elif autograde == "pct_correct": # prorate credit based on percentage correct # 2.x result return int(((pct_correct * points)/100.0)) - return int(Decimal((pct_correct * points)/100.0).quantize(D1, ROUND_HALF_UP) ) - elif autograde == 'all_or_nothing' or autograde == 'unittest': + return int(Decimal((pct_correct * points) / 100.0).quantize(D1, ROUND_HALF_UP)) + elif autograde == "all_or_nothing" or autograde == "unittest": # 'unittest' is legacy, now deprecated # have to get *all* tests to pass in order to get any credit if pct_correct == 100: @@ -47,14 +51,16 @@ def _score_from_pct_correct(pct_correct, points, autograde): def _score_one_code_run(row, points, autograde): # row is one row from useinfo table # second element of act is the percentage of tests that passed - if autograde == 'interact': + if autograde == "interact": return _score_one_interaction(row, points, autograde) try: - (ignore, pct, ignore, passed, ignore, failed) = row.act.split(':') - pct_correct = 100 * float(passed)/(int(failed) + int(passed)) + (ignore, pct, ignore, passed, ignore, failed) = row.act.split(":") + pct_correct = 100 * float(passed) / (int(failed) + int(passed)) except: - pct_correct = 0 # can still get credit if autograde is 'interact' or 'visited'; but no autograded value + pct_correct = ( + 0 + ) # can still get credit if autograde is 'interact' or 'visited'; but no autograded value return _score_from_pct_correct(pct_correct, points, autograde) @@ -130,12 +136,21 @@ def _score_one_lp(row, points, autograde): return _score_from_pct_correct(row.correct or 0, points, autograde) -def _scorable_mchoice_answers(course_name, sid, question_name, points, deadline, practice_start_time=None, db=None, - now=None): - query = ((db.mchoice_answers.course_name == course_name) & \ - (db.mchoice_answers.sid == sid) & \ - (db.mchoice_answers.div_id == question_name) \ - ) +def _scorable_mchoice_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.mchoice_answers.course_name == course_name) + & (db.mchoice_answers.sid == sid) + & (db.mchoice_answers.div_id == question_name) + ) if deadline: query = query & (db.mchoice_answers.timestamp < deadline) if practice_start_time: @@ -145,16 +160,25 @@ def _scorable_mchoice_answers(course_name, sid, question_name, points, deadline, return db(query).select(orderby=db.mchoice_answers.timestamp) -def _scorable_useinfos(course_name, sid, div_id, points, deadline, event_filter=None, question_type=None, - practice_start_time=None, db=None, now=None): +def _scorable_useinfos( + course_name, + sid, + div_id, + points, + deadline, + event_filter=None, + question_type=None, + practice_start_time=None, + db=None, + now=None, +): # look in useinfo, to see if visited (before deadline) # sid matches auth_user.username, not auth_user.id # if question type is page we must do better with the div_id - query = ((db.useinfo.course_id == course_name) & \ - (db.useinfo.sid == sid)) + query = (db.useinfo.course_id == course_name) & (db.useinfo.sid == sid) - if question_type == 'page': + if question_type == "page": quest = db(db.questions.name == div_id).select().first() div_id = u"{}/{}.html".format(quest.chapter, quest.subchapter) query = query & (db.useinfo.div_id.endswith(div_id)) @@ -172,12 +196,21 @@ def _scorable_useinfos(course_name, sid, div_id, points, deadline, event_filter= return db(query).select(db.useinfo.id, db.useinfo.act, orderby=db.useinfo.timestamp) -def _scorable_parsons_answers(course_name, sid, question_name, points, deadline, practice_start_time=None, db=None, - now=None): - query = ((db.parsons_answers.course_name == course_name) & \ - (db.parsons_answers.sid == sid) & \ - (db.parsons_answers.div_id == question_name) \ - ) +def _scorable_parsons_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.parsons_answers.course_name == course_name) + & (db.parsons_answers.sid == sid) + & (db.parsons_answers.div_id == question_name) + ) if deadline: query = query & (db.parsons_answers.timestamp < deadline) if practice_start_time: @@ -187,12 +220,21 @@ def _scorable_parsons_answers(course_name, sid, question_name, points, deadline, return db(query).select(orderby=db.parsons_answers.timestamp) -def _scorable_fitb_answers(course_name, sid, question_name, points, deadline, practice_start_time=None, db=None, - now=None): - query = ((db.fitb_answers.course_name == course_name) & \ - (db.fitb_answers.sid == sid) & \ - (db.fitb_answers.div_id == question_name) \ - ) +def _scorable_fitb_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.fitb_answers.course_name == course_name) + & (db.fitb_answers.sid == sid) + & (db.fitb_answers.div_id == question_name) + ) if deadline: query = query & (db.fitb_answers.timestamp < deadline) if practice_start_time: @@ -202,12 +244,21 @@ def _scorable_fitb_answers(course_name, sid, question_name, points, deadline, pr return db(query).select(orderby=db.fitb_answers.timestamp) -def _scorable_clickablearea_answers(course_name, sid, question_name, points, deadline, practice_start_time=None, - db=None, now=None): - query = ((db.clickablearea_answers.course_name == course_name) & \ - (db.clickablearea_answers.sid == sid) & \ - (db.clickablearea_answers.div_id == question_name) \ - ) +def _scorable_clickablearea_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.clickablearea_answers.course_name == course_name) + & (db.clickablearea_answers.sid == sid) + & (db.clickablearea_answers.div_id == question_name) + ) if deadline: query = query & (db.clickablearea_answers.timestamp < deadline) if practice_start_time: @@ -217,12 +268,21 @@ def _scorable_clickablearea_answers(course_name, sid, question_name, points, dea return db(query).select(orderby=db.clickablearea_answers.timestamp) -def _scorable_dragndrop_answers(course_name, sid, question_name, points, deadline, practice_start_time=None, db=None, - now=None): - query = ((db.dragndrop_answers.course_name == course_name) & \ - (db.dragndrop_answers.sid == sid) & \ - (db.dragndrop_answers.div_id == question_name) \ - ) +def _scorable_dragndrop_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.dragndrop_answers.course_name == course_name) + & (db.dragndrop_answers.sid == sid) + & (db.dragndrop_answers.div_id == question_name) + ) if deadline: query = query & (db.dragndrop_answers.timestamp < deadline) if practice_start_time: @@ -232,12 +292,21 @@ def _scorable_dragndrop_answers(course_name, sid, question_name, points, deadlin return db(query).select(orderby=db.dragndrop_answers.timestamp) -def _scorable_codelens_answers(course_name, sid, question_name, points, deadline, practice_start_time=None, db=None, - now=None): - query = ((db.codelens_answers.course_name == course_name) & \ - (db.codelens_answers.sid == sid) & \ - (db.codelens_answers.div_id == question_name) \ - ) +def _scorable_codelens_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.codelens_answers.course_name == course_name) + & (db.codelens_answers.sid == sid) + & (db.codelens_answers.div_id == question_name) + ) if deadline: query = query & (db.codelens_answers.timestamp < deadline) if practice_start_time: @@ -247,12 +316,21 @@ def _scorable_codelens_answers(course_name, sid, question_name, points, deadline return db(query).select(orderby=db.codelens_answers.timestamp) -def _scorable_lp_answers(course_name, sid, question_name, points, deadline, - practice_start_time=None, db=None, now=None): - query = ((db.lp_answers.course_name == course_name) & \ - (db.lp_answers.sid == sid) & \ - (db.lp_answers.div_id == question_name) \ - ) +def _scorable_lp_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.lp_answers.course_name == course_name) + & (db.lp_answers.sid == sid) + & (db.lp_answers.div_id == question_name) + ) if deadline: query = query & (db.lp_answers.timestamp < deadline) if practice_start_time: @@ -263,24 +341,51 @@ def _scorable_lp_answers(course_name, sid, question_name, points, deadline, return db(query).select(orderby=db.lp_answers.timestamp) -def _autograde_one_q(course_name, sid, question_name, points, question_type, - deadline=None, autograde=None, which_to_grade=None, save_score=True, - practice_start_time=None, db=None, now=None): - logger.debug("autograding %s %s %s %s %s %s", course_name, question_name, sid, deadline, autograde, which_to_grade) +def _autograde_one_q( + course_name, + sid, + question_name, + points, + question_type, + deadline=None, + autograde=None, + which_to_grade=None, + save_score=True, + practice_start_time=None, + db=None, + now=None, +): + logger.debug( + "autograding %s %s %s %s %s %s", + course_name, + question_name, + sid, + deadline, + autograde, + which_to_grade, + ) if not autograde: logger.debug("autograde not set returning 0") return 0 # If previously manually graded and it is required to save the score, don't overwrite. - existing = db((db.question_grades.sid == sid) \ - & (db.question_grades.course_name == course_name) \ - & (db.question_grades.div_id == question_name) \ - ).select().first() + existing = ( + db( + (db.question_grades.sid == sid) + & (db.question_grades.course_name == course_name) + & (db.question_grades.div_id == question_name) + ) + .select() + .first() + ) if save_score and existing and (existing.comment != "autograded"): - logger.debug("skipping; previously manually graded, comment = {}".format(existing.comment)) + logger.debug( + "skipping; previously manually graded, comment = {}".format( + existing.comment + ) + ) return 0 - # For all question types, and values of which_to_grade, we have the same basic structure: # 1. Query the appropriate table to get rows representing student responses # 2. Apply a scoring function to the first, last, or all rows @@ -289,56 +394,149 @@ def _autograde_one_q(course_name, sid, question_name, points, question_type, # affect how the score is determined. # get the results from the right table, and choose the scoring function - if question_type in ['activecode', 'actex']: - if autograde in ['pct_correct', 'all_or_nothing', 'unittest']: - event_filter = 'unittest' + if question_type in ["activecode", "actex"]: + if autograde in ["pct_correct", "all_or_nothing", "unittest"]: + event_filter = "unittest" else: event_filter = None - results = _scorable_useinfos(course_name, sid, question_name, points, deadline, event_filter, - practice_start_time=practice_start_time, db=db, now=now) + results = _scorable_useinfos( + course_name, + sid, + question_name, + points, + deadline, + event_filter, + practice_start_time=practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_code_run - elif question_type == 'mchoice': - results = _scorable_mchoice_answers(course_name, sid, question_name, points, deadline, practice_start_time, - db=db, now=now) + elif question_type == "mchoice": + results = _scorable_mchoice_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_mchoice - elif question_type == 'page': + elif question_type == "page": # question_name does not help us - results = _scorable_useinfos(course_name, sid, question_name, points, deadline, question_type='page', - practice_start_time=practice_start_time, db=db, now=now) + results = _scorable_useinfos( + course_name, + sid, + question_name, + points, + deadline, + question_type="page", + practice_start_time=practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_interaction - elif question_type == 'parsonsprob': - results = _scorable_parsons_answers(course_name, sid, question_name, points, deadline, practice_start_time, - db=db, now=now) + elif question_type == "parsonsprob": + results = _scorable_parsons_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_parsons - elif question_type == 'fillintheblank': - results = _scorable_fitb_answers(course_name, sid, question_name, points, deadline, practice_start_time, db=db, - now=now) + elif question_type == "fillintheblank": + results = _scorable_fitb_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_fitb - elif question_type == 'clickablearea': - results = _scorable_clickablearea_answers(course_name, sid, question_name, points, deadline, - practice_start_time, db=db, now=now) + elif question_type == "clickablearea": + results = _scorable_clickablearea_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_clickablearea - elif question_type == 'dragndrop': - results = _scorable_dragndrop_answers(course_name, sid, question_name, points, deadline, practice_start_time, - db=db, now=now) + elif question_type == "dragndrop": + results = _scorable_dragndrop_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_dragndrop - elif question_type == 'codelens': - if autograde == 'interact': # this is probably what we want for *most* codelens it will not be correct when it is an actual codelens question in a reading - results = _scorable_useinfos(course_name, sid, question_name, points, deadline, - practice_start_time=practice_start_time, db=db, now=now) + elif question_type == "codelens": + if ( + autograde == "interact" + ): # this is probably what we want for *most* codelens it will not be correct when it is an actual codelens question in a reading + results = _scorable_useinfos( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_interaction else: - results = _scorable_codelens_answers(course_name, sid, question_name, points, deadline, practice_start_time, - db=db, now=now) + results = _scorable_codelens_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_codelens - elif question_type in ['video', 'showeval', 'youtube', 'shortanswer', 'poll']: + elif question_type in ["video", "showeval", "youtube", "shortanswer", "poll"]: # question_name does not help us - results = _scorable_useinfos(course_name, sid, question_name, points, deadline, question_type='video', - practice_start_time=practice_start_time, db=db, now=now) + results = _scorable_useinfos( + course_name, + sid, + question_name, + points, + deadline, + question_type="video", + practice_start_time=practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_interaction - elif question_type == 'lp_build': - results = _scorable_lp_answers(course_name, sid, question_name, points, - deadline, practice_start_time=practice_start_time, db=db, now=now) + elif question_type == "lp_build": + results = _scorable_lp_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=practice_start_time, + db=db, + now=now, + ) scoring_fn = _score_one_lp else: @@ -347,11 +545,11 @@ def _autograde_one_q(course_name, sid, question_name, points, question_type, # use query results and the scoring function if results: - if which_to_grade in ['first_answer', 'last_answer', None]: + if which_to_grade in ["first_answer", "last_answer", None]: # get single row - if which_to_grade == 'first_answer': + if which_to_grade == "first_answer": row = results.first() - elif which_to_grade == 'last_answer': + elif which_to_grade == "last_answer": row = results.last() else: # default is last @@ -359,9 +557,9 @@ def _autograde_one_q(course_name, sid, question_name, points, question_type, # extract its score and id id = row.id score = scoring_fn(row, points, autograde) - elif which_to_grade == 'best_answer': + elif which_to_grade == "best_answer": # score all rows and take the best one - best_row = max(results, key = lambda row: scoring_fn(row, points, autograde)) + best_row = max(results, key=lambda row: scoring_fn(row, points, autograde)) id = best_row.id score = scoring_fn(best_row, points, autograde) logger.debug("SCORE = %s by %s", score, scoring_fn) @@ -379,31 +577,36 @@ def _autograde_one_q(course_name, sid, question_name, points, question_type, _save_question_grade(sid, course_name, question_name, score, id, deadline, db) if practice_start_time: - return _score_practice_quality(practice_start_time, - course_name, - sid, - points, - score, - len(results) if results else 0, - db, - now) + return _score_practice_quality( + practice_start_time, + course_name, + sid, + points, + score, + len(results) if results else 0, + db, + now, + ) return score -def _save_question_grade(sid, course_name, question_name, score, useinfo_id=None, deadline=None, db=None): +def _save_question_grade( + sid, course_name, question_name, score, useinfo_id=None, deadline=None, db=None +): try: db.question_grades.update_or_insert( - ((db.question_grades.sid == sid) & - (db.question_grades.course_name == course_name) & - (db.question_grades.div_id == question_name) + ( + (db.question_grades.sid == sid) + & (db.question_grades.course_name == course_name) + & (db.question_grades.div_id == question_name) ), sid=sid, course_name=course_name, div_id=question_name, - score = score, - comment = "autograded", - useinfo_id = None, - deadline=deadline + score=score, + comment="autograded", + useinfo_id=None, + deadline=deadline, ) except IntegrityError: logger.error("IntegrityError {} {} {}".format(sid, course_name, question_name)) @@ -420,21 +623,28 @@ def _compute_assignment_total(student, assignment, course_name, db=None): # div_id is found in questions; questions are associated with assignments, which have assignment_id # compute the score - query = (db.question_grades.sid == student.username) \ - & (db.question_grades.div_id == db.questions.name) \ - & (db.questions.id == db.assignment_questions.question_id) \ - & (db.assignment_questions.assignment_id == assignment.id) \ - & (db.question_grades.course_name == course_name ) + query = ( + (db.question_grades.sid == student.username) + & (db.question_grades.div_id == db.questions.name) + & (db.questions.id == db.assignment_questions.question_id) + & (db.assignment_questions.assignment_id == assignment.id) + & (db.question_grades.course_name == course_name) + ) scores = db(query).select(db.question_grades.score) - logger.debug("List of scores to add for %s is %s",student.username, scores) + logger.debug("List of scores to add for %s is %s", student.username, scores) score = sum([row.score for row in scores if row.score]) # check for threshold scoring for the assignment record = db.assignments(assignment.id) - if record and record.threshold_pct and score/record.points > record.threshold_pct: + if record and record.threshold_pct and score / record.points > record.threshold_pct: score = record.points - grade = db( - (db.grades.auth_user == student.id) & - (db.grades.assignment == assignment.id)).select().first() + grade = ( + db( + (db.grades.auth_user == student.id) + & (db.grades.assignment == assignment.id) + ) + .select() + .first() + ) if grade and grade.manual_total: # don't save it; return the calculated and the previous manual score @@ -443,31 +653,41 @@ def _compute_assignment_total(student, assignment, course_name, db=None): # Write the score to the grades table try: db.grades.update_or_insert( - ((db.grades.auth_user == student.id) & - (db.grades.assignment == assignment.id)), - auth_user = student.id, - assignment = assignment.id, - score=score) + ( + (db.grades.auth_user == student.id) + & (db.grades.assignment == assignment.id) + ), + auth_user=student.id, + assignment=assignment.id, + score=score, + ) except IntegrityError: - logger.error("IntegrityError update or insert {} {} with score {}" - .format(student.id, assignment.id, score)) + logger.error( + "IntegrityError update or insert {} {} with score {}".format( + student.id, assignment.id, score + ) + ) return score, None -def _get_students(course_id=None, sid = None, student_rownum=None, db=None): + +def _get_students(course_id=None, sid=None, student_rownum=None, db=None): print("_get_students", course_id, sid, student_rownum) if student_rownum: # get the student id as well as username - student_rows = db((db.auth_user.id == student_rownum) - ).select(db.auth_user.username, db.auth_user.id) + student_rows = db((db.auth_user.id == student_rownum)).select( + db.auth_user.username, db.auth_user.id + ) elif sid: # fetch based on username rather db row number - student_rows = db((db.auth_user.username == sid) - ).select(db.auth_user.username, db.auth_user.id) + student_rows = db((db.auth_user.username == sid)).select( + db.auth_user.username, db.auth_user.id + ) elif course_id: # get all student usernames for this course - student_rows = db((db.user_courses.course_id == course_id) & - (db.user_courses.user_id == db.auth_user.id) - ).select(db.auth_user.username, db.auth_user.id) + student_rows = db( + (db.user_courses.course_id == course_id) + & (db.user_courses.user_id == db.auth_user.id) + ).select(db.auth_user.username, db.auth_user.id) else: student_rows = [] @@ -479,99 +699,148 @@ def _get_assignment(assignment_id): def _get_lti_record(oauth_consumer_key): - return current.db(current.db.lti_keys.consumer == oauth_consumer_key).select().first() + return ( + current.db(current.db.lti_keys.consumer == oauth_consumer_key).select().first() + ) def _try_to_send_lti_grade(student_row_num, assignment_id): # try to send lti grades assignment = _get_assignment(assignment_id) if not assignment: - current.session.flash = "Failed to find assignment object for assignment {}".format(assignment_id) + current.session.flash = "Failed to find assignment object for assignment {}".format( + assignment_id + ) return False else: - grade = current.db( - (current.db.grades.auth_user == student_row_num) & - (current.db.grades.assignment == assignment_id)).select().first() + grade = ( + current.db( + (current.db.grades.auth_user == student_row_num) + & (current.db.grades.assignment == assignment_id) + ) + .select() + .first() + ) if not grade: - current.session.flash = "Failed to find grade object for user {} and assignment {}".format(auth.user.id, - assignment_id) + current.session.flash = "Failed to find grade object for user {} and assignment {}".format( + auth.user.id, assignment_id + ) return False else: lti_record = _get_lti_record(current.session.oauth_consumer_key) - if (not lti_record) or (not grade.lis_result_sourcedid) or (not grade.lis_outcome_url): + if ( + (not lti_record) + or (not grade.lis_result_sourcedid) + or (not grade.lis_outcome_url) + ): current.session.flash = "Failed to send grade back to LMS (Coursera, Canvas, Blackboard...), probably because the student accessed this assignment directly rather than using a link from the LMS, or because there is an error in the assignment link in the LMS. Please report this error." return False else: # really sending # print("send_lti_grade({}, {}, {}, {}, {}, {}".format(assignment.points, grade.score, lti_record.consumer, lti_record.secret, grade.lis_outcome_url, grade.lis_result_sourcedid)) - send_lti_grade(assignment.points, - score=grade.score, - consumer=lti_record.consumer, - secret=lti_record.secret, - outcome_url=grade.lis_outcome_url, - result_sourcedid=grade.lis_result_sourcedid) + send_lti_grade( + assignment.points, + score=grade.score, + consumer=lti_record.consumer, + secret=lti_record.secret, + outcome_url=grade.lis_outcome_url, + result_sourcedid=grade.lis_result_sourcedid, + ) return True -def send_lti_grade(assignment_points, score, consumer, secret, outcome_url, result_sourcedid): +def send_lti_grade( + assignment_points, score, consumer, secret, outcome_url, result_sourcedid +): pct = score / float(assignment_points) if score and assignment_points else 0.0 # print "pct", pct # send it back to the LMS # print("score", score, points, pct) - request = OutcomeRequest({"consumer_key": consumer, - "consumer_secret": secret, - "lis_outcome_service_url": outcome_url, - "lis_result_sourcedid": result_sourcedid}) + request = OutcomeRequest( + { + "consumer_key": consumer, + "consumer_secret": secret, + "lis_outcome_service_url": outcome_url, + "lis_result_sourcedid": result_sourcedid, + } + ) resp = request.post_replace_result(pct) # print(resp) return pct + def send_lti_grades(assignment_id, assignment_points, course_id, lti_record, db): - #print("sending lti grades") + # print("sending lti grades") student_rows = _get_students(course_id=course_id, db=db) for student in student_rows: - grade = db( - (db.grades.auth_user == student.id) & - (db.grades.assignment == assignment_id)).select().first() + grade = ( + db( + (db.grades.auth_user == student.id) + & (db.grades.assignment == assignment_id) + ) + .select() + .first() + ) if grade and grade.lis_result_sourcedid and grade.lis_outcome_url: - send_lti_grade(assignment_points, - score=grade.score, - consumer=lti_record.consumer, - secret=lti_record.secret, - outcome_url=grade.lis_outcome_url, - result_sourcedid= grade.lis_result_sourcedid) - #print("done sending lti grades") - -def do_calculate_totals(assignment, course_id, course_name, sid, student_rownum, db, settings): - student_rows = _get_students(course_id=course_id, sid=sid, student_rownum=student_rownum, db=db) - - results = {'success':True} + send_lti_grade( + assignment_points, + score=grade.score, + consumer=lti_record.consumer, + secret=lti_record.secret, + outcome_url=grade.lis_outcome_url, + result_sourcedid=grade.lis_result_sourcedid, + ) + # print("done sending lti grades") + + +def do_calculate_totals( + assignment, course_id, course_name, sid, student_rownum, db, settings +): + student_rows = _get_students( + course_id=course_id, sid=sid, student_rownum=student_rownum, db=db + ) + + results = {"success": True} if sid: - computed_total, manual_score = _compute_assignment_total(student_rows[0], assignment, course_name, db) - results['message'] = "Total for {} is {}".format(sid, computed_total) - results['computed_score'] = computed_total - results['manual_score'] = manual_score + computed_total, manual_score = _compute_assignment_total( + student_rows[0], assignment, course_name, db + ) + results["message"] = "Total for {} is {}".format(sid, computed_total) + results["computed_score"] = computed_total + results["manual_score"] = manual_score else: # compute total score for the assignment for each sid; also saves in DB unless manual value saved - scores = [_compute_assignment_total(student, assignment, course_name, db)[0] for student in student_rows] - results['message'] = "Calculated totals for {} students\n\tmax: {}\n\tmin: {}\n\tmean: {}".format( - len(scores), - max(scores), - min(scores), - sum(scores)/float(len(scores)) + scores = [ + _compute_assignment_total(student, assignment, course_name, db)[0] + for student in student_rows + ] + results[ + "message" + ] = "Calculated totals for {} students\n\tmax: {}\n\tmin: {}\n\tmean: {}".format( + len(scores), max(scores), min(scores), sum(scores) / float(len(scores)) ) return results -def do_autograde(assignment, course_id, course_name, sid, student_rownum, question_name, enforce_deadline, timezoneoffset, - db, settings): +def do_autograde( + assignment, + course_id, + course_name, + sid, + student_rownum, + question_name, + enforce_deadline, + timezoneoffset, + db, + settings, +): start = datetime.datetime.now() - if enforce_deadline == 'true': + if enforce_deadline == "true": # get the deadline associated with the assignment deadline = assignment.duedate else: @@ -579,37 +848,50 @@ def do_autograde(assignment, course_id, course_name, sid, student_rownum, questi if timezoneoffset and deadline: deadline = deadline + datetime.timedelta(hours=float(timezoneoffset)) - logger.debug("ASSIGNMENT DEADLINE OFFSET %s",deadline) + logger.debug("ASSIGNMENT DEADLINE OFFSET %s", deadline) - student_rows = _get_students(course_id=course_id, sid=sid, student_rownum=student_rownum, db=db) + student_rows = _get_students( + course_id=course_id, sid=sid, student_rownum=student_rownum, db=db + ) sids = [row.username for row in student_rows] if question_name: questions_query = db( - (db.assignment_questions.assignment_id == assignment.id) & - (db.assignment_questions.question_id == db.questions.id) & - (db.questions.name == question_name) - ).select() + (db.assignment_questions.assignment_id == assignment.id) + & (db.assignment_questions.question_id == db.questions.id) + & (db.questions.name == question_name) + ).select() else: # get all qids and point values for this assignment - questions_query = db((db.assignment_questions.assignment_id == assignment.id) & - (db.assignment_questions.question_id == db.questions.id) - ).select() + questions_query = db( + (db.assignment_questions.assignment_id == assignment.id) + & (db.assignment_questions.question_id == db.questions.id) + ).select() # _profile(start, "after questions fetched") - readings = [(row.questions.name, - row.questions.chapter, - row.questions.subchapter, - row.assignment_questions.points, - row.assignment_questions.activities_required, - row.assignment_questions.autograde, - row.assignment_questions.which_to_grade, - ) for row in questions_query if row.assignment_questions.reading_assignment == True] + readings = [ + ( + row.questions.name, + row.questions.chapter, + row.questions.subchapter, + row.assignment_questions.points, + row.assignment_questions.activities_required, + row.assignment_questions.autograde, + row.assignment_questions.which_to_grade, + ) + for row in questions_query + if row.assignment_questions.reading_assignment == True + ] logger.debug("GRADING READINGS") # Now for each reading, get all of the questions in that subsection # call _autograde_one_q using the autograde and which to grade for that section. likely interact # - base_course = db(db.courses.id == course_id).select(db.courses.base_course).first().base_course + base_course = ( + db(db.courses.id == course_id) + .select(db.courses.base_course) + .first() + .base_course + ) count = 0 # _profile(start, "after readings fetched") for (name, chapter, subchapter, points, ar, ag, wtg) in readings: @@ -618,13 +900,25 @@ def do_autograde(assignment, course_id, course_name, sid, student_rownum, questi for s in sids: # print("."), score = 0 - rows = db((db.questions.chapter == chapter) & - (db.questions.subchapter == subchapter) & - (db.questions.base_course == base_course)).select() + rows = db( + (db.questions.chapter == chapter) + & (db.questions.subchapter == subchapter) + & (db.questions.base_course == base_course) + ).select() # _profile(start, "\t{}. rows fetched for {}/{}".format(count, chapter, subchapter)) for row in rows: - score += _autograde_one_q(course_name, s, row.name, 1, row.question_type, - deadline=deadline, autograde=ag, which_to_grade=wtg, save_score=False, db=db) + score += _autograde_one_q( + course_name, + s, + row.name, + 1, + row.question_type, + deadline=deadline, + autograde=ag, + which_to_grade=wtg, + save_score=False, + db=db, + ) logger.debug("Score is now %s for %s for %s", score, row.name, sid) if score >= ar: save_points = points @@ -633,33 +927,57 @@ def do_autograde(assignment, course_id, course_name, sid, student_rownum, questi save_points = 0 logger.debug("no points for %s on %s", sid, name) # _profile(start, "\t\tgraded") - _save_question_grade(s, course_name, name, save_points, useinfo_id=None, deadline=deadline, db=db) - #_profile(start, "\t\tsaved") + _save_question_grade( + s, + course_name, + name, + save_points, + useinfo_id=None, + deadline=deadline, + db=db, + ) + # _profile(start, "\t\tsaved") # _profile(start, "after readings graded") logger.debug("GRADING QUESTIONS") - questions = [(row.questions.name, - row.assignment_questions.points, - row.assignment_questions.autograde, - row.assignment_questions.which_to_grade, - row.questions.question_type) for row in questions_query - if row.assignment_questions.reading_assignment == False or - row.assignment_questions.reading_assignment == None] + questions = [ + ( + row.questions.name, + row.assignment_questions.points, + row.assignment_questions.autograde, + row.assignment_questions.which_to_grade, + row.questions.question_type, + ) + for row in questions_query + if row.assignment_questions.reading_assignment == False + or row.assignment_questions.reading_assignment == None + ] # _profile(start, "after questions fetched") logger.debug("questions to grade = %s", questions) for (qdiv, points, autograde, which_to_grade, question_type) in questions: for s in sids: - if autograde != 'manual': - _autograde_one_q(course_name, s, qdiv, points, question_type, - deadline=deadline, autograde=autograde, which_to_grade=which_to_grade, db=db) + if autograde != "manual": + _autograde_one_q( + course_name, + s, + qdiv, + points, + question_type, + deadline=deadline, + autograde=autograde, + which_to_grade=which_to_grade, + db=db, + ) count += 1 # _profile(start, "after calls to _autograde_one_q") return count + #### stuff for the practice feature + def _get_next_i_interval(flashcard, q): """Get next inter-repetition interval after the n-th repetition""" if q == -1 or q == 1 or q == 2: @@ -685,16 +1003,24 @@ def _change_e_factor(flashcard, q): return flashcard -def do_check_answer(sid, course_name, qid, username, q, db, settings, now, timezoneoffset): +def do_check_answer( + sid, course_name, qid, username, q, db, settings, now, timezoneoffset +): now_local = now - datetime.timedelta(hours=timezoneoffset) lastQuestion = db(db.questions.id == int(qid)).select().first() - chapter_label, sub_chapter_label = lastQuestion.topic.split('/') - - flashcard = db((db.user_topic_practice.user_id == sid) & - (db.user_topic_practice.course_name == course_name) & - (db.user_topic_practice.chapter_label == chapter_label) & - (db.user_topic_practice.sub_chapter_label == sub_chapter_label) & - (db.user_topic_practice.question_name == lastQuestion.name)).select().first() + chapter_label, sub_chapter_label = lastQuestion.topic.split("/") + + flashcard = ( + db( + (db.user_topic_practice.user_id == sid) + & (db.user_topic_practice.course_name == course_name) + & (db.user_topic_practice.chapter_label == chapter_label) + & (db.user_topic_practice.sub_chapter_label == sub_chapter_label) + & (db.user_topic_practice.question_name == lastQuestion.name) + ) + .select() + .first() + ) if not flashcard: # the flashcard for this question has been deleted since the practice page was loaded, probably @@ -702,10 +1028,14 @@ def do_check_answer(sid, course_name, qid, username, q, db, settings, now, timez return # Retrieve all the falshcards created for this user in the current course and order them by their order of creation. - flashcards = db((db.user_topic_practice.course_name == course_name) & - (db.user_topic_practice.user_id == sid)).select() + flashcards = db( + (db.user_topic_practice.course_name == course_name) + & (db.user_topic_practice.user_id == sid) + ).select() # Select only those where enough time has passed since last presentation. - presentable_flashcards = [f for f in flashcards if now_local.date() >= f.next_eligible_date] + presentable_flashcards = [ + f for f in flashcards if now_local.date() >= f.next_eligible_date + ] if q: # User clicked one of the self-evaluated answer buttons. @@ -713,15 +1043,28 @@ def do_check_answer(sid, course_name, qid, username, q, db, settings, now, timez trials_num = 1 else: # Compute q using the auto grader - autograde = 'pct_correct' + autograde = "pct_correct" if lastQuestion.autograde is not None: autograde = lastQuestion.autograde - q, trials_num = _autograde_one_q(course_name, username, lastQuestion.name, 100, - lastQuestion.question_type, None, autograde, 'last_answer', False, - flashcard.last_presented, db=db, now=now) + q, trials_num = _autograde_one_q( + course_name, + username, + lastQuestion.name, + 100, + lastQuestion.question_type, + None, + autograde, + "last_answer", + False, + flashcard.last_presented, + db=db, + now=now, + ) flashcard = _change_e_factor(flashcard, q) flashcard = _get_next_i_interval(flashcard, q) - flashcard.next_eligible_date = (now_local + datetime.timedelta(days=flashcard.i_interval)).date() + flashcard.next_eligible_date = ( + now_local + datetime.timedelta(days=flashcard.i_interval) + ).date() flashcard.last_completed = now flashcard.timezoneoffset = timezoneoffset flashcard.q = q @@ -741,18 +1084,21 @@ def do_check_answer(sid, course_name, qid, username, q, db, settings, now, timez available_flashcards=len(presentable_flashcards), start_practice=flashcard.last_presented, end_practice=now, - timezoneoffset=timezoneoffset + timezoneoffset=timezoneoffset, ) db.commit() -def _score_practice_quality(practice_start_time, course_name, sid, points, score, trials_count, db, now): - page_visits = db((db.useinfo.course_id == course_name) & \ - (db.useinfo.sid == sid) & \ - (db.useinfo.event == 'page') & \ - (db.useinfo.timestamp >= practice_start_time) & \ - (db.useinfo.timestamp <= now)) \ - .select() +def _score_practice_quality( + practice_start_time, course_name, sid, points, score, trials_count, db, now +): + page_visits = db( + (db.useinfo.course_id == course_name) + & (db.useinfo.sid == sid) + & (db.useinfo.event == "page") + & (db.useinfo.timestamp >= practice_start_time) + & (db.useinfo.timestamp <= now) + ).select() practice_duration = (now - practice_start_time).seconds / 60 practice_score = 0 if score == points: @@ -775,12 +1121,19 @@ def do_fill_user_topic_practice_log_missings(db, settings, testing_mode=None): flashcards = db(db.user_topic_practice.id > 0).select() for flashcard in flashcards: if flashcard.creation_time is None: - flashcard_logs = db((db.user_topic_practice_log.course_name == flashcard.course_name) & - (db.user_topic_practice_log.chapter_label == flashcard.chapter_label) & - (db.user_topic_practice_log.sub_chapter_label <= flashcard.sub_chapter_label)).select() - flashcard.creation_time = (min([f.start_practice for f in flashcard_logs]) - if len(flashcard_logs) > 0 - else flashcard.last_presented + datetime.timedelta(days=1)) + flashcard_logs = db( + (db.user_topic_practice_log.course_name == flashcard.course_name) + & (db.user_topic_practice_log.chapter_label == flashcard.chapter_label) + & ( + db.user_topic_practice_log.sub_chapter_label + <= flashcard.sub_chapter_label + ) + ).select() + flashcard.creation_time = ( + min([f.start_practice for f in flashcard_logs]) + if len(flashcard_logs) > 0 + else flashcard.last_presented + datetime.timedelta(days=1) + ) if not testing_mode: flashcard.update_record() # There are many questions that students have forgotten and we need to ask them again to make sure they've @@ -794,13 +1147,15 @@ def do_fill_user_topic_practice_log_missings(db, settings, testing_mode=None): students = db(db.auth_user.id > 0).select() for student in students: # A) Retrieve all their practice logs, ordered by timestamp. - flashcard_logs = db((db.user_topic_practice_log.user_id == student.id) & - (db.user_topic_practice_log.course_name == student.course_name) - ).select(orderby= db.user_topic_practice_log.start_practice) + flashcard_logs = db( + (db.user_topic_practice_log.user_id == student.id) + & (db.user_topic_practice_log.course_name == student.course_name) + ).select(orderby=db.user_topic_practice_log.start_practice) # Retrieve all their flashcards, ordered by creation_time. - flashcards = db((db.user_topic_practice.course_name == student.course_name) & - (db.user_topic_practice.user_id == student.id) - ).select(orderby= db.user_topic_practice.creation_time) + flashcards = db( + (db.user_topic_practice.course_name == student.course_name) + & (db.user_topic_practice.user_id == student.id) + ).select(orderby=db.user_topic_practice.creation_time) # The retrieved flashcards are not unique, i.e., after practicing a flashcard, if they submit a wrong answer # they'll do it again in the same day, otherwise, they'll do it tomorrow. So, we'll have multiple records in # user_topic_practice_log for the same topic. To this end, in the last_practiced dictionary, we keep @@ -819,62 +1174,128 @@ def do_fill_user_topic_practice_log_missings(db, settings, testing_mode=None): # presentable_topics keeps track of the filtered list of topics that are presentable today. presentable_topics = {} # Retrieve all the flashcards that were created on or before flashcard_log_date. - created_flashcards = [f for f in flashcards - if f.creation_time.date() <= flashcard_log_date] + created_flashcards = [ + f + for f in flashcards + if f.creation_time.date() <= flashcard_log_date + ] for f in created_flashcards: # If the flashcard does not have a corresponding key in last_practiced: - if (f.chapter_label + f.sub_chapter_label) not in last_practiced: - presentable_topics[f.chapter_label + f.sub_chapter_label] = f + if ( + f.chapter_label + f.sub_chapter_label + ) not in last_practiced: + presentable_topics[ + f.chapter_label + f.sub_chapter_label + ] = f # have a corresponding key in last_practiced where the time of the corresponding # practice_log fits in the i_interval that makes it eligible to present on `flashcard_log_date`. - elif ((flashcard_log.end_practice.date() - - last_practiced[f.chapter_label + f.sub_chapter_label].end_practice.date()).days >= - last_practiced[f.chapter_label + f.sub_chapter_label].i_interval): - presentable_topics[f.chapter_label + f.sub_chapter_label] = f + elif ( + ( + flashcard_log.end_practice.date() + - last_practiced[ + f.chapter_label + f.sub_chapter_label + ].end_practice.date() + ).days + >= last_practiced[ + f.chapter_label + f.sub_chapter_label + ].i_interval + ): + presentable_topics[ + f.chapter_label + f.sub_chapter_label + ] = f # Update current_date for the next iteration. current_date = flashcard_log_date - if flashcard_log.id < 42904 and flashcard_log.available_flashcards == -1: + if ( + flashcard_log.id < 42904 + and flashcard_log.available_flashcards == -1 + ): flashcard_log.available_flashcards = len(presentable_topics) if not testing_mode: flashcard_log.update_record() - if (testing_mode and flashcard_log.id >= 42904 and - (flashcard_log.available_flashcards != len(presentable_topics))): - print("I calculated for the following flashcard available_flashcardsq =", len(presentable_topics), - "However:") + if ( + testing_mode + and flashcard_log.id >= 42904 + and (flashcard_log.available_flashcards != len(presentable_topics)) + ): + print( + "I calculated for the following flashcard available_flashcardsq =", + len(presentable_topics), + "However:", + ) print(flashcard_log) # Now that the flashcard is practiced, it's not available anymore. So we should remove it. - if (flashcard_log.chapter_label + flashcard_log.sub_chapter_label in presentable_topics and - flashcard_log.i_interval != 0): - del presentable_topics[flashcard_log.chapter_label + flashcard_log.sub_chapter_label] + if ( + flashcard_log.chapter_label + flashcard_log.sub_chapter_label + in presentable_topics + and flashcard_log.i_interval != 0 + ): + del presentable_topics[ + flashcard_log.chapter_label + flashcard_log.sub_chapter_label + ] # As we go through the practice_log entries for this user, in timestamp order, we always keep track in # last_practiced of the last practice_log for each topic. Keys are topics; values are practice_log rows. - last_practiced[flashcard_log.chapter_label + flashcard_log.sub_chapter_label] = flashcard_log + last_practiced[ + flashcard_log.chapter_label + flashcard_log.sub_chapter_label + ] = flashcard_log if testing_mode or flashcard_log.q == -1: user = db(db.auth_user.id == flashcard_log.user_id).select().first() - course = db(db.courses.course_name == flashcard_log.course_name).select().first() - - question = db((db.questions.base_course == course.base_course) & \ - (db.questions.name == flashcard_log.question_name) & \ - (db.questions.topic == "{}/{}".format(flashcard_log.chapter_label, - flashcard_log.sub_chapter_label)) & \ - (db.questions.practice == True)).select().first() + course = ( + db(db.courses.course_name == flashcard_log.course_name) + .select() + .first() + ) + + question = ( + db( + (db.questions.base_course == course.base_course) + & (db.questions.name == flashcard_log.question_name) + & ( + db.questions.topic + == "{}/{}".format( + flashcard_log.chapter_label, + flashcard_log.sub_chapter_label, + ) + ) + & (db.questions.practice == True) + ) + .select() + .first() + ) # Compute q using the auto grader - autograde = 'pct_correct' + autograde = "pct_correct" if question.autograde is not None: autograde = question.autograde - q, trials_num = _autograde_one_q(course.course_name, user.username, question.name, 100, - question.question_type, None, autograde, 'last_answer', False, - flashcard_log.start_practice + datetime.timedelta(hours=5), - db=db, - now=flashcard_log.end_practice + datetime.timedelta(hours=5)) + q, trials_num = _autograde_one_q( + course.course_name, + user.username, + question.name, + 100, + question.question_type, + None, + autograde, + "last_answer", + False, + flashcard_log.start_practice + datetime.timedelta(hours=5), + db=db, + now=flashcard_log.end_practice + datetime.timedelta(hours=5), + ) if flashcard_log.q == -1: flashcard_log.q = q flashcard_log.trials_num = trials_num if not testing_mode: flashcard_log.update_record() - if testing_mode and flashcard_log.id >= 20854 and \ - flashcard_log.q != q and flashcard_log.trials_num != trials_num: - print("I calculated for the following flashcard q =", q, "and trials_num =", trials_num, "However:") + if ( + testing_mode + and flashcard_log.id >= 20854 + and flashcard_log.q != q + and flashcard_log.trials_num != trials_num + ): + print( + "I calculated for the following flashcard q =", + q, + "and trials_num =", + trials_num, + "However:", + ) print(flashcard_log) - diff --git a/modules/stripe_form.py b/modules/stripe_form.py index 736825361..f4f005308 100644 --- a/modules/stripe_form.py +++ b/modules/stripe_form.py @@ -5,20 +5,23 @@ import stripe - class StripeForm(object): - def __init__(self, - pk, sk, - amount, # in cents - description, - currency = 'usd', - currency_symbol = '$', - security_notice = True, - disclosure_notice = True, - template = None): + def __init__( + self, + pk, + sk, + amount, # in cents + description, + currency="usd", + currency_symbol="$", + security_notice=True, + disclosure_notice=True, + template=None, + ): from gluon import current, redirect, URL + if not (current.request.is_local or current.request.is_https): - redirect(URL(args=current.request.args,scheme='https')) + redirect(URL(args=current.request.args, scheme="https")) self.pk = pk self.sk = sk self.amount = amount @@ -30,10 +33,13 @@ def __init__(self, self.template = template or TEMPLATE self.accepted = None self.errors = None - self.signature = sha1(repr((self.amount, self.description)).encode('utf-8')).hexdigest() + self.signature = sha1( + repr((self.amount, self.description)).encode("utf-8") + ).hexdigest() def process(self): from gluon import current + request = current.request if request.post_vars: if self.signature == request.post_vars.signature: @@ -43,17 +49,18 @@ def process(self): card=request.post_vars.stripeToken, amount=self.amount, description=self.description, - currency=self.currency) + currency=self.currency, + ) # See https://stripe.com/docs/api/errors/handling?lang=python. Any errors will cause ``self.errors`` to be True. except stripe.error.CardError as e: # Since it's a decline, stripe.error.CardError will be caught. body = e.json_body - err = body.get('error', {}) + err = body.get("error", {}) self.response = dict(error=err, status=e.http_status) except Exception as e: - self.response = {'error': {'message': str(e)}} + self.response = {"error": {"message": str(e)}} else: - if self.response.get('paid', False): + if self.response.get("paid", False): self.accepted = True return self self.errors = True @@ -61,16 +68,20 @@ def process(self): def xml(self): from gluon.template import render + if self.accepted: return "Your payment was processed successfully" elif self.errors: return "There was an processing error" else: - context = dict(amount=self.amount, - signature=self.signature, pk=self.pk, - currency_symbol=self.currency_symbol, - security_notice=self.security_notice, - disclosure_notice=self.disclosure_notice) + context = dict( + amount=self.amount, + signature=self.signature, + pk=self.pk, + currency_symbol=self.currency_symbol, + security_notice=self.security_notice, + disclosure_notice=self.disclosure_notice, + ) return render(content=self.template, context=context) diff --git a/tests/ci_utils.py b/tests/ci_utils.py index 63b3f4799..2ae352303 100644 --- a/tests/ci_utils.py +++ b/tests/ci_utils.py @@ -14,16 +14,17 @@ import sys import os import os.path + # # OS detection # ============ # This follows the `Python recommendations `_. -is_win = sys.platform == 'win32' -is_linux = sys.platform.startswith('linux') -is_darwin = sys.platform == 'darwin' +is_win = sys.platform == "win32" +is_linux = sys.platform.startswith("linux") +is_darwin = sys.platform == "darwin" # Copied from https://docs.python.org/3.5/library/platform.html#cross-platform. -is_64bits = sys.maxsize > 2**32 +is_64bits = sys.maxsize > 2 ** 32 # Support code @@ -56,10 +57,12 @@ def xqt( flush_print(_) # Use bash instead of sh, so that ``source`` and other bash syntax # works. See https://docs.python.org/3/library/subprocess.html#subprocess.Popen. - executable = ('/bin/bash' if is_linux or is_darwin - else None) - ret.append(run(_, shell=True, stdout=PIPE, stderr=PIPE, - executable=executable, **kwargs)) + executable = "/bin/bash" if is_linux or is_darwin else None + ret.append( + run( + _, shell=True, stdout=PIPE, stderr=PIPE, executable=executable, **kwargs + ) + ) # Return a list only if there were multiple commands to execute. return ret[0] if len(ret) == 1 else ret @@ -69,20 +72,21 @@ def xqt( # ----- # A context manager for pushd. class pushd: - def __init__(self, + def __init__( + self, # The path to change to upon entering the context manager. - path + path, ): self.path = path def __enter__(self): - flush_print('pushd {}'.format(self.path)) + flush_print("pushd {}".format(self.path)) self.cwd = os.getcwd() os.chdir(self.path) def __exit__(self, type_, value, traceback): - flush_print('popd - returning to {}.'.format(self.cwd)) + flush_print("popd - returning to {}.".format(self.cwd)) os.chdir(self.cwd) return False @@ -93,14 +97,14 @@ def __exit__(self, type_, value, traceback): # chdir # ----- def chdir(path): - flush_print('cd ' + path) + flush_print("cd " + path) os.chdir(path) # mkdir # ----- def mkdir(path): - flush_print('mkdir ' + path) + flush_print("mkdir " + path) os.mkdir(path) @@ -120,7 +124,7 @@ def flush_print(*args, **kwargs): # ------ def isfile(f): _ = os.path.isfile(f) - flush_print('File {} {}.'.format(f, 'exists' if _ else 'does not exist')) + flush_print("File {} {}.".format(f, "exists" if _ else "does not exist")) return _ @@ -128,5 +132,5 @@ def isfile(f): # ------ def isdir(f): _ = os.path.isdir(f) - flush_print('Directory {} {}.'.format(f, 'exists' if _ else 'does not exist')) + flush_print("Directory {} {}.".format(f, "exists" if _ else "does not exist")) return _ diff --git a/tests/conftest.py b/tests/conftest.py index 24c8167a1..c54131223 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,21 +58,27 @@ def pytest_addoption(parser): # Per the `API reference `, # options are argparse style. - parser.addoption('--skipdbinit', action='store_true', - help='Skip initialization of the test database.') - parser.addoption('--skip_w3_validate', action='store_true', - help='Skip W3C validation of web pages.') + parser.addoption( + "--skipdbinit", + action="store_true", + help="Skip initialization of the test database.", + ) + parser.addoption( + "--skip_w3_validate", + action="store_true", + help="Skip W3C validation of web pages.", + ) # Output a coverage report when testing is done. See https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_terminal_summary. def pytest_terminal_summary(terminalreporter): - with pushd('../../..'): - cp = xqt('{} -m coverage report'.format(sys.executable), - universal_newlines=True) + with pushd("../../.."): + cp = xqt( + "{} -m coverage report".format(sys.executable), universal_newlines=True + ) terminalreporter.write_line(cp.stdout + cp.stderr) - # Utilities # ========= # A simple data-struct object. @@ -82,8 +88,9 @@ class _object(object): # Create a web2py controller environment. This is taken from pieces of ``gluon.shell.run``. It returns a ``dict`` containing the environment. def web2py_controller_env( - # _`application`: The name of the application to run in, as a string. - application): + # _`application`: The name of the application to run in, as a string. + application +): env = gluon.shell.env(application, import_models=True) env.update(gluon.shell.exec_pythonrc()) @@ -92,27 +99,28 @@ def web2py_controller_env( # Create a web2py controller environment. Given ``ctl_env = web2py_controller('app_name')``, then ``ctl_env.db`` refers to the usual DAL object for database access, ``ctl_env.request`` is an (empty) Request object, etc. def web2py_controller( - # See env_. - env): + # See env_. + env +): return DictToObject(env) # Fixtures # ======== -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def web2py_server_address(): - return 'http://127.0.0.1:8000' + return "http://127.0.0.1:8000" # This fixture starts and shuts down the web2py server. # # Execute this `fixture `_ once per `session `_. -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def web2py_server(runestone_name, web2py_server_address, pytestconfig): - password = 'pass' + password = "pass" - os.environ['WEB2PY_CONFIG'] = 'test' + os.environ["WEB2PY_CONFIG"] = "test" # HINT: make sure that ``0.py`` has something like the following, that reads this environment variable: # # .. code:: Python @@ -132,44 +140,48 @@ def web2py_server(runestone_name, web2py_server_address, pytestconfig): # HINT: make sure that you export ``TEST_DBURL`` in your environment; it is # not set here because it's specific to the local setup, possibly with a # password, and thus can't be committed to the repo. - assert os.environ['TEST_DBURL'] + assert os.environ["TEST_DBURL"] # Extract the components of the DBURL. The expected format is ``postgresql://user:password@netloc/dbname``, a simplified form of the `connection URI `_. - empty1, postgres_ql, pguser, pgpassword, pgnetloc, dbname, empty2 = re.split('^postgres(ql)?://(.*):(.*)@(.*)/(.*)$', os.environ['TEST_DBURL']) + empty1, postgres_ql, pguser, pgpassword, pgnetloc, dbname, empty2 = re.split( + "^postgres(ql)?://(.*):(.*)@(.*)/(.*)$", os.environ["TEST_DBURL"] + ) assert (not empty1) and (not empty2) - os.environ['PGPASSWORD'] = pgpassword - os.environ['PGUSER'] = pguser - os.environ['DBHOST'] = pgnetloc + os.environ["PGPASSWORD"] = pgpassword + os.environ["PGUSER"] = pguser + os.environ["DBHOST"] = pgnetloc # Assume we are running with working directory in tests. - if pytestconfig.getoption('skipdbinit'): - print('Skipping DB initialization.') + if pytestconfig.getoption("skipdbinit"): + print("Skipping DB initialization.") else: # In the future, to print the output of the init/build process, see `pytest #1599 `_ for code to enable/disable output capture inside a test. # # Make sure runestone_test is nice and clean -- this will remove many # tables that web2py will then re-create. - xqt('rsmanage --verbose initdb --reset --force') + xqt("rsmanage --verbose initdb --reset --force") # Copy the test book to the books directory. - rmtree('../books/test_course_1', ignore_errors=True) + rmtree("../books/test_course_1", ignore_errors=True) # Sometimes this fails for no good reason on Windows. Retry. for retry in range(100): try: - copytree('test_course_1', '../books/test_course_1') + copytree("test_course_1", "../books/test_course_1") break except WindowsError: if retry == 99: raise # Build the test book to add in db fields needed. - with pushd('../books/test_course_1'): + with pushd("../books/test_course_1"): # The runestone build process only looks at ``DBURL``. - os.environ['DBURL'] = os.environ['TEST_DBURL'] - xqt('{} -m runestone build --all'.format(sys.executable), - '{} -m runestone deploy'.format(sys.executable)) + os.environ["DBURL"] = os.environ["TEST_DBURL"] + xqt( + "{} -m runestone build --all".format(sys.executable), + "{} -m runestone deploy".format(sys.executable), + ) - with pushd('../../..'): - xqt('{} -m coverage erase'.format(sys.executable)) + with pushd("../../.."): + xqt("{} -m coverage erase".format(sys.executable)) # For debug, uncomment the next three lines, then run web2py manually to see all debug messages. Use a command line like ``python web2py.py -a pass -X -K runestone,runestone &`` to also start the workers for the scheduler. ##import pdb; pdb.set_trace() @@ -178,12 +190,24 @@ def web2py_server(runestone_name, web2py_server_address, pytestconfig): # Start the web2py server and the `web2py scheduler `_. web2py_server = subprocess.Popen( - [sys.executable, '-m', 'coverage', 'run', '--append', - '--source=' + COVER_DIRS, 'web2py.py', '-a', password, - '--nogui', '--minthreads=10', '--maxthreads=20'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, + [ + sys.executable, + "-m", + "coverage", + "run", + "--append", + "--source=" + COVER_DIRS, + "web2py.py", + "-a", + password, + "--nogui", + "--minthreads=10", + "--maxthreads=20", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, # Produce text (not binary) output for nice output in ``echo()`` below. - universal_newlines=True + universal_newlines=True, ) # Wait for the webserver to come up. for tries in range(50): @@ -197,22 +221,29 @@ def web2py_server(runestone_name, web2py_server_address, pytestconfig): break # Running two processes doesn't produce two active workers. Running with ``-K runestone,runestone`` means additional subprocesses are launched that we lack the PID necessary to kill. So, just use one worker. web2py_scheduler = subprocess.Popen( - [sys.executable, '-m', 'coverage', 'run', '--append', - '--source=' + COVER_DIRS, 'web2py.py', '-K', runestone_name], - stdout=subprocess.PIPE, stderr=subprocess.PIPE + [ + sys.executable, + "-m", + "coverage", + "run", + "--append", + "--source=" + COVER_DIRS, + "web2py.py", + "-K", + runestone_name, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) # Start a thread to read web2py output and echo it. def echo(): stdout, stderr = web2py_server.communicate() - print('\n' - 'web2py server stdout\n' - '--------------------\n') + print("\n" "web2py server stdout\n" "--------------------\n") print(stdout) - print('\n' - 'web2py server stderr\n' - '--------------------\n') + print("\n" "web2py server stderr\n" "--------------------\n") print(stderr) + echo_thread = Thread(target=echo) echo_thread.start() @@ -230,9 +261,9 @@ def echo(): # The name of the Runestone controller. It must be module scoped to allow the ``web2py_server`` to use it. -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def runestone_name(): - return 'runestone' + return "runestone" # The environment of a web2py controller. @@ -258,7 +289,7 @@ def runestone_db(runestone_controller): yield db # Restore the database state after the test finishes. - #---------------------------------------------------- + # ---------------------------------------------------- # Rollback changes, which ensures that any errors in the database connection # will be cleared. db.rollback() @@ -275,7 +306,7 @@ def runestone_db(runestone_controller): # The query is: ## SELECT input_table_name || ',' AS truncate_query FROM(SELECT table_schema || '.' || table_name AS input_table_name FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_name NOT IN ('questions', 'source_code', 'chapters', 'sub_chapters', 'scheduler_run', 'scheduler_task', 'scheduler_task_deps', 'scheduler_worker') AND table_schema NOT LIKE 'pg_toast%') AS information order by input_table_name; db.executesql( -"""TRUNCATE + """TRUNCATE public.acerror_log, public.assignment_questions, public.assignments, @@ -321,7 +352,8 @@ def runestone_db(runestone_controller): public.user_topic_practice_log, public.user_topic_practice_survey, public.web2py_session_runestone CASCADE; - """) + """ + ) db.commit() @@ -331,27 +363,31 @@ def __init__(self, runestone_db): self.db = runestone_db # Create a new course. It returns an object with information about the created course. - def create_course(self, + def create_course( + self, # The name of the course to create, as a string. - course_name='test_child_course_1', + course_name="test_child_course_1", # The start date of the course, as a string. - term_start_date='2000-01-01', + term_start_date="2000-01-01", # The value of the ``login_required`` flag for the course. login_required=True, # The base course for this course. If ``None``, it will use ``course_name``. - base_course='test_course_1', + base_course="test_course_1", # The student price for this course. - student_price=None): + student_price=None, + ): # Sanity check: this class shouldn't exist. db = self.db assert not db(db.courses.course_name == course_name).select().first() # Create the base course if it doesn't exist. - if (course_name != base_course and - not db(db.courses.course_name == base_course).select(db.courses.id)): - self.create_course(base_course, term_start_date, login_required, - base_course, student_price) + if course_name != base_course and not db( + db.courses.course_name == base_course + ).select(db.courses.id): + self.create_course( + base_course, term_start_date, login_required, base_course, student_price + ) # Store these values in an object for convenient access. obj = _object() @@ -361,7 +397,8 @@ def create_course(self, obj.base_course = base_course obj.student_price = student_price obj.course_id = db.courses.insert( - course_name=course_name, base_course=obj.base_course, + course_name=course_name, + base_course=obj.base_course, term_start_date=term_start_date, login_required=login_required, student_price=student_price, @@ -369,15 +406,18 @@ def create_course(self, db.commit() return obj - - def make_instructor(self, + def make_instructor( + self, # The ID of the user to make an instructor. user_id, # The ID of the course in which the user will be an instructor. - course_id): + course_id, + ): db = self.db - course_instructor_id = db.course_instructor.insert(course=course_id, instructor=user_id) + course_instructor_id = db.course_instructor.insert( + course=course_id, instructor=user_id + ) db.commit() return course_instructor_id @@ -390,33 +430,43 @@ def runestone_db_tools(runestone_db): # Given the ``test_client.text``, prepare to write it to a file. def _html_prep(text_str): - _str = text_str.replace('\r\n', '\n') + _str = text_str.replace("\r\n", "\n") # Deal with fun Python 2 encoding quirk. - return _str if six.PY2 else _str.encode('utf-8') + return _str if six.PY2 else _str.encode("utf-8") # Create a client for accessing the Runestone server. class _TestClient(WebClient): - def __init__(self, web2py_server, web2py_server_address, runestone_name, tmp_path, pytestconfig): + def __init__( + self, + web2py_server, + web2py_server_address, + runestone_name, + tmp_path, + pytestconfig, + ): self.web2py_server = web2py_server self.web2py_server_address = web2py_server_address self.tmp_path = tmp_path self.pytestconfig = pytestconfig - super(_TestClient, self).__init__('{}/{}/'.format(self.web2py_server_address, runestone_name), - postbacks=True) + super(_TestClient, self).__init__( + "{}/{}/".format(self.web2py_server_address, runestone_name), postbacks=True + ) # Use the W3C validator to check the HTML at the given URL. - def validate(self, + def validate( + self, # The relative URL to validate. url, # An optional string that, if provided, must be in the text returned by the server. If this is a sequence of strings, all of the provided strings must be in the text returned by the server. - expected_string='', + expected_string="", # The number of validation errors expected. If None, no validation is performed. expected_errors=None, # The expected status code from the request. expected_status=200, # All additional keyword arguments are passed to the ``post`` method. - **kwargs): + **kwargs + ): try: try: @@ -427,7 +477,7 @@ def validate(self, # Since this is an error of some type, these paramets must be empty, since they can't be checked. assert not expected_string assert not expected_errors - return '' + return "" else: raise assert self.status == expected_status @@ -438,13 +488,14 @@ def validate(self, # Assume ``expected_string`` is a sequence of strings. assert all(string in self.text for string in expected_string) - if (expected_errors is not None and - not self.pytestconfig.getoption('skip_w3_validate')): + if expected_errors is not None and not self.pytestconfig.getoption( + "skip_w3_validate" + ): # Redo this section using html5validate command line vld = Validator(errors_only=True, stack_size=2048) - tmpname = self.tmp_path / 'tmphtml.html' - with open(tmpname, 'w', encoding='utf8') as f: + tmpname = self.tmp_path / "tmphtml.html" + with open(tmpname, "w", encoding="utf8") as f: f.write(self.text) errors = vld.validate([str(tmpname)]) @@ -454,36 +505,36 @@ def validate(self, except AssertionError: # Save the HTML to make fixing the errors easier. Note that ``self.text`` is already encoded as utf-8. - validation_file = url.replace('/', '-') + '.html' - with open(validation_file, 'wb') as f: + validation_file = url.replace("/", "-") + ".html" + with open(validation_file, "wb") as f: f.write(_html_prep(self.text)) - print('Validation failure saved to {}.'.format(validation_file)) + print("Validation failure saved to {}.".format(validation_file)) raise except RuntimeError as e: # Provide special handling for web2py exceptions by saving the # resulting traceback. - if e.args[0].startswith('ticket '): + if e.args[0].startswith("ticket "): # Create a client to access the admin interface. - admin_client = WebClient('{}/admin/'.format(self.web2py_server_address), - postbacks=True) + admin_client = WebClient( + "{}/admin/".format(self.web2py_server_address), postbacks=True + ) # Log in. - admin_client.post('', data={'password': - self.web2py_server.password}) + admin_client.post("", data={"password": self.web2py_server.password}) assert admin_client.status == 200 # Get the error. - error_code = e.args[0][len('ticket '):] - admin_client.get('default/ticket/' + error_code) + error_code = e.args[0][len("ticket ") :] + admin_client.get("default/ticket/" + error_code) assert admin_client.status == 200 # Save it to a file. - traceback_file = url.replace('/', '-') + '_traceback.html' - with open(traceback_file, 'wb') as f: + traceback_file = url.replace("/", "-") + "_traceback.html" + with open(traceback_file, "wb") as f: f.write(_html_prep(admin_client.text)) - print('Traceback saved to {}.'.format(traceback_file)) + print("Traceback saved to {}.".format(traceback_file)) raise def logout(self): - self.validate('default/user/logout', 'Logged out') + self.validate("default/user/logout", "Logged out") # Always logout after a test finishes. def tearDown(self): @@ -492,17 +543,23 @@ def tearDown(self): # Present ``_TestClient`` as a fixure. @pytest.fixture -def test_client(web2py_server, web2py_server_address, runestone_name, tmp_path, pytestconfig): - tc = _TestClient(web2py_server, web2py_server_address, runestone_name, tmp_path, pytestconfig) +def test_client( + web2py_server, web2py_server_address, runestone_name, tmp_path, pytestconfig +): + tc = _TestClient( + web2py_server, web2py_server_address, runestone_name, tmp_path, pytestconfig + ) yield tc tc.tearDown() # This class allows creating a user inside a context manager. class _TestUser(object): - def __init__(self, + def __init__( + self, # These are fixtures. - test_client, runestone_db_tools, + test_client, + runestone_db_tools, # The username for this user. username, # The password for this user. @@ -512,16 +569,17 @@ def __init__(self, # True if the course is free (no payment required); False otherwise. is_free=True, # The first name for this user. - first_name='test', + first_name="test", # The last name for this user. - last_name='user'): + last_name="user", + ): self.test_client = test_client self.runestone_db_tools = runestone_db_tools self.username = username self.first_name = first_name self.last_name = last_name - self.email = self.username + '@foo.com' + self.email = self.username + "@foo.com" self.password = password self.course = course self.is_free = is_free @@ -529,8 +587,9 @@ def __init__(self, # Registration doesn't work unless we're logged out. self.test_client.logout() # Now, post the registration. - self.test_client.validate('default/user/register', - 'Support Runestone Interactive' if self.is_free else 'Payment Amount', + self.test_client.validate( + "default/user/register", + "Support Runestone Interactive" if self.is_free else "Payment Amount", data=dict( username=self.username, first_name=self.first_name, @@ -541,23 +600,29 @@ def __init__(self, password_two=self.password, # Note that ``course_id`` is (on the form) actually a course name. course_id=self.course.course_name, - accept_tcp='on', - donate='0', - _next='/runestone/default/index', - _formname='register', - ) + accept_tcp="on", + donate="0", + _next="/runestone/default/index", + _formname="register", + ), ) # Record IDs db = self.runestone_db_tools.db - self.user_id = db(db.auth_user.username == self.username).select(db.auth_user.id).first().id + self.user_id = ( + db(db.auth_user.username == self.username) + .select(db.auth_user.id) + .first() + .id + ) def login(self): - self.test_client.validate('default/user/login', data=dict( - username=self.username, - password=self.password, - _formname='login', - )) + self.test_client.validate( + "default/user/login", + data=dict( + username=self.username, password=self.password, _formname="login" + ), + ) def logout(self): self.test_client.logout() @@ -568,7 +633,8 @@ def make_instructor(self, course_id=None): return self.runestone_db_tools.make_instructor(self.user_id, course_id) # A context manager to update this user's profile. If a course was added, it returns that course's ID; otherwise, it returns None. - def update_profile(self, + def update_profile( + self, # This parameter is passed to ``test_client.validate``. expected_string=None, # An updated username, or ``None`` to use ``self.username``. @@ -581,18 +647,20 @@ def update_profile(self, email=None, # An updated last name, or ``None`` to use ``self.course.course_name``. course_name=None, - section='', + section="", # A shortcut for specifying the ``expected_string``, which only applies if ``expected_string`` is not set. Use ``None`` if a course will not be added, ``True`` if the added course is free, or ``False`` if the added course is paid. is_free=None, # The value of the ``accept_tcp`` checkbox; provide an empty string to leave unchecked. The default value leaves it checked. - accept_tcp='on'): + accept_tcp="on", + ): if expected_string is None: if is_free is None: - expected_string = 'Course Selection' + expected_string = "Course Selection" else: - expected_string = 'Support Runestone Interactive' \ - if is_free else 'Payment Amount' + expected_string = ( + "Support Runestone Interactive" if is_free else "Payment Amount" + ) username = username or self.username first_name = first_name or self.first_name last_name = last_name or self.last_name @@ -600,7 +668,8 @@ def update_profile(self, course_name = course_name or self.course.course_name # Perform the update. - self.test_client.validate('default/user/profile', + self.test_client.validate( + "default/user/profile", expected_string, data=dict( username=username, @@ -611,62 +680,77 @@ def update_profile(self, course_id=course_name, accept_tcp=accept_tcp, section=section, - _next='/runestone/default/index', + _next="/runestone/default/index", id=str(self.user_id), - _formname='auth_user/' + str(self.user_id), - ) + _formname="auth_user/" + str(self.user_id), + ), ) # Call this after registering for a new course or adding a new course via ``update_profile`` to pay for the course. - def make_payment(self, + def make_payment( + self, # The `Stripe test tokens `_ to use for payment. stripe_token, # The course ID of the course to pay for. None specifies ``self.course.course_id``. - course_id=None): + course_id=None, + ): course_id = course_id or self.course.course_id # Get the signature from the HTML of the payment page. - self.test_client.validate('default/payment') - match = re.search('', - self.test_client.text) + self.test_client.validate("default/payment") + match = re.search( + '', + self.test_client.text, + ) signature = match.group(1) - html = self.test_client.validate('default/payment', - data=dict(stripeToken=stripe_token, signature=signature) + html = self.test_client.validate( + "default/payment", data=dict(stripeToken=stripe_token, signature=signature) ) - assert ('Thank you for your payment' in html) or ('Payment failed' in html) + assert ("Thank you for your payment" in html) or ("Payment failed" in html) def hsblog(self, **kwargs): # Get the time, rounded down to a second, before posting to the server. ts = datetime.datetime.utcnow() ts -= datetime.timedelta(microseconds=ts.microsecond) - if 'course' not in kwargs: - kwargs['course'] = self.course.course_name + if "course" not in kwargs: + kwargs["course"] = self.course.course_name - if 'answer' not in kwargs and 'act' in kwargs: - kwargs['answer'] = kwargs['act'] + if "answer" not in kwargs and "act" in kwargs: + kwargs["answer"] = kwargs["act"] # Post to the server. - return json.loads(self.test_client.validate('ajax/hsblog', data=kwargs)) + return json.loads(self.test_client.validate("ajax/hsblog", data=kwargs)) # Present ``_TestUser`` as a fixture. @pytest.fixture def test_user(test_client, runestone_db_tools): - return lambda *args, **kwargs: _TestUser(test_client, runestone_db_tools, *args, **kwargs) + return lambda *args, **kwargs: _TestUser( + test_client, runestone_db_tools, *args, **kwargs + ) # Provide easy access to a test user and course. @pytest.fixture def test_user_1(runestone_db_tools, test_user): course = runestone_db_tools.create_course() - return test_user('test_user_1', 'password_1', course) + return test_user("test_user_1", "password_1", course) class _TestAssignment(object): assignment_count = 0 - def __init__(self, test_client, test_user, runestone_db_tools, aname, course, is_visible=False): + + def __init__( + self, + test_client, + test_user, + runestone_db_tools, + aname, + course, + is_visible=False, + ): self.test_client = test_client self.runestone_db_tools = runestone_db_tools self.assignment_name = aname @@ -674,39 +758,40 @@ def __init__(self, test_client, test_user, runestone_db_tools, aname, course, is self.description = "default description" self.is_visible = is_visible self.due = datetime.datetime.utcnow() + datetime.timedelta(days=7) - self.assignment_instructor = test_user('assign_instructor_{}'.format(_TestAssignment.assignment_count), - 'password', course) + self.assignment_instructor = test_user( + "assign_instructor_{}".format(_TestAssignment.assignment_count), + "password", + course, + ) self.assignment_instructor.make_instructor() self.assignment_instructor.login() self.assignment_id = json.loads( - self.test_client.validate('admin/createAssignment', - data={'name': self.assignment_name}) + self.test_client.validate( + "admin/createAssignment", data={"name": self.assignment_name} + ) )[self.assignment_name] assert self.assignment_id _TestAssignment.assignment_count += 1 - def addq_to_assignment(self, **kwargs): - if 'points' not in kwargs: - kwargs['points'] = 1 - kwargs['assignment'] = self.assignment_id + if "points" not in kwargs: + kwargs["points"] = 1 + kwargs["assignment"] = self.assignment_id res = self.test_client.validate( - 'admin/add__or_update_assignment_question', data=kwargs) + "admin/add__or_update_assignment_question", data=kwargs + ) res = json.loads(res) - assert res['status'] == 'success' - + assert res["status"] == "success" - def autograde(self,sid=None): - print('autograding', self.assignment_name) + def autograde(self, sid=None): + print("autograding", self.assignment_name) vars = dict(assignment=self.assignment_name) if sid: - vars['sid'] = sid - res = json.loads(self.test_client.validate('assignments/autograde', - data=vars)) - assert res['message'].startswith('autograded') + vars["sid"] = sid + res = json.loads(self.test_client.validate("assignments/autograde", data=vars)) + assert res["message"].startswith("autograded") return res - def questions(self): """ Return a list of all (id, name) values for each question @@ -714,22 +799,23 @@ def questions(self): """ db = self.runestone_db_tools.db - a_q_rows = db((db.assignment_questions.assignment_id == self.assignment_id) & - (db.assignment_questions.question_id == db.questions.id) - ).select(orderby=db.assignment_questions.sorting_priority) + a_q_rows = db( + (db.assignment_questions.assignment_id == self.assignment_id) + & (db.assignment_questions.question_id == db.questions.id) + ).select(orderby=db.assignment_questions.sorting_priority) res = [] for row in a_q_rows: res.append(tuple([row.questions.id, row.questions.name])) return res - def calculate_totals(self): assert json.loads( - self.test_client.validate('assignments/calculate_totals', - data=dict(assignment=self.assignment_name)) - )['success'] - + self.test_client.validate( + "assignments/calculate_totals", + data=dict(assignment=self.assignment_name), + ) + )["success"] def make_visible(self): self.is_visible = True @@ -743,21 +829,31 @@ def set_duedate(self, newdeadline): self.save_assignment() def save_assignment(self): - assert json.loads( - self.test_client.validate('admin/save_assignment', - data=dict(assignment_id=self.assignment_id, - visible='T' if self.is_visible else 'F', - description=self.description, - due=str(self.due))))['status'] == 'success' - + assert ( + json.loads( + self.test_client.validate( + "admin/save_assignment", + data=dict( + assignment_id=self.assignment_id, + visible="T" if self.is_visible else "F", + description=self.description, + due=str(self.due), + ), + ) + )["status"] + == "success" + ) def release_grades(self): - self.test_client.post('admin/releasegrades', - data=dict(assignmentid=self.assignment_id, - released='yes')) - assert self.test_client.text == 'Success' + self.test_client.post( + "admin/releasegrades", + data=dict(assignmentid=self.assignment_id, released="yes"), + ) + assert self.test_client.text == "Success" @pytest.fixture def test_assignment(test_client, test_user, runestone_db_tools): - return lambda *args, **kwargs: _TestAssignment(test_client, test_user, runestone_db_tools, *args, **kwargs) + return lambda *args, **kwargs: _TestAssignment( + test_client, test_user, runestone_db_tools, *args, **kwargs + ) diff --git a/tests/locustfile.py b/tests/locustfile.py index 36e534adc..380bd8390 100644 --- a/tests/locustfile.py +++ b/tests/locustfile.py @@ -3,30 +3,30 @@ import random import sys, os + class WebsiteTasks(TaskSet): def on_start(self): res = self.client.get("/runestone/default/user/login") - pq = bs4.BeautifulSoup(res.content, features='lxml') + pq = bs4.BeautifulSoup(res.content, features="lxml") # Get the csrf key for successful submission i = pq.select('input[name="_formkey"]') - token = i[0]['value'] + token = i[0]["value"] # login a user try: - user = os.environ['RUNESTONE_TESTUSER'] - pw = os.environ['RUNESTONE_TESTPW'] + user = os.environ["RUNESTONE_TESTUSER"] + pw = os.environ["RUNESTONE_TESTPW"] except: print("ERROR please set RUNESTONE_TESTUSER and RUNESTONE_TESTPW ") sys.exit(-1) - res = self.client.post("/runestone/default/user/login", - {"username": user, - "password": pw, - "_formkey": token, - "_formname": 'login'}) + res = self.client.post( + "/runestone/default/user/login", + {"username": user, "password": pw, "_formkey": token, "_formname": "login"}, + ) # Get the index and make a list of all chapters/subchapters res = self.client.get("/runestone/books/published/fopp/index.html") - pq = bs4.BeautifulSoup(res.content, features='lxml') - pages = pq.select('.toctree-l2 a') - self.bookpages = [p['href'] for p in pages] + pq = bs4.BeautifulSoup(res.content, features="lxml") + pages = pq.select(".toctree-l2 a") + self.bookpages = [p["href"] for p in pages] @task(5) def index(self): @@ -36,22 +36,27 @@ def index(self): def boookpage(self): # pick a page at random url = random.choice(self.bookpages) - base = '/runestone/books/published/fopp/' + base = "/runestone/books/published/fopp/" res = self.client.get(base + url) - pq = bs4.BeautifulSoup(res.content, features='lxml') + pq = bs4.BeautifulSoup(res.content, features="lxml") # client.get ONLY gets the html, so we need to simulate getting all # of the static assets ourselves. - for s in pq.select('script'): - if s.has_attr('src'): - if s['src'].startswith(("http","//")) == False: - js = self.client.get(base + s['src'].replace('../',''), name="scripts") - for s in pq.select('link'): - if s.has_attr('href'): - if s['href'].startswith(("http","//")) == False: - css = self.client.get(base + s['href'].replace('../',''), name='css') + for s in pq.select("script"): + if s.has_attr("src"): + if s["src"].startswith(("http", "//")) == False: + js = self.client.get( + base + s["src"].replace("../", ""), name="scripts" + ) + for s in pq.select("link"): + if s.has_attr("href"): + if s["href"].startswith(("http", "//")) == False: + css = self.client.get( + base + s["href"].replace("../", ""), name="css" + ) + class WebsiteUser(HttpLocust): - host='http://localhost' + host = "http://localhost" task_set = WebsiteTasks min_wait = 1000 - max_wait = 15000 \ No newline at end of file + max_wait = 15000 diff --git a/tests/test_admin.py b/tests/test_admin.py index 4b2b876a6..cceeec027 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,8 +1,9 @@ import json import pytest + def test_add_assignment(test_assignment, test_user_1, runestone_db_tools): - my_ass = test_assignment('test_assignment', test_user_1.course) + my_ass = test_assignment("test_assignment", test_user_1.course) # Should provide the following to addq_to_assignment # -- assignment (an integer) # -- question == div_id @@ -10,11 +11,11 @@ def test_add_assignment(test_assignment, test_user_1, runestone_db_tools): # -- autograde one of ['manual', 'all_or_nothing', 'pct_correct', 'interact'] # -- which_to_grade one of ['first_answer', 'last_answer', 'best_answer'] # -- reading_assignment (boolean, true if it's a page to visit rather than a directive to interact with) - my_ass.addq_to_assignment(question='subc_b_fitb',points=10) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) print(my_ass.questions()) db = runestone_db_tools.db my_ass.save_assignment() - res = db(db.assignments.name == 'test_assignment').select().first() + res = db(db.assignments.name == "test_assignment").select().first() assert res.description == my_ass.description assert str(res.duedate.date()) == str(my_ass.due.date()) my_ass.autograde() @@ -25,50 +26,66 @@ def test_add_assignment(test_assignment, test_user_1, runestone_db_tools): def test_choose_assignment(test_assignment, test_client, test_user_1): - my_ass = test_assignment('test_assignment', test_user_1.course) - my_ass.addq_to_assignment(question='subc_b_fitb',points=10) - my_ass.description = 'Test Assignment Description' + my_ass = test_assignment("test_assignment", test_user_1.course) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) + my_ass.description = "Test Assignment Description" my_ass.make_visible() test_user_1.login() - test_client.validate('assignments/chooseAssignment.html','Test Assignment Description') + test_client.validate( + "assignments/chooseAssignment.html", "Test Assignment Description" + ) + def test_do_assignment(test_assignment, test_client, test_user_1): - my_ass = test_assignment('test_assignment', test_user_1.course) - my_ass.addq_to_assignment(question='subc_b_fitb',points=10) - my_ass.description = 'Test Assignment Description' + my_ass = test_assignment("test_assignment", test_user_1.course) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) + my_ass.description = "Test Assignment Description" my_ass.make_visible() test_user_1.login() # This assignment has the fill in the blank for may had a |blank| lamb - test_client.validate('assignments/doAssignment.html', 'Mary had a', - data=dict(assignment_id=my_ass.assignment_id)) + test_client.validate( + "assignments/doAssignment.html", + "Mary had a", + data=dict(assignment_id=my_ass.assignment_id), + ) + def test_question_text(test_client, test_user_1): test_user_1.make_instructor() test_user_1.login() - test_client.validate('admin/question_text', 'Mary had a', - data=dict(question_name='subc_b_fitb')) - test_client.validate('admin/question_text', 'Error: ', - data=dict(question_name='non_existant_question')) + test_client.validate( + "admin/question_text", "Mary had a", data=dict(question_name="subc_b_fitb") + ) + test_client.validate( + "admin/question_text", + "Error: ", + data=dict(question_name="non_existant_question"), + ) + def test_removeinstructor(test_user, test_client, test_user_1): - my_inst = test_user('new_instructor', 'password', test_user_1.course) + my_inst = test_user("new_instructor", "password", test_user_1.course) my_inst.make_instructor() my_inst.login() - res = test_client.validate('admin/addinstructor/{}'.format(test_user_1.user_id)) - assert json.loads(res) == 'Success' - res = test_client.validate('admin/removeinstructor/{}'.format(test_user_1.user_id)) + res = test_client.validate("admin/addinstructor/{}".format(test_user_1.user_id)) + assert json.loads(res) == "Success" + res = test_client.validate("admin/removeinstructor/{}".format(test_user_1.user_id)) assert json.loads(res) == [True] - res = test_client.validate('admin/removeinstructor/{}'.format(my_inst.user_id)) + res = test_client.validate("admin/removeinstructor/{}".format(my_inst.user_id)) assert json.loads(res) == [False] - res = test_client.validate('admin/addinstructor/{}'.format(9999999)) - assert 'Cannot add non-existent user ' in json.loads(res) + res = test_client.validate("admin/addinstructor/{}".format(9999999)) + assert "Cannot add non-existent user " in json.loads(res) + def test_removestudents(test_user, test_client, test_user_1, runestone_db_tools): - my_inst = test_user('new_instructor', 'password', test_user_1.course) + my_inst = test_user("new_instructor", "password", test_user_1.course) my_inst.make_instructor() my_inst.login() - res = test_client.validate('admin/removeStudents', 'Assignments', - data=dict(studentList=test_user_1.user_id)) + res = test_client.validate( + "admin/removeStudents", + "Assignments", + data=dict(studentList=test_user_1.user_id), + ) db = runestone_db_tools.db res = db(db.auth_user.id == test_user_1.user_id).select().first() @@ -78,30 +95,26 @@ def test_removestudents(test_user, test_client, test_user_1, runestone_db_tools) def test_htmlsrc(test_client, test_user_1): test_user_1.make_instructor() test_user_1.login() - test_client.validate('admin/htmlsrc', 'Mary had a', - data=dict(acid='subc_b_fitb')) - test_client.validate('admin/htmlsrc', 'No preview Available', - data=dict(acid='non_existant_question')) + test_client.validate("admin/htmlsrc", "Mary had a", data=dict(acid="subc_b_fitb")) + test_client.validate( + "admin/htmlsrc", "No preview Available", data=dict(acid="non_existant_question") + ) def test_qbank(test_client, test_user_1): test_user_1.make_instructor() test_user_1.login() - qname = 'subc_b_fitb' - res = test_client.validate('admin/questionBank', - data=dict(term=qname - )) + qname = "subc_b_fitb" + res = test_client.validate("admin/questionBank", data=dict(term=qname)) res = json.loads(res) assert qname in res - res = test_client.validate('admin/questionBank', - data=dict(chapter='test_chapter_1' - )) + res = test_client.validate( + "admin/questionBank", data=dict(chapter="test_chapter_1") + ) res = json.loads(res) assert qname in res assert len(res) >= 4 - res = test_client.validate('admin/questionBank', - data=dict(author='test_author' - )) + res = test_client.validate("admin/questionBank", data=dict(author="test_author")) res = json.loads(res) assert qname in res assert len(res) == 2 @@ -110,78 +123,81 @@ def test_qbank(test_client, test_user_1): def test_gettemplate(test_user_1, test_client): test_user_1.make_instructor() test_user_1.login() - dirlist = ['activecode', 'mchoice', 'fillintheblank'] + dirlist = ["activecode", "mchoice", "fillintheblank"] for d in dirlist: - res = test_client.validate('admin/gettemplate/{}'.format(d)) + res = test_client.validate("admin/gettemplate/{}".format(d)) res = json.loads(res) assert res - assert d in res['template'] + assert d in res["template"] def test_question_info(test_assignment, test_user_1, test_client): test_user_1.make_instructor() test_user_1.login() - my_ass = test_assignment('test_assignment', test_user_1.course) - my_ass.addq_to_assignment(question='subc_b_fitb',points=10) - res = test_client.validate('admin/getQuestionInfo', data=dict( - assignment=my_ass.assignment_id, - question='subc_b_fitb', - )) + my_ass = test_assignment("test_assignment", test_user_1.course) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) + res = test_client.validate( + "admin/getQuestionInfo", + data=dict(assignment=my_ass.assignment_id, question="subc_b_fitb"), + ) res = json.loads(res) assert res - assert res['code'] - assert res['htmlsrc'] + assert res["code"] + assert res["htmlsrc"] def test_create_question(test_assignment, test_user_1, runestone_db_tools, test_client): test_user_1.make_instructor() test_user_1.login() - my_ass = test_assignment('test_assignment', test_user_1.course) + my_ass = test_assignment("test_assignment", test_user_1.course) data = { - 'template': 'mchoice', - 'name': 'test_question_1', - 'question': "This is fake text for a fake question", - 'difficulty': 0, - 'tags': None, - 'chapter': 'test_chapter_1', - 'subchapter': 'Exercises', - 'isprivate': False, - 'assignmentid': my_ass.assignment_id, - 'points': 10, - 'timed': False, - 'htmlsrc': "

Hello World

" + "template": "mchoice", + "name": "test_question_1", + "question": "This is fake text for a fake question", + "difficulty": 0, + "tags": None, + "chapter": "test_chapter_1", + "subchapter": "Exercises", + "isprivate": False, + "assignmentid": my_ass.assignment_id, + "points": 10, + "timed": False, + "htmlsrc": "

Hello World

", } - res = test_client.validate('admin/createquestion', data=data) + res = test_client.validate("admin/createquestion", data=data) res = json.loads(res) assert res - assert res['test_question_1'] + assert res["test_question_1"] db = runestone_db_tools.db - row = db(db.questions.id == res['test_question_1']).select().first() + row = db(db.questions.id == res["test_question_1"]).select().first() - assert row['question'] == "This is fake text for a fake question" + assert row["question"] == "This is fake text for a fake question" def test_get_assignment(test_assignment, test_user_1, test_client): test_user_1.make_instructor() test_user_1.login() - my_ass = test_assignment('test_assignment', test_user_1.course) - my_ass.addq_to_assignment(question='subc_b_fitb', points=10) + my_ass = test_assignment("test_assignment", test_user_1.course) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) - res = test_client.validate('admin/get_assignment', data=dict( - assignmentid=my_ass.assignment_id - )) + res = test_client.validate( + "admin/get_assignment", data=dict(assignmentid=my_ass.assignment_id) + ) res = json.loads(res) assert res - assert res['questions_data'] + assert res["questions_data"] + -@pytest.mark.parametrize('assign_id',[ (-1), (0) ]) -def test_copy_assignment(assign_id, test_assignment, test_client, test_user_1, runestone_db_tools): +@pytest.mark.parametrize("assign_id", [(-1), (0)]) +def test_copy_assignment( + assign_id, test_assignment, test_client, test_user_1, runestone_db_tools +): test_user_1.make_instructor() test_user_1.login() course1_id = test_user_1.course.course_id - my_ass = test_assignment('test_assignment', test_user_1.course) + my_ass = test_assignment("test_assignment", test_user_1.course) # Should provide the following to addq_to_assignment # -- assignment (an integer) # -- question == div_id @@ -189,28 +205,36 @@ def test_copy_assignment(assign_id, test_assignment, test_client, test_user_1, r # -- autograde one of ['manual', 'all_or_nothing', 'pct_correct', 'interact'] # -- which_to_grade one of ['first_answer', 'last_answer', 'best_answer'] # -- reading_assignment (boolean, true if it's a page to visit rather than a directive to interact with) - my_ass.addq_to_assignment(question='subc_b_fitb',points=10) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) print(my_ass.questions()) db = runestone_db_tools.db my_ass.save_assignment() - course_3 = runestone_db_tools.create_course('test_course_3', base_course='test_course_1') + course_3 = runestone_db_tools.create_course( + "test_course_3", base_course="test_course_1" + ) test_user_1.make_instructor(course_3.course_id) - db(db.auth_user.id == test_user_1.user_id).update(course_id=course_3.course_id, course_name='test_course_3') + db(db.auth_user.id == test_user_1.user_id).update( + course_id=course_3.course_id, course_name="test_course_3" + ) db.commit() test_user_1.logout() test_user_1.login() if assign_id == 0: assign_id = my_ass.assignment_id - res = test_client.validate('admin/copy_assignment', data=dict( - oldassignment=assign_id, - course='test_child_course_1' - )) + res = test_client.validate( + "admin/copy_assignment", + data=dict(oldassignment=assign_id, course="test_child_course_1"), + ) assert res == "success" - rows = db(db.assignments.name == 'test_assignment').count() + rows = db(db.assignments.name == "test_assignment").count() assert rows == 2 - row = db(db.assignments.name == 'test_assignment').select(orderby=~db.assignments.id).first() + row = ( + db(db.assignments.name == "test_assignment") + .select(orderby=~db.assignments.id) + .first() + ) rows = db(db.assignment_questions.assignment_id == row.id).count() assert rows == 1 diff --git a/tests/test_ajax2.py b/tests/test_ajax2.py index dfba20f94..078710fa9 100644 --- a/tests/test_ajax2.py +++ b/tests/test_ajax2.py @@ -23,19 +23,27 @@ def test_poll(test_client, test_user_1, test_user, runestone_db_tools): # Using hsblog have the user respond to a poll # this is what you would do to simulate a user activity an any kind of runeston # component. - test_user_1.hsblog(event='poll', act='1', div_id="LearningZone_poll", course=test_user_1.course.course_name) + test_user_1.hsblog( + event="poll", + act="1", + div_id="LearningZone_poll", + course=test_user_1.course.course_name, + ) # Now lets get a handle on the database db = runestone_db_tools.db # Manually check that the response made it to the database - res = db(db.useinfo.div_id=='LearningZone_poll').select().first() + res = db(db.useinfo.div_id == "LearningZone_poll").select().first() assert res - assert res['act'] == "1" + assert res["act"] == "1" # Next we'll invoke the API call that returns the poll results. this is a list # [ [option list] [response list] divid myvote] - test_client.post('ajax/getpollresults', data=dict(course=test_user_1.course.course_name, div_id='LearningZone_poll')) + test_client.post( + "ajax/getpollresults", + data=dict(course=test_user_1.course.course_name, div_id="LearningZone_poll"), + ) # print statements are useful for debugging and only shown in the Captured stdout call # section of the output from pytest if the test fails. Otherwise print output is # hidden @@ -46,11 +54,19 @@ def test_poll(test_client, test_user_1, test_user, runestone_db_tools): assert res[-1] == "1" # Now lets have a second user respond to the poll. - user2 = test_user('test_user_2', 'password', test_user_1.course) + user2 = test_user("test_user_2", "password", test_user_1.course) test_user_1.logout() user2.login() - user2.hsblog(event='poll', act='2', div_id="LearningZone_poll", course=user2.course.course_name) - test_client.post('ajax/getpollresults', data=dict(course=user2.course.course_name, div_id='LearningZone_poll')) + user2.hsblog( + event="poll", + act="2", + div_id="LearningZone_poll", + course=user2.course.course_name, + ) + test_client.post( + "ajax/getpollresults", + data=dict(course=user2.course.course_name, div_id="LearningZone_poll"), + ) res = json.loads(test_client.text) assert res[0] == 2 assert res[1] == [0, 1, 2] @@ -62,20 +78,22 @@ def test_hsblog(test_client, test_user_1, test_user, runestone_db_tools): test_user_1.login() kwargs = dict( - act = 'run', - event = 'acivecode', - course = test_user_1.course.course_name, - div_id = 'unit_test_1', - ) + act="run", + event="acivecode", + course=test_user_1.course.course_name, + div_id="unit_test_1", + ) res = test_user_1.hsblog(**kwargs) print(res) assert len(res.keys()) == 2 - assert res['log'] == True - time_delta = datetime.datetime.utcnow() - datetime.datetime.strptime(res['timestamp'], '%Y-%m-%d %H:%M:%S') + assert res["log"] == True + time_delta = datetime.datetime.utcnow() - datetime.datetime.strptime( + res["timestamp"], "%Y-%m-%d %H:%M:%S" + ) assert time_delta < datetime.timedelta(seconds=1) db = runestone_db_tools.db - dbres = db(db.useinfo.div_id == 'unit_test_1').select(db.useinfo.ALL) + dbres = db(db.useinfo.div_id == "unit_test_1").select(db.useinfo.ALL) assert len(dbres) == 1 assert dbres[0].course_id == test_user_1.course.course_name @@ -85,10 +103,11 @@ def ajaxCall(client, funcName, **kwargs): Call the funcName using the client Returns json.loads(funcName()) """ - client.post('ajax/' + funcName, data = kwargs) + client.post("ajax/" + funcName, data=kwargs) print(client.text) - if client.text != 'None': - return(json.loads(client.text)) + if client.text != "None": + return json.loads(client.text) + def genericGetAssessResults(test_client, test_user, **kwargs): """ @@ -107,7 +126,12 @@ def genericGetAssessResults(test_client, test_user, **kwargs): test_user.hsblog(**kwargs) # Next we'll invoke the API call that returns the event results. - test_client.post('ajax/getAssessResults', data=dict(course=kwargs['course'], div_id=kwargs['div_id'], event=kwargs['event'])) + test_client.post( + "ajax/getAssessResults", + data=dict( + course=kwargs["course"], div_id=kwargs["div_id"], event=kwargs["event"] + ), + ) # print statements are useful for debugging and only shown in the Captured stdout call # section of the output from pytest if the test fails. Otherwise print output is @@ -116,305 +140,349 @@ def genericGetAssessResults(test_client, test_user, **kwargs): res = json.loads(test_client.text) return res + # The following tests are a port from the test_ajax.py def test_GetMChoiceResults(test_client, test_user_1): # Generate a incorrect mChoice answer - val = '1' - res = genericGetAssessResults(test_client, test_user_1, - event = 'mChoice', - div_id = 'test_mchoice_1', - answer = val, - act = val, - correct = 'F', - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert not res['correct'] + val = "1" + res = genericGetAssessResults( + test_client, + test_user_1, + event="mChoice", + div_id="test_mchoice_1", + answer=val, + act=val, + correct="F", + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert not res["correct"] # Generate a correct mChoice answer - val = '3' - res = genericGetAssessResults(test_client, test_user_1, - event = 'mChoice', - div_id = 'test_mchoice_1', - answer = val, - act = val, - correct = 'T', - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert res['correct'] - - -def test_GetParsonsResults(test_client, test_user_1,): - val = '0_0-1_2_0-3_4_0-5_1-6_1-7_0' - res = genericGetAssessResults(test_client, test_user_1, - event = 'parsons', - div_id = 'test_parsons_1', - answer = val, - act = val, - correct = 'F', - course = test_user_1.course.course_name, - source = 'test_source_1' - ) - assert res['answer'] == val + val = "3" + res = genericGetAssessResults( + test_client, + test_user_1, + event="mChoice", + div_id="test_mchoice_1", + answer=val, + act=val, + correct="T", + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert res["correct"] + + +def test_GetParsonsResults(test_client, test_user_1): + val = "0_0-1_2_0-3_4_0-5_1-6_1-7_0" + res = genericGetAssessResults( + test_client, + test_user_1, + event="parsons", + div_id="test_parsons_1", + answer=val, + act=val, + correct="F", + course=test_user_1.course.course_name, + source="test_source_1", + ) + assert res["answer"] == val + def test_GetClickableResults(test_client, test_user_1): - val = '0;1' - res = genericGetAssessResults(test_client, test_user_1, - event = 'clickableArea', - div_id = 'test_clickable_1', - answer = val, - act = val, - correct = 'F', - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert not res['correct'] + val = "0;1" + res = genericGetAssessResults( + test_client, + test_user_1, + event="clickableArea", + div_id="test_clickable_1", + answer=val, + act=val, + correct="F", + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert not res["correct"] + def test_GetShortAnswerResults(test_client, test_user_1): - val = 'hello_test' - res = genericGetAssessResults(test_client, test_user_1, - event = 'shortanswer', - div_id = 'test_short_answer_1', - answer = val, - act = val, - correct = 'F', - course = test_user_1.course.course_name - ) - assert res['answer'] == val + val = "hello_test" + res = genericGetAssessResults( + test_client, + test_user_1, + event="shortanswer", + div_id="test_short_answer_1", + answer=val, + act=val, + correct="F", + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + def test_GetFITBAnswerResults(test_client, test_user_1, runestone_db_tools): # Test old format, server-side grading ## ----------------------------------- # A correct answer. - val = 'red,away' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_1', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert res['correct'] + val = "red,away" + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_1", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert res["correct"] # An incorrect answer. - val = 'blue,away' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_1', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert not res['correct'] - + val = "blue,away" + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_1", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert not res["correct"] # Test new format, server-side grading ## ----------------------------------- # A correct answer. Add spaces to verify these are ignored. val = '[" red ","away"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_1', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_1", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert res["correct"] # An incorrect answer. val = '["blue","away"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_1', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert not res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_1", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert not res["correct"] # Test server-side grading of a regex val = '["mARy"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_regex', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_regex", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["correct"] val = '["mairI"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_regex', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_regex", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["correct"] val = '["mairy"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_regex', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert not res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_regex", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert not res["correct"] # Test server-side grading of a range of numbers, using various bases. val = '["10"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["correct"] # Sphinx 1.8.5 and Sphinx 2.0 render text a bit differently. - assert res['displayFeed'] in (['Correct.'], ['

Correct.

\n']) + assert res["displayFeed"] in (["Correct."], ["

Correct.

\n"]) val = '["0b1010"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["correct"] val = '["0xA"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert res["correct"] val = '["9"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert not res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert not res["correct"] # Sphinx 1.8.5 and Sphinx 2.0 render text a bit differently. - assert res['displayFeed'] in (['Close.'], ['

Close.

\n']) - + assert res["displayFeed"] in (["Close."], ["

Close.

\n"]) val = '["11"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert not res['correct'] - assert res['displayFeed'] in (['Close.'], ['

Close.

\n']) - + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert not res["correct"] + assert res["displayFeed"] in (["Close."], ["

Close.

\n"]) val = '["8"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - course = test_user_1.course.course_name - ) - assert not res['correct'] - assert res['displayFeed'] in (['Nope.'], ['

Nope.

\n']) - + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + course=test_user_1.course.course_name, + ) + assert not res["correct"] + assert res["displayFeed"] in (["Nope."], ["

Nope.

\n"]) # Test client-side grading. db = runestone_db_tools.db - db(db.courses.course_name == test_user_1.course.course_name).update(login_required=False) + db(db.courses.course_name == test_user_1.course.course_name).update( + login_required=False + ) val = '["blue","away"]' - res = genericGetAssessResults(test_client, test_user_1, - event = 'fillb', - div_id = 'test_fitb_numeric', - answer = val, - act = val, - correct = 'F', - course = test_user_1.course.course_name - ) - assert res['answer'] == val - assert not res['correct'] + res = genericGetAssessResults( + test_client, + test_user_1, + event="fillb", + div_id="test_fitb_numeric", + answer=val, + act=val, + correct="F", + course=test_user_1.course.course_name, + ) + assert res["answer"] == val + assert not res["correct"] + def test_GetDragNDropResults(test_client, test_user_1): - val = '0;1;2' - res = genericGetAssessResults(test_client, test_user_1, - event = 'dragNdrop', - div_id = 'test_dnd_1', - answer = val, - act = val, - correct = 'T', - minHeight = '512', - course = test_user_1.course.course_name - ) - assert res['correct'] + val = "0;1;2" + res = genericGetAssessResults( + test_client, + test_user_1, + event="dragNdrop", + div_id="test_dnd_1", + answer=val, + act=val, + correct="T", + minHeight="512", + course=test_user_1.course.course_name, + ) + assert res["correct"] + def test_GetHist(test_client, test_user_1): test_user_1.login() kwargs = dict( - course = test_user_1.course.course_name, - sid = 'test_user_1', - div_id = 'test_activecode_1', - error_info = 'success', - event = 'acivecode', - to_save = 'true' - ) + course=test_user_1.course.course_name, + sid="test_user_1", + div_id="test_activecode_1", + error_info="success", + event="acivecode", + to_save="true", + ) for x in range(0, 10): - kwargs['code'] = 'test_code_{}'.format(x) - test_client.post('ajax/runlog', data = kwargs) + kwargs["code"] = "test_code_{}".format(x) + test_client.post("ajax/runlog", data=kwargs) - kwargs = dict( - acid = 'test_activecode_1', - sid = 'test_user_1' - ) - test_client.post('ajax/gethist', data = kwargs) + kwargs = dict(acid="test_activecode_1", sid="test_user_1") + test_client.post("ajax/gethist", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert len(res['timestamps']) == 0 - assert len(res['history']) == 0 + assert len(res["timestamps"]) == 0 + assert len(res["history"]) == 0 kwargs = dict( - acid = 'test_activecode_1', - #sid = 'test_user_1' - ) - test_client.post('ajax/gethist', data = kwargs) + acid="test_activecode_1", + # sid = 'test_user_1' + ) + test_client.post("ajax/gethist", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert len(res['timestamps']) == 10 - assert len(res['history']) == 10 + assert len(res["timestamps"]) == 10 + assert len(res["history"]) == 10 - time_delta = datetime.datetime.utcnow() - datetime.datetime.strptime(res['timestamps'][-1], '%Y-%m-%dT%H:%M:%S') + time_delta = datetime.datetime.utcnow() - datetime.datetime.strptime( + res["timestamps"][-1], "%Y-%m-%dT%H:%M:%S" + ) assert time_delta < datetime.timedelta(seconds=2) - test_client.post('ajax/getprog', data = kwargs) + test_client.post("ajax/getprog", data=kwargs) print(test_client.text) prog = json.loads(test_client.text) - assert res['history'][-1] == prog[0]['source'] + assert res["history"][-1] == prog[0]["source"] + def test_RunLog(test_client, test_user_1): @@ -425,85 +493,85 @@ def test_RunLog(test_client, test_user_1): test_user_1.login() kwargs = dict( - course = test_user_1.course.course_name, - sid = 'test_user_1', - div_id = 'test_activecode_1', - code = "this is a unittest", - error_info = 'success', - event = 'acivecode', - to_save = 'True' - ) - test_client.post('ajax/runlog', data = kwargs) - - kwargs = dict( - acid = 'test_activecode_1', - sid = 'test_user_1' - ) - test_client.post('ajax/getprog', data = kwargs) + course=test_user_1.course.course_name, + sid="test_user_1", + div_id="test_activecode_1", + code="this is a unittest", + error_info="success", + event="acivecode", + to_save="True", + ) + test_client.post("ajax/runlog", data=kwargs) + + kwargs = dict(acid="test_activecode_1", sid="test_user_1") + test_client.post("ajax/getprog", data=kwargs) print(test_client.text) prog = json.loads(test_client.text) - assert prog[0]['source'] == "this is a unittest" + assert prog[0]["source"] == "this is a unittest" + def test_GetLastPage(test_client, test_user_1): test_user_1.login() kwargs = dict( - course = test_user_1.course.course_name, - lastPageUrl = 'test_chapter_1/subchapter_a.html', - lastPageScrollLocation = 100, - completionFlag = '1' - ) + course=test_user_1.course.course_name, + lastPageUrl="test_chapter_1/subchapter_a.html", + lastPageScrollLocation=100, + completionFlag="1", + ) # Call ``getlastpage`` first to insert a new record. - test_client.post('ajax/getlastpage', data=kwargs) + test_client.post("ajax/getlastpage", data=kwargs) # Then, we can update it with the required info. - test_client.post('ajax/updatelastpage', data=kwargs) + test_client.post("ajax/updatelastpage", data=kwargs) # Now, test a query. - res = ajaxCall(test_client, 'getlastpage', **kwargs) - assert res[0]['lastPageUrl'] == 'test_chapter_1/subchapter_a.html' - assert res[0]['lastPageChapter'] == 'Test chapter 1' + res = ajaxCall(test_client, "getlastpage", **kwargs) + assert res[0]["lastPageUrl"] == "test_chapter_1/subchapter_a.html" + assert res[0]["lastPageChapter"] == "Test chapter 1" + def test_GetNumOnline(test_client, test_user_1, test_user): test_GetTop10Answers(test_client, test_user_1, test_user) - test_client.post('ajax/getnumonline') + test_client.post("ajax/getnumonline") print(test_client.text) res = json.loads(test_client.text) - assert res[0]['online'] == 6 + assert res[0]["online"] == 6 + def test_GetTop10Answers(test_client, test_user_1, test_user): user_ids = [] for index in range(0, 6): - user = test_user('test_user_{}'.format(index+2), 'password', test_user_1.course) + user = test_user( + "test_user_{}".format(index + 2), "password", test_user_1.course + ) user_ids.append(user) user.login() kwargs = dict( - event = 'fillb', - course = user.course.course_name, - div_id = 'test_fitb_1' - ) + event="fillb", course=user.course.course_name, div_id="test_fitb_1" + ) if index % 2 == 1: - kwargs['answer'] = '42' - kwargs['correct'] = 'T' + kwargs["answer"] = "42" + kwargs["correct"] = "T" else: - kwargs['answer'] = '41' - kwargs['correct'] = 'F' - kwargs['act'] = kwargs['answer'] + kwargs["answer"] = "41" + kwargs["correct"] = "F" + kwargs["act"] = kwargs["answer"] - test_client.post('ajax/hsblog', data=kwargs) + test_client.post("ajax/hsblog", data=kwargs) user.logout() user_ids[0].login() - test_client.post('ajax/gettop10Answers', data=kwargs) + test_client.post("ajax/gettop10Answers", data=kwargs) print(test_client.text) res, misc = json.loads(test_client.text) - assert res[0]['answer'] == '41' - assert res[0]['count'] == 3 - assert res[1]['answer'] == '42' - assert res[1]['count'] == 3 - assert misc['yourpct'] == 0 + assert res[0]["answer"] == "41" + assert res[0]["count"] == 3 + assert res[1]["answer"] == "42" + assert res[1]["count"] == 3 + assert misc["yourpct"] == 0 # @unittest.skipIf(not is_linux, 'preview_question only runs under Linux.') FIXME @@ -518,78 +586,80 @@ def testPreviewQuestion(test_client, test_user_1): """ test_user_1.login() - kwargs = dict( - code = json.dumps(src) - ) - test_client.post('ajax/preview_question', data = kwargs) + kwargs = dict(code=json.dumps(src)) + test_client.post("ajax/preview_question", data=kwargs) print(test_client.text) res = json.loads(test_client.text) assert 'id="preview_test1"' in res assert 'print("Hello World")' in res - assert 'textarea>' in res + assert "textarea>" in res assert 'textarea data-component="activecode"' in res assert 'div data-childcomponent="preview_test1"' in res + def test_GetUserLoggedIn(test_client, test_user_1): test_user_1.login() - test_client.post('ajax/getuser') + test_client.post("ajax/getuser") print(test_client.text) res = json.loads(test_client.text) - assert res[0]['nick'] == test_user_1.username + assert res[0]["nick"] == test_user_1.username def test_GetUserNotLoggedIn(test_client, test_user_1): - test_user_1.logout() # make sure user is logged off... - test_client.post('ajax/getuser') + test_user_1.logout() # make sure user is logged off... + test_client.post("ajax/getuser") print(test_client.text) res = json.loads(test_client.text)[0] - assert 'redirect' in res + assert "redirect" in res + def test_Donations(test_client, test_user_1): test_user_1.login() - res = ajaxCall(test_client, 'save_donate') + res = ajaxCall(test_client, "save_donate") assert res == None - res = ajaxCall(test_client, 'did_donate') - assert res['donate'] == True + res = ajaxCall(test_client, "did_donate") + assert res["donate"] == True + def test_NonDonor(test_client, test_user_1): test_user_1.login() - res = ajaxCall(test_client, 'did_donate') - assert not res['donate'] + res = ajaxCall(test_client, "did_donate") + assert not res["donate"] + def test_GetAgregateResults(test_client, test_user_1, test_user): # creat a bunch of users and have each one answer a multiple choice questions according to this table - table = [ # sid correct answer - ('user_1662', 'F', '0'), - ('user_1662', 'T', '1'), - ('user_1663', 'T', '1'), - ('user_1665', 'T', '1'), - ('user_1667', 'F', '0'), - ('user_1667', 'T', '1'), - ('user_1668', 'F', '0'), - ('user_1668', 'T', '1'), - ('user_1669', 'T', '1'), - ('user_1670', 'T', '1'), - ('user_1671', 'T', '1'), - ('user_1672', 'F', '0'), - ('user_1672', 'T', '1'), - ('user_1673', 'F', '0'), - ('user_1673', 'T', '1'), - ('user_1674', 'T', '1'), - ('user_1675', 'F', '0'), - ('user_1675', 'T', '1'), - ('user_1675', 'T', '1'), - ('user_1676', 'T', '1'), - ('user_1677', 'T', '1'), - ('user_1751', 'F', '0'), - ('user_1751', 'T', '1'), - ('user_2521', 'T', '1') - ] + table = [ # sid correct answer + ("user_1662", "F", "0"), + ("user_1662", "T", "1"), + ("user_1663", "T", "1"), + ("user_1665", "T", "1"), + ("user_1667", "F", "0"), + ("user_1667", "T", "1"), + ("user_1668", "F", "0"), + ("user_1668", "T", "1"), + ("user_1669", "T", "1"), + ("user_1670", "T", "1"), + ("user_1671", "T", "1"), + ("user_1672", "F", "0"), + ("user_1672", "T", "1"), + ("user_1673", "F", "0"), + ("user_1673", "T", "1"), + ("user_1674", "T", "1"), + ("user_1675", "F", "0"), + ("user_1675", "T", "1"), + ("user_1675", "T", "1"), + ("user_1676", "T", "1"), + ("user_1677", "T", "1"), + ("user_1751", "F", "0"), + ("user_1751", "T", "1"), + ("user_2521", "T", "1"), + ] users = {} for t in table: # create the user if user has not been created yet @@ -598,121 +668,131 @@ def test_GetAgregateResults(test_client, test_user_1, test_user): answer = t[2] logAnswer = "answer:" + answer + ":" + ("correct" if (correct == "T") else "no") if user_name not in users.keys(): - user = test_user(user_name, 'password', test_user_1.course) + user = test_user(user_name, "password", test_user_1.course) users[user_name] = user # logon user = users[user_name] user.login() # enter mchoice answer - user.hsblog(event='mChoice', div_id="test_mchoice_1", course=user.course.course_name, - correct = correct, act = logAnswer, answer = answer) + user.hsblog( + event="mChoice", + div_id="test_mchoice_1", + course=user.course.course_name, + correct=correct, + act=logAnswer, + answer=answer, + ) # logout user.logout() # get a particular user - user = users['user_1675'] + user = users["user_1675"] user.login() - kwargs = dict( - course = user.course.course_name, - div_id = 'test_mchoice_1' - ) + kwargs = dict(course=user.course.course_name, div_id="test_mchoice_1") - test_client.validate('ajax/getaggregateresults', data = kwargs) + test_client.validate("ajax/getaggregateresults", data=kwargs) print(test_client.text) res = json.loads(test_client.text) res = res[0] - assert res['misc']['yourpct'] == 67 - assert res['answerDict']['0'] == 29 - assert res['answerDict']['1'] == 71 + assert res["misc"]["yourpct"] == 67 + assert res["answerDict"]["0"] == 29 + assert res["answerDict"]["1"] == 71 user.logout() # Now test for the instructor: user.make_instructor() user.login() - test_client.validate('ajax/getaggregateresults', data = kwargs) + test_client.validate("ajax/getaggregateresults", data=kwargs) print(test_client.text) res = json.loads(test_client.text) res = res[0] expect = { - 'user_1662': [u'0', u'1'], - 'user_1663': [u'1'], - 'user_1665': [u'1'], - 'user_1667': [u'0', u'1'], - 'user_1668': [u'0', u'1'], - 'user_1669': [u'1'], - 'user_1670': [u'1'], - 'user_1671': [u'1'], - 'user_1672': [u'0', u'1'], - 'user_1673': [u'0', u'1'], - 'user_1674': [u'1'], - 'user_1675': [u'0', u'1', u'1'], - 'user_1676': [u'1'], - 'user_1677': [u'1'], - 'user_1751': [u'0', u'1'], - 'user_2521': [u'1'] , + "user_1662": [u"0", u"1"], + "user_1663": [u"1"], + "user_1665": [u"1"], + "user_1667": [u"0", u"1"], + "user_1668": [u"0", u"1"], + "user_1669": [u"1"], + "user_1670": [u"1"], + "user_1671": [u"1"], + "user_1672": [u"0", u"1"], + "user_1673": [u"0", u"1"], + "user_1674": [u"1"], + "user_1675": [u"0", u"1", u"1"], + "user_1676": [u"1"], + "user_1677": [u"1"], + "user_1751": [u"0", u"1"], + "user_2521": [u"1"], } - for student in res['reslist']: + for student in res["reslist"]: assert student[1] == expect[student[0]] + def test_GetCompletionStatus(test_client, test_user_1, runestone_db_tools): test_user_1.login() # Check an unviewed page kwargs = dict( - lastPageUrl = 'https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html' - ) + lastPageUrl="https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html" + ) - test_client.validate('ajax/getCompletionStatus', data = kwargs) + test_client.validate("ajax/getCompletionStatus", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res[0]['completionStatus'] == -1 + assert res[0]["completionStatus"] == -1 # check that the unviewed page gets added into the database with a start_date of today db = runestone_db_tools.db - row = db((db.user_sub_chapter_progress.chapter_id == 'test_chapter_1') & (db.user_sub_chapter_progress.sub_chapter_id == 'subchapter_a')).select().first() - print (row) + row = ( + db( + (db.user_sub_chapter_progress.chapter_id == "test_chapter_1") + & (db.user_sub_chapter_progress.sub_chapter_id == "subchapter_a") + ) + .select() + .first() + ) + print(row) assert row is not None assert row.end_date is None today = datetime.datetime.utcnow() - assert row.start_date.month == today.month - assert row.start_date.day == today.day - assert row.start_date.year == today.year + assert row.start_date.month == today.month + assert row.start_date.day == today.day + assert row.start_date.year == today.year # Check a viewed page w/ completion status 0 # 'View the page' kwargs = dict( - lastPageUrl = 'https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html', - lastPageScrollLocation = 0, - course = test_user_1.course.course_name, - completionFlag = 0 - ) - test_client.validate('ajax/updatelastpage', data = kwargs) + lastPageUrl="https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html", + lastPageScrollLocation=0, + course=test_user_1.course.course_name, + completionFlag=0, + ) + test_client.validate("ajax/updatelastpage", data=kwargs) # Check it - test_client.validate('ajax/getCompletionStatus', data = kwargs) + test_client.validate("ajax/getCompletionStatus", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res[0]['completionStatus'] == 0 + assert res[0]["completionStatus"] == 0 # Check a viewed page w/ completion status 1 # 'View the page and check the completion button' kwargs = dict( - lastPageUrl = 'https://runestone.academy/runestone/static/test_course_1/test_chapter_1/subchapter_a.html', - lastPageScrollLocation = 0, - course = test_user_1.course.course_name, - completionFlag = 1 - ) - test_client.validate('ajax/updatelastpage', data = kwargs) + lastPageUrl="https://runestone.academy/runestone/static/test_course_1/test_chapter_1/subchapter_a.html", + lastPageScrollLocation=0, + course=test_user_1.course.course_name, + completionFlag=1, + ) + test_client.validate("ajax/updatelastpage", data=kwargs) # Check it - test_client.validate('ajax/getCompletionStatus', data = kwargs) + test_client.validate("ajax/getCompletionStatus", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res[0]['completionStatus'] == 1 - + assert res[0]["completionStatus"] == 1 # Test getAllCompletionStatus() - test_client.validate('ajax/getAllCompletionStatus') + test_client.validate("ajax/getAllCompletionStatus") res = json.loads(test_client.text) print(res) assert len(res) == 2 @@ -721,22 +801,28 @@ def test_GetCompletionStatus(test_client, test_user_1, runestone_db_tools): def test_updatelastpage(test_client, test_user_1, runestone_db_tools): # Check an unviewed page kwargs = dict( - lastPageUrl = 'https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html' - ) + lastPageUrl="https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html" + ) - test_client.validate('ajax/getCompletionStatus', data = kwargs) + test_client.validate("ajax/getCompletionStatus", data=kwargs) test_user_1.login() kwargs = dict( - lastPageUrl = 'https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html', - lastPageScrollLocation = 0, - course = test_user_1.course.course_name, - completionFlag = 1 - ) - test_client.validate('ajax/updatelastpage', data = kwargs) + lastPageUrl="https://runestone.academy/runestone/books/published/test_course_1/test_chapter_1/subchapter_a.html", + lastPageScrollLocation=0, + course=test_user_1.course.course_name, + completionFlag=1, + ) + test_client.validate("ajax/updatelastpage", data=kwargs) db = runestone_db_tools.db - res = db((db.user_sub_chapter_progress.user_id == test_user_1.user_id) & - (db.user_sub_chapter_progress.sub_chapter_id == 'subchapter_a')).select().first() + res = ( + db( + (db.user_sub_chapter_progress.user_id == test_user_1.user_id) + & (db.user_sub_chapter_progress.sub_chapter_id == "subchapter_a") + ) + .select() + .first() + ) print(res) now = datetime.datetime.utcnow() @@ -749,42 +835,39 @@ def test_updatelastpage(test_client, test_user_1, runestone_db_tools): def test_getassignmentgrade(test_assignment, test_user_1, test_user, test_client): # make a dummy student to do work - student1 = test_user('student1', 'password', test_user_1.course) + student1 = test_user("student1", "password", test_user_1.course) student1.logout() test_user_1.make_instructor() test_user_1.login() # make dummy assignment - my_ass = test_assignment('test_assignment', test_user_1.course) - my_ass.addq_to_assignment(question='subc_b_fitb',points=10) + my_ass = test_assignment("test_assignment", test_user_1.course) + my_ass.addq_to_assignment(question="subc_b_fitb", points=10) my_ass.save_assignment() # record a grade for that student on an assignment sid = student1.username - acid = 'subc_b_fitb' + acid = "subc_b_fitb" grade = 5 - comment = 'OK job' - res = test_client.validate('assignments/record_grade', - data=dict(sid=sid, - acid = acid, - grade = grade, - comment = comment)) + comment = "OK job" + res = test_client.validate( + "assignments/record_grade", + data=dict(sid=sid, acid=acid, grade=grade, comment=comment), + ) test_user_1.logout() # check unreleased assignment grade student1.login() - kwargs = dict( - div_id = acid - ) - test_client.validate('ajax/getassignmentgrade', data = kwargs) + kwargs = dict(div_id=acid) + test_client.validate("ajax/getassignmentgrade", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res[0]['grade'] == 'Not graded yet' - assert res[0]['comment'] == 'No Comments' - assert res[0]['avg'] == 'None' - assert res[0]['count'] == 'None' + assert res[0]["grade"] == "Not graded yet" + assert res[0]["comment"] == "No Comments" + assert res[0]["avg"] == "None" + assert res[0]["count"] == "None" student1.logout() # release grade @@ -794,43 +877,39 @@ def test_getassignmentgrade(test_assignment, test_user_1, test_user, test_client # check grade again student1.login() - kwargs = dict( - div_id = acid - ) - test_client.validate('ajax/getassignmentgrade', data = kwargs) + kwargs = dict(div_id=acid) + test_client.validate("ajax/getassignmentgrade", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res[0]['grade'] == 5 - assert res[0]['version'] == 2 - assert res[0]['max'] == 10 - assert res[0]['comment'] == comment + assert res[0]["grade"] == 5 + assert res[0]["version"] == 2 + assert res[0]["max"] == 10 + assert res[0]["comment"] == comment def test_get_datafile(test_client, test_user_1, runestone_db_tools): # Create some datafile into the db and then read it out using the ajax/get_datafile() db = runestone_db_tools.db - db.source_code.insert(course_id=test_user_1.course.course_name, - acid='mystery.txt', - main_code = 'hello world') + db.source_code.insert( + course_id=test_user_1.course.course_name, + acid="mystery.txt", + main_code="hello world", + ) test_user_1.make_instructor() test_user_1.login() - kwargs = dict( - course_id = test_user_1.course.course_name, - acid = 'mystery.txt' - ) - test_client.validate('ajax/get_datafile', data = kwargs) + kwargs = dict(course_id=test_user_1.course.course_name, acid="mystery.txt") + test_client.validate("ajax/get_datafile", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res['data'] == 'hello world' + assert res["data"] == "hello world" # non-existant datafile kwargs = dict( - course_id = test_user_1.course.course_name, - acid = 'thisWillNotBeThere.txt' - ) - test_client.validate('ajax/get_datafile', data = kwargs) + course_id=test_user_1.course.course_name, acid="thisWillNotBeThere.txt" + ) + test_client.validate("ajax/get_datafile", data=kwargs) print(test_client.text) res = json.loads(test_client.text) - assert res['data'] is None + assert res["data"] is None diff --git a/tests/test_autograder.py b/tests/test_autograder.py index e093bae2c..8e577c6ad 100644 --- a/tests/test_autograder.py +++ b/tests/test_autograder.py @@ -11,56 +11,152 @@ # 4. correct_scores # # Test fillb, mChoice, dragNdrop, clickableArea, parsons, unittest -WHICH_ANSWER = ['first_answer', 'last_answer', 'best_answer'] -HOW_TO_SCORE = ['all_or_nothing', 'pct_correct', 'interact'] -@pytest.mark.parametrize('div_id, event, good_answer, bad_answer, correct_scores, which_tg, grade_type', -[ ('test_fitb_1','fillb',['red','away'], ['blue','green'], [0, 0, 10], WHICH_ANSWER, HOW_TO_SCORE), - ('subc_b_1', 'mChoice', 'answer:1:correct', 'answer:0:no', [0, 0, 10], WHICH_ANSWER, HOW_TO_SCORE), - ('subc_b_dd', 'dragNdrop', '1;2;3', '3;2;1', [0,0,10], WHICH_ANSWER, HOW_TO_SCORE), - ('click1', 'clickableArea', '1;2;3', '0', [0,0, 10], WHICH_ANSWER, HOW_TO_SCORE), - ('parsons_ag1', 'parsons', 'correct|-|1_1-2_1-3_3', 'incorrect|-|1_2-2_3-3_1', [0, 0, 10], WHICH_ANSWER, HOW_TO_SCORE), - ('units1', 'unittest', 'percent:100:passed:2:failed:0', 'percent:0:passed:0:failed:2', [0, 0, 10], WHICH_ANSWER, HOW_TO_SCORE), - ('units1', 'unittest', 'percent:50:passed:1:failed:1', 'percent:0:passed:0:failed:2', [5,0], ['best_answer', 'last_answer'], ['pct_correct']), - ('LearningZone_poll', 'poll', '1', '2', [10], ['first_answer'], ['interact']), - ('yt_vid_ex1', 'video', 'play', 'play', [10], ['last_answer'], ['interact']), - ('showEval_0', 'showeval', 'next', 'next', [10], ['last_answer'], ['interact']), - ('shorta1', 'shortanswer', 'Hello world', 'Just as good', [0, 10], ['last_answer'], ['manual', 'interact']) -]) -def test_grade_one_student(div_id, event, good_answer, bad_answer, correct_scores, which_tg, grade_type,\ - test_assignment, test_user_1, test_user, runestone_db_tools, test_client): +WHICH_ANSWER = ["first_answer", "last_answer", "best_answer"] +HOW_TO_SCORE = ["all_or_nothing", "pct_correct", "interact"] + + +@pytest.mark.parametrize( + "div_id, event, good_answer, bad_answer, correct_scores, which_tg, grade_type", + [ + ( + "test_fitb_1", + "fillb", + ["red", "away"], + ["blue", "green"], + [0, 0, 10], + WHICH_ANSWER, + HOW_TO_SCORE, + ), + ( + "subc_b_1", + "mChoice", + "answer:1:correct", + "answer:0:no", + [0, 0, 10], + WHICH_ANSWER, + HOW_TO_SCORE, + ), + ( + "subc_b_dd", + "dragNdrop", + "1;2;3", + "3;2;1", + [0, 0, 10], + WHICH_ANSWER, + HOW_TO_SCORE, + ), + ( + "click1", + "clickableArea", + "1;2;3", + "0", + [0, 0, 10], + WHICH_ANSWER, + HOW_TO_SCORE, + ), + ( + "parsons_ag1", + "parsons", + "correct|-|1_1-2_1-3_3", + "incorrect|-|1_2-2_3-3_1", + [0, 0, 10], + WHICH_ANSWER, + HOW_TO_SCORE, + ), + ( + "units1", + "unittest", + "percent:100:passed:2:failed:0", + "percent:0:passed:0:failed:2", + [0, 0, 10], + WHICH_ANSWER, + HOW_TO_SCORE, + ), + ( + "units1", + "unittest", + "percent:50:passed:1:failed:1", + "percent:0:passed:0:failed:2", + [5, 0], + ["best_answer", "last_answer"], + ["pct_correct"], + ), + ("LearningZone_poll", "poll", "1", "2", [10], ["first_answer"], ["interact"]), + ("yt_vid_ex1", "video", "play", "play", [10], ["last_answer"], ["interact"]), + ("showEval_0", "showeval", "next", "next", [10], ["last_answer"], ["interact"]), + ( + "shorta1", + "shortanswer", + "Hello world", + "Just as good", + [0, 10], + ["last_answer"], + ["manual", "interact"], + ), + ], +) +def test_grade_one_student( + div_id, + event, + good_answer, + bad_answer, + correct_scores, + which_tg, + grade_type, + test_assignment, + test_user_1, + test_user, + runestone_db_tools, + test_client, +): test_user_1.make_instructor() test_user_1.login() # Should test all combinations of # which_to_grade = first_answer, last_answer, best_answer # autograde = all_or_nothing, manual, pct_correct, interact - my_ass = test_assignment('test_assignment', test_user_1.course) - my_ass.addq_to_assignment(question=div_id, points=10, - which_to_grade='best_answer', - autograde='all_or_nothing') + my_ass = test_assignment("test_assignment", test_user_1.course) + my_ass.addq_to_assignment( + question=div_id, + points=10, + which_to_grade="best_answer", + autograde="all_or_nothing", + ) find_id = dict([reversed(i) for i in my_ass.questions()]) - student1 = test_user('student1', 'password', test_user_1.course) + student1 = test_user("student1", "password", test_user_1.course) student1.login() # unittest does not json encode its results - if event != 'unittest': + if event != "unittest": good_answer = json.dumps(good_answer) bad_answer = json.dumps(bad_answer) - student1.hsblog(event=event, act=bad_answer, correct='F', + student1.hsblog( + event=event, + act=bad_answer, + correct="F", answer=bad_answer, div_id=div_id, - course=student1.course.course_name) + course=student1.course.course_name, + ) time.sleep(1) - student1.hsblog(event=event, act=good_answer, correct='T', + student1.hsblog( + event=event, + act=good_answer, + correct="T", answer=good_answer, div_id=div_id, - course=student1.course.course_name) + course=student1.course.course_name, + ) time.sleep(1) - student1.hsblog(event=event, act=bad_answer, correct='F', + student1.hsblog( + event=event, + act=bad_answer, + correct="F", answer=bad_answer, div_id=div_id, - course=student1.course.course_name) + course=student1.course.course_name, + ) student1.logout() test_user_1.login() @@ -69,71 +165,90 @@ def test_grade_one_student(div_id, event, good_answer, bad_answer, correct_score for ix, grun in enumerate(which_tg): for gt in grade_type: - up = db((db.assignment_questions.assignment_id == my_ass.assignment_id) & - (db.assignment_questions.question_id == qid)).update(which_to_grade=grun, autograde=gt) + up = db( + (db.assignment_questions.assignment_id == my_ass.assignment_id) + & (db.assignment_questions.question_id == qid) + ).update(which_to_grade=grun, autograde=gt) db.commit() assert up == 1 - mess = my_ass.autograde(sid='student1') + mess = my_ass.autograde(sid="student1") print(mess) my_ass.calculate_totals() - res = db( (db.question_grades.sid == student1.username) & - (db.question_grades.div_id == div_id) & - (db.question_grades.course_name == student1.course.course_name) - ).select().first() - - if gt == 'manual': + res = ( + db( + (db.question_grades.sid == student1.username) + & (db.question_grades.div_id == div_id) + & (db.question_grades.course_name == student1.course.course_name) + ) + .select() + .first() + ) + + if gt == "manual": assert not res else: assert res - if gt != 'interact': - assert res['score'] == correct_scores[ix] + if gt != "interact": + assert res["score"] == correct_scores[ix] else: - assert res['score'] == 10 + assert res["score"] == 10 - totres = db( (db.grades.assignment == my_ass.assignment_id) & - (db.grades.auth_user == student1.user_id) - ).select().first() + totres = ( + db( + (db.grades.assignment == my_ass.assignment_id) + & (db.grades.auth_user == student1.user_id) + ) + .select() + .first() + ) assert totres - if gt != 'interact': - assert totres['score'] == correct_scores[ix] + if gt != "interact": + assert totres["score"] == correct_scores[ix] else: - assert totres['score'] == 10 + assert totres["score"] == 10 + + +SCA = "/srv/web2py/applications/runestone/books/test_course_1/published/test_course_1/test_chapter_1/subchapter_a.html" +SCB = "/srv/web2py/applications/runestone/books/test_course_1/published/test_course_1/test_chapter_1/subchapter_b.html" -SCA = '/srv/web2py/applications/runestone/books/test_course_1/published/test_course_1/test_chapter_1/subchapter_a.html' -SCB = '/srv/web2py/applications/runestone/books/test_course_1/published/test_course_1/test_chapter_1/subchapter_b.html' -def test_reading(test_assignment, test_user_1, test_user, runestone_db_tools, test_client): +def test_reading( + test_assignment, test_user_1, test_user, runestone_db_tools, test_client +): test_user_1.make_instructor() test_user_1.login() - my_ass = test_assignment('reading_test', test_user_1.course) - my_ass.addq_to_assignment(question='Test chapter 1/Subchapter A', points=10, - which_to_grade='best_answer', - autograde='interact', + my_ass = test_assignment("reading_test", test_user_1.course) + my_ass.addq_to_assignment( + question="Test chapter 1/Subchapter A", + points=10, + which_to_grade="best_answer", + autograde="interact", reading_assignment=True, - activities_required=1 - ) - my_ass.addq_to_assignment(question='Test chapter 1/Subchapter B', points=10, - which_to_grade='best_answer', - autograde='interact', + activities_required=1, + ) + my_ass.addq_to_assignment( + question="Test chapter 1/Subchapter B", + points=10, + which_to_grade="best_answer", + autograde="interact", reading_assignment=True, - activities_required=8 - ) + activities_required=8, + ) test_user_1.logout() # Now lets do some page views - student1 = test_user('student1', 'password', test_user_1.course) + student1 = test_user("student1", "password", test_user_1.course) student1.login() - student1.hsblog(event='page', act='view', - div_id=SCA, - course=test_user_1.course.course_name) - student1.hsblog(event='page', act='view', - div_id=SCB, - course=test_user_1.course.course_name) - + student1.hsblog( + event="page", act="view", div_id=SCA, course=test_user_1.course.course_name + ) + student1.hsblog( + event="page", act="view", div_id=SCB, course=test_user_1.course.course_name + ) student1.logout() test_user_1.login() @@ -141,22 +256,27 @@ def test_reading(test_assignment, test_user_1, test_user, runestone_db_tools, te mess = my_ass.autograde() print(mess) my_ass.calculate_totals() - totres = db( (db.grades.assignment == my_ass.assignment_id) & - (db.grades.auth_user == student1.user_id) - ).select().first() + totres = ( + db( + (db.grades.assignment == my_ass.assignment_id) + & (db.grades.auth_user == student1.user_id) + ) + .select() + .first() + ) assert totres - assert totres['score'] == 10 + assert totres["score"] == 10 # todo: expand this to include all question types and make all of the required act_list = [ - dict(event='fillb', act=json.dumps(['Mary']), div_id='subc_b_fitb'), - dict(event='mChoice', act='answer:1:correct', div_id='subc_b_1'), - dict(event='dragNdrop', act='1;2;3', div_id='subc_b_dd'), - dict(event='parsons', act='correct|-|1_1-2_1-3_3', div_id='parsons_ag1'), - dict(event='video', act='play', div_id='yt_vid_ex1'), - dict(event='showeval', act='next', div_id='showEval_0'), - dict(event='clickableArea', act='1;2;3', div_id='click1'), + dict(event="fillb", act=json.dumps(["Mary"]), div_id="subc_b_fitb"), + dict(event="mChoice", act="answer:1:correct", div_id="subc_b_1"), + dict(event="dragNdrop", act="1;2;3", div_id="subc_b_dd"), + dict(event="parsons", act="correct|-|1_1-2_1-3_3", div_id="parsons_ag1"), + dict(event="video", act="play", div_id="yt_vid_ex1"), + dict(event="showeval", act="next", div_id="showEval_0"), + dict(event="clickableArea", act="1;2;3", div_id="click1"), ] test_user_1.logout() @@ -171,16 +291,21 @@ def test_reading(test_assignment, test_user_1, test_user, runestone_db_tools, te mess = my_ass.autograde() print(mess) my_ass.calculate_totals() - totres = db( (db.grades.assignment == my_ass.assignment_id) & - (db.grades.auth_user == student1.user_id) - ).select().first() + totres = ( + db( + (db.grades.assignment == my_ass.assignment_id) + & (db.grades.auth_user == student1.user_id) + ) + .select() + .first() + ) assert totres - assert totres['score'] == 20 + assert totres["score"] == 20 def test_record_grade(test_user_1, test_user, runestone_db_tools, test_client): - student1 = test_user('student1', 'password', test_user_1.course) + student1 = test_user("student1", "password", test_user_1.course) student1.logout() test_user_1.make_instructor() test_user_1.login() @@ -188,23 +313,24 @@ def test_record_grade(test_user_1, test_user, runestone_db_tools, test_client): # put this in a loop because we want to make sure update_or_insert is correct, so we are testing # the initial grade plus updates to the grade for g in [10, 9, 1]: - res = test_client.validate('assignments/record_grade', - data=dict(sid=student1.username, - acid='shorta1', - grade=g, - comment='very good test')) + res = test_client.validate( + "assignments/record_grade", + data=dict( + sid=student1.username, acid="shorta1", grade=g, comment="very good test" + ), + ) res = json.loads(res) - assert res['response'] == 'replaced' + assert res["response"] == "replaced" - res = test_client.validate('admin/getGradeComments', - data=dict(acid='shorta1', sid=student1.username)) + res = test_client.validate( + "admin/getGradeComments", data=dict(acid="shorta1", sid=student1.username) + ) res = json.loads(res) assert res - assert res['grade'] == g - assert res['comments'] == 'very good test' - + assert res["grade"] == g + assert res["comments"] == "very good test" def test_getproblem(test_user_1, test_user, runestone_db_tools, test_client): @@ -216,29 +342,33 @@ def test_getproblem(test_user_1, test_user, runestone_db_tools, test_client): code = """ print("Hello World!") """ - student1 = test_user('student1', 'password', test_user_1.course) + student1 = test_user("student1", "password", test_user_1.course) student1.login() - res = test_client.validate('ajax/runlog', - data={'div_id': 'units1', - 'code': code, - 'lang': 'python', - 'errinfo': 'success', - 'to_save': 'true', - 'prefix': "", - 'suffix': "", - 'course': student1.course.course_name}) + res = test_client.validate( + "ajax/runlog", + data={ + "div_id": "units1", + "code": code, + "lang": "python", + "errinfo": "success", + "to_save": "true", + "prefix": "", + "suffix": "", + "course": student1.course.course_name, + }, + ) assert res student1.logout() test_user_1.login() - res = test_client.validate('assignments/get_problem', - data=dict(sid=student1.username, - acid='units1')) + res = test_client.validate( + "assignments/get_problem", data=dict(sid=student1.username, acid="units1") + ) assert res res = json.loads(res) - assert res['acid'] == 'units1' - assert res['code'] == code + assert res["acid"] == "units1" + assert res["code"] == code # todo: add the question to an assignment and retest - test case where code is after the deadline @@ -247,83 +377,106 @@ def test_student_autograde(test_user_1, test_user, runestone_db_tools, test_assi test_user_1.make_instructor() test_user_1.logout() - student1 = test_user('student1', 'password', test_user_1.course) - - - my_a = test_assignment('assignment1', test_user_1.course, is_visible='True') - my_a.addq_to_assignment(question='shorta1', - points=2, - autograde='null', - which_to_grade='best_answer', - reading_assignment=False) + student1 = test_user("student1", "password", test_user_1.course) + + my_a = test_assignment("assignment1", test_user_1.course, is_visible="True") + my_a.addq_to_assignment( + question="shorta1", + points=2, + autograde="null", + which_to_grade="best_answer", + reading_assignment=False, + ) my_a.save_assignment() assignment_id = my_a.assignment_id # check if score is 0% for the student student1.login() - res = student1.test_client.validate('assignments/doAssignment'.format(assignment_id), - 'Score: 0 of 2 = 0.0%', - data=dict(assignment_id=assignment_id)) + res = student1.test_client.validate( + "assignments/doAssignment".format(assignment_id), + "Score: 0 of 2 = 0.0%", + data=dict(assignment_id=assignment_id), + ) student1.logout() test_user_1.login() # record grades for individual questions - res = test_user_1.test_client.validate('assignments/record_grade', - data=dict(sid=student1.username, - acid='shorta1', - grade=1, - comment='very good')) + res = test_user_1.test_client.validate( + "assignments/record_grade", + data=dict(sid=student1.username, acid="shorta1", grade=1, comment="very good"), + ) res = json.loads(res) - assert res['response'] == 'replaced' + assert res["response"] == "replaced" test_user_1.logout() student1.login() # try to have student self-grade - res = student1.test_client.validate('assignments/student_autograde', - data=dict(assignment_id=assignment_id)) + res = student1.test_client.validate( + "assignments/student_autograde", data=dict(assignment_id=assignment_id) + ) # but make sure that the grade has *not* been written into the db db = runestone_db_tools.db - grade = db((db.grades.auth_user == student1.user_id) - & (db.grades.assignment == assignment_id)).select().first() + grade = ( + db( + (db.grades.auth_user == student1.user_id) + & (db.grades.assignment == assignment_id) + ) + .select() + .first() + ) assert not grade res = json.loads(res) - assert res['success'] + assert res["success"] print(res) # check if score is now 50% - res = student1.test_client.validate('assignments/doAssignment'.format(assignment_id), - 'Score: 1.0 of 2 = 50.0%', - data=dict(assignment_id=assignment_id)) + res = student1.test_client.validate( + "assignments/doAssignment".format(assignment_id), + "Score: 1.0 of 2 = 50.0%", + data=dict(assignment_id=assignment_id), + ) # and that the grade has still *not* been written into the db - grade = db((db.grades.auth_user == student1.user_id) - & (db.grades.assignment == assignment_id)).select().first() + grade = ( + db( + (db.grades.auth_user == student1.user_id) + & (db.grades.assignment == assignment_id) + ) + .select() + .first() + ) assert not grade - # ******** change the settings and try again, # the total should be calculated and stored in db now *********** - with settings_context({'settings.coursera_mode': True}): + with settings_context({"settings.coursera_mode": True}): # try to have student self-grade - res = student1.test_client.validate('assignments/student_autograde', - data=dict(assignment_id=assignment_id)) + res = student1.test_client.validate( + "assignments/student_autograde", data=dict(assignment_id=assignment_id) + ) res = json.loads(res) - assert res['success'] + assert res["success"] # check if score is still 50% - res = student1.test_client.validate('assignments/doAssignment'.format(assignment_id), - 'Score: 1.0 of 2 = 50.0%', - data=dict(assignment_id=assignment_id)) + res = student1.test_client.validate( + "assignments/doAssignment".format(assignment_id), + "Score: 1.0 of 2 = 50.0%", + data=dict(assignment_id=assignment_id), + ) # and that the grade **has** been written into the db - grade = db((db.grades.auth_user == student1.user_id) - & (db.grades.assignment == assignment_id)).select().first() + grade = ( + db( + (db.grades.auth_user == student1.user_id) + & (db.grades.assignment == assignment_id) + ) + .select() + .first() + ) assert grade.score == 1.0 - - # other tests to implement.... # no assignment_id sent; # user not logged in; shouldn't do anything diff --git a/tests/test_course_1/_sources/lp_demo-test.py b/tests/test_course_1/_sources/lp_demo-test.py index 7e8a199e8..99f1957c8 100644 --- a/tests/test_course_1/_sources/lp_demo-test.py +++ b/tests/test_course_1/_sources/lp_demo-test.py @@ -2,4 +2,5 @@ # |docname| -- test code for :doc:`lp_demo.py` # ============================================ from lp_demo import one + assert one() == 1 diff --git a/tests/test_course_1/_sources/lp_demo.py b/tests/test_course_1/_sources/lp_demo.py index 9257c3d73..da749c655 100644 --- a/tests/test_course_1/_sources/lp_demo.py +++ b/tests/test_course_1/_sources/lp_demo.py @@ -6,6 +6,8 @@ # SOLUTION_BEGIN def one(): return 1 + + # SOLUTION_END # .. lp_build:: lp_demo_1 diff --git a/tests/test_course_1/conf.py b/tests/test_course_1/conf.py index eff14d503..67dc66519 100644 --- a/tests/test_course_1/conf.py +++ b/tests/test_course_1/conf.py @@ -16,7 +16,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('../modules')) +# sys.path.insert(0, os.path.abspath('../modules')) from runestone import runestone_static_dirs, runestone_extensions import pkg_resources @@ -24,72 +24,74 @@ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.mathjax'] + runestone_extensions() +extensions = ["sphinx.ext.mathjax"] + runestone_extensions() -#,'runestone.video','runestone.reveal','runestone.poll','runestone.tabbedStuff','runestone.disqus','runestone.codelens','runestone.activecode', 'runestone.assess', 'runestone.animation','runestone.meta', 'runestone.parsons', 'runestone.blockly', 'runestone.livecode','runestone.accessibility'] +# ,'runestone.video','runestone.reveal','runestone.poll','runestone.tabbedStuff','runestone.disqus','runestone.codelens','runestone.activecode', 'runestone.assess', 'runestone.animation','runestone.meta', 'runestone.parsons', 'runestone.blockly', 'runestone.livecode','runestone.accessibility'] # Add any paths that contain templates here, relative to this directory. -templates_path = [pkg_resources.resource_filename('runestone', 'common/project_template/_templates')] +templates_path = [ + pkg_resources.resource_filename("runestone", "common/project_template/_templates") +] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Runestone Interactive Overview' -copyright = '2017 bjones' +project = "Runestone Interactive Overview" +copyright = "2017 bjones" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.0.1' +version = "0.0.1" # The full version, including alpha/beta/rc tags. -release = '0.0' +release = "0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # `keep_warnings `_: # If true, keep warnings as “system message” paragraphs in the built documents. @@ -101,14 +103,13 @@ # A string of reStructuredText that will be included at the beginning of every # source file that is read. rst_prolog = ( -# For fill-in-the-blank questions, provide a convenient means to indicate a blank. -""" + # For fill-in-the-blank questions, provide a convenient means to indicate a blank. + """ .. |blank| replace:: :blank:`x` """ - -# For literate programming files, provide a convenient way to refer to a source file's name. See `runestone.lp.lp._docname_role`. -""".. |docname| replace:: :docname:`name` + # For literate programming files, provide a convenient way to refer to a source file's name. See `runestone.lp.lp._docname_role`. + """.. |docname| replace:: :docname:`name` """ ) @@ -125,23 +126,20 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_bootstrap' +html_theme = "sphinx_bootstrap" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {'nosidebar': 'true'} +# html_theme_options = {'nosidebar': 'true'} html_theme_options = { # Navigation bar title. (Default: ``project`` value) - 'navbar_title': "Test book", - + "navbar_title": "Test book", # Tab name for entire site. (Default: "Site") - 'navbar_site_name': "Chapters", - + "navbar_site_name": "Chapters", # Global TOC depth for "site" navbar tab. (Default: 1) # Switching to -1 shows all levels. - 'globaltoc_depth': 1, - + "globaltoc_depth": 1, # Include hidden TOCs in Site navbar? # # Note: If this is "false", you cannot have mixed ``:hidden:`` and @@ -149,20 +147,16 @@ # will break. # # Values: "true" (default) or "false" - 'globaltoc_includehidden': "true", - + "globaltoc_includehidden": "true", # HTML navbar class (Default: "navbar") to attach to
element. # For black navbar, do "navbar navbar-inverse" - 'navbar_class': "navbar", - + "navbar_class": "navbar", # Fix navigation bar to top of page? # Values: "true" (default) or "false" - 'navbar_fixed_top': "true", - + "navbar_fixed_top": "true", # Location of link to source. # Options are "nav" (default), "footer" or anything else to exclude. - 'source_link_position': "nav", - + "source_link_position": "nav", # Bootswatch (http://bootswatch.com/) theme. # # Options are nothing with "" (default) or the name of a valid theme @@ -172,96 +166,100 @@ #'bootswatch_theme': "slate", } -#html_style = "style.css" +# html_style = "style.css" # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [pkg_resources.resource_filename('runestone', 'common/project_template/_templates/plugin_layouts')] +html_theme_path = [ + pkg_resources.resource_filename( + "runestone", "common/project_template/_templates/plugin_layouts" + ) +] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = 'Runestone Test Book' +html_title = "Runestone Test Book" # A shorter title for the navigation bar. Default is the same as html_title. -html_short_title ='Runestone Interactive Overview' +html_short_title = "Runestone Interactive Overview" # The name of an image file (relative to this directory) to place at the top # of the sidebar. # logo is included in layout file -#html_logo = "../source/_static/logo_small.png" +# html_logo = "../source/_static/logo_small.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = runestone_static_dirs() +html_static_path = runestone_static_dirs() # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'PythonCoursewareProjectdoc' +htmlhelp_basename = "PythonCoursewareProjectdoc" # 'accessibility_style' config value is defined in the 'accessibility' extension. # By this config value you can select what accessibility stylesheet # you want to add ('normal', 'light', 'darkest' or 'none') -#accessibility_style = 'normal' +# accessibility_style = 'normal' # Config values for specific Runestone components # -#activecode_div_class = 'runestone explainer ac_section alert alert-warning' -#activecode_hide_load_history = False -#mchoice_div_class = 'runestone alert alert-warning' -#clickable_div_class = 'runestone alert alert-warning' -#codelens_div_class = 'alert alert-warning cd_section' -#dragndrop_div_class = 'runestone' -#fitb_div_class = 'runestone' -#parsons_div_class = 'runestone' -#poll_div_class = 'alert alert-warning' -#shortanswer_div_class = 'journal alert alert-warning' -#shortanswer_optional_div_class = 'journal alert alert-success' -#showeval_div_class = 'runestone explainer alert alert-warning' -#tabbed_div_class = 'alert alert-warning' +# activecode_div_class = 'runestone explainer ac_section alert alert-warning' +# activecode_hide_load_history = False +# mchoice_div_class = 'runestone alert alert-warning' +# clickable_div_class = 'runestone alert alert-warning' +# codelens_div_class = 'alert alert-warning cd_section' +# dragndrop_div_class = 'runestone' +# fitb_div_class = 'runestone' +# parsons_div_class = 'runestone' +# poll_div_class = 'alert alert-warning' +# shortanswer_div_class = 'journal alert alert-warning' +# shortanswer_optional_div_class = 'journal alert alert-success' +# showeval_div_class = 'runestone explainer alert alert-warning' +# tabbed_div_class = 'alert alert-warning' diff --git a/tests/test_course_1/pavement.py b/tests/test_course_1/pavement.py index a9389140d..9f7b29e1b 100644 --- a/tests/test_course_1/pavement.py +++ b/tests/test_course_1/pavement.py @@ -1,6 +1,7 @@ import paver from paver.easy import * import paver.setuputils + paver.setuputils.install_distutils_tasks() import os, sys from runestone.server import get_dburl @@ -13,53 +14,53 @@ # The project name, for use below. project_name = os.path.basename(os.path.dirname(os.path.abspath(__file__))) # True if this project uses Runestone services. -use_services = 'true' +use_services = "true" # The root directory for ``runestone serve``. serving_dir = "./build/" + project_name # The destination directory for ``runestone deploy``. dest = "./published" options( - sphinx = Bunch(docroot=".",), - - build = Bunch( + sphinx=Bunch(docroot="."), + build=Bunch( builddir=serving_dir, sourcedir="_sources", outdir=serving_dir, confdir=".", - template_args={'login_required':'true', - 'loglevel': 10, - 'course_title': project_name, - 'python3': 'false', - 'dburl': 'postgresql://user:password@localhost/runestone', - 'default_ac_lang': 'python', - 'jobe_server': 'http://jobe2.cosc.canterbury.ac.nz', - 'proxy_uri_runs': '/jobe/index.php/restapi/runs/', - 'proxy_uri_files': '/jobe/index.php/restapi/files/', - 'downloads_enabled': 'false', - 'enable_chatcodes': 'False', - 'allow_pairs': 'False', - 'dynamic_pages': 'True', - 'use_services': use_services, - 'basecourse': project_name, - # If ``use_services`` is 'true', then the following values are ignored, since they're provided by the server. - 'course_id': project_name, - 'appname': 'runestone', - 'course_url': '', - } - ) + template_args={ + "login_required": "true", + "loglevel": 10, + "course_title": project_name, + "python3": "false", + "dburl": "postgresql://user:password@localhost/runestone", + "default_ac_lang": "python", + "jobe_server": "http://jobe2.cosc.canterbury.ac.nz", + "proxy_uri_runs": "/jobe/index.php/restapi/runs/", + "proxy_uri_files": "/jobe/index.php/restapi/files/", + "downloads_enabled": "false", + "enable_chatcodes": "False", + "allow_pairs": "False", + "dynamic_pages": "True", + "use_services": use_services, + "basecourse": project_name, + # If ``use_services`` is 'true', then the following values are ignored, since they're provided by the server. + "course_id": project_name, + "appname": "runestone", + "course_url": "", + }, + ), ) # if we are on runestone-deploy then use the proxy server not canterbury -if gethostname() == 'runestone-deploy': - del options.build.template_args['jobe_server'] - del options.build.template_args['proxy_uri_runs'] - del options.build.template_args['proxy_uri_files'] +if gethostname() == "runestone-deploy": + del options.build.template_args["jobe_server"] + del options.build.template_args["proxy_uri_runs"] + del options.build.template_args["proxy_uri_files"] version = pkg_resources.require("runestone")[0].version -options.build.template_args['runestone_version'] = version +options.build.template_args["runestone_version"] = version # If DBURL is in the environment override dburl -options.build.template_args['dburl'] = get_dburl(outer=locals()) +options.build.template_args["dburl"] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 1f360aa7c..da9468f34 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -9,59 +9,92 @@ # # def test_student_report(test_client, runestone_db_tools, test_user, test_user_1): - course_3 = runestone_db_tools.create_course('test_course_3') - test_instructor_1 = test_user('test_instructor_1', 'password_1', course_3) + course_3 = runestone_db_tools.create_course("test_course_3") + test_instructor_1 = test_user("test_instructor_1", "password_1", course_3) test_instructor_1.make_instructor() test_instructor_1.login() db = runestone_db_tools.db # Create an assignment -- using createAssignment - #test_client.post('dashboard/studentreport', + # test_client.post('dashboard/studentreport', # data=dict(id='test_user_1')) - test_client.validate('dashboard/studentreport','Recent Activity', data=dict(id='test_instructor_1')) + test_client.validate( + "dashboard/studentreport", "Recent Activity", data=dict(id="test_instructor_1") + ) - test_instructor_1.hsblog(event="mChoice", - act="answer:1:correct",answer="1",correct="T",div_id="subc_b_1", - course="test_course_3") + test_instructor_1.hsblog( + event="mChoice", + act="answer:1:correct", + answer="1", + correct="T", + div_id="subc_b_1", + course="test_course_3", + ) - test_client.validate('dashboard/studentreport','subc_b_1', data=dict(id='test_instructor_1')) + test_client.validate( + "dashboard/studentreport", "subc_b_1", data=dict(id="test_instructor_1") + ) def test_subchapteroverview(test_client, runestone_db_tools, test_user, test_user_1): - course_3 = runestone_db_tools.create_course('test_course_3', base_course='test_course_1') - test_instructor_1 = test_user('test_instructor_1', 'password_1', course_3) + course_3 = runestone_db_tools.create_course( + "test_course_3", base_course="test_course_1" + ) + test_instructor_1 = test_user("test_instructor_1", "password_1", course_3) test_instructor_1.make_instructor() test_instructor_1.login() db = runestone_db_tools.db - test_client.validate('dashboard/subchapoverview','chapter_num') - test_client.validate('dashboard/subchapoverview','div_id', data=dict(tablekind='dividnum')) + test_client.validate("dashboard/subchapoverview", "chapter_num") + test_client.validate( + "dashboard/subchapoverview", "div_id", data=dict(tablekind="dividnum") + ) + + test_instructor_1.hsblog( + event="mChoice", + act="answer:1:correct", + answer="1", + correct="T", + div_id="subc_b_1", + course="test_course_3", + ) + + test_client.validate( + "dashboard/subchapoverview", "subc_b_1", data=dict(tablekind="dividnum") + ) + test_client.validate( + "dashboard/subchapoverview", "div_id", data=dict(tablekind="dividmin") + ) + test_client.validate( + "dashboard/subchapoverview", "div_id", data=dict(tablekind="dividmax") + ) - test_instructor_1.hsblog(event="mChoice", - act="answer:1:correct",answer="1",correct="T",div_id="subc_b_1", - course="test_course_3") - - test_client.validate('dashboard/subchapoverview','subc_b_1', data=dict(tablekind='dividnum')) - test_client.validate('dashboard/subchapoverview','div_id', data=dict(tablekind='dividmin')) - test_client.validate('dashboard/subchapoverview','div_id', data=dict(tablekind='dividmax')) def test_exercisemetrics(test_client, runestone_db_tools, test_user, test_user_1): - course_3 = runestone_db_tools.create_course('test_course_3', base_course='test_course_1') - test_instructor_1 = test_user('test_instructor_1', 'password_1', course_3) + course_3 = runestone_db_tools.create_course( + "test_course_3", base_course="test_course_1" + ) + test_instructor_1 = test_user("test_instructor_1", "password_1", course_3) test_instructor_1.make_instructor() test_instructor_1.login() - test_instructor_1.hsblog(event='mChoice', act='answer:1:correct', correct='T', - answer='answer:1:correct', - div_id='subc_b_1', - course='test_course_3') - - res = test_instructor_1.test_client.validate('dashboard/exercisemetrics', 'Responses by Student', - data=dict(chapter='test_chapter_1', id='subc_b_1')) - + test_instructor_1.hsblog( + event="mChoice", + act="answer:1:correct", + correct="T", + answer="answer:1:correct", + div_id="subc_b_1", + course="test_course_3", + ) + + res = test_instructor_1.test_client.validate( + "dashboard/exercisemetrics", + "Responses by Student", + data=dict(chapter="test_chapter_1", id="subc_b_1"), + ) # TODO: diff --git a/tests/test_designer.py b/tests/test_designer.py index 99f771570..6415e8497 100644 --- a/tests/test_designer.py +++ b/tests/test_designer.py @@ -1,35 +1,40 @@ - def test_build(test_client, test_user_1, runestone_db_tools): test_user_1.make_instructor() test_user_1.login() - test_client.validate('designer/build', 'build_course_1', + test_client.validate( + "designer/build", + "build_course_1", data=dict( - coursetype=test_user_1.course.course_name, - institution='Runestone', - startdate='01/01/2019', - python3='T', - login_required='T', - instructor='T', - projectname='build_course_1', - projectdescription='Build a course' - )) + coursetype=test_user_1.course.course_name, + institution="Runestone", + startdate="01/01/2019", + python3="T", + login_required="T", + instructor="T", + projectname="build_course_1", + projectdescription="Build a course", + ), + ) db = runestone_db_tools.db - res = db(db.courses.course_name == 'build_course_1').select().first() - assert res.institution == 'Runestone' + res = db(db.courses.course_name == "build_course_1").select().first() + assert res.institution == "Runestone" assert res.base_course == test_user_1.course.course_name # Now delete it - test_client.validate('admin/deletecourse', 'About Runestone') - res = db(db.courses.course_name == 'build_course_1').select().first() + test_client.validate("admin/deletecourse", "About Runestone") + res = db(db.courses.course_name == "build_course_1").select().first() assert not res - test_client.validate('designer/build', 'build_course_2', + test_client.validate( + "designer/build", + "build_course_2", data=dict( - coursetype=test_user_1.course.course_name, - instructor='T', - startdate='', - projectname='build_course_2', - projectdescription='Build a course' - )) + coursetype=test_user_1.course.course_name, + instructor="T", + startdate="", + projectname="build_course_2", + projectdescription="Build a course", + ), + ) diff --git a/tests/test_server.py b/tests/test_server.py index 82a96029c..1c5a8c506 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -37,14 +37,17 @@ # Tests # ===== # Use for easy manual testing of the server, by setting up a user and class automatically. Comment out the line below to enable it. -@pytest.mark.skip(reason='Only needed for manual testing.') +@pytest.mark.skip(reason="Only needed for manual testing.") def test_manual(runestone_db_tools, test_user): # Modify this as desired to create courses, users, etc. for manual testing. course_1 = runestone_db_tools.create_course() - test_user('bob', 'bob', course_1) + test_user("bob", "bob", course_1) # Pause in the debugginer until manual testing is done. - import pdb; pdb.set_trace() + import pdb + + pdb.set_trace() + def test_killer(test_assignment, test_client, test_user_1, runestone_db_tools): """ @@ -57,188 +60,214 @@ def test_killer(test_assignment, test_client, test_user_1, runestone_db_tools): for testing purposes we don't want web2py to capture 500 errors. """ with pytest.raises(Exception) as excinfo: - test_client.post('admin/killer') + test_client.post("admin/killer") assert test_client.text == "" print(excinfo.value) assert "ticket" in str(excinfo.value) or "INTERNAL" in str(excinfo.value) + # Validate the HTML produced by various web2py pages. # NOTE -- this is the start of a really really long decorator for test_1 -@pytest.mark.parametrize('url, requires_login, expected_string, expected_errors', -[ - # **Admin** - #---------- - # FIXME: Flashed messages don't seem to work. - #('admin/index', False, 'You must be registered for a course to access this page', 1), - #('admin/index', True, 'You must be an instructor to access this page', 1), - ('admin/doc', True, 'Runestone Help and Documentation', 1), - - # **Assignments** - #---------------- - ('assignments/chooseAssignment', True, 'Assignments', 1), - ('assignments/doAssignment', True, 'Bad Assignment ID', 1), - ('assignments/index', True, 'Student Progress for', 1), - # TODO: Why 2 errors here? Was just 1. - ('assignments/practice', True, 'Practice tool is not set up for this course yet.', 2), - ('assignments/practiceNotStartedYet', True, 'test_course_1', 2), - - # **Default** - #------------ - # *User* - # - # The `authentication `_ section gives the URLs exposed by web2py. Check these. - ('default/user/login', False, 'Login', 1), - ('default/user/register', False, 'Registration', 1), - ('default/user/logout', True, 'Logged out', 1), - # One profile error is a result of removing the input field for the e-mail, but web2py still tries to label it, which is an error. - ('default/user/profile', True, 'Profile', 2), - ('default/user/change_password', True, 'Change password', 1), - # Runestone doesn't support this. - #'default/user/verify_email', False, 'Verify email', 1), - ('default/user/retrieve_username', False, 'Retrieve username', 1), - ('default/user/request_reset_password', False, 'Request reset password', 1), - # This doesn't display a webpage, but instead redirects to courses. - #('default/user/reset_password, False, 'Reset password', 1), - ('default/user/impersonate', True, 'Impersonate', 1), - # FIXME: This produces an exception. - #'default/user/groups', True, 'Groups', 1), - ('default/user/not_authorized', False, 'Not authorized', 1), - # Returns a 404. - #('default/user/navbar'=(False, 'xxx', 1), - - # *Other pages* - # - # TODO: What is this for? - #('default/call', False, 'Not found', 0), - # TODO: weird returned HTML. ??? - #('default/index', True, 'Course Selection', 1), - ('default/about', False, 'About Us', 1), - ('default/error', False, 'Error: the document does not exist', 1), - ('default/ack', False, 'Acknowledgements', 1), - # web2py generates invalid labels for the radio buttons in this form. - ('default/bio', True, 'Tell Us About Yourself', 3), - ('default/courses', True, 'Course Selection', 1), - ('default/remove', True, 'Remove a Course', 1), - # Should work in both cases. - ('default/reportabug', False, 'Report a Bug', 1), - ('default/reportabug', True, 'Report a Bug', 1), - # TODO: weird returned HTML. ??? - #('default/sendreport', True, 'Could not create issue', 1), - ('default/terms', False, 'Terms and Conditions', 1), - ('default/privacy', False, 'Runestone Academy Privacy Policy', 1), - ('default/donate', False, 'Support Runestone Interactive', 1), - # TODO: This soesn't really test the body of either of these - ('default/coursechooser', True, 'Course Selection', 1), - ('default/removecourse', True, 'Course Selection', 1), - - # **Dashboard** - #-------------- - ('dashboard/index', True, 'Instructor Dashboard', 1), - ('dashboard/grades', True, 'Gradebook', 1), - ('dashboard/studentreport', True, 'Please make sure you are in the correct course', 1), - # TODO: This doesn't really test anything about either - # exercisemetrics or questiongrades other than properly handling a call with no information - ('dashboard/exercisemetrics', True, 'Instructor Dashboard', 1), - ('dashboard/questiongrades', True, 'Instructor Dashboard', 1), - - # **Designer** - #------------- - ('designer/index', True, 'This page allows you to select a book for your own class.', 1), - - # **OAuth** - #---------- - ('oauth/index', False, 'This page is a utility for accepting redirects from external services like Spotify or LinkedIn that use oauth.', 1), - - ('books/index', False, 'Runestone Test Book', 1), - ('books/published', False, 'Runestone Test Book', 1), - - # TODO: Many other views! -]) -def test_validate_user_pages(url, requires_login, expected_string, - expected_errors, test_client, test_user_1): +@pytest.mark.parametrize( + "url, requires_login, expected_string, expected_errors", + [ + # **Admin** + # ---------- + # FIXME: Flashed messages don't seem to work. + # ('admin/index', False, 'You must be registered for a course to access this page', 1), + # ('admin/index', True, 'You must be an instructor to access this page', 1), + ("admin/doc", True, "Runestone Help and Documentation", 1), + # **Assignments** + # ---------------- + ("assignments/chooseAssignment", True, "Assignments", 1), + ("assignments/doAssignment", True, "Bad Assignment ID", 1), + ("assignments/index", True, "Student Progress for", 1), + # TODO: Why 2 errors here? Was just 1. + ( + "assignments/practice", + True, + "Practice tool is not set up for this course yet.", + 2, + ), + ("assignments/practiceNotStartedYet", True, "test_course_1", 2), + # **Default** + # ------------ + # *User* + # + # The `authentication `_ section gives the URLs exposed by web2py. Check these. + ("default/user/login", False, "Login", 1), + ("default/user/register", False, "Registration", 1), + ("default/user/logout", True, "Logged out", 1), + # One profile error is a result of removing the input field for the e-mail, but web2py still tries to label it, which is an error. + ("default/user/profile", True, "Profile", 2), + ("default/user/change_password", True, "Change password", 1), + # Runestone doesn't support this. + #'default/user/verify_email', False, 'Verify email', 1), + ("default/user/retrieve_username", False, "Retrieve username", 1), + ("default/user/request_reset_password", False, "Request reset password", 1), + # This doesn't display a webpage, but instead redirects to courses. + # ('default/user/reset_password, False, 'Reset password', 1), + ("default/user/impersonate", True, "Impersonate", 1), + # FIXME: This produces an exception. + #'default/user/groups', True, 'Groups', 1), + ("default/user/not_authorized", False, "Not authorized", 1), + # Returns a 404. + # ('default/user/navbar'=(False, 'xxx', 1), + # *Other pages* + # + # TODO: What is this for? + # ('default/call', False, 'Not found', 0), + # TODO: weird returned HTML. ??? + # ('default/index', True, 'Course Selection', 1), + ("default/about", False, "About Us", 1), + ("default/error", False, "Error: the document does not exist", 1), + ("default/ack", False, "Acknowledgements", 1), + # web2py generates invalid labels for the radio buttons in this form. + ("default/bio", True, "Tell Us About Yourself", 3), + ("default/courses", True, "Course Selection", 1), + ("default/remove", True, "Remove a Course", 1), + # Should work in both cases. + ("default/reportabug", False, "Report a Bug", 1), + ("default/reportabug", True, "Report a Bug", 1), + # TODO: weird returned HTML. ??? + # ('default/sendreport', True, 'Could not create issue', 1), + ("default/terms", False, "Terms and Conditions", 1), + ("default/privacy", False, "Runestone Academy Privacy Policy", 1), + ("default/donate", False, "Support Runestone Interactive", 1), + # TODO: This soesn't really test the body of either of these + ("default/coursechooser", True, "Course Selection", 1), + ("default/removecourse", True, "Course Selection", 1), + # **Dashboard** + # -------------- + ("dashboard/index", True, "Instructor Dashboard", 1), + ("dashboard/grades", True, "Gradebook", 1), + ( + "dashboard/studentreport", + True, + "Please make sure you are in the correct course", + 1, + ), + # TODO: This doesn't really test anything about either + # exercisemetrics or questiongrades other than properly handling a call with no information + ("dashboard/exercisemetrics", True, "Instructor Dashboard", 1), + ("dashboard/questiongrades", True, "Instructor Dashboard", 1), + # **Designer** + # ------------- + ( + "designer/index", + True, + "This page allows you to select a book for your own class.", + 1, + ), + # **OAuth** + # ---------- + ( + "oauth/index", + False, + "This page is a utility for accepting redirects from external services like Spotify or LinkedIn that use oauth.", + 1, + ), + ("books/index", False, "Runestone Test Book", 1), + ("books/published", False, "Runestone Test Book", 1), + # TODO: Many other views! + ], +) +def test_validate_user_pages( + url, requires_login, expected_string, expected_errors, test_client, test_user_1 +): if requires_login: test_user_1.login() else: test_client.logout() - test_client.validate(url, expected_string, - expected_errors) + test_client.validate(url, expected_string, expected_errors) # Validate the HTML in instructor-only pages. # NOTE -- this is the start of a really really long decorator for test_2 -@pytest.mark.parametrize('url, expected_string, expected_errors', -[ - # **Default** - #------------ - # web2py-generated stuff produces two extra errors. - ('default/bios', 'Bios', 3), - # FIXME: The element ``
`` in ``views/admin/admin.html`` produces the error ``Bad value \u201c\u201d for attribute \u201caction\u201d on element \u201cform\u201d: Must be non-empty.``. - # - # **Admin** - #---------- - ('admin/admin', 'Manage Section', 1), - ('admin/course_students', '"test_user_1"', 2), - ('admin/createAssignment', 'ERROR', None), - ('admin/grading', 'assignment', 1), - # TODO: This produces an exception. - #('admin/practice', 'Choose when students should start their practice.', 1), - ('admin/sections_list', 'db tables', 1), - ('admin/sections_create', 'Create New Section', 1), - ('admin/sections_delete', 'db tables', 1), - ('admin/sections_update', 'db tables', 1), - # TODO: This deletes the course, making the test framework raise an exception. Need a separate case to catch this. - #('admin/deletecourse', 'Manage Section', 2), - # FIXME: these raise an exception. - #('admin/addinstructor', 'Trying to add non-user', 1), -- this is an api call - #('admin/add_practice_items', 'xxx', 1), -- this is an api call - ('admin/assignments', 'Assignment', 3), # labels for hidden elements - #('admin/backup', 'xxx', 1), - ('admin/practice', 'Choose when students should start', 1), - #('admin/removeassign', 'Cannot remove assignment with id of', 1), - #('admin/removeinstructor', 'xxx', 1), - #('admin/removeStudents', 'xxx', 1), - # TODO: added to the ``createAssignment`` endpoint so far. -]) -def test_validate_instructor_pages(url, expected_string, expected_errors, - test_client, test_user, test_user_1): - test_instructor_1 = test_user('test_instructor_1', 'password_1', - test_user_1.course) +@pytest.mark.parametrize( + "url, expected_string, expected_errors", + [ + # **Default** + # ------------ + # web2py-generated stuff produces two extra errors. + ("default/bios", "Bios", 3), + # FIXME: The element ```` in ``views/admin/admin.html`` produces the error ``Bad value \u201c\u201d for attribute \u201caction\u201d on element \u201cform\u201d: Must be non-empty.``. + # + # **Admin** + # ---------- + ("admin/admin", "Manage Section", 1), + ("admin/course_students", '"test_user_1"', 2), + ("admin/createAssignment", "ERROR", None), + ("admin/grading", "assignment", 1), + # TODO: This produces an exception. + # ('admin/practice', 'Choose when students should start their practice.', 1), + ("admin/sections_list", "db tables", 1), + ("admin/sections_create", "Create New Section", 1), + ("admin/sections_delete", "db tables", 1), + ("admin/sections_update", "db tables", 1), + # TODO: This deletes the course, making the test framework raise an exception. Need a separate case to catch this. + # ('admin/deletecourse', 'Manage Section', 2), + # FIXME: these raise an exception. + # ('admin/addinstructor', 'Trying to add non-user', 1), -- this is an api call + # ('admin/add_practice_items', 'xxx', 1), -- this is an api call + ("admin/assignments", "Assignment", 3), # labels for hidden elements + # ('admin/backup', 'xxx', 1), + ("admin/practice", "Choose when students should start", 1), + # ('admin/removeassign', 'Cannot remove assignment with id of', 1), + # ('admin/removeinstructor', 'xxx', 1), + # ('admin/removeStudents', 'xxx', 1), + # TODO: added to the ``createAssignment`` endpoint so far. + ], +) +def test_validate_instructor_pages( + url, expected_string, expected_errors, test_client, test_user, test_user_1 +): + test_instructor_1 = test_user("test_instructor_1", "password_1", test_user_1.course) test_instructor_1.make_instructor() # Make sure that non-instructors are redirected. test_client.logout() - test_client.validate(url, 'Login') + test_client.validate(url, "Login") test_user_1.login() - test_client.validate(url, 'Insufficient privileges') + test_client.validate(url, "Insufficient privileges") test_client.logout() # Test the instructor results. test_instructor_1.login() - test_client.validate(url, expected_string, - expected_errors) + test_client.validate(url, expected_string, expected_errors) # Test the ``ajax/preview_question`` endpoint. def test_preview_question(test_client, test_user_1): - preview_question = 'ajax/preview_question' + preview_question = "ajax/preview_question" # Passing no parameters should raise an error. - test_client.validate(preview_question, 'Error: ') + test_client.validate(preview_question, "Error: ") # Passing something not JSON-encoded should raise an error. - test_client.validate(preview_question, 'Error: ', data={'code': 'xxx'}) + test_client.validate(preview_question, "Error: ", data={"code": "xxx"}) # Passing invalid RST should produce a Sphinx warning. - test_client.validate(preview_question, 'WARNING', data={'code': '"*hi"'}) + test_client.validate(preview_question, "WARNING", data={"code": '"*hi"'}) # Passing valid RST with no Runestone component should produce an error. - test_client.validate(preview_question, 'Error: ', data={'code': '"*hi*"'}) + test_client.validate(preview_question, "Error: ", data={"code": '"*hi*"'}) # Passing a string with Unicode should work. Note that 0x0263 == 611; the JSON-encoded result will use this. - test_client.validate(preview_question, 'ɣ', data={'code': json.dumps(dedent(u'''\ + test_client.validate( + preview_question, + "ɣ", + data={ + "code": json.dumps( + dedent( + u"""\ .. fillintheblank:: question_1 Mary had a \u0263. - :x: Whatever. - '''))}) + """ + ) + ) + }, + ) # Verify that ``question_1`` is not in the database. TODO: This passes even if the ``DBURL`` env variable in ``ajax.py`` fucntion ``preview_question`` isn't deleted. So, this test doesn't work. db = test_user_1.runestone_db_tools.db - assert len(db(db.fitb_answers.div_id == 'question_1').select()) == 0 + assert len(db(db.fitb_answers.div_id == "question_1").select()) == 0 # TODO: Add a test case for when the runestone build produces a non-zero return code. @@ -246,25 +275,33 @@ def test_preview_question(test_client, test_user_1): def test_user_profile(test_client, test_user_1): test_user_1.login() runestone_db_tools = test_user_1.runestone_db_tools - course_name = 'test_course_2' + course_name = "test_course_2" test_course_2 = runestone_db_tools.create_course(course_name) # Test a non-existant course. - test_user_1.update_profile(expected_string='Errors in form', - course_name='does_not_exist') + test_user_1.update_profile( + expected_string="Errors in form", course_name="does_not_exist" + ) # Test an invalid e-mail address. TODO: This doesn't produce an error message. ##test_user_1.update_profile(expected_string='Errors in form', ## email='not a valid e-mail address') # Change the user's profile data; add a new course. - username = 'a_different_username' - first_name = 'a different first' - last_name = 'a different last' - email = 'a_different_email@foo.com' - section = 'a_different_section' - test_user_1.update_profile(username=username, first_name=first_name, - last_name=last_name, email=email, section=section, - course_name=course_name, accept_tcp='', is_free=True) + username = "a_different_username" + first_name = "a different first" + last_name = "a different last" + email = "a_different_email@foo.com" + section = "a_different_section" + test_user_1.update_profile( + username=username, + first_name=first_name, + last_name=last_name, + email=email, + section=section, + course_name=course_name, + accept_tcp="", + is_free=True, + ) # Check the values. db = runestone_db_tools.db @@ -274,50 +311,56 @@ def test_user_profile(test_client, test_user_1): assert user.first_name == first_name assert user.last_name == last_name # TODO: The e-mail address isn't updated. - #assert user.email == email + # assert user.email == email assert user.course_id == test_course_2.course_id assert user.accept_tcp == False # TODO: I'm not sure where the section is stored. - #assert user.section == section + # assert user.section == section # Test that the course name is correctly preserved across registrations if other fields are invalid. def test_registration(test_client, runestone_db_tools): # Registration doesn't work unless we're logged out. test_client.logout() - course_name = 'a_course_name' + course_name = "a_course_name" runestone_db_tools.create_course(course_name) # Now, post the registration. - username = 'username' - first_name = 'first' - last_name = 'last' - email = 'e@mail.com' - password = 'password' - test_client.validate('default/user/register', 'Please fix the following errors in your registration', data=dict( - username=username, - first_name=first_name, - last_name=last_name, - # The e-mail address must be unique. - email=email, - password=password, - password_two=password + 'oops', - # Note that ``course_id`` is (on the form) actually a course name. - course_id=course_name, - accept_tcp='on', - donate='0', - _next='/runestone/default/index', - _formname='register', - )) + username = "username" + first_name = "first" + last_name = "last" + email = "e@mail.com" + password = "password" + test_client.validate( + "default/user/register", + "Please fix the following errors in your registration", + data=dict( + username=username, + first_name=first_name, + last_name=last_name, + # The e-mail address must be unique. + email=email, + password=password, + password_two=password + "oops", + # Note that ``course_id`` is (on the form) actually a course name. + course_id=course_name, + accept_tcp="on", + donate="0", + _next="/runestone/default/index", + _formname="register", + ), + ) # Check that the pricing system works correctly. def test_pricing(runestone_db_tools, runestone_env): # Check the pricing. - default_controller = web2py_controller_import(runestone_env, 'default') + default_controller = web2py_controller_import(runestone_env, "default") db = runestone_db_tools.db base_course = runestone_db_tools.create_course() - child_course = runestone_db_tools.create_course('test_child_course', base_course=base_course.course_name) + child_course = runestone_db_tools.create_course( + "test_child_course", base_course=base_course.course_name + ) # First, test on a base course. for expected_price, actual_price in [(0, None), (0, -100), (0, 0), (15, 15)]: db(db.courses.id == base_course.course_id).update(student_price=actual_price) @@ -325,115 +368,173 @@ def test_pricing(runestone_db_tools, runestone_env): # Test in a child course as well. Create a matrix of all base course prices by all child course prices. for expected_price, actual_base_price, actual_child_price in [ - (0, None, None), (0, None, 0), (0, None, -1), (2, None, 2), - (0, 0, None), (0, 0, 0), (0, 0, -1), (2, 0, 2), - (0, -2, None), (0, -2, 0), (0, -2, -1), (2, -2, 2), - (3, 3, None), (0, 3, 0), (0, 3, -1), (2, 3, 2)]: - - db(db.courses.id == base_course.course_id).update(student_price=actual_base_price) - db(db.courses.id == child_course.course_id).update(student_price=actual_child_price) - assert default_controller._course_price(child_course.course_id) == expected_price + (0, None, None), + (0, None, 0), + (0, None, -1), + (2, None, 2), + (0, 0, None), + (0, 0, 0), + (0, 0, -1), + (2, 0, 2), + (0, -2, None), + (0, -2, 0), + (0, -2, -1), + (2, -2, 2), + (3, 3, None), + (0, 3, 0), + (0, 3, -1), + (2, 3, 2), + ]: + + db(db.courses.id == base_course.course_id).update( + student_price=actual_base_price + ) + db(db.courses.id == child_course.course_id).update( + student_price=actual_child_price + ) + assert ( + default_controller._course_price(child_course.course_id) == expected_price + ) # Check that setting the price causes redirects to the correct location (payment vs. donation) when registering for a course or adding a new course. def test_price_free(runestone_db_tools, test_user): db = runestone_db_tools.db course_1 = runestone_db_tools.create_course(student_price=0) - course_2 = runestone_db_tools.create_course('test_course_2', student_price=0) + course_2 = runestone_db_tools.create_course("test_course_2", student_price=0) # Check registering for a free course. - test_user_1 = test_user('test_user_1', 'password_1', course_1, is_free=True) + test_user_1 = test_user("test_user_1", "password_1", course_1, is_free=True) # Verify the user was added to the ``user_courses`` table. - assert db(( db.user_courses.course_id == test_user_1.course.course_id) & (db.user_courses.user_id == test_user_1.user_id) ).select().first() + assert ( + db( + (db.user_courses.course_id == test_user_1.course.course_id) + & (db.user_courses.user_id == test_user_1.user_id) + ) + .select() + .first() + ) # Check adding a free course. test_user_1.update_profile(course_name=course_2.course_name, is_free=True) # Same as above. - assert db(( db.user_courses.course_id == course_2.course_id) & (db.user_courses.user_id == test_user_1.user_id) ).select().first() + assert ( + db( + (db.user_courses.course_id == course_2.course_id) + & (db.user_courses.user_id == test_user_1.user_id) + ) + .select() + .first() + ) def test_price_paid(runestone_db_tools, test_user): db = runestone_db_tools.db # Check registering for a paid course. course_1 = runestone_db_tools.create_course(student_price=1) - course_2 = runestone_db_tools.create_course('test_course_2', student_price=1) + course_2 = runestone_db_tools.create_course("test_course_2", student_price=1) # Check registering for a paid course. - test_user_1 = test_user('test_user_1', 'password_1', course_1, - is_free=False) + test_user_1 = test_user("test_user_1", "password_1", course_1, is_free=False) # Until payment is provided, the user shouldn't be added to the ``user_courses`` table. Ensure that refresh, login/logout, profile changes, adding another class, etc. don't allow access. test_user_1.test_client.logout() test_user_1.login() - test_user_1.test_client.validate('default/index') + test_user_1.test_client.validate("default/index") # Check adding a paid course. test_user_1.update_profile(course_name=course_2.course_name, is_free=False) # Verify no access without payment. - assert not db(( db.user_courses.course_id == course_1.course_id) & (db.user_courses.user_id == test_user_1.user_id) ).select().first() - assert not db(( db.user_courses.course_id == course_2.course_id) & (db.user_courses.user_id == test_user_1.user_id) ).select().first() + assert ( + not db( + (db.user_courses.course_id == course_1.course_id) + & (db.user_courses.user_id == test_user_1.user_id) + ) + .select() + .first() + ) + assert ( + not db( + (db.user_courses.course_id == course_2.course_id) + & (db.user_courses.user_id == test_user_1.user_id) + ) + .select() + .first() + ) # Check that payments are handled correctly. def test_payments(runestone_controller, runestone_db_tools, test_user): if not runestone_controller.settings.STRIPE_SECRET_KEY: - pytest.skip('No Stripe keys provided.') + pytest.skip("No Stripe keys provided.") db = runestone_db_tools.db course_1 = runestone_db_tools.create_course(student_price=100) - test_user_1 = test_user('test_user_1', 'password_1', course_1, - is_free=False) + test_user_1 = test_user("test_user_1", "password_1", course_1, is_free=False) def did_payment(): - return db(( db.user_courses.course_id == course_1.course_id) & (db.user_courses.user_id == test_user_1.user_id) ).select().first() + return ( + db( + (db.user_courses.course_id == course_1.course_id) + & (db.user_courses.user_id == test_user_1.user_id) + ) + .select() + .first() + ) # Test some failing tokens. assert not did_payment() - for token in ['tok_chargeCustomerFail', 'tok_chargeDeclined']: + for token in ["tok_chargeCustomerFail", "tok_chargeDeclined"]: test_user_1.make_payment(token) assert not did_payment() - test_user_1.make_payment('tok_visa') + test_user_1.make_payment("tok_visa") assert did_payment() # Check that the payment record is correct. - payment = db((db.user_courses.user_id == test_user_1.user_id) & - (db.user_courses.course_id == course_1.course_id) & - (db.user_courses.id == db.payments.user_courses_id)) \ - .select(db.payments.charge_id).first() + payment = ( + db( + (db.user_courses.user_id == test_user_1.user_id) + & (db.user_courses.course_id == course_1.course_id) + & (db.user_courses.id == db.payments.user_courses_id) + ) + .select(db.payments.charge_id) + .first() + ) assert payment.charge_id # Test the LP endpoint. -@pytest.mark.skipif(six.PY2, reason='Requires Python 3.') +@pytest.mark.skipif(six.PY2, reason="Requires Python 3.") def test_lp(test_user_1): test_user_1.login() # Check that omitting parameters produces an error. - ret = test_user_1.hsblog(event='lp_build') - assert 'No feedback provided' in ret['errors'][0] + ret = test_user_1.hsblog(event="lp_build") + assert "No feedback provided" in ret["errors"][0] # Check that database entries are validated. ret = test_user_1.hsblog( - event='lp_build', + event="lp_build", # This div_id is too long. Everything else is OK. - div_id='X'*1000, + div_id="X" * 1000, course=test_user_1.course.course_name, - builder='unsafe-python', + builder="unsafe-python", answer=json.dumps({"code_snippets": ["def one(): return 1"]}), ) - assert 'div_id' in ret['errors'][0] + assert "div_id" in ret["errors"][0] # Check a passing case def assert_passing(): ret = test_user_1.hsblog( - event='lp_build', - div_id='lp_demo_1', + event="lp_build", + div_id="lp_demo_1", course=test_user_1.course.course_name, - builder='unsafe-python', + builder="unsafe-python", answer=json.dumps({"code_snippets": ["def one(): return 1"]}), ) - assert 'errors' not in ret - assert ret['correct'] == 100 + assert "errors" not in ret + assert ret["correct"] == 100 + assert_passing() # Send lots of jobs to test out the queue. Skip this for now -- not all the useinfo entries get deleted, which causes ``test_getNumOnline`` to fail. @@ -453,8 +554,10 @@ def test_dynamic_book_routing_1(test_client, test_user_1): # Test that a draft is accessible only to instructors. test_user_1.make_instructor() test_user_1.update_profile(course_name=test_user_1.course.course_name) - test_client.validate('books/draft/{}/index.html'.format(test_user_1.course.base_course), - 'The red car drove away.') + test_client.validate( + "books/draft/{}/index.html".format(test_user_1.course.base_course), + "The red car drove away.", + ) # Test the no-login case. @@ -462,7 +565,9 @@ def test_dynamic_book_routing_2(test_client, test_user_1): test_client.logout() # Test for a book that doesn't require a login. First, change the book to not require a login. db = test_user_1.runestone_db_tools.db - db(db.courses.course_name == test_user_1.course.base_course).update(login_required=False) + db(db.courses.course_name == test_user_1.course.base_course).update( + login_required=False + ) db.commit() dbr_tester(test_client, test_user_1, False) @@ -474,18 +579,17 @@ def dbr_tester(test_client, test_user_1, is_logged_in): base_course = test_user_1.course.base_course # A non-existant course. if is_logged_in: - validate('books/published/xxx', 'Course Selection') + validate("books/published/xxx", "Course Selection") else: - validate('books/published/xxx', expected_status=404) + validate("books/published/xxx", expected_status=404) # A non-existant page. - validate('books/published/{}/xxx'.format(base_course), - expected_status=404) + validate("books/published/{}/xxx".format(base_course), expected_status=404) # A directory. - validate('books/published/{}/test_chapter_1'.format(base_course), - expected_status=404) + validate( + "books/published/{}/test_chapter_1".format(base_course), expected_status=404 + ) # Attempt to access files outside a course. - validate('books/published/{}/../conf.py'.format(base_course), - expected_status=404) + validate("books/published/{}/../conf.py".format(base_course), expected_status=404) # Attempt to access a course we're not registered for. TODO: Need to create another base course for this to work. ##if is_logged_in: ## #validate('books/published/{}/index.html'.format(base_course), [ @@ -493,94 +597,118 @@ def dbr_tester(test_client, test_user_1, is_logged_in): ## ]) # A valid page. Check the book config as well. - validate('books/published/{}/index.html'.format(base_course), [ - 'The red car drove away.', - "eBookConfig.course = '{}';".format(test_user_1.course.course_name if is_logged_in else base_course), - "eBookConfig.basecourse = '{}';".format(base_course), - ]) + validate( + "books/published/{}/index.html".format(base_course), + [ + "The red car drove away.", + "eBookConfig.course = '{}';".format( + test_user_1.course.course_name if is_logged_in else base_course + ), + "eBookConfig.basecourse = '{}';".format(base_course), + ], + ) # Drafts shouldn't be accessible by students. - validate('books/draft/{}/index.html'.format(base_course), - 'Insufficient privileges' if is_logged_in else 'Username') + validate( + "books/draft/{}/index.html".format(base_course), + "Insufficient privileges" if is_logged_in else "Username", + ) # Check routing in a base course. if is_logged_in: - test_user_1.update_profile(course_name=test_user_1.course.base_course, - is_free=True) - validate('books/published/{}/index.html'.format(base_course), [ - 'The red car drove away.', - "eBookConfig.course = '{}';".format(base_course), - "eBookConfig.basecourse = '{}';".format(base_course), - ]) + test_user_1.update_profile( + course_name=test_user_1.course.base_course, is_free=True + ) + validate( + "books/published/{}/index.html".format(base_course), + [ + "The red car drove away.", + "eBookConfig.course = '{}';".format(base_course), + "eBookConfig.basecourse = '{}';".format(base_course), + ], + ) # Test static content. - validate('books/published/{}/_static/runestone-custom-sphinx-bootstrap.css'.format(base_course), - 'background-color: #fafafa;') + validate( + "books/published/{}/_static/runestone-custom-sphinx-bootstrap.css".format( + base_course + ), + "background-color: #fafafa;", + ) def test_assignments(test_client, runestone_db_tools, test_user): - course_3 = runestone_db_tools.create_course('test_course_3') - test_instructor_1 = test_user('test_instructor_1', 'password_1', course_3) + course_3 = runestone_db_tools.create_course("test_course_3") + test_instructor_1 = test_user("test_instructor_1", "password_1", course_3) test_instructor_1.make_instructor() test_instructor_1.login() db = runestone_db_tools.db - name_1 = 'test_assignment_1' - name_2 = 'test_assignment_2' - name_3 = 'test_assignment_3' + name_1 = "test_assignment_1" + name_2 = "test_assignment_2" + name_3 = "test_assignment_3" # Create an assignment -- using createAssignment - test_client.post('admin/createAssignment', - data=dict(name=name_1)) + test_client.post("admin/createAssignment", data=dict(name=name_1)) - assign1 = db( - (db.assignments.name == name_1) & - (db.assignments.course == test_instructor_1.course.course_id) - ).select().first() + assign1 = ( + db( + (db.assignments.name == name_1) + & (db.assignments.course == test_instructor_1.course.course_id) + ) + .select() + .first() + ) assert assign1 # Make sure you can't create two assignments with the same name - test_client.post('admin/createAssignment', - data=dict(name=name_1)) + test_client.post("admin/createAssignment", data=dict(name=name_1)) assert "EXISTS" in test_client.text # Rename assignment - test_client.post('admin/createAssignment', - data=dict(name=name_2)) - assign2 = db( - (db.assignments.name == name_2) & - (db.assignments.course == test_instructor_1.course.course_id) - ).select().first() + test_client.post("admin/createAssignment", data=dict(name=name_2)) + assign2 = ( + db( + (db.assignments.name == name_2) + & (db.assignments.course == test_instructor_1.course.course_id) + ) + .select() + .first() + ) assert assign2 - test_client.post('admin/renameAssignment', - data=dict(name=name_3,original=assign2.id)) + test_client.post( + "admin/renameAssignment", data=dict(name=name_3, original=assign2.id) + ) assert db(db.assignments.name == name_3).select().first() assert not db(db.assignments.name == name_2).select().first() # Make sure you can't rename an assignment to an already used assignment - test_client.post('admin/renameAssignment', - data=dict(name=name_3,original=assign1.id)) + test_client.post( + "admin/renameAssignment", data=dict(name=name_3, original=assign1.id) + ) assert "EXISTS" in test_client.text # Delete an assignment -- using removeassignment - test_client.post('admin/removeassign', data=dict(assignid=assign1.id)) + test_client.post("admin/removeassign", data=dict(assignid=assign1.id)) assert not db(db.assignments.name == name_1).select().first() - test_client.post('admin/removeassign', data=dict(assignid=assign2.id)) + test_client.post("admin/removeassign", data=dict(assignid=assign2.id)) assert not db(db.assignments.name == name_3).select().first() - test_client.post('admin/removeassign', data=dict(assignid=9999999)) + test_client.post("admin/removeassign", data=dict(assignid=9999999)) assert "Error" in test_client.text def test_instructor_practice_admin(test_client, runestone_db_tools, test_user): - course_4 = runestone_db_tools.create_course('test_course_1') - test_student_1 = test_user('test_student_1', 'password_1', course_4) + course_4 = runestone_db_tools.create_course("test_course_1") + test_student_1 = test_user("test_student_1", "password_1", course_4) test_student_1.logout() - test_instructor_1 = test_user('test_instructor_1', 'password_1', course_4) + test_instructor_1 = test_user("test_instructor_1", "password_1", course_4) test_instructor_1.make_instructor() test_instructor_1.login() db = runestone_db_tools.db - course_start_date = datetime.datetime.strptime(course_4.term_start_date, '%Y-%m-%d').date() + course_start_date = datetime.datetime.strptime( + course_4.term_start_date, "%Y-%m-%d" + ).date() today = datetime.datetime.today() start_date = course_start_date + datetime.timedelta(days=13) @@ -594,31 +722,44 @@ def test_instructor_practice_admin(test_client, runestone_db_tools, test_user): # Test the practice tool settings for the course. flashcard_creation_method = 2 - test_client.post('admin/practice', - data = {"StartDate": start_date, - "EndDate": end_date, - "graded": graded, - 'maxPracticeDays': max_practice_days, - 'maxPracticeQuestions': max_practice_questions, - 'pointsPerDay': day_points, - 'pointsPerQuestion': question_points, - 'questionsPerDay': questions_to_complete_day, - 'flashcardsCreationType': 2, - 'question_points': question_points}) - - practice_settings_1 = db( - (db.course_practice.auth_user_id == test_instructor_1.user_id) & - (db.course_practice.course_name == course_4.course_name) & - (db.course_practice.start_date == start_date) & - (db.course_practice.end_date == end_date) & - (db.course_practice.flashcard_creation_method == flashcard_creation_method) & - (db.course_practice.graded == graded) - ).select().first() + test_client.post( + "admin/practice", + data={ + "StartDate": start_date, + "EndDate": end_date, + "graded": graded, + "maxPracticeDays": max_practice_days, + "maxPracticeQuestions": max_practice_questions, + "pointsPerDay": day_points, + "pointsPerQuestion": question_points, + "questionsPerDay": questions_to_complete_day, + "flashcardsCreationType": 2, + "question_points": question_points, + }, + ) + + practice_settings_1 = ( + db( + (db.course_practice.auth_user_id == test_instructor_1.user_id) + & (db.course_practice.course_name == course_4.course_name) + & (db.course_practice.start_date == start_date) + & (db.course_practice.end_date == end_date) + & ( + db.course_practice.flashcard_creation_method + == flashcard_creation_method + ) + & (db.course_practice.graded == graded) + ) + .select() + .first() + ) assert practice_settings_1 if practice_settings_1.spacing == 1: assert practice_settings_1.max_practice_days == max_practice_days assert practice_settings_1.day_points == day_points - assert practice_settings_1.questions_to_complete_day == questions_to_complete_day + assert ( + practice_settings_1.questions_to_complete_day == questions_to_complete_day + ) else: assert practice_settings_1.max_practice_questions == max_practice_questions assert practice_settings_1.question_points == question_points @@ -626,20 +767,23 @@ def test_instructor_practice_admin(test_client, runestone_db_tools, test_user): # Test instructor adding a subchapter to the practice tool for students. # I need to call set_tz_offset to set timezoneoffset in the session. - test_client.post('ajax/set_tz_offset', - data = { 'timezoneoffset': 0 }) + test_client.post("ajax/set_tz_offset", data={"timezoneoffset": 0}) # The reason I'm manually stringifying the list value is that test_client.post does something strange with compound objects instead of passing them to json.dumps. - test_client.post('admin/add_practice_items', - data = { 'data': '["Test chapter 1/Subchapter B"]' }) - + test_client.post( + "admin/add_practice_items", data={"data": '["Test chapter 1/Subchapter B"]'} + ) - practice_settings_1 = db( - (db.user_topic_practice.user_id == test_student_1.user_id) & - (db.user_topic_practice.course_name == course_4.course_name) & - (db.user_topic_practice.chapter_label == "test_chapter_1") & - (db.user_topic_practice.sub_chapter_label == "subchapter_b") - ).select().first() + practice_settings_1 = ( + db( + (db.user_topic_practice.user_id == test_student_1.user_id) + & (db.user_topic_practice.course_name == course_4.course_name) + & (db.user_topic_practice.chapter_label == "test_chapter_1") + & (db.user_topic_practice.sub_chapter_label == "subchapter_b") + ) + .select() + .first() + ) assert practice_settings_1 # Testing whether a student can answer a practice question. @@ -669,44 +813,71 @@ def test_instructor_practice_admin(test_client, runestone_db_tools, test_user): def test_deleteaccount(test_client, runestone_db_tools, test_user): - course_3 = runestone_db_tools.create_course('test_course_3') - the_user = test_user('user_to_delete', 'password_1', course_3) + course_3 = runestone_db_tools.create_course("test_course_3") + the_user = test_user("user_to_delete", "password_1", course_3) the_user.login() validate = the_user.test_client.validate - the_user.hsblog(event="mChoice", - act="answer:1:correct",answer="1",correct="T",div_id="subc_b_1", - course="test_course_3") - validate('default/delete', 'About Runestone', data=dict(deleteaccount='checked')) + the_user.hsblog( + event="mChoice", + act="answer:1:correct", + answer="1", + correct="T", + div_id="subc_b_1", + course="test_course_3", + ) + validate("default/delete", "About Runestone", data=dict(deleteaccount="checked")) db = runestone_db_tools.db - res = db(db.auth_user.username == 'user_to_delete').select().first() + res = db(db.auth_user.username == "user_to_delete").select().first() print(res) - assert not db(db.useinfo.sid == 'user_to_delete').select().first() - assert not db(db.code.sid == 'user_to_delete').select().first() - assert not db(db.acerror_log.sid == 'user_to_delete').select().first() - for t in ['clickablearea','codelens','dragndrop','fitb','lp','mchoice','parsons','shortanswer']: - assert not db(db['{}_answers'.format(t)].sid == 'user_to_delete').select().first() + assert not db(db.useinfo.sid == "user_to_delete").select().first() + assert not db(db.code.sid == "user_to_delete").select().first() + assert not db(db.acerror_log.sid == "user_to_delete").select().first() + for t in [ + "clickablearea", + "codelens", + "dragndrop", + "fitb", + "lp", + "mchoice", + "parsons", + "shortanswer", + ]: + assert ( + not db(db["{}_answers".format(t)].sid == "user_to_delete").select().first() + ) + def test_pageprogress(test_client, runestone_db_tools, test_user_1): test_user_1.login() - test_user_1.hsblog(event="mChoice", - act="answer:1:correct",answer="1",correct="T",div_id="subc_b_1", - course=test_user_1.course.course_name) + test_user_1.hsblog( + event="mChoice", + act="answer:1:correct", + answer="1", + correct="T", + div_id="subc_b_1", + course=test_user_1.course.course_name, + ) # Since the user has answered the question the count for subc_b_1 should be 1 # cannot test the totals on the client without javascript but that is covered in the # selenium tests on the components side. - test_user_1.test_client.validate('books/published/{}/test_chapter_1/subchapter_b.html'.format(test_user_1.course.base_course), - '"subc_b_1": 1') + test_user_1.test_client.validate( + "books/published/{}/test_chapter_1/subchapter_b.html".format( + test_user_1.course.base_course + ), + '"subc_b_1": 1', + ) assert '"LearningZone_poll": 0' in test_user_1.test_client.text assert '"subc_b_fitb": 0' in test_user_1.test_client.text + def test_lockdown(test_client, test_user_1): test_user_1.login() base_course = test_user_1.course.base_course - res = test_client.validate('books/published/{}/index.html'.format(base_course)) + res = test_client.validate("books/published/{}/index.html".format(base_course)) assert '/default/user/login">  ' in res - assert 'Runestone in social media:' in res - assert '>Change Course' in res + assert "Runestone in social media:" in res + assert ">Change Course" in res assert 'id="profilelink">Edit' in res assert '