def test_language_override_course_has_force_lang(self): self.course.force_lang = "zh-hans" self.course.save() with LanguageOverride(course=self.course): self.assertEqual(translation.get_language(), "zh-hans") self.assertEqual(translation.get_language(), "ko")
def test_language_override_deactivate(self): self.course.force_lang = "zh-hans" self.course.save() with LanguageOverride(course=self.course, deactivate=True): self.assertEqual(translation.get_language(), "zh-hans") self.assertEqual(translation.ugettext("user"), u"用户") self.assertEqual(translation.get_language(), "en-us")
def test_language_override_no_course_force_lang_no_admin_lang(self): if self.course.force_lang: self.course.force_lang = "" self.course.save() with LanguageOverride(course=self.course): self.assertEqual(translation.get_language(), None) self.assertEqual(translation.ugettext("whatever"), "whatever") self.assertEqual(translation.get_language(), "en-us")
def test_language_override_no_course_force_lang(self): if self.course.force_lang: self.course.force_lang = "" self.course.save() with LanguageOverride(course=self.course): self.assertEqual(translation.get_language(), "de") self.assertEqual(translation.ugettext("user"), u"Benutzer") self.assertEqual(translation.get_language(), "ko") self.assertEqual(translation.ugettext("user"), u"사용자")
def test_language_override_no_course_force_lang_no_langcode(self): if self.course.force_lang: self.course.force_lang = "" self.course.save() translation.deactivate_all() with LanguageOverride(course=self.course): self.assertEqual(translation.get_language(), "de") self.assertEqual(translation.ugettext("user"), u"Benutzer") self.assertEqual(translation.get_language(), None) self.assertEqual(translation.ugettext("whatever"), "whatever")
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 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 grade_flow_page(pctx, flow_session_id, page_ordinal): # type: (CoursePageContext, int, int) -> http.HttpResponse now_datetime = get_now_or_fake_time(pctx.request) page_ordinal = int(page_ordinal) viewing_prev_grade = False prev_grade_id = pctx.request.GET.get("grade_id") if prev_grade_id is not None: try: prev_grade_id = int(prev_grade_id) viewing_prev_grade = True except ValueError: raise SuspiciousOperation("non-integer passed for 'grade_id'") if not pctx.has_permission(pperm.view_gradebook): raise PermissionDenied(_("may not view grade book")) flow_session = get_object_or_404(FlowSession, id=int(flow_session_id)) if flow_session.course.pk != pctx.course.pk: raise SuspiciousOperation( _("Flow session not part of specified course")) if flow_session.participation is None: raise SuspiciousOperation(_("Cannot grade anonymous session")) from course.flow import adjust_flow_session_page_data adjust_flow_session_page_data(pctx.repo, flow_session, 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) if fpctx.page_desc is None: raise http.Http404() assert fpctx.page is not None assert fpctx.page_context is not None # {{{ enable flow session zapping all_flow_sessions = list( FlowSession.objects.filter(course=pctx.course, flow_id=flow_session.flow_id, participation__isnull=False, in_progress=flow_session.in_progress). order_by( # Datatables will default to sorting the user list # by the first column, which happens to be the username. # Match that sorting. "participation__user__username", "start_time")) next_flow_session_id = None prev_flow_session_id = None for i, other_flow_session in enumerate(all_flow_sessions): if other_flow_session.pk == flow_session.pk: if i > 0: prev_flow_session_id = all_flow_sessions[i - 1].id if i + 1 < len(all_flow_sessions): next_flow_session_id = all_flow_sessions[i + 1].id # }}} prev_grades = get_prev_visit_grades(pctx.course_identifier, flow_session_id, page_ordinal) # {{{ reproduce student view form = None feedback = None answer_data = None grade_data = None shown_grade = None page_expects_answer = fpctx.page.expects_answer() if page_expects_answer: if fpctx.prev_answer_visit is not None and prev_grade_id is None: answer_data = fpctx.prev_answer_visit.answer shown_grade = fpctx.prev_answer_visit.get_most_recent_grade() if shown_grade is not None: feedback = get_feedback_for_grade(shown_grade) grade_data = shown_grade.grade_data else: feedback = None grade_data = None if shown_grade is not None: prev_grade_id = shown_grade.id elif prev_grade_id is not None: try: shown_grade = prev_grades.filter(id=prev_grade_id).get() except ObjectDoesNotExist: raise http.Http404() feedback = get_feedback_for_grade(shown_grade) grade_data = shown_grade.grade_data answer_data = shown_grade.visit.answer else: feedback = None from course.page.base import PageBehavior page_behavior = PageBehavior(show_correctness=True, show_answer=False, may_change_answer=False) try: form = fpctx.page.make_form(fpctx.page_context, fpctx.page_data.data, answer_data, page_behavior) except InvalidPageData as e: messages.add_message( pctx.request, messages.ERROR, _("The page data stored in the database was found " "to be invalid for the page as given in the " "course content. Likely the course content was " "changed in an incompatible way (say, by adding " "an option to a choice question) without changing " "the question ID. The precise error encountered " "was the following: ") + str(e)) return render_course_page(pctx, "course/course-base.html", {}) if form is not None: form_html = fpctx.page.form_to_html(pctx.request, fpctx.page_context, form, answer_data) else: form_html = None # }}} # {{{ grading form if (page_expects_answer and fpctx.page.is_answer_gradable() and fpctx.prev_answer_visit is not None and not flow_session.in_progress and not viewing_prev_grade): request = pctx.request if pctx.request.method == "POST": if not pctx.has_permission(pperm.assign_grade): raise PermissionDenied(_("may not assign grades")) grading_form = fpctx.page.post_grading_form( fpctx.page_context, fpctx.page_data, grade_data, request.POST, request.FILES) if grading_form.is_valid(): grade_data = fpctx.page.update_grade_data_from_grading_form_v2( request, fpctx.page_context, fpctx.page_data, grade_data, grading_form, request.FILES) from course.utils import LanguageOverride with LanguageOverride(pctx.course): feedback = fpctx.page.grade(fpctx.page_context, fpctx.page_data.data, answer_data, grade_data) if feedback is not None: correctness = feedback.correctness else: correctness = None feedback_json = None # type: Optional[Dict[Text, Any]] bulk_feedback_json = None # type: Optional[Dict[Text, Any]] if feedback is not None: feedback_json, bulk_feedback_json = feedback.as_json() else: feedback_json = bulk_feedback_json = None most_recent_grade = FlowPageVisitGrade( visit=fpctx.prev_answer_visit, grader=pctx.request.user, graded_at_git_commit_sha=pctx.course_commit_sha, grade_data=grade_data, max_points=fpctx.page.max_points(fpctx.page_data), correctness=correctness, feedback=feedback_json) prev_grade_id = _save_grade(fpctx, flow_session, most_recent_grade, bulk_feedback_json, now_datetime) else: grading_form = fpctx.page.make_grading_form( fpctx.page_context, fpctx.page_data, grade_data) else: grading_form = None grading_form_html = None # type: Optional[Text] if grading_form is not None: from crispy_forms.layout import Submit grading_form.helper.form_class += " relate-grading-form" grading_form.helper.add_input( Submit("submit", _("Submit"), accesskey="s", css_class="relate-grading-save-button")) grading_form_html = fpctx.page.grading_form_to_html( pctx.request, fpctx.page_context, grading_form, grade_data) # }}} # {{{ compute points_awarded max_points = None points_awarded = None if (page_expects_answer and fpctx.page.is_answer_gradable()): max_points = fpctx.page.max_points(fpctx.page_data) if feedback is not None and feedback.correctness is not None: points_awarded = max_points * feedback.correctness # }}} grading_rule = get_session_grading_rule(flow_session, fpctx.flow_desc, get_now_or_fake_time(pctx.request)) if grading_rule.grade_identifier is not None: grading_opportunity = get_flow_grading_opportunity( pctx.course, flow_session.flow_id, fpctx.flow_desc, grading_rule.grade_identifier, grading_rule. grade_aggregation_strategy) # type: Optional[GradingOpportunity] else: grading_opportunity = None return render_course_page( pctx, "course/grade-flow-page.html", { "flow_identifier": fpctx.flow_id, "flow_session": flow_session, "flow_desc": fpctx.flow_desc, "page_ordinal": fpctx.page_ordinal, "page_data": fpctx.page_data, "body": fpctx.page.body(fpctx.page_context, fpctx.page_data.data), "form": form, "form_html": form_html, "feedback": feedback, "max_points": max_points, "points_awarded": points_awarded, "shown_grade": shown_grade, "prev_grade_id": prev_grade_id, "expects_answer": page_expects_answer, "grading_opportunity": grading_opportunity, "prev_flow_session_id": prev_flow_session_id, "next_flow_session_id": next_flow_session_id, "grading_form": grading_form, "grading_form_html": grading_form_html, "correct_answer": fpctx.page.correct_answer(fpctx.page_context, fpctx.page_data.data, answer_data, grade_data), # Wrappers used by JavaScript template (tmpl) so as not to # conflict with Django template's tag wrapper "JQ_OPEN": '{%', 'JQ_CLOSE': '%}', })
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