def sign_in_by_email(request): if not settings.RELATE_SIGN_IN_BY_EMAIL_ENABLED: messages.add_message(request, messages.ERROR, _("Email-based sign-in is not being used")) return redirect("relate-sign_in_choice") if request.method == 'POST': form = SignInByEmailForm(request.POST) if form.is_valid(): email = form.cleaned_data["email"] user, created = get_user_model().objects.get_or_create( email__iexact=email, defaults=dict(username=email, email=email)) if created: user.set_unusable_password() user.status = user_status.unconfirmed, user.sign_in_key = make_sign_in_key(user) user.save() from relate.utils import render_email_template message = render_email_template("course/sign-in-email.txt", { "user": user, "sign_in_uri": request.build_absolute_uri( reverse( "relate-sign_in_stage2_with_token", args=(user.id, user.sign_in_key,))), "home_uri": request.build_absolute_uri(reverse("relate-home")) }) from django.core.mail import EmailMessage msg = EmailMessage( _("Your %(relate_site_name)s sign-in link") % {"relate_site_name": _(get_site_name())}, message, getattr(settings, "NO_REPLY_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [email]) from relate.utils import get_outbound_mail_connection msg.connection = ( get_outbound_mail_connection("no_reply") if hasattr(settings, "NO_REPLY_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() messages.add_message(request, messages.INFO, _("Email sent. Please check your email and click the link.")) return redirect("relate-home") else: form = SignInByEmailForm() return render(request, "course/login-by-email.html", { "form_description": "", "form": form })
def send_enrollment_decision(participation, approved, request=None): # type: (Participation, bool, http.HttpRequest) -> None with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): course = participation.course if request: course_uri = request.build_absolute_uri( reverse("relate-course_page", args=(course.identifier,))) else: # This will happen when this method is triggered by # a model signal which doesn't contain a request object. from six.moves.urllib.parse import urljoin course_uri = urljoin(getattr(settings, "RELATE_BASE_URL"), course.get_absolute_url()) from relate.utils import render_email_template message = render_email_template("course/enrollment-decision-email.txt", { "user": participation.user, "approved": approved, "course": course, "course_uri": course_uri }) from django.core.mail import EmailMessage email_kwargs = {} if settings.RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER: from_email = course.get_from_email() else: from_email = getattr(settings, "ENROLLMENT_EMAIL_FROM", settings.ROBOT_EMAIL_FROM) from relate.utils import get_outbound_mail_connection email_kwargs.update( {"connection": ( get_outbound_mail_connection("enroll") if hasattr(settings, "ENROLLMENT_EMAIL_FROM") else get_outbound_mail_connection("robot"))}) msg = EmailMessage( string_concat("[%s] ", _("Your enrollment request")) % course.identifier, message, from_email, [participation.user.email], **email_kwargs) msg.bcc = [course.notify_email] msg.send()
def send_enrollment_decision(participation, approved, request=None): # type: (Participation, bool, http.HttpRequest) -> None course = participation.course with LanguageOverride(course=course): if request: course_uri = request.build_absolute_uri( reverse("relate-course_page", args=(course.identifier,))) else: # This will happen when this method is triggered by # a model signal which doesn't contain a request object. from six.moves.urllib.parse import urljoin course_uri = urljoin(getattr(settings, "RELATE_BASE_URL"), course.get_absolute_url()) from relate.utils import render_email_template message = render_email_template("course/enrollment-decision-email.txt", { "user": participation.user, "approved": approved, "course": course, "course_uri": course_uri }) from django.core.mail import EmailMessage email_kwargs = {} if settings.RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER: from_email = course.get_from_email() else: from_email = getattr(settings, "ENROLLMENT_EMAIL_FROM", settings.ROBOT_EMAIL_FROM) from relate.utils import get_outbound_mail_connection email_kwargs.update( {"connection": ( get_outbound_mail_connection("enroll") if hasattr(settings, "ENROLLMENT_EMAIL_FROM") else get_outbound_mail_connection("robot"))}) msg = EmailMessage( string_concat("[%s] ", _("Your enrollment request")) % course.identifier, message, from_email, [participation.user.email], **email_kwargs) msg.bcc = [course.notify_email] msg.send()
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) user_code = answer_data["answer"] # {{{ request run run_req = {"compile_only": False, "user_code": user_code} def transfer_attr(name): if hasattr(self.page_desc, name): run_req[name] = getattr(self.page_desc, name) transfer_attr("setup_code") transfer_attr("names_for_user") transfer_attr("names_from_user") run_req["test_code"] = self.get_test_code() if hasattr(self.page_desc, "data_files"): run_req["data_files"] = {} from course.content import get_repo_blob for data_file in self.page_desc.data_files: from base64 import b64encode run_req["data_files"][data_file] = \ b64encode( get_repo_blob( page_context.repo, data_file, page_context.commit_sha).data).decode() try: response_dict = request_run_with_retries( run_req, run_timeout=self.page_desc.timeout, image=self.container_image) except Exception: from traceback import format_exc response_dict = { "result": "uncaught_error", "message": "Error connecting to container", "traceback": "".join(format_exc()), } # }}} feedback_bits = [] correctness = None if "points" in response_dict: correctness = response_dict["points"] try: feedback_bits.append("<p><b>%s</b></p>" % _(get_auto_feedback(correctness))) except Exception as e: correctness = None response_dict["result"] = "setup_error" response_dict["message"] = ("%s: %s" % (type(e).__name__, str(e))) # {{{ send email if the grading code broke if response_dict["result"] in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: error_msg_parts = ["RESULT: %s" % response_dict["result"]] for key, val in sorted(response_dict.items()): if (key not in ["result", "figures"] and val and isinstance(val, str)): error_msg_parts.append( "-------------------------------------") error_msg_parts.append(key) error_msg_parts.append( "-------------------------------------") error_msg_parts.append(val) error_msg_parts.append("-------------------------------------") error_msg_parts.append("user code") error_msg_parts.append("-------------------------------------") error_msg_parts.append(user_code) error_msg_parts.append("-------------------------------------") error_msg = "\n".join(error_msg_parts) from relate.utils import local_now, format_datetime_local from course.utils import LanguageOverride with LanguageOverride(page_context.course): from relate.utils import render_email_template message = render_email_template( "course/broken-code-question-email.txt", { "site": getattr(settings, "RELATE_BASE_URL"), "page_id": self.page_desc.id, "course": page_context.course, "error_message": error_msg, "review_uri": page_context.page_uri, "time": format_datetime_local(local_now()) }) if (not page_context.in_sandbox and not is_nuisance_failure(response_dict)): try: from django.core.mail import EmailMessage msg = EmailMessage( "".join([ "[%s:%s] ", _("code question execution failed") ]) % (page_context.course.identifier, page_context.flow_session.flow_id if page_context.flow_session is not None else _("<unknown flow>")), message, settings.ROBOT_EMAIL_FROM, [page_context.course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("robot") msg.send() except Exception: from traceback import format_exc feedback_bits.append( str( string_concat( "<p>", _("Both the grading code and the attempt to " "notify course staff about the issue failed. " "Please contact the course or site staff and " "inform them of this issue, mentioning this " "entire error message:"), "</p>", "<p>", _("Sending an email to the course staff about the " "following failure failed with " "the following error message:"), "<pre>", "".join(format_exc()), "</pre>", _("The original failure message follows:"), "</p>"))) # }}} if hasattr(self.page_desc, "correct_code"): def normalize_code(s): return (s.replace(" ", "").replace("\r", "").replace( "\n", "").replace("\t", "")) if (normalize_code(user_code) == normalize_code( self.page_desc.correct_code)): feedback_bits.append( "<p><b>%s</b></p>" % _("It looks like you submitted code that is identical to " "the reference solution. This is not allowed.")) from relate.utils import dict_to_struct response = dict_to_struct(response_dict) bulk_feedback_bits = [] if response.result == "success": pass elif response.result in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: feedback_bits.append("".join([ "<p>", _("The grading code failed. Sorry about that. " "The staff has been informed, and if this problem is " "due to an issue with the grading code, " "it will be fixed as soon as possible. " "In the meantime, you'll see a traceback " "below that may help you figure out what went wrong."), "</p>" ])) elif response.result == "timeout": feedback_bits.append("".join([ "<p>", _("Your code took too long to execute. The problem " "specifies that your code may take at most %s seconds " "to run. " "It took longer than that and was aborted."), "</p>" ]) % self.page_desc.timeout) correctness = 0 elif response.result == "user_compile_error": feedback_bits.append("".join([ "<p>", _("Your code failed to compile. An error message is " "below."), "</p>" ])) correctness = 0 elif response.result == "user_error": feedback_bits.append("".join([ "<p>", _("Your code failed with an exception. " "A traceback is below."), "</p>" ])) correctness = 0 else: raise RuntimeError("invalid run result: %s" % response.result) if hasattr(response, "feedback") and response.feedback: def sanitize(s): import bleach return bleach.clean(s, tags=["p", "pre"]) feedback_bits.append("".join([ "<p>", _("Here is some feedback on your code"), ":" "<ul>%s</ul></p>" ]) % "".join("<li>%s</li>" % sanitize(fb_item) for fb_item in response.feedback)) if hasattr(response, "traceback") and response.traceback: feedback_bits.append("".join([ "<p>", _("This is the exception traceback"), ":" "<pre>%s</pre></p>" ]) % escape(response.traceback)) if hasattr(response, "exec_host") and response.exec_host != "localhost": import socket try: exec_host_name, dummy, dummy = socket.gethostbyaddr( response.exec_host) except socket.error: exec_host_name = response.exec_host feedback_bits.append("".join( ["<p>", _("Your code ran on %s.") % exec_host_name, "</p>"])) if hasattr(response, "stdout") and response.stdout: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following output"), ":" "<pre>%s</pre></p>" ]) % escape(response.stdout)) if hasattr(response, "stderr") and response.stderr: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following error messages"), ":" "<pre>%s</pre></p>" ]) % escape(response.stderr)) if hasattr(response, "figures") and response.figures: fig_lines = [ "".join([ "<p>", _("Your code produced the following plots"), ":</p>" ]), '<dl class="result-figure-list">', ] for nr, mime_type, b64data in response.figures: if mime_type in ["image/jpeg", "image/png"]: fig_lines.extend([ "".join(["<dt>", _("Figure"), "%d<dt>"]) % nr, '<dd><img alt="Figure %d" src="data:%s;base64,%s"></dd>' % (nr, mime_type, b64data) ]) fig_lines.append("</dl>") bulk_feedback_bits.extend(fig_lines) # {{{ html output / santization if hasattr(response, "html") and response.html: def is_allowed_data_uri(allowed_mimetypes, uri): import re m = re.match(r"^data:([-a-z0-9]+/[-a-z0-9]+);base64,", uri) if not m: return False mimetype = m.group(1) return mimetype in allowed_mimetypes def sanitize(s): import bleach def filter_audio_attributes(tag, name, value): if name in ["controls"]: return True else: return False def filter_source_attributes(tag, name, value): if name in ["type"]: return True elif name == "src": if is_allowed_data_uri([ "audio/wav", ], value): return bleach.sanitizer.VALUE_SAFE else: return False else: return False def filter_img_attributes(tag, name, value): if name in ["alt", "title"]: return True elif name == "src": return is_allowed_data_uri([ "image/png", "image/jpeg", ], value) else: return False if not isinstance(s, str): return _("(Non-string in 'HTML' output filtered out)") return bleach.clean(s, tags=bleach.ALLOWED_TAGS + ["audio", "video", "source"], attributes={ "audio": filter_audio_attributes, "source": filter_source_attributes, "img": filter_img_attributes, }) bulk_feedback_bits.extend( sanitize(snippet) for snippet in response.html) # }}} return AnswerFeedback(correctness=correctness, feedback="\n".join(feedback_bits), bulk_feedback="\n".join(bulk_feedback_bits))
def enroll_view(request, course_identifier): # type: (http.HttpRequest, str) -> http.HttpResponse course = get_object_or_404(Course, identifier=course_identifier) user = request.user participations = Participation.objects.filter(course=course, user=user) if not participations.count(): participation = None else: participation = participations.first() if participation is not None: if participation.status == participation_status.requested: messages.add_message( request, messages.ERROR, _("You have previously sent the enrollment " "request. Re-sending the request is not " "allowed.")) return redirect("relate-course_page", course_identifier) elif participation.status == participation_status.denied: messages.add_message( request, messages.ERROR, _("Your enrollment request had been denied. " "Enrollment is not allowed.")) return redirect("relate-course_page", course_identifier) elif participation.status == participation_status.dropped: messages.add_message( request, messages.ERROR, _("You had been dropped from the course. " "Re-enrollment is not allowed.")) return redirect("relate-course_page", course_identifier) else: assert participation.status == participation_status.active messages.add_message(request, messages.ERROR, _("Already enrolled. Cannot re-enroll.")) return redirect("relate-course_page", course_identifier) if not course.accepts_enrollment: messages.add_message(request, messages.ERROR, _("Course is not accepting enrollments.")) return redirect("relate-course_page", course_identifier) if request.method != "POST": # This can happen if someone tries to refresh the page, or switches to # desktop view on mobile. messages.add_message(request, messages.ERROR, _("Can only enroll using POST request")) return redirect("relate-course_page", course_identifier) if user.status != user_status.active: messages.add_message( request, messages.ERROR, _("Your email address is not yet confirmed. " "Confirm your email to continue.")) return redirect("relate-course_page", course_identifier) preapproval = None if request.user.email: # pragma: no branch (user email NOT NULL constraint) try: preapproval = ParticipationPreapproval.objects.get( course=course, email__iexact=request.user.email) except ParticipationPreapproval.DoesNotExist: pass if preapproval is None: if user.institutional_id: if not (course.preapproval_require_verified_inst_id and not user.institutional_id_verified): try: preapproval = ParticipationPreapproval.objects.get( course=course, institutional_id__iexact=user.institutional_id) except ParticipationPreapproval.DoesNotExist: pass def email_suffix_matches(email, suffix): # type: (Text, Text) -> bool if suffix.startswith("@"): return email.endswith(suffix) else: return email.endswith("@%s" % suffix) or email.endswith( ".%s" % suffix) if (preapproval is None and course.enrollment_required_email_suffix and not email_suffix_matches( user.email, course.enrollment_required_email_suffix)): messages.add_message( request, messages.ERROR, _("Enrollment not allowed. Please use your '%s' email to " "enroll.") % course.enrollment_required_email_suffix) return redirect("relate-course_page", course_identifier) roles = ParticipationRole.objects.filter( course=course, is_default_for_new_participants=True) if preapproval is not None: roles = list(preapproval.roles.all()) try: if course.enrollment_approval_required and preapproval is None: participation = handle_enrollment_request( course, user, participation_status.requested, roles, request) assert participation is not None with LanguageOverride(course=course): from relate.utils import render_email_template message = render_email_template( "course/enrollment-request-email.txt", { "user": user, "course": course, "admin_uri": mark_safe( request.build_absolute_uri( reverse("relate-edit_participation", args=(course.identifier, participation.id)))) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] ", _("New enrollment request")) % course_identifier, message, getattr(settings, "ENROLLMENT_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = (get_outbound_mail_connection("enroll") if hasattr(settings, "ENROLLMENT_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() messages.add_message( request, messages.INFO, _("Enrollment request sent. You will receive notifcation " "by email once your request has been acted upon.")) else: handle_enrollment_request(course, user, participation_status.active, roles, request) messages.add_message(request, messages.SUCCESS, _("Successfully enrolled.")) except IntegrityError: messages.add_message( request, messages.ERROR, _("A participation already exists. Enrollment attempt aborted.")) return redirect("relate-course_page", course_identifier)
def test_context_is_none(self): with mock.patch("django.template.loader.render_to_string" ) as mock_render_to_string: render_email_template("abcd", context=None) self.assertDictEqual(mock_render_to_string.call_args[0][1], {'relate_site_name': 'RELATE'})
def reset_password(request, field="email"): if not settings.RELATE_REGISTRATION_ENABLED: raise SuspiciousOperation(_("self-registration is not enabled")) # return form class by string of class name ResetPasswordForm = globals()["ResetPasswordFormBy" + field.title()] # noqa if request.method == 'POST': form = ResetPasswordForm(request.POST) user = None if form.is_valid(): exist_users_with_same_email = False if field == "instid": inst_id = form.cleaned_data["instid"] try: user = get_user_model().objects.get( institutional_id__iexact=inst_id) except ObjectDoesNotExist: pass if field == "email": email = form.cleaned_data["email"] try: user = get_user_model().objects.get(email__iexact=email) except ObjectDoesNotExist: pass except MultipleObjectsReturned: exist_users_with_same_email = True if exist_users_with_same_email: # This is for backward compatibility. messages.add_message( request, messages.ERROR, _("Failed to send an email: multiple users were " "unexpectedly using that same " "email address. Please " "contact site staff.")) else: if user is None: FIELD_DICT = { # noqa "email": _("email address"), "instid": _("institutional ID") } messages.add_message( request, messages.ERROR, _("That %(field)s doesn't have an " "associated user account. Are you " "sure you've registered?") % {"field": FIELD_DICT[field]}) else: if not user.email: messages.add_message( request, messages.ERROR, _("The account with that institution ID " "doesn't have an associated email.")) else: email = user.email user.sign_in_key = make_sign_in_key(user) user.save() from relate.utils import render_email_template message = render_email_template( "course/sign-in-email.txt", { "user": user, "sign_in_uri": request.build_absolute_uri( reverse("relate-reset_password_stage2", args=( user.id, user.sign_in_key, ))), "home_uri": request.build_absolute_uri( reverse("relate-home")) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] " % _(get_site_name()), _("Password reset")), message, getattr(settings, "NO_REPLY_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [email]) from relate.utils import get_outbound_mail_connection msg.connection = ( get_outbound_mail_connection("no_reply") if hasattr(settings, "NO_REPLY_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() if field == "instid": messages.add_message( request, messages.INFO, _("The email address associated with that " "account is %s.") % masked_email(email)) messages.add_message( request, messages.INFO, _("Email sent. Please check your email and " "click the link.")) return redirect("relate-home") else: form = ResetPasswordForm() return render( request, "reset-passwd-form.html", { "field": field, "form_description": _("Password reset on %(site_name)s") % { "site_name": _(get_site_name()) }, "form": form })
def sign_up(request): if not settings.RELATE_REGISTRATION_ENABLED: raise SuspiciousOperation(_("self-registration is not enabled")) if request.method == 'POST': form = SignUpForm(request.POST) if form.is_valid(): if get_user_model().objects.filter( username=form.cleaned_data["username"]).count(): messages.add_message( request, messages.ERROR, _("A user with that username already exists.")) else: email = form.cleaned_data["email"] user = get_user_model()(email=email, username=form.cleaned_data["username"]) user.set_unusable_password() user.status = user_status.unconfirmed user.sign_in_key = make_sign_in_key(user) user.save() from relate.utils import render_email_template message = render_email_template( "course/sign-in-email.txt", { "user": user, "sign_in_uri": request.build_absolute_uri( reverse("relate-reset_password_stage2", args=( user.id, user.sign_in_key, )) + "?to_profile=1"), "home_uri": request.build_absolute_uri(reverse("relate-home")) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] " % _(get_site_name()), _("Verify your email")), message, getattr(settings, "NO_REPLY_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [email]) from relate.utils import get_outbound_mail_connection msg.connection = (get_outbound_mail_connection("no_reply") if hasattr(settings, "NO_REPLY_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() messages.add_message( request, messages.INFO, _("Email sent. Please check your email and click " "the link.")) return redirect("relate-home") else: if ("email" in form.errors and "That email address is already in use." in form.errors["email"]): messages.add_message( request, messages.ERROR, _("That email address is already in use. " "Would you like to " "<a href='%s'>reset your password</a> instead?") % reverse("relate-reset_password")) else: form = SignUpForm() return render(request, "generic-form.html", { "form_description": _("Sign up"), "form": form })
def update_grade_data_from_grading_form_v2(self, request, page_context, page_data, grade_data, grading_form, files_data): if grade_data is None: grade_data = {} for k in self.grade_data_attrs: if k == "grade_percent": grade_data[k] = grading_form.cleaned_percent() else: grade_data[k] = grading_form.cleaned_data[k] if grading_form.cleaned_data["notify"] and page_context.flow_session: from course.utils import LanguageOverride with LanguageOverride(page_context.course): from relate.utils import render_email_template from course.utils import will_use_masked_profile_for_email staff_email = [page_context.course.notify_email, request.user.email] message = render_email_template("course/grade-notify.txt", { "page_title": self.title(page_context, page_data), "course": page_context.course, "participation": page_context.flow_session.participation, "feedback_text": grade_data["feedback_text"], "flow_session": page_context.flow_session, "review_uri": page_context.page_uri, "use_masked_profile": will_use_masked_profile_for_email(staff_email) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%(identifier)s:%(flow_id)s] ", _("New notification")) % {'identifier': page_context.course.identifier, 'flow_id': page_context.flow_session.flow_id}, message, getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM", page_context.course.get_from_email()), [page_context.flow_session.participation.user.email]) msg.bcc = [page_context.course.notify_email] if grading_form.cleaned_data["may_reply"]: msg.reply_to = [request.user.email] if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"): from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("grader_feedback") msg.send() if (grading_form.cleaned_data["notes"] and grading_form.cleaned_data["notify_instructor"] and page_context.flow_session): from course.utils import LanguageOverride with LanguageOverride(page_context.course): from relate.utils import render_email_template from course.utils import will_use_masked_profile_for_email staff_email = [page_context.course.notify_email, request.user.email] use_masked_profile = will_use_masked_profile_for_email(staff_email) if use_masked_profile: username = ( page_context.flow_session.user.get_masked_profile()) else: username = ( page_context.flow_session.user.get_email_appellation()) message = render_email_template( "course/grade-internal-notes-notify.txt", { "page_title": self.title(page_context, page_data), "username": username, "course": page_context.course, "participation": page_context.flow_session.participation, "notes_text": grade_data["notes"], "flow_session": page_context.flow_session, "review_uri": page_context.page_uri, "sender": request.user, }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%(identifier)s:%(flow_id)s] ", _("Grading notes from %(ta)s")) % {'identifier': page_context.course.identifier, 'flow_id': page_context.flow_session.flow_id, 'ta': request.user.get_full_name() }, message, getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM", page_context.course.get_from_email()), [page_context.course.notify_email]) msg.bcc = [request.user.email] msg.reply_to = [request.user.email] if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"): from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("grader_feedback") msg.send() return grade_data
def enroll_view(request, course_identifier): # type: (http.HttpRequest, str) -> http.HttpResponse course = get_object_or_404(Course, identifier=course_identifier) user = request.user participations = Participation.objects.filter(course=course, user=user) if not participations.count(): participation = None else: participation = participations.first() if participation is not None: if participation.status == participation_status.requested: messages.add_message(request, messages.ERROR, _("You have previously sent the enrollment " "request. Re-sending the request is not " "allowed.")) return redirect("relate-course_page", course_identifier) elif participation.status == participation_status.denied: messages.add_message(request, messages.ERROR, _("Your enrollment request had been denied. " "Enrollment is not allowed.")) return redirect("relate-course_page", course_identifier) elif participation.status == participation_status.dropped: messages.add_message(request, messages.ERROR, _("You had been dropped from the course. " "Re-enrollment is not allowed.")) return redirect("relate-course_page", course_identifier) else: assert participation.status == participation_status.active messages.add_message(request, messages.ERROR, _("Already enrolled. Cannot re-enroll.")) return redirect("relate-course_page", course_identifier) if not course.accepts_enrollment: messages.add_message(request, messages.ERROR, _("Course is not accepting enrollments.")) return redirect("relate-course_page", course_identifier) if request.method != "POST": # This can happen if someone tries to refresh the page, or switches to # desktop view on mobile. messages.add_message(request, messages.ERROR, _("Can only enroll using POST request")) return redirect("relate-course_page", course_identifier) if user.status != user_status.active: messages.add_message(request, messages.ERROR, _("Your email address is not yet confirmed. " "Confirm your email to continue.")) return redirect("relate-course_page", course_identifier) preapproval = None if request.user.email: try: preapproval = ParticipationPreapproval.objects.get( course=course, email__iexact=request.user.email) except ParticipationPreapproval.DoesNotExist: pass if preapproval is None: if user.institutional_id: if not (course.preapproval_require_verified_inst_id and not user.institutional_id_verified): try: preapproval = ParticipationPreapproval.objects.get( course=course, institutional_id__iexact=user.institutional_id) except ParticipationPreapproval.DoesNotExist: pass def email_suffix_matches(email, suffix): # type: (Text, Text) -> bool if suffix.startswith("@"): return email.endswith(suffix) else: return email.endswith("@%s" % suffix) or email.endswith(".%s" % suffix) if (preapproval is None and course.enrollment_required_email_suffix and not email_suffix_matches( user.email, course.enrollment_required_email_suffix)): messages.add_message(request, messages.ERROR, _("Enrollment not allowed. Please use your '%s' email to " "enroll.") % course.enrollment_required_email_suffix) return redirect("relate-course_page", course_identifier) roles = ParticipationRole.objects.filter( course=course, is_default_for_new_participants=True) if preapproval is not None: roles = list(preapproval.roles.all()) try: if course.enrollment_approval_required and preapproval is None: participation = handle_enrollment_request( course, user, participation_status.requested, roles, request) assert participation is not None with LanguageOverride(course=course): from relate.utils import render_email_template message = render_email_template( "course/enrollment-request-email.txt", { "user": user, "course": course, "admin_uri": mark_safe( request.build_absolute_uri( reverse("relate-edit_participation", args=(course.identifier, participation.id)))) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] ", _("New enrollment request")) % course_identifier, message, getattr(settings, "ENROLLMENT_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = ( get_outbound_mail_connection("enroll") if hasattr(settings, "ENROLLMENT_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() messages.add_message(request, messages.INFO, _("Enrollment request sent. You will receive notifcation " "by email once your request has been acted upon.")) else: handle_enrollment_request(course, user, participation_status.active, roles, request) messages.add_message(request, messages.SUCCESS, _("Successfully enrolled.")) except IntegrityError: messages.add_message(request, messages.ERROR, _("A participation already exists. Enrollment attempt aborted.")) return redirect("relate-course_page", course_identifier)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) if grade_data is not None and not grade_data["released"]: grade_data = None code_feedback = PythonCodeQuestion.grade(self, page_context, page_data, answer_data, grade_data) human_points = self.human_feedback_point_value(page_context, page_data) code_points = self.page_desc.value - human_points correctness = None percentage = None if (code_feedback is not None and code_feedback.correctness is not None and grade_data is not None and grade_data["grade_percent"] is not None): code_feedback_percentage = 100 - self.human_feedback_percentage percentage = ( code_feedback.correctness * code_feedback_percentage + grade_data["grade_percent"] / 100 * self.human_feedback_percentage) correctness = percentage / 100 elif (self.human_feedback_percentage == 100 and grade_data is not None and grade_data["grade_percent"] is not None): correctness = grade_data["grade_percent"] / 100 percentage = correctness * 100 elif (self.human_feedback_percentage == 0 and code_feedback.correctness is not None): correctness = code_feedback.correctness percentage = correctness * 100 human_feedback_text = None human_feedback_points = None if grade_data is not None: if grade_data["feedback_text"] is not None: human_feedback_text = markup_to_html( page_context, grade_data["feedback_text"]) human_graded_percentage = grade_data["grade_percent"] if human_graded_percentage is not None: human_feedback_points = (human_graded_percentage / 100. * human_points) code_feedback_points = None if (code_feedback is not None and code_feedback.correctness is not None): code_feedback_points = code_feedback.correctness * code_points from relate.utils import render_email_template feedback = render_email_template( "course/feedback-code-with-human.html", { "percentage": percentage, "code_feedback": code_feedback, "code_feedback_points": code_feedback_points, "code_points": code_points, "human_feedback_text": human_feedback_text, "human_feedback_points": human_feedback_points, "human_points": human_points, }) return AnswerFeedback(correctness=correctness, feedback=feedback, bulk_feedback=code_feedback.bulk_feedback)
def test_context_is_none(self): with mock.patch( "django.template.loader.render_to_string") as mock_render_to_string: render_email_template("abcd", context=None) self.assertDictEqual(mock_render_to_string.call_args[0][1], {'relate_site_name': 'RELATE'})
def reset_password(request, field="email"): if not settings.RELATE_REGISTRATION_ENABLED: raise SuspiciousOperation( _("self-registration is not enabled")) # return form class by string of class name ResetPasswordForm = globals()["ResetPasswordFormBy" + field.title()] # noqa if request.method == 'POST': form = ResetPasswordForm(request.POST) user = None if form.is_valid(): exist_users_with_same_email = False if field == "instid": inst_id = form.cleaned_data["instid"] try: user = get_user_model().objects.get( institutional_id__iexact=inst_id) except ObjectDoesNotExist: pass if field == "email": email = form.cleaned_data["email"] try: user = get_user_model().objects.get(email__iexact=email) except ObjectDoesNotExist: pass except MultipleObjectsReturned: exist_users_with_same_email = True if exist_users_with_same_email: # This is for backward compatibility. messages.add_message(request, messages.ERROR, _("Failed to send an email: multiple users were " "unexpectedly using that same " "email address. Please " "contact site staff.")) else: if user is None: FIELD_DICT = { # noqa "email": _("email address"), "instid": _("institutional ID") } messages.add_message(request, messages.ERROR, _("That %(field)s doesn't have an " "associated user account. Are you " "sure you've registered?") % {"field": FIELD_DICT[field]}) else: if not user.email: messages.add_message(request, messages.ERROR, _("The account with that institution ID " "doesn't have an associated email.")) else: email = user.email user.sign_in_key = make_sign_in_key(user) user.save() from relate.utils import render_email_template message = render_email_template( "course/sign-in-email.txt", { "user": user, "sign_in_uri": request.build_absolute_uri( reverse( "relate-reset_password_stage2", args=(user.id, user.sign_in_key,))), "home_uri": request.build_absolute_uri( reverse("relate-home")) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] " % _(get_site_name()), _("Password reset")), message, getattr(settings, "NO_REPLY_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [email]) from relate.utils import get_outbound_mail_connection msg.connection = ( get_outbound_mail_connection("no_reply") if hasattr(settings, "NO_REPLY_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() if field == "instid": messages.add_message(request, messages.INFO, _("The email address associated with that " "account is %s.") % masked_email(email)) messages.add_message(request, messages.INFO, _("Email sent. Please check your email and " "click the link.")) return redirect("relate-home") else: form = ResetPasswordForm() return render(request, "reset-passwd-form.html", { "field": field, "form_description": _("Password reset on %(site_name)s") % {"site_name": _(get_site_name())}, "form": form })
def sign_up(request): if not settings.RELATE_REGISTRATION_ENABLED: raise SuspiciousOperation( _("self-registration is not enabled")) if request.method == 'POST': form = SignUpForm(request.POST) if form.is_valid(): if get_user_model().objects.filter( username=form.cleaned_data["username"]).count(): messages.add_message(request, messages.ERROR, _("A user with that username already exists.")) else: email = form.cleaned_data["email"] user = get_user_model()( email=email, username=form.cleaned_data["username"]) user.set_unusable_password() user.status = user_status.unconfirmed user.sign_in_key = make_sign_in_key(user) user.save() from relate.utils import render_email_template message = render_email_template("course/sign-in-email.txt", { "user": user, "sign_in_uri": request.build_absolute_uri( reverse( "relate-reset_password_stage2", args=(user.id, user.sign_in_key,)) + "?to_profile=1"), "home_uri": request.build_absolute_uri( reverse("relate-home")) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] " % _(get_site_name()), _("Verify your email")), message, getattr(settings, "NO_REPLY_EMAIL_FROM", settings.ROBOT_EMAIL_FROM), [email]) from relate.utils import get_outbound_mail_connection msg.connection = ( get_outbound_mail_connection("no_reply") if hasattr(settings, "NO_REPLY_EMAIL_FROM") else get_outbound_mail_connection("robot")) msg.send() messages.add_message(request, messages.INFO, _("Email sent. Please check your email and click " "the link.")) return redirect("relate-home") else: if ("email" in form.errors and "That email address is already in use." in form.errors["email"]): messages.add_message(request, messages.ERROR, _("That email address is already in use. " "Would you like to " "<a href='%s'>reset your password</a> instead?") % reverse( "relate-reset_password")) else: form = SignUpForm() return render(request, "generic-form.html", { "form_description": _("Sign up"), "form": form })
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) user_code = answer_data["answer"] # {{{ request run run_req = {"compile_only": False, "user_code": user_code} def transfer_attr(name): if hasattr(self.page_desc, name): run_req[name] = getattr(self.page_desc, name) transfer_attr("setup_code") transfer_attr("names_for_user") transfer_attr("names_from_user") run_req["test_code"] = self.get_test_code() if hasattr(self.page_desc, "data_files"): run_req["data_files"] = {} from course.content import get_repo_blob for data_file in self.page_desc.data_files: from base64 import b64encode run_req["data_files"][data_file] = \ b64encode( get_repo_blob( page_context.repo, data_file, page_context.commit_sha).data).decode() try: response_dict = request_python_run_with_retries(run_req, run_timeout=self.page_desc.timeout) except Exception: from traceback import format_exc response_dict = { "result": "uncaught_error", "message": "Error connecting to container", "traceback": "".join(format_exc()), } # }}} feedback_bits = [] correctness = None if "points" in response_dict: correctness = response_dict["points"] try: feedback_bits.append( "<p><b>%s</b></p>" % _(get_auto_feedback(correctness))) except Exception as e: correctness = None response_dict["result"] = "setup_error" response_dict["message"] = ( "%s: %s" % (type(e).__name__, str(e)) ) # {{{ send email if the grading code broke if response_dict["result"] in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error"]: error_msg_parts = ["RESULT: %s" % response_dict["result"]] for key, val in sorted(response_dict.items()): if (key not in ["result", "figures"] and val and isinstance(val, six.string_types)): error_msg_parts.append("-------------------------------------") error_msg_parts.append(key) error_msg_parts.append("-------------------------------------") error_msg_parts.append(val) error_msg_parts.append("-------------------------------------") error_msg_parts.append("user code") error_msg_parts.append("-------------------------------------") error_msg_parts.append(user_code) error_msg_parts.append("-------------------------------------") error_msg = "\n".join(error_msg_parts) from relate.utils import local_now, format_datetime_local from course.utils import LanguageOverride with LanguageOverride(page_context.course): from relate.utils import render_email_template message = render_email_template( "course/broken-code-question-email.txt", { "site": getattr(settings, "RELATE_BASE_URL"), "page_id": self.page_desc.id, "course": page_context.course, "error_message": error_msg, "review_uri": page_context.page_uri, "time": format_datetime_local(local_now()) }) if ( not page_context.in_sandbox and not is_nuisance_failure(response_dict)): try: from django.core.mail import EmailMessage msg = EmailMessage("".join(["[%s:%s] ", _("code question execution failed")]) % ( page_context.course.identifier, page_context.flow_session.flow_id if page_context.flow_session is not None else _("<unknown flow>")), message, settings.ROBOT_EMAIL_FROM, [page_context.course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("robot") msg.send() except Exception: from traceback import format_exc feedback_bits.append( six.text_type(string_concat( "<p>", _( "Both the grading code and the attempt to " "notify course staff about the issue failed. " "Please contact the course or site staff and " "inform them of this issue, mentioning this " "entire error message:"), "</p>", "<p>", _( "Sending an email to the course staff about the " "following failure failed with " "the following error message:"), "<pre>", "".join(format_exc()), "</pre>", _("The original failure message follows:"), "</p>"))) # }}} if hasattr(self.page_desc, "correct_code"): def normalize_code(s): return (s .replace(" ", "") .replace("\r", "") .replace("\n", "") .replace("\t", "")) if (normalize_code(user_code) == normalize_code(self.page_desc.correct_code)): feedback_bits.append( "<p><b>%s</b></p>" % _("It looks like you submitted code that is identical to " "the reference solution. This is not allowed.")) from relate.utils import dict_to_struct response = dict_to_struct(response_dict) bulk_feedback_bits = [] if response.result == "success": pass elif response.result in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error"]: feedback_bits.append("".join([ "<p>", _( "The grading code failed. Sorry about that. " "The staff has been informed, and if this problem is " "due to an issue with the grading code, " "it will be fixed as soon as possible. " "In the meantime, you'll see a traceback " "below that may help you figure out what went wrong." ), "</p>"])) elif response.result == "timeout": feedback_bits.append("".join([ "<p>", _( "Your code took too long to execute. The problem " "specifies that your code may take at most %s seconds " "to run. " "It took longer than that and was aborted." ), "</p>"]) % self.page_desc.timeout) correctness = 0 elif response.result == "user_compile_error": feedback_bits.append("".join([ "<p>", _("Your code failed to compile. An error message is " "below."), "</p>"])) correctness = 0 elif response.result == "user_error": feedback_bits.append("".join([ "<p>", _("Your code failed with an exception. " "A traceback is below."), "</p>"])) correctness = 0 else: raise RuntimeError("invalid runpy result: %s" % response.result) if hasattr(response, "feedback") and response.feedback: def sanitize(s): import bleach return bleach.clean(s, tags=["p", "pre"]) feedback_bits.append("".join([ "<p>", _("Here is some feedback on your code"), ":" "<ul>%s</ul></p>"]) % "".join( "<li>%s</li>" % sanitize(fb_item) for fb_item in response.feedback)) if hasattr(response, "traceback") and response.traceback: feedback_bits.append("".join([ "<p>", _("This is the exception traceback"), ":" "<pre>%s</pre></p>"]) % escape(response.traceback)) if hasattr(response, "exec_host") and response.exec_host != "localhost": import socket try: exec_host_name, dummy, dummy = socket.gethostbyaddr( response.exec_host) except socket.error: exec_host_name = response.exec_host feedback_bits.append("".join([ "<p>", _("Your code ran on %s.") % exec_host_name, "</p>"])) if hasattr(response, "stdout") and response.stdout: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following output"), ":" "<pre>%s</pre></p>"]) % escape(response.stdout)) if hasattr(response, "stderr") and response.stderr: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following error messages"), ":" "<pre>%s</pre></p>"]) % escape(response.stderr)) if hasattr(response, "figures") and response.figures: fig_lines = ["".join([ "<p>", _("Your code produced the following plots"), ":</p>"]), '<dl class="result-figure-list">', ] for nr, mime_type, b64data in response.figures: if mime_type in ["image/jpeg", "image/png"]: fig_lines.extend([ "".join([ "<dt>", _("Figure"), "%d<dt>"]) % nr, '<dd><img alt="Figure %d" src="data:%s;base64,%s"></dd>' % (nr, mime_type, b64data)]) fig_lines.append("</dl>") bulk_feedback_bits.extend(fig_lines) # {{{ html output / santization if hasattr(response, "html") and response.html: def is_allowed_data_uri(allowed_mimetypes, uri): import re m = re.match(r"^data:([-a-z0-9]+/[-a-z0-9]+);base64,", uri) if not m: return False mimetype = m.group(1) return mimetype in allowed_mimetypes def sanitize(s): import bleach def filter_audio_attributes(tag, name, value): if name in ["controls"]: return True else: return False def filter_source_attributes(tag, name, value): if name in ["type"]: return True elif name == "src": if is_allowed_data_uri([ "audio/wav", ], value): return bleach.sanitizer.VALUE_SAFE else: return False else: return False def filter_img_attributes(tag, name, value): if name in ["alt", "title"]: return True elif name == "src": return is_allowed_data_uri([ "image/png", "image/jpeg", ], value) else: return False if not isinstance(s, six.text_type): return _("(Non-string in 'HTML' output filtered out)") return bleach.clean(s, tags=bleach.ALLOWED_TAGS + ["audio", "video", "source"], attributes={ "audio": filter_audio_attributes, "source": filter_source_attributes, "img": filter_img_attributes, }) bulk_feedback_bits.extend( sanitize(snippet) for snippet in response.html) # }}} return AnswerFeedback( correctness=correctness, feedback="\n".join(feedback_bits), bulk_feedback="\n".join(bulk_feedback_bits))