diff --git a/accounts/migrations/0003_user_schema_migration.py b/accounts/migrations/0003_user_schema_migration.py index 40688b94294886b7f137eea780d86712491ba3a7..30767298b1f740da8a196a72ee35db5303935bce 100644 --- a/accounts/migrations/0003_user_schema_migration.py +++ b/accounts/migrations/0003_user_schema_migration.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals, print_function +from __future__ import unicode_literals from django.db import models, migrations +import logging +logger = logging.getLogger('django') def forwards(apps, schema_editor): @@ -26,10 +28,9 @@ def change_foreign_keys(apps, schema_editor, from_app, from_model_name, to_app, import sys if sys.version_info >= (3,): - from warnings import warn - warn("This migration seems to only work on Py2, as of Django 1.9") + logger.warning("\nWarning: This migration seems to only work on Py2, " + "as of Django 1.9") - print() fields = FromModel._meta.get_fields(include_hidden=True) + ToModel._meta.get_fields(include_hidden=True) for rel in fields: @@ -55,7 +56,7 @@ def change_foreign_keys(apps, schema_editor, from_app, from_model_name, to_app, # we've already dealt with this, by virtue of the data migration # that populates the auto-created M2M field. if fk_field.model._meta.auto_created in [ToModel, FromModel]: - print("Skipping {0}".format(repr(rel))) + logger.info("Skipping {0}".format(repr(rel))) continue # In this case (FK fields that are part of an autogenerated M2M), @@ -79,7 +80,7 @@ def change_foreign_keys(apps, schema_editor, from_app, from_model_name, to_app, new_field.column = fk_field.column show = lambda m: "{0}.{1}".format(m._meta.app_label, m.__name__) - print("Fixing FK in {0}, col {1} -> {2}, from {3} -> {4}".format( + logger.info("Fixing FK in {0}, col {1} -> {2}, from {3} -> {4}".format( show(fk_field.model), old_field.column, new_field.column, show(old_field.remote_field.to), show(new_field.remote_field.to), diff --git a/course/admin.py b/course/admin.py index ace25a802a41fd5c869d0bf336fb645281b57e46..faf862d6d2c762db7a70dfadb40ca2c14d2fea37 100644 --- a/course/admin.py +++ b/course/admin.py @@ -532,7 +532,7 @@ class FlowPageVisitAdmin(admin.ModelAdmin): get_flow_id.admin_order_field = "flow_session__flow_id" # type: ignore def get_page_id(self, obj): - if obj.page_data.ordinal is None: + if obj.page_data.page_ordinal is None: return string_concat("%s/%s (", _("not in use"), ")") % ( obj.page_data.group_id, obj.page_data.page_id) @@ -540,7 +540,7 @@ class FlowPageVisitAdmin(admin.ModelAdmin): return "%s/%s (%s)" % ( obj.page_data.group_id, obj.page_data.page_id, - obj.page_data.ordinal) + obj.page_data.page_ordinal) get_page_id.short_description = _("Page ID") # type: ignore get_page_id.admin_order_field = "page_data__page_id" # type: ignore diff --git a/course/auth.py b/course/auth.py index 1508d094adc5fbb2d517288a8d270bba1dbf30b0..2199cf2be66e16cf224ba7250aa7c713742eae7a 100644 --- a/course/auth.py +++ b/course/auth.py @@ -266,7 +266,28 @@ class StopImpersonatingForm(forms.Form): def stop_impersonating(request): + if not request.user.is_authenticated: + raise PermissionDenied() + if not hasattr(request, "relate_impersonate_original_user"): + # prevent user without pperm to stop_impersonating + my_participations = Participation.objects.filter( + user=request.user, + status=participation_status.active) + + may_impersonate = False + for part in my_participations: + perms = [ + perm + for perm, argument in part.permissions() + if perm == pperm.impersonate_role] + if any(perms): + may_impersonate = True + break + + if not may_impersonate: + raise PermissionDenied() + messages.add_message(request, messages.ERROR, _("Not currently impersonating anyone.")) return redirect("relate-home") diff --git a/course/content.py b/course/content.py index 2eb45fb6d62dace4c5ba8b0e804fd402831c22c1..eade7a0ee1739d98318fe18b76669e2772a752d3 100644 --- a/course/content.py +++ b/course/content.py @@ -389,6 +389,7 @@ YAML_BLOCK_START_SCALAR_RE = re.compile( r"(?:\s*\#.*)?" "$") +IN_BLOCK_END_RAW_RE = re.compile(r"(.*)({%-?\s*endraw\s*-?%})(.*)") GROUP_COMMENT_START = re.compile(r"^\s*#\s*\{\{\{") LEADING_SPACES_RE = re.compile(r"^( *)") @@ -426,14 +427,12 @@ def process_yaml_for_expansion(yaml_str): i += 1 continue - if '{% endraw %}' in ln: - endraw_ = "{% endraw %}\n{{ '{% endraw %}' }}\n{% raw %}" - ln = ln.replace('{% endraw %}', endraw_) - line_indent = len(LEADING_SPACES_RE.match(ln).group(1)) if line_indent <= block_start_indent: break else: + ln = IN_BLOCK_END_RAW_RE.sub( + r"\1{% endraw %}{{ '\2' }}{% raw %}\3", ln) unprocessed_block_lines.append(ln.rstrip()) i += 1 @@ -452,7 +451,6 @@ def process_yaml_for_expansion(yaml_str): else: jinja_lines.append(ln) i += 1 - return "\n".join(jinja_lines) @@ -601,20 +599,24 @@ def get_yaml_from_repo(repo, full_name, commit_sha, cached=True): """ if cached: - from six.moves.urllib.parse import quote_plus - cache_key = "%%%2".join( - (CACHE_KEY_ROOT, - quote_plus(repo.controldir()), quote_plus(full_name), - commit_sha.decode())) + try: + import django.core.cache as cache + except ImproperlyConfigured: + cached = False + else: + from six.moves.urllib.parse import quote_plus + cache_key = "%%%2".join( + (CACHE_KEY_ROOT, + quote_plus(repo.controldir()), quote_plus(full_name), + commit_sha.decode())) - import django.core.cache as cache - def_cache = cache.caches["default"] - result = None - # Memcache is apparently limited to 250 characters. - if len(cache_key) < 240: - result = def_cache.get(cache_key) - if result is not None: - return result + def_cache = cache.caches["default"] + result = None + # Memcache is apparently limited to 250 characters. + if len(cache_key) < 240: + result = def_cache.get(cache_key) + if result is not None: + return result yaml_bytestream = get_repo_blob( repo, full_name, commit_sha, allow_tree=False).data @@ -1514,7 +1516,7 @@ def list_flow_ids(repo, commit_sha): else: for entry in flows_tree.items(): if entry.path.endswith(b".yml"): - flow_ids.append(entry.path[:-4]) + flow_ids.append(entry.path[:-4].decode("utf-8")) return sorted(flow_ids) diff --git a/course/flow.py b/course/flow.py index 3ce19d96c61dd178a6890b93bc421d4abd927b5f..56f4484e64f27cc479659e35a19e22e0df347d2b 100644 --- a/course/flow.py +++ b/course/flow.py @@ -125,8 +125,8 @@ def _adjust_flow_session_page_data_inner(repo, flow_session, from course.models import FlowPageData def remove_page(fpd): - if fpd.ordinal is not None: - fpd.ordinal = None + if fpd.page_ordinal is not None: + fpd.page_ordinal = None fpd.save() desc_group_ids = [] @@ -173,7 +173,7 @@ def _adjust_flow_session_page_data_inner(repo, flow_session, data = page.initialize_page_data(pctx) return FlowPageData( flow_session=flow_session, - ordinal=None, + page_ordinal=None, page_type=new_page_desc.type, group_id=grp.id, page_id=new_page_desc.id, @@ -181,8 +181,8 @@ def _adjust_flow_session_page_data_inner(repo, flow_session, title=page.title(pctx, data)) def add_page(fpd): - if fpd.ordinal != ordinal[0]: - fpd.ordinal = ordinal[0] + if fpd.page_ordinal != ordinal[0]: + fpd.page_ordinal = ordinal[0] fpd.save() page_desc = find_page_desc(fpd.page_id) @@ -206,8 +206,8 @@ def _adjust_flow_session_page_data_inner(repo, flow_session, .filter( flow_session=flow_session, group_id=grp.id, - ordinal__isnull=False) - .order_by("ordinal")): + page_ordinal__isnull=False) + .order_by("page_ordinal")): if (fpd.page_id in available_page_ids and len(group_pages) < max_page_count): @@ -270,7 +270,7 @@ def _adjust_flow_session_page_data_inner(repo, flow_session, FlowPageData.objects .filter( flow_session=flow_session, - ordinal__isnull=False) + page_ordinal__isnull=False) .exclude(group_id__in=desc_group_ids) ): remove_page(fpd) @@ -524,14 +524,14 @@ def assemble_page_grades(flow_sessions): all_answer_visits = ( get_multiple_flow_session_graded_answers_qset(flow_sessions) .order_by("visit_time") - .values("id", "flow_session_id", "page_data__ordinal", + .values("id", "flow_session_id", "page_data__page_ordinal", "is_submitted_answer")) for answer_visit in all_answer_visits: fsess_idx = id_to_fsess_idx[answer_visit["flow_session_id"]] - ordinal = answer_visit["page_data__ordinal"] - if ordinal is not None: - answer_visit_ids[fsess_idx][ordinal] = answer_visit["id"] + page_ordinal = answer_visit["page_data__page_ordinal"] + if page_ordinal is not None: + answer_visit_ids[fsess_idx][page_ordinal] = answer_visit["id"] if not flow_sessions[fsess_idx].in_progress: assert answer_visit["is_submitted_answer"] is True @@ -571,8 +571,8 @@ def assemble_answer_visits(flow_session): .order_by("visit_time")) for page_visit in answer_page_visits: - if page_visit.page_data.ordinal is not None: - answer_visits[page_visit.page_data.ordinal] = page_visit + if page_visit.page_data.page_ordinal is not None: + answer_visits[page_visit.page_data.page_ordinal] = page_visit if not flow_session.in_progress: assert page_visit.is_submitted_answer is True @@ -586,8 +586,8 @@ def get_all_page_data(flow_session): return (FlowPageData.objects .filter( flow_session=flow_session, - ordinal__isnull=False) - .order_by("ordinal")) + page_ordinal__isnull=False) + .order_by("page_ordinal")) def get_interaction_kind( @@ -601,7 +601,7 @@ def get_interaction_kind( ikind = flow_session_interaction_kind.noninteractive for i, page_data in enumerate(all_page_data): - assert i == page_data.ordinal + assert i == page_data.page_ordinal page = instantiate_flow_page_with_ctx(fctx, page_data) if page.expects_answer(): @@ -629,7 +629,7 @@ def get_session_answered_page_data( is_interactive_flow = False # type: bool for i, page_data in enumerate(all_page_data): - assert i == page_data.ordinal + assert i == page_data.page_ordinal avisit = answer_visits[i] if avisit is not None: @@ -815,7 +815,7 @@ def gather_grade_info( for i, page_data in enumerate(all_page_data): page = instantiate_flow_page_with_ctx(fctx, page_data) - assert i == page_data.ordinal + assert i == page_data.page_ordinal av = answer_visits[i] @@ -927,7 +927,7 @@ def grade_page_visits( answer_visit.save() else: - page_data = flow_session.page_data.get(ordinal=i) + page_data = flow_session.page_data.get(page_ordinal=i) page = instantiate_flow_page_with_ctx(fctx, page_data) if not page.expects_answer(): @@ -1701,7 +1701,7 @@ def add_buttons_to_form(form, fpctx, flow_session, permissions): css_class="relate-save-button relate-submit-button")) else: # Only offer 'save and move on' if student will receive no feedback - if fpctx.page_data.ordinal + 1 < flow_session.page_count: + if fpctx.page_data.page_ordinal + 1 < flow_session.page_count: form.helper.add_input( Submit("save_and_next", mark_safe_lazy( @@ -1744,13 +1744,13 @@ def create_flow_page_visit(request, flow_session, page_data): @course_view -def view_flow_page(pctx, flow_session_id, ordinal): +def view_flow_page(pctx, flow_session_id, page_ordinal): # type: (CoursePageContext, int, int) -> http.HttpResponse request = pctx.request login_exam_ticket = get_login_exam_ticket(request) - ordinal = int(ordinal) + page_ordinal = int(page_ordinal) flow_session_id = int(flow_session_id) flow_session = get_and_check_flow_session(pctx, flow_session_id) @@ -1769,10 +1769,10 @@ def view_flow_page(pctx, flow_session_id, ordinal): respect_preview=True) try: - fpctx = FlowPageContext(pctx.repo, pctx.course, flow_id, ordinal, - participation=pctx.participation, - flow_session=flow_session, - request=pctx.request) + fpctx = FlowPageContext(pctx.repo, pctx.course, flow_id, page_ordinal, + participation=pctx.participation, + flow_session=flow_session, + request=pctx.request) except PageOrdinalOutOfRange: return redirect("relate-view_flow_page", pctx.course.identifier, @@ -2012,13 +2012,13 @@ def view_flow_page(pctx, flow_session_id, ordinal): from django.db import connection with connection.cursor() as c: c.execute( - "SELECT DISTINCT course_flowpagedata.ordinal " + "SELECT DISTINCT course_flowpagedata.page_ordinal " "FROM course_flowpagevisit " "INNER JOIN course_flowpagedata " "ON course_flowpagedata.id = course_flowpagevisit.page_data_id " "WHERE course_flowpagedata.flow_session_id = %s " "AND course_flowpagevisit.answer IS NOT NULL " - "ORDER BY course_flowpagedata.ordinal", + "ORDER BY course_flowpagedata.page_ordinal", [flow_session.id]) flow_page_ordinals_with_answers = set(row[0] for row in c.fetchall()) @@ -2026,9 +2026,9 @@ def view_flow_page(pctx, flow_session_id, ordinal): args = { "flow_identifier": fpctx.flow_id, "flow_desc": fpctx.flow_desc, - "ordinal": fpctx.ordinal, + "page_ordinal": fpctx.page_ordinal, "page_data": fpctx.page_data, - "percentage": int(100*(fpctx.ordinal+1) / flow_session.page_count), + "percentage": int(100 * (fpctx.page_ordinal+1) / flow_session.page_count), "flow_session": flow_session, "all_page_data": all_page_data, "flow_page_ordinals_with_answers": flow_page_ordinals_with_answers, @@ -2106,7 +2106,7 @@ def get_prev_answer_visits_dropdown_content(pctx, flow_session_id, page_ordinal) flow_session = get_and_check_flow_session(pctx, int(flow_session_id)) page_data = get_object_or_404( - FlowPageData, flow_session=flow_session, ordinal=page_ordinal) + FlowPageData, flow_session=flow_session, page_ordinal=page_ordinal) prev_answer_visits = get_prev_answer_visits_qset(page_data) def serialize(obj): @@ -2243,9 +2243,9 @@ def post_flow_page( if (pressed_button == "save_and_next" and not will_receive_feedback(permissions)): return redirect("relate-view_flow_page", - fpctx.course.identifier, - flow_session.id, - fpctx.ordinal + 1) + fpctx.course.identifier, + flow_session.id, + fpctx.page_ordinal + 1) elif (pressed_button == "save_and_finish" and not will_receive_feedback(permissions)): return redirect("relate-finish_flow_session_view", @@ -2292,16 +2292,16 @@ def post_flow_page( # {{{ view: send interaction email to course staffs in flow pages @course_view -def send_email_about_flow_page(pctx, flow_session_id, ordinal): +def send_email_about_flow_page(pctx, flow_session_id, page_ordinal): # {{{ check if interaction email is allowed for this page. - ordinal = int(ordinal) + page_ordinal = int(page_ordinal) flow_session_id = int(flow_session_id) flow_session = get_and_check_flow_session(pctx, flow_session_id) flow_id = flow_session.flow_id - fpctx = FlowPageContext(pctx.repo, pctx.course, flow_id, ordinal, + fpctx = FlowPageContext(pctx.repo, pctx.course, flow_id, page_ordinal, participation=pctx.participation, flow_session=flow_session, request=pctx.request) @@ -2332,13 +2332,13 @@ def send_email_about_flow_page(pctx, flow_session_id, ordinal): FlowSession, id=int(flow_session_id)) from course.models import FlowPageData page_id = FlowPageData.objects.get( - flow_session=flow_session_id, ordinal=ordinal).page_id + flow_session=flow_session_id, page_ordinal=page_ordinal).page_id review_url = reverse( "relate-view_flow_page", kwargs={'course_identifier': pctx.course.identifier, 'flow_session_id': flow_session_id, - 'ordinal': ordinal + 'page_ordinal': page_ordinal } ) @@ -2428,7 +2428,7 @@ def send_email_about_flow_page(pctx, flow_session_id, ordinal): "also receive a copy of the email.")) return redirect("relate-view_flow_page", - pctx.course.identifier, flow_session_id, ordinal) + pctx.course.identifier, flow_session_id, page_ordinal) else: form = FlowPageInteractionEmailForm(review_uri) @@ -2471,7 +2471,7 @@ class FlowPageInteractionEmailForm(StyledForm): # {{{ view: update page bookmark state @course_view -def update_page_bookmark_state(pctx, flow_session_id, ordinal): +def update_page_bookmark_state(pctx, flow_session_id, page_ordinal): if pctx.request.method != "POST": raise SuspiciousOperation(_("only POST allowed")) @@ -2488,8 +2488,8 @@ def update_page_bookmark_state(pctx, flow_session_id, ordinal): bookmark_state = bookmark_state == "1" fpd = get_object_or_404(FlowPageData.objects, - flow_session=flow_session, - ordinal=ordinal) + flow_session=flow_session, + page_ordinal=page_ordinal) fpd.bookmarked = bookmark_state fpd.save() @@ -2841,13 +2841,13 @@ class UnsubmitFlowPageForm(forms.Form): @course_view -def view_unsubmit_flow_page(pctx, flow_session_id, ordinal): +def view_unsubmit_flow_page(pctx, flow_session_id, page_ordinal): # type: (CoursePageContext, int, int) -> http.HttpResponse request = pctx.request now_datetime = get_now_or_fake_time(request) - ordinal = int(ordinal) + page_ordinal = int(page_ordinal) flow_session_id = int(flow_session_id) try: @@ -2880,7 +2880,7 @@ def view_unsubmit_flow_page(pctx, flow_session_id, ordinal): # }}} page_data = get_object_or_404( - FlowPageData, flow_session=flow_session, ordinal=ordinal) + FlowPageData, flow_session=flow_session, page_ordinal=page_ordinal) visit = get_first_from_qset( get_prev_answer_visits_qset(page_data) @@ -2890,7 +2890,7 @@ def view_unsubmit_flow_page(pctx, flow_session_id, ordinal): messages.add_message(request, messages.INFO, _("No prior answers found that could be un-submitted.")) return redirect("relate-view_flow_page", - pctx.course.identifier, flow_session_id, ordinal) + pctx.course.identifier, flow_session_id, page_ordinal) if request.method == 'POST': form = UnsubmitFlowPageForm(request.POST) @@ -2901,7 +2901,7 @@ def view_unsubmit_flow_page(pctx, flow_session_id, ordinal): _("Flow page changes reallowed. ")) return redirect("relate-view_flow_page", - pctx.course.identifier, flow_session_id, ordinal) + pctx.course.identifier, flow_session_id, page_ordinal) else: form = UnsubmitFlowPageForm() diff --git a/course/grading.py b/course/grading.py index c7d42d76c4524814b8261fce91955f6c6c2ba54c..9ddca160348e8ffd28ef242757420ec89dd29225 100644 --- a/course/grading.py +++ b/course/grading.py @@ -81,7 +81,7 @@ def get_prev_visit_grades( return (FlowPageVisitGrade.objects .filter( visit__flow_session_id=flow_session_id, - visit__page_data__ordinal=page_ordinal, + visit__page_data__page_ordinal=page_ordinal, visit__is_submitted_answer=True) .order_by(*order_by_args) .select_related("visit")) @@ -157,8 +157,8 @@ def grade_flow_page(pctx, flow_session_id, page_ordinal): pctx.course.identifier, respect_preview=False) fpctx = FlowPageContext(pctx.repo, pctx.course, flow_session.flow_id, - page_ordinal, participation=flow_session.participation, - flow_session=flow_session, request=pctx.request) + page_ordinal, participation=flow_session.participation, + flow_session=flow_session, request=pctx.request) if fpctx.page_desc is None: raise http.Http404() @@ -369,7 +369,7 @@ def grade_flow_page(pctx, flow_session_id, page_ordinal): "flow_identifier": fpctx.flow_id, "flow_session": flow_session, "flow_desc": fpctx.flow_desc, - "ordinal": fpctx.ordinal, + "page_ordinal": fpctx.page_ordinal, "page_data": fpctx.page_data, "body": fpctx.page.body( @@ -454,7 +454,7 @@ def show_grader_statistics(pctx, flow_id): graders = set() - # tuples: (ordinal, id) + # tuples: (page_ordinal, id) pages = set() counts = {} @@ -463,7 +463,7 @@ def show_grader_statistics(pctx, flow_id): def commit_grade_info(grade): grader = grade.grader - page = (grade.visit.page_data.ordinal, + page = (grade.visit.page_data.page_ordinal, grade.visit.page_data.group_id + "/" + grade.visit.page_data.page_id) graders.add(grader) diff --git a/course/migrations/0107_rename_flowpagedata_ordinal_to_page_ordinal.py b/course/migrations/0107_rename_flowpagedata_ordinal_to_page_ordinal.py new file mode 100644 index 0000000000000000000000000000000000000000..b0d102eeba429cd0c45e83fbfde33919b50f1dc9 --- /dev/null +++ b/course/migrations/0107_rename_flowpagedata_ordinal_to_page_ordinal.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-19 02:37 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0106_add_auth_tokens_permission'), + ] + + operations = [ + migrations.RenameField( + model_name='flowpagedata', + old_name='ordinal', + new_name='page_ordinal', + ), + ] diff --git a/course/migrations/0108_alter_page_ordinal_verbose_name.py b/course/migrations/0108_alter_page_ordinal_verbose_name.py new file mode 100644 index 0000000000000000000000000000000000000000..db491409be66f085cc13f9621cf5713a2db9143a --- /dev/null +++ b/course/migrations/0108_alter_page_ordinal_verbose_name.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-19 02:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0107_rename_flowpagedata_ordinal_to_page_ordinal'), + ] + + operations = [ + migrations.AlterField( + model_name='flowpagedata', + name='page_ordinal', + field=models.IntegerField(blank=True, null=True, verbose_name='Page ordinal'), + ), + ] diff --git a/course/migrations/0109_add_manage_authentication_tokens_permssion.py b/course/migrations/0109_add_manage_authentication_tokens_permssion.py new file mode 100644 index 0000000000000000000000000000000000000000..8116b95dab45c141f51d17dc108c7efd98354a53 --- /dev/null +++ b/course/migrations/0109_add_manage_authentication_tokens_permssion.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-19 02:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0108_alter_page_ordinal_verbose_name'), + ] + + operations = [ + migrations.AlterField( + model_name='participationpermission', + name='permission', + field=models.CharField(choices=[('edit_course', 'Edit course'), ('use_admin_interface', 'Use admin interface'), ('manage_authentication_tokens', 'Manage authentication tokens'), ('impersonate_role', 'Impersonate role'), ('set_fake_time', 'Set fake time'), ('set_pretend_facility', 'Pretend to be in facility'), ('edit_course_permissions', 'Edit course permissions'), ('view_hidden_course_page', 'View hidden course page'), ('view_calendar', 'View calendar'), ('send_instant_message', 'Send instant message'), ('access_files_for', 'Access files for'), ('included_in_grade_statistics', 'Included in grade statistics'), ('skip_during_manual_grading', 'Skip during manual grading'), ('edit_exam', 'Edit exam'), ('issue_exam_ticket', 'Issue exam ticket'), ('batch_issue_exam_ticket', 'Batch issue exam ticket'), ('view_participant_masked_profile', "View participants' masked profile only"), ('view_flow_sessions_from_role', 'View flow sessions from role'), ('view_gradebook', 'View gradebook'), ('edit_grading_opportunity', 'Edit grading opportunity'), ('assign_grade', 'Assign grade'), ('view_grader_stats', 'View grader stats'), ('batch_import_grade', 'Batch-import grades'), ('batch_export_grade', 'Batch-export grades'), ('batch_download_submission', 'Batch-download submissions'), ('impose_flow_session_deadline', 'Impose flow session deadline'), ('batch_impose_flow_session_deadline', 'Batch-impose flow session deadline'), ('end_flow_session', 'End flow session'), ('batch_end_flow_session', 'Batch-end flow sessions'), ('regrade_flow_session', 'Regrade flow session'), ('batch_regrade_flow_session', 'Batch-regrade flow sessions'), ('recalculate_flow_session_grade', 'Recalculate flow session grade'), ('batch_recalculate_flow_session_grade', 'Batch-recalculate flow sesssion grades'), ('reopen_flow_session', 'Reopen flow session'), ('grant_exception', 'Grant exception'), ('view_analytics', 'View analytics'), ('preview_content', 'Preview content'), ('update_content', 'Update content'), ('use_markup_sandbox', 'Use markup sandbox'), ('use_page_sandbox', 'Use page sandbox'), ('test_flow', 'Test flow'), ('edit_events', 'Edit events'), ('query_participation', 'Query participation'), ('edit_participation', 'Edit participation'), ('preapprove_participation', 'Preapprove participation'), ('manage_instant_flow_requests', 'Manage instant flow requests')], db_index=True, max_length=200, verbose_name='Permission'), + ), + migrations.AlterField( + model_name='participationrolepermission', + name='permission', + field=models.CharField(choices=[('edit_course', 'Edit course'), ('use_admin_interface', 'Use admin interface'), ('manage_authentication_tokens', 'Manage authentication tokens'), ('impersonate_role', 'Impersonate role'), ('set_fake_time', 'Set fake time'), ('set_pretend_facility', 'Pretend to be in facility'), ('edit_course_permissions', 'Edit course permissions'), ('view_hidden_course_page', 'View hidden course page'), ('view_calendar', 'View calendar'), ('send_instant_message', 'Send instant message'), ('access_files_for', 'Access files for'), ('included_in_grade_statistics', 'Included in grade statistics'), ('skip_during_manual_grading', 'Skip during manual grading'), ('edit_exam', 'Edit exam'), ('issue_exam_ticket', 'Issue exam ticket'), ('batch_issue_exam_ticket', 'Batch issue exam ticket'), ('view_participant_masked_profile', "View participants' masked profile only"), ('view_flow_sessions_from_role', 'View flow sessions from role'), ('view_gradebook', 'View gradebook'), ('edit_grading_opportunity', 'Edit grading opportunity'), ('assign_grade', 'Assign grade'), ('view_grader_stats', 'View grader stats'), ('batch_import_grade', 'Batch-import grades'), ('batch_export_grade', 'Batch-export grades'), ('batch_download_submission', 'Batch-download submissions'), ('impose_flow_session_deadline', 'Impose flow session deadline'), ('batch_impose_flow_session_deadline', 'Batch-impose flow session deadline'), ('end_flow_session', 'End flow session'), ('batch_end_flow_session', 'Batch-end flow sessions'), ('regrade_flow_session', 'Regrade flow session'), ('batch_regrade_flow_session', 'Batch-regrade flow sessions'), ('recalculate_flow_session_grade', 'Recalculate flow session grade'), ('batch_recalculate_flow_session_grade', 'Batch-recalculate flow sesssion grades'), ('reopen_flow_session', 'Reopen flow session'), ('grant_exception', 'Grant exception'), ('view_analytics', 'View analytics'), ('preview_content', 'Preview content'), ('update_content', 'Update content'), ('use_markup_sandbox', 'Use markup sandbox'), ('use_page_sandbox', 'Use page sandbox'), ('test_flow', 'Test flow'), ('edit_events', 'Edit events'), ('query_participation', 'Query participation'), ('edit_participation', 'Edit participation'), ('preapprove_participation', 'Preapprove participation'), ('manage_instant_flow_requests', 'Manage instant flow requests')], db_index=True, max_length=200, verbose_name='Permission'), + ), + ] diff --git a/course/models.py b/course/models.py index f7165d86e8dd6ebdd983330bec79715a07562a3a..f33bf50c3711da9427f758188de01bbd79e99ad4 100644 --- a/course/models.py +++ b/course/models.py @@ -910,8 +910,8 @@ class FlowSession(models.Model): class FlowPageData(models.Model): flow_session = models.ForeignKey(FlowSession, related_name="page_data", verbose_name=_('Flow session'), on_delete=models.CASCADE) - ordinal = models.IntegerField(null=True, blank=True, - verbose_name=_('Ordinal')) + page_ordinal = models.IntegerField(null=True, blank=True, + verbose_name=_('Page ordinal')) # This exists to catch changing page types in course content, # which will generally lead to an inconsistency disaster. @@ -943,10 +943,10 @@ class FlowPageData(models.Model): def __unicode__(self): # flow page data return (_("Data for page '%(group_id)s/%(page_id)s' " - "(ordinal %(ordinal)s) in %(flow_session)s") % { + "(page ordinal %(page_ordinal)s) in %(flow_session)s") % { 'group_id': self.group_id, 'page_id': self.page_id, - 'ordinal': self.ordinal, + 'page_ordinal': self.page_ordinal, 'flow_session': self.flow_session}) if six.PY3: @@ -954,13 +954,13 @@ class FlowPageData(models.Model): # Django's templates are a little daft. No arithmetic--really? def previous_ordinal(self): - return self.ordinal - 1 + return self.page_ordinal - 1 def next_ordinal(self): - return self.ordinal + 1 + return self.page_ordinal + 1 def human_readable_ordinal(self): - return self.ordinal + 1 + return self.page_ordinal + 1 # }}} diff --git a/course/page/code.py b/course/page/code.py index c872326148ee04c741971760910a6960fb960905..32074af62a5f7af112e535afec4a815bdd1c32ff 100644 --- a/course/page/code.py +++ b/course/page/code.py @@ -43,6 +43,11 @@ from course.page.base import ( get_editor_interaction_mode) from course.constants import flow_permission +# DEBUGGING SWITCH: +# True for 'spawn containers' (normal operation) +# False for 'just connect to localhost:RUNPY_PORT' for runpy' +SPAWN_CONTAINERS_FOR_RUNPY = True + # {{{ python code question @@ -95,8 +100,7 @@ def request_python_run(run_req, run_timeout, image=None): docker_timeout = 15 - # DEBUGGING SWITCH: 1 for 'spawn container', 0 for 'static container' - if 1: + if SPAWN_CONTAINERS_FOR_RUNPY: docker_url = getattr(settings, "RELATE_DOCKER_URL", "unix://var/run/docker.sock") docker_tls = getattr(settings, "RELATE_DOCKER_TLS_CONFIG", diff --git a/course/page/code_runpy_backend.py b/course/page/code_runpy_backend.py index d7d1a7f72a911a48a8897976c97488e6d003d3e3..16cdf00fb04cf1ea00a99c42774d7e06eb9d6191 100644 --- a/course/page/code_runpy_backend.py +++ b/course/page/code_runpy_backend.py @@ -31,6 +31,8 @@ try: from .code_feedback import Feedback, GradingComplete except SystemError: from code_feedback import Feedback, GradingComplete # type: ignore +except ImportError: + from code_feedback import Feedback, GradingComplete # type: ignore __doc__ = """ diff --git a/course/page/text.py b/course/page/text.py index f568ceb236684542398c7006db9213bf02ccc6d2..dc5a5fb27f14bcb8c897e2d1d1a7c9b707de2c81 100644 --- a/course/page/text.py +++ b/course/page/text.py @@ -41,6 +41,8 @@ from course.page.base import ( import re import sys +CORRECT_ANSWER_PATTERN = string_concat(_("A correct answer is"), ": '%s'.") # noqa + class TextAnswerForm(StyledForm): # prevents form submission with codemirror's empty textarea @@ -961,8 +963,6 @@ class TextQuestion(TextQuestionBase, PageBaseWithValue): def correct_answer(self, page_context, page_data, answer_data, grade_data): # FIXME: Could use 'best' match to answer - CA_PATTERN = string_concat(_("A correct answer is"), ": '%s'.") # noqa - for matcher in self.matchers: unspec_correct_answer_text = matcher.correct_answer_text() if unspec_correct_answer_text is not None: @@ -970,7 +970,7 @@ class TextQuestion(TextQuestionBase, PageBaseWithValue): assert unspec_correct_answer_text - result = CA_PATTERN % unspec_correct_answer_text + result = CORRECT_ANSWER_PATTERN % unspec_correct_answer_text if hasattr(self.page_desc, "answer_explanation"): result += markup_to_html(page_context, self.page_desc.answer_explanation) diff --git a/course/templates/course/flow-confirm-completion.html b/course/templates/course/flow-confirm-completion.html index f2913372dc3d6b801ab0949dc49b6ba73971e871..3e5062566588e5b13a51ffe38d6e7c1259f64a4e 100644 --- a/course/templates/course/flow-confirm-completion.html +++ b/course/templates/course/flow-confirm-completion.html @@ -16,7 +16,7 @@