Commit dd6be839 by sjang92

Merge pull request #94 from Stanford-Online/sjang92/enrollment_email_fix

Fix Enrollment Email UI/UX
parents 5c64b239 2456063f
......@@ -43,7 +43,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
self.assertFalse(details.enable_enrollment_email, "Enrollment Email should be initialized as false")
self.assertTrue(details.enable_default_enrollment_email, "Default Template option for enrollment email should be initialized as true")
def test_encoder(self):
details = CourseDetails.fetch(self.course.id)
......
......@@ -14,14 +14,16 @@ from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse
from smtplib import SMTPException
from util.json_request import JsonResponse
from edxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response, render_to_string
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs
from xmodule.partitions.partitions import UserPartition, Group
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from opaque_keys import InvalidKeyError
......@@ -68,8 +70,8 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
'grading_handler',
'advanced_settings_handler',
'textbooks_list_handler', 'textbooks_detail_handler',
'group_configurations_list_handler', 'group_configurations_detail_handler']
'group_configurations_list_handler', 'group_configurations_detail_handler',
'send_test_enrollment_email']
class AccessListFallback(Exception):
"""
......@@ -468,6 +470,23 @@ def course_info_update_handler(request, course_key_string, provided_id=None):
)
@require_http_methods("POST")
def send_test_enrollment_email(request):
"""
Handles ajax request for sending test enrollment emails to the instructor
"""
user = request.user
from_address = microsite.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
subject = request.POST.get('subject')
message = request.POST.get('message')
try:
user.email_user(subject, message, from_address)
except SMTPException:
return HttpResponseBadRequest(_("Error while sending test email."))
return HttpResponse()
@login_required
@ensure_csrf_cookie
@require_http_methods(("GET", "PUT", "POST"))
......@@ -496,6 +515,9 @@ def settings_handler(request, course_key_string):
short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True)
default_enroll_email_template_pre = render_to_string('emails/default_pre_enrollment_message.txt', {})
default_enroll_email_template_post = render_to_string('emails/default_post_enrollment_message.txt', {})
return render_to_response('settings.html', {
'context_course': course_module,
'course_locator': course_key,
......@@ -504,7 +526,9 @@ def settings_handler(request, course_key_string):
'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable,
'upload_asset_url': upload_asset_url
'upload_asset_url': upload_asset_url,
'default_pre_template': default_enroll_email_template_pre,
'default_post_template': default_enroll_email_template_post,
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
......
......@@ -10,12 +10,13 @@ from contentstore.utils import course_image_url
from models.settings import course_grading
from xmodule.fields import Date
from xmodule.modulestore.django import modulestore
from edxmako.shortcuts import render_to_string
class CourseDetails(object):
def __init__(self, org, course_id, run):
# still need these for now b/c the client's screen shows these 3 fields
self.org = org
self.course_id = course_id
self.course_id = course_id # This actually holds the course number.
self.run = run
self.start_date = None # 'start'
self.end_date = None # 'end'
......@@ -24,16 +25,15 @@ class CourseDetails(object):
self.syllabus = None # a pdf file asset
self.short_description = ""
self.overview = "" # html to render as the overview
self.pre_enrollment_email = "" # html to render as the pre-enrollment email
self.post_enrollment_email = "" # html to render as the post-enrollment email
self.pre_enrollment_email_subject = "" # header of the pre_enrollment_email
self.post_enrollment_email_subject = "" # header of the post_enrollment_email
self.pre_enrollment_email = render_to_string('emails/default_pre_enrollment_message.txt', {})
self.post_enrollment_email = render_to_string('emails/default_post_enrollment_message.txt', {})
self.pre_enrollment_email_subject = "Thanks for Enrolling in {}".format(self.course_id)
self.post_enrollment_email_subject = "Thanks for Enrolling in {}".format(self.course_id)
self.intro_video = None # a video pointer
self.effort = None # int hours/week
self.course_image_name = ""
self.course_image_asset_path = "" # URL of the course image
self.enable_enrollment_email = False
self.enable_default_enrollment_email = True
@classmethod
def fetch(cls, course_key):
......@@ -50,7 +50,6 @@ class CourseDetails(object):
course_details.course_image_name = descriptor.course_image
course_details.course_image_asset_path = course_image_url(descriptor)
course_details.enable_enrollment_email = descriptor.enable_enrollment_email
course_details.enable_default_enrollment_email = descriptor.enable_default_enrollment_email
temploc = course_key.make_usage_key('about', 'syllabus')
try:
......@@ -140,11 +139,6 @@ class CourseDetails(object):
# into the model is nasty, convert the JSON Date to a Python date, which is what the
# setter expects as input.
date = Date()
# Added to allow admins to enable/disable default enrollment emails
if 'enable_default_enrollment_email' in jsondict:
descriptor.enable_default_enrollment_email = jsondict['enable_default_enrollment_email']
dirty = True
# Added to allow admins to enable/disable enrollment emails
if 'enable_enrollment_email' in jsondict:
......
......@@ -26,7 +26,8 @@ class CourseMetadata(object):
'name', # from xblock
'tags', # from xblock
'video_speed_optimizations',
'visible_to_staff_only'
'visible_to_staff_only',
'enable_enrollment_email'
]
@classmethod
......
......@@ -13,14 +13,17 @@ var DetailsView = ValidatingView.extend({
'click .remove-course-introduction-video' : "removeVideo",
'focus #course-overview' : "codeMirrorize",
'click #enable-enrollment-email' : "toggleEnrollmentEmails",
'click #enable-default-enrollment-email' : "toggleDefaultEnrollmentEmails",
'focus #pre-enrollment-email' : "codeMirrorize",
'focus #post-enrollment-email' : "codeMirrorize",
'click #test_email_pre': "sendTestEmail",
'click #test_email_post': "sendTestEmail",
'click #fill_default_email_pre': "showDefaultTemplate",
'click #fill_default_email_post': "showDefaultTemplate",
'mouseover #timezone' : "updateTime",
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
'focus :input' : "inputFocus",
'blur :input' : "inputUnfocus",
'click .action-upload-image': "uploadImage"
'click .action-upload-image': "uploadImage",
},
initialize : function() {
......@@ -46,6 +49,8 @@ var DetailsView = ValidatingView.extend({
this.selectorToField = _.invert(this.fieldToSelectorMap);
/* Memoize html elements for enrollment emails */
this.enrollment_email_settings = this.$el.find('#enrollment-email-settings');
this.custom_enrollment_email_settings = this.$el.find('#custom-enrollment-email-settings');
this.pre_enrollment_email_elem = this.$el.find('#' + this.fieldToSelectorMap['pre_enrollment_email']);
this.pre_enrollment_email_subject_elem = this.$el.find('#' + this.fieldToSelectorMap['pre_enrollment_email_subject']);
......@@ -58,7 +63,9 @@ var DetailsView = ValidatingView.extend({
this.post_enrollment_email_subject_field = this.$el.find('#field-post-enrollment-email-subject');
this.enable_enrollment_email_box = this.$el.find('#' + this.fieldToSelectorMap['enable_enrollment_email'])[0];
this.enable_default_enrollment_email_box = this.$el.find('#' + this.fieldToSelectorMap['enable_default_enrollment_email'])[0];
this.default_pre_template = this.$el.find('#default_pre_enrollment_email_template');
this.default_post_template = this.$el.find('#default_post_enrollment_email_template');
},
render: function() {
......@@ -81,18 +88,10 @@ var DetailsView = ValidatingView.extend({
this.enable_enrollment_email_box.checked = this.model.get('enable_enrollment_email');
this.enable_default_enrollment_email_box.checked = this.model.get('enable_default_enrollment_email');
if (this.model.get('enable_enrollment_email')) {
this.pre_enrollment_email_field.show();
this.post_enrollment_email_field.show();
this.pre_enrollment_email_subject_field.show();
this.post_enrollment_email_subject_field.show();
if (this.enable_enrollment_email_box.checked) {
this.enrollment_email_settings.show();
} else {
this.pre_enrollment_email_field.hide();
this.post_enrollment_email_field.hide();
this.pre_enrollment_email_subject_field.hide();
this.post_enrollment_email_subject_field.hide();
this.enrollment_email_settings.hide();
}
this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description'));
......@@ -204,7 +203,7 @@ var DetailsView = ValidatingView.extend({
break;
case 'pre-enrollment-email-subject':
this.setField(event);
break;
break;
case 'post-enrollment-email-subject':
this.setField(event);
break;
......@@ -245,21 +244,11 @@ var DetailsView = ValidatingView.extend({
toggleEnrollmentEmails: function(event) {
var isChecked = this.enable_enrollment_email_box.checked;
/* enable & disable default will show the template */
if(isChecked) {
this.pre_enrollment_email_field.show();
this.post_enrollment_email_field.show();
this.pre_enrollment_email_subject_field.show();
this.post_enrollment_email_subject_field.show();
this.enrollment_email_settings.slideDown();
} else {
this.pre_enrollment_email_field.hide();
this.post_enrollment_email_field.hide();
this.pre_enrollment_email_subject_field.hide();
this.post_enrollment_email_subject_field.hide();
/* If enrollment email sending option is turned off, default email option should be turned off as well. */
this.enable_default_enrollment_email_box.checked = false;
this.setAndValidate(this.selectorToField['enable-default-enrollment-email'], false);
this.enrollment_email_settings.slideUp();
}
var field = this.selectorToField['enable-enrollment-email'];
......@@ -268,14 +257,6 @@ var DetailsView = ValidatingView.extend({
}
},
toggleDefaultEnrollmentEmails: function(event) {
var isChecked = this.enable_default_enrollment_email_box.checked;
var field = this.selectorToField['enable-default-enrollment-email'];
if (this.model.get(field) != isChecked) {
this.setAndValidate(field, isChecked);
}
},
codeMirrors : {},
codeMirrorize: function (e, forcedTarget) {
var thisTarget;
......@@ -368,6 +349,57 @@ var DetailsView = ValidatingView.extend({
}
});
modal.show();
},
sendTestEmail: function (event) {
event.preventDefault();
var email_type = event.target.id;
var subject = "";
var message = "";
if (email_type === "test_email_pre") {
subject = this.pre_enrollment_email_subject_elem.val();
message = this.pre_enrollment_email_elem.val();
} else {
subject = this.post_enrollment_email_subject_elem.val();
message = this.post_enrollment_email_elem.val();
}
$.post("/settings/send_test_enrollment_email",
{
subject: subject,
message:message,org:this.model.get('org'),
number: this.model.get('course_id'),
run: this.model.get('run'),
},
function (data) {
alert(gettext("Test email sent! Please check your inbox. Don't forget to save!"));
}
);
},
showDefaultTemplate: function (event) {
event.preventDefault();
var content = "";
var codeMirrorItem;
var oldContent = "";
var target_id = event.target.id;
if (target_id === "fill_default_email_pre") {
content = $('#default_pre_enrollment_email_template').text();
codeMirrorItem = this.codeMirrors[this.pre_enrollment_email_elem[0].id];
oldContent = codeMirrorItem.getValue();
} else {
content = $('#default_post_enrollment_email_template').text();
codeMirrorItem = this.codeMirrors[this.post_enrollment_email_elem[0].id];
oldContent = codeMirrorItem.getValue();
}
if (oldContent.trim() !== "") {
var confirmed = confirm(gettext("This will overwrite the current message with the default one. Do you wish to continue?"));
if (!confirmed) return;
}
codeMirrorItem.setValue(content);
}
});
......
......@@ -119,6 +119,17 @@
font-weight: 400;
}
.send-test-email {
@extend %ui-btn-flat-outline;
float: right;
}
.fill-default-email {
@extend %ui-btn-flat-outline;
float: right;
margin-right: 10px;
}
.new-button {
// @extend %t-action3; - bad buttons won't render this properly
@include font-size(14);
......@@ -133,6 +144,11 @@
.field {
margin: 0 0 ($baseline*2) 0;
&.text .subject {
height: 40px;
resize: none;
}
&:last-child {
margin-bottom: 0;
}
......
......@@ -225,53 +225,6 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
</li>
% endif
<li class="field text" id="field-enable-enrollment-email">
<label for="course-email">${_("Course Enrollment Email")}</label>
<input type="checkbox" id="enable-enrollment-email"/>
<label for="enable-enrollment-email">${_("Enable enrollment emails")}</label>
</li>
<li class="field text" id="field-enable-default-enrollment-email">
<input type="checkbox" id="enable-default-enrollment-email"/>
<label for="enable-enrollment-email">${_("Use default enrollment emails")}</label>
</li>
<li class="field text" id="field-pre-enrollment-email-subject">
<label for="pre-enrollment-email-subject">${_("Subject of the Email sent to students who enroll before the course starts")}</label>
<textarea class = 'text' id="pre-enrollment-email-subject"></textarea>
</li>
<li class="field text" id="field-pre-enrollment-email">
<label for="pre-enrollment-email">${_("Email sent to students who enroll before the course starts")}</label>
<textarea class="tinymce text-editor" id="pre-enrollment-email"></textarea>
<span class="tip tip-stacked">${_("This email will be sent to any student who enrolls in the course before its start date")}</span>
<ul class="list-actions">
<li class="action-item">
<a title="${_('Send me a copy of this via email')}"
href="" class="action action-primary">
${_("Send me a test email")}</a>
</li>
</ul>
</li>
<li class="field text" id="field-post-enrollment-email-subject">
<label for="post-enrollment-email-subject">${_("Subject of the Email sent to students who enroll after the course starts")}</label>
<textarea class = 'text' id="post-enrollment-email-subject"></textarea>
</li>
<li class="field text" id="field-post-enrollment-email">
<label for="post-enrollment-email">${_("Email sent to students who enroll after the course starts")}</label>
<textarea class="tinymce text-editor" id="post-enrollment-email"></textarea>
<span class="tip tip-stacked">${_("This email will be sent to any student who enrolls in the course after its start date")}</span>
<ul class="list-actions">
<li class="action-item">
<a title="${_('Send me a copy of this via email')}"
href="" class="action action-primary">
${_("Send me a test email")}</a>
</li>
</ul>
</li>
<li class="field image" id="field-course-image">
<label>${_("Course Image")}</label>
<div class="current current-course-image">
......@@ -322,6 +275,65 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
</ol>
</section>
<hr class="divide" />
<section class="group-settings enrollment-email">
<header>
<h2 class="title-2">${_("Course Enrollment Email")}</h2>
<span class="tip">${_("Settings for emails that students receive upon course enrollment")}</span>
</header>
<section class="group-settings">
<p><strong>By default, this feature is disabled.</strong> You can either use the default email content or create your own.<br />
Students enrolling right on the start date will receive the post-course-start email.<br />
</p>
<input type="checkbox" id="enable-enrollment-email"/><!-- IMPORTANT -->
<label for="enable-enrollment-email">${_("Enable enrollment emails")}</label>
</section>
<section class="group-settings" id="enrollment-email-settings"><!-- IMPORTANT -->
<section class="group-settings" id="custom-enrollment-email-settings"><!-- IMPORTANT -->
<section class="group-settings">
<p>
${_("Message for students who enroll {strong_start}before the start date{strong_end}").format(strong_start="<strong>", strong_end="</strong>")}
<a class="send-test-email" id='test_email_pre' title="${_('Send me a copy of this via email')}" href="#">${_("Send me a test email")}</a>
<a class="fill-default-email" id='fill_default_email_pre' title="${_('Reset to default content')}" href="#">${_("Reset to default content")}</a>
</p>
<ol class="list-input">
<li class="field text">
<p>
<label for="pre-enrollment-email-subject">${_("Subject of the Email")}</label>
<input type="text" class='text subject' id="pre-enrollment-email-subject"/>
</p>
<p>
<label for="pre-enrollment-email">${_("Body of the Email")}</label>
<textarea class="tinymce text-editor" id="pre-enrollment-email"></textarea>
<span class="tip tip-stacked">${_("This email will be sent to any student who enrolls in the course before its start date.")}</span>
</p>
</li>
</ol>
</section>
<section class="group-settings">
<p>
${_("Message for students who enroll {strong_start}on or after the start date{strong_end}").format(strong_start="<strong>", strong_end="</strong>")}
<a class="send-test-email" id='test_email_post' title="${_('Send me a copy of this via email')}" href="#">${_("Send me a test email")}</a>
<a class="fill-default-email" id='fill_default_email_post' title="${_('Reset to default content')}" href="#">${_("Reset to default content")}</a>
</p>
<ol class="list-input">
<li class="field text">
<p>
<label for="post-enrollment-email-subject">${_("Subject of the Email")}</label>
<input type="text" class='text subject' id="post-enrollment-email-subject"/>
</p>
<p>
<label for="post-enrollment-email">${_("Body of the Email")}</label>
<textarea class="tinymce text-editor" id="post-enrollment-email"></textarea>
<span class="tip tip-stacked">${_("This email will be sent to any student who enrolls in the course after its start date.")}</span>
</p>
</li>
</ol>
</section>
</section>
</section>
</section>
% if about_page_editable:
<hr class="divide" />
......@@ -372,5 +384,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
</div>
</aside>
</section>
<div id="default_pre_enrollment_email_template" style="display:none;">${default_pre_template}</div>
<div id="default_post_enrollment_email_template" style="display:none;">${default_post_template}</div>
</div>
</%block>
......@@ -91,6 +91,7 @@ urlpatterns += patterns(
url(r'^settings/details/{}$'.format(settings.COURSE_KEY_PATTERN), 'settings_handler'),
url(r'^settings/grading/{}(/)?(?P<grader_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'grading_handler'),
url(r'^settings/advanced/{}$'.format(settings.COURSE_KEY_PATTERN), 'advanced_settings_handler'),
url(r'^settings/send_test_enrollment_email$', 'send_test_enrollment_email', name='send_test_enrollment_email'),
url(r'^textbooks/{}$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_list_handler'),
url(r'^textbooks/{}/(?P<textbook_id>\d[^/]*)$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_detail_handler'),
)
......
......@@ -88,20 +88,10 @@ class EnrollmentEmailTests(TestCase):
def test_custom_enrollment_email_sent(self):
""" Test sending of enrollment emails when enable_default_enrollment_email setting is disabled. """
self.course.enable_enrollment_email = True
self.course.enable_default_enrollment_email = False
email_result = self.send_enrollment_email()
self.assertNotIn('email_did_fire', email_result)
self.assertIn('is_success', email_result)
def test_default_enrollment_email_sent(self):
""" Test sending of enrollment emails when enable_default_enrollment_email setting is enabled. """
self.course.enable_enrollment_email = True
self.course.enable_default_enrollment_email = True
email_result = self.send_enrollment_email()
self.assertNotIn('email_did_fire', email_result)
self.assertIn('is_success', email_result)
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
@patch('django.contrib.auth.models.User.email_user')
class ReactivationEmailTests(EmailTestMixin, TestCase):
......
......@@ -60,6 +60,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore import ModuleStoreEnum
from xmodule.course_module import CourseDescriptor
from collections import namedtuple
......@@ -738,37 +739,23 @@ def notify_enrollment_by_email(course, user, request):
from_address = microsite.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
try:
if (not course.enable_default_enrollment_email):
# Check if the course has already started and set subject & message accordingly
if (course.has_started):
subject = get_course_about_section(course, 'post_enrollment_email_subject')
message = get_course_about_section(course, 'post_enrollment_email')
else:
subject = get_course_about_section(course, 'pre_enrollment_email_subject')
message = get_course_about_section(course, 'pre_enrollment_email')
# Check if the course has already started and set subject & message accordingly
if course.has_started():
subject = get_course_about_section(course, 'post_enrollment_email_subject')
message = get_course_about_section(course, 'post_enrollment_email')
else:
# If not default, use the default emailing template
course_url = reverse('info', args=(course.id.to_deprecated_string(),))
context = {
'course':course,
'full_name':user.profile.name,
'site_name': microsite.get_value('SITE_NAME', settings.SITE_NAME),
'course_url':request.build_absolute_uri(course_url),
}
subject = render_to_string('emails/enroll_email_enrolledsubject.txt', context)
message = render_to_string('emails/enroll_email_enrolledmessage.txt', context)
subject = ''.join(subject.splitlines())
subject = get_course_about_section(course, 'pre_enrollment_email_subject')
message = get_course_about_section(course, 'pre_enrollment_email')
user.email_user(subject, message, from_address)
except Exception:
log.error('unable to send course enrollment verification email to user from "{from_address}"'.format(
from_address=from_address), exc_info = True)
return JsonResponse({"is_success": False, "error": _("Could not send enrollment email to the user"),})
return JsonResponse({"is_success": True, "subject": subject, "message": message})
else:
return JsonResponse({"email_did_fire": False})
......
......@@ -171,7 +171,6 @@ class CourseFields(object):
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
enable_enrollment_email = Boolean(help="Whether to send notification email upon enrollment or not", default=False, scope=Scope.settings)
enable_default_enrollment_email = Boolean(help="Whether to use default enrollment email for enrollment notification", default=True, scope=Scope.settings)
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
start = Date(help="Start time when this module is visible",
......
Hi there!
Thank you for enrolling in a Stanford Online course. You are receiving this
email from the Stanford Online Team as a confirmation that you have
successfully enrolled in one of our online courses. Your chosen course will
now appear on your Stanford Online (https://class.stanford.edu/dashboard)
dashboard. This course is already under way, so make sure to sign in
(https://class.stanford.edu/login) and click "View Course" from your dashboard
in order to get started. Doing this will take you to the Course Info page for
more information.
Have more questions? Please visit the course's About page or review some
frequently asked questions in the Help Center at
https://stanfordonline.zendesk.com/hc/en-us. Or, send the Stanford Online
course support team a message by clicking on the Help tab that displays on all
https://class.stanford.edu pages.
See you soon!
The Stanford Online Team
Hi there!
Thank you for enrolling in a Stanford Online course. You are receiving this
email from the Stanford Online Team as a confirmation that you have
successfully enrolled in one of our online courses. Your chosen course will
now appear on your Stanford Online (https://class.stanford.edu/dashboard)
dashboard. As it has not yet started, you'll notice that it isn't yet possible
to access the course site. Once the course begins, the course team will send
out a welcome email to everyone enrolled with more information.
For the time being, you can return to the course listing page at
https://class.stanford.edu/courses and click to access your course's About page
for more information, such as when the course will start, and what topics will
be covered in the course.
Have more questions? Please visit the Help Center at
https://stanfordonline.zendesk.com/hc/en-us, or send the Stanford Online course
support team a message by clicking on the Help tab that displays on all
https://class.stanford.edu pages.
See you soon!
The Stanford Online Team
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment