Commit 28186e23 by Renzo Lucioni

Merge pull request #6062 from edx/renzo/email-opt-in

Updating the marketing iframe, courseware views, and student views for email opt-in
parents 7d6dd565 8961ec4f
...@@ -3,6 +3,7 @@ Tests for student enrollment. ...@@ -3,6 +3,7 @@ Tests for student enrollment.
""" """
import ddt import ddt
import unittest import unittest
from mock import patch
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
...@@ -104,6 +105,33 @@ class EnrollmentTest(ModuleStoreTestCase): ...@@ -104,6 +105,33 @@ class EnrollmentTest(ModuleStoreTestCase):
# Expect that we're no longer enrolled # Expect that we're no longer enrolled
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
@patch('user_api.api.profile.update_email_opt_in')
@ddt.data(
([], 'true'),
([], 'false'),
(['honor', 'verified'], 'true'),
(['honor', 'verified'], 'false'),
(['professional'], 'true'),
(['professional'], 'false'),
)
@ddt.unpack
def test_enroll_with_email_opt_in(self, course_modes, email_opt_in, mock_update_email_opt_in):
# Create the course modes (if any) required for this test case
for mode_slug in course_modes:
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode_slug,
mode_display_name=mode_slug,
)
# Enroll in the course
self._change_enrollment('enroll', email_opt_in=email_opt_in)
# Verify that the profile API has been called as expected
opt_in = email_opt_in == 'true'
mock_update_email_opt_in.assert_called_once_with(self.USERNAME, self.course.org, opt_in)
def test_user_not_authenticated(self): def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated # Log out, so we're no longer authenticated
self.client.logout() self.client.logout()
...@@ -133,7 +161,7 @@ class EnrollmentTest(ModuleStoreTestCase): ...@@ -133,7 +161,7 @@ class EnrollmentTest(ModuleStoreTestCase):
resp = self._change_enrollment('unenroll', course_id="edx/") resp = self._change_enrollment('unenroll', course_id="edx/")
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
def _change_enrollment(self, action, course_id=None): def _change_enrollment(self, action, course_id=None, email_opt_in=None):
"""Change the student's enrollment status in a course. """Change the student's enrollment status in a course.
Args: Args:
...@@ -142,6 +170,8 @@ class EnrollmentTest(ModuleStoreTestCase): ...@@ -142,6 +170,8 @@ class EnrollmentTest(ModuleStoreTestCase):
Keyword Args: Keyword Args:
course_id (unicode): If provided, use this course ID. Otherwise, use the course_id (unicode): If provided, use this course ID. Otherwise, use the
course ID created in the setup for this test. course ID created in the setup for this test.
email_opt_in (unicode): If provided, pass this value along as
an additional GET parameter.
Returns: Returns:
Response Response
...@@ -154,4 +184,8 @@ class EnrollmentTest(ModuleStoreTestCase): ...@@ -154,4 +184,8 @@ class EnrollmentTest(ModuleStoreTestCase):
'enrollment_action': action, 'enrollment_action': action,
'course_id': course_id 'course_id': course_id
} }
if email_opt_in:
params['email_opt_in'] = email_opt_in
return self.client.post(reverse('change_enrollment'), params) return self.client.post(reverse('change_enrollment'), params)
...@@ -102,6 +102,7 @@ from third_party_auth import pipeline, provider ...@@ -102,6 +102,7 @@ from third_party_auth import pipeline, provider
from student.helpers import auth_pipeline_urls, set_logged_in_cookie from student.helpers import auth_pipeline_urls, set_logged_in_cookie
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from user_api.api import profile as profile_api
import analytics import analytics
from eventtracking import tracker from eventtracking import tracker
...@@ -739,6 +740,12 @@ def try_change_enrollment(request): ...@@ -739,6 +740,12 @@ def try_change_enrollment(request):
log.exception("Exception automatically enrolling after login: %s", exc) log.exception("Exception automatically enrolling after login: %s", exc)
def _update_email_opt_in(request, username, org):
"""Helper function used to hit the profile API if email opt-in is enabled."""
email_opt_in = request.POST.get('email_opt_in') == 'true'
profile_api.update_email_opt_in(username, org, email_opt_in)
@require_POST @require_POST
@commit_on_success_with_read_committed @commit_on_success_with_read_committed
def change_enrollment(request, check_access=True): def change_enrollment(request, check_access=True):
...@@ -804,6 +811,10 @@ def change_enrollment(request, check_access=True): ...@@ -804,6 +811,10 @@ def change_enrollment(request, check_access=True):
.format(user.username, course_id)) .format(user.username, course_id))
return HttpResponseBadRequest(_("Course id is invalid")) return HttpResponseBadRequest(_("Course id is invalid"))
# Record the user's email opt-in preference
if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
_update_email_opt_in(request, user.username, course_id.org)
available_modes = CourseMode.modes_for_course_dict(course_id) available_modes = CourseMode.modes_for_course_dict(course_id)
# Check that auto enrollment is allowed for this course # Check that auto enrollment is allowed for this course
......
...@@ -20,8 +20,9 @@ except Exception: ...@@ -20,8 +20,9 @@ except Exception:
cache = cache.cache cache = cache.cache
def cache_if_anonymous(view_func): def cache_if_anonymous(*get_parameters):
""" """Cache a page for anonymous users.
Many of the pages in edX are identical when the user is not logged Many of the pages in edX are identical when the user is not logged
in, but should not be cached when the user is logged in (because in, but should not be cached when the user is logged in (because
of the navigation bar at the top with the username). of the navigation bar at the top with the username).
...@@ -31,32 +32,42 @@ def cache_if_anonymous(view_func): ...@@ -31,32 +32,42 @@ def cache_if_anonymous(view_func):
the cookie to the vary header, and so every page is cached seperately the cookie to the vary header, and so every page is cached seperately
for each user (because each user has a different csrf token). for each user (because each user has a different csrf token).
Optionally, provide a series of GET parameters as arguments to cache
pages with these GET parameters separately.
Note that this decorator should only be used on views that do not Note that this decorator should only be used on views that do not
contain the csrftoken within the html. The csrf token can be included contain the csrftoken within the html. The csrf token can be included
in the header by ordering the decorators as such: in the header by ordering the decorators as such:
@ensure_csrftoken @ensure_csrftoken
@cache_if_anonymous @cache_if_anonymous()
def myView(request): def myView(request):
""" """
def decorator(view_func):
"""The outer wrapper, used to allow the decorator to take optional arguments."""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
"""The inner wrapper, which wraps the view function."""
if not request.user.is_authenticated():
# Use the cache. The same view accessed through different domain names may
# return different things, so include the domain name in the key.
domain = str(request.META.get('HTTP_HOST')) + '.'
cache_key = domain + "cache_if_anonymous." + get_language() + '.' + request.path
# Include the values of GET parameters in the cache key.
for get_parameter in get_parameters:
cache_key = cache_key + '.' + unicode(request.GET.get(get_parameter))
response = cache.get(cache_key) # pylint: disable=maybe-no-member
if not response:
response = view_func(request, *args, **kwargs)
cache.set(cache_key, response, 60 * 3) # pylint: disable=maybe-no-member
return response
else:
# Don't use the cache.
return view_func(request, *args, **kwargs)
@wraps(view_func) return wrapper
def _decorated(request, *args, **kwargs): return decorator
if not request.user.is_authenticated():
#Use the cache
# same view accessed through different domain names may
# return different things, so include the domain name in the key.
domain = str(request.META.get('HTTP_HOST')) + '.'
cache_key = domain + "cache_if_anonymous." + get_language() + '.' + request.path
response = cache.get(cache_key)
if not response:
response = view_func(request, *args, **kwargs)
cache.set(cache_key, response, 60 * 3)
return response
else:
#Don't use the cache
return view_func(request, *args, **kwargs)
return _decorated
...@@ -33,7 +33,7 @@ def get_course_enrollments(user): ...@@ -33,7 +33,7 @@ def get_course_enrollments(user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def index(request): def index(request):
''' '''
Redirects to main page -- info page if user authenticated, or marketing if not Redirects to main page -- info page if user authenticated, or marketing if not
...@@ -81,7 +81,7 @@ def index(request): ...@@ -81,7 +81,7 @@ def index(request):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def courses(request): def courses(request):
""" """
Render the "find courses" page. If the marketing site is enabled, redirect Render the "find courses" page. If the marketing site is enabled, redirect
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
Tests courseware views.py Tests courseware views.py
""" """
import unittest import unittest
import cgi
from datetime import datetime from datetime import datetime
from mock import MagicMock, patch, create_autospec from mock import MagicMock, patch, create_autospec
...@@ -99,6 +100,10 @@ class ViewsTestCase(TestCase): ...@@ -99,6 +100,10 @@ class ViewsTestCase(TestCase):
chapter = 'Overview' chapter = 'Overview'
self.chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter) self.chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
# For marketing email opt-in
self.organization_full_name = u"𝖀𝖒𝖇𝖗𝖊𝖑𝖑𝖆 𝕮𝖔𝖗𝖕𝖔𝖗𝖆𝖙𝖎𝖔𝖓"
self.organization_html = "<p>'+Umbrella/Corporation+'</p>"
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings") @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True}) @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_course_about_in_cart(self): def test_course_about_in_cart(self):
...@@ -256,17 +261,26 @@ class ViewsTestCase(TestCase): ...@@ -256,17 +261,26 @@ class ViewsTestCase(TestCase):
# generate/store a real password. # generate/store a real password.
self.assertEqual(chat_settings['password'], "johndoe@%s" % domain) self.assertEqual(chat_settings['password'], "johndoe@%s" % domain)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_about_coming_soon(self): def test_course_mktg_about_coming_soon(self):
# we should not be able to find this course # We should not be able to find this course
url = reverse('mktg_about_course', kwargs={'course_id': 'no/course/here'}) url = reverse('mktg_about_course', kwargs={'course_id': 'no/course/here'})
response = self.client.get(url) response = self.client.get(url, {'organization_full_name': self.organization_full_name})
self.assertIn('Coming Soon', response.content) self.assertIn('Coming Soon', response.content)
# Verify that the checkbox is not displayed
self._email_opt_in_checkbox(response)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_register(self): def test_course_mktg_register(self):
response = self._load_mktg_about() response = self._load_mktg_about(organization_full_name=self.organization_full_name)
self.assertIn('Enroll in', response.content) self.assertIn('Enroll in', response.content)
self.assertNotIn('and choose your student track', response.content) self.assertNotIn('and choose your student track', response.content)
# Verify that the checkbox is displayed
self._email_opt_in_checkbox(response, self.organization_full_name)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_register_multiple_modes(self): def test_course_mktg_register_multiple_modes(self):
CourseMode.objects.get_or_create( CourseMode.objects.get_or_create(
mode_slug='honor', mode_slug='honor',
...@@ -279,12 +293,42 @@ class ViewsTestCase(TestCase): ...@@ -279,12 +293,42 @@ class ViewsTestCase(TestCase):
course_id=self.course_key course_id=self.course_key
) )
response = self._load_mktg_about() response = self._load_mktg_about(organization_full_name=self.organization_full_name)
self.assertIn('Enroll in', response.content) self.assertIn('Enroll in', response.content)
self.assertIn('and choose your student track', response.content) self.assertIn('and choose your student track', response.content)
# Verify that the checkbox is displayed
self._email_opt_in_checkbox(response, self.organization_full_name)
# clean up course modes # clean up course modes
CourseMode.objects.all().delete() CourseMode.objects.all().delete()
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_no_organization_name(self):
# Don't pass an organization name as a GET parameter, even though the email
# opt-in feature is enabled.
response = response = self._load_mktg_about()
# Verify that the checkbox is not displayed
self._email_opt_in_checkbox(response)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': False})
def test_course_mktg_opt_in_disabled(self):
# Pass an organization name as a GET parameter, even though the email
# opt-in feature is disabled.
response = self._load_mktg_about(organization_full_name=self.organization_full_name)
# Verify that the checkbox is not displayed
self._email_opt_in_checkbox(response)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_organization_html(self):
response = self._load_mktg_about(organization_full_name=self.organization_html)
# Verify that the checkbox is displayed with the organization name
# in the label escaped as expected.
self._email_opt_in_checkbox(response, cgi.escape(self.organization_html))
@patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': True}) @patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': True})
def test_mktg_about_language_edx_domain(self): def test_mktg_about_language_edx_domain(self):
# Since we're in an edx-controlled domain, and our marketing site # Since we're in an edx-controlled domain, and our marketing site
...@@ -340,9 +384,8 @@ class ViewsTestCase(TestCase): ...@@ -340,9 +384,8 @@ class ViewsTestCase(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertFalse('<script>' in response.content) self.assertFalse('<script>' in response.content)
def _load_mktg_about(self, language=None): def _load_mktg_about(self, language=None, organization_full_name=None):
""" """Retrieve the marketing about button (iframed into the marketing site)
Retrieve the marketing about button (iframed into the marketing site)
and return the HTTP response. and return the HTTP response.
Keyword Args: Keyword Args:
...@@ -362,7 +405,22 @@ class ViewsTestCase(TestCase): ...@@ -362,7 +405,22 @@ class ViewsTestCase(TestCase):
headers['HTTP_ACCEPT_LANGUAGE'] = language headers['HTTP_ACCEPT_LANGUAGE'] = language
url = reverse('mktg_about_course', kwargs={'course_id': unicode(self.course_key)}) url = reverse('mktg_about_course', kwargs={'course_id': unicode(self.course_key)})
return self.client.get(url, **headers) if organization_full_name:
return self.client.get(url, {'organization_full_name': organization_full_name}, **headers)
else:
return self.client.get(url, **headers)
def _email_opt_in_checkbox(self, response, organization_full_name=None):
"""Check if the email opt-in checkbox appears in the response content."""
checkbox_html = '<input id="email-opt-in" type="checkbox" name="opt-in" class="email-opt-in" value="true" checked>'
if organization_full_name:
# Verify that the email opt-in checkbox appears, and that the expected
# organization name is displayed.
self.assertContains(response, checkbox_html, html=True)
self.assertContains(response, organization_full_name)
else:
# Verify that the email opt-in checkbox does not appear
self.assertNotContains(response, checkbox_html, html=True)
# setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly # setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly
......
...@@ -5,6 +5,7 @@ Courseware views functions ...@@ -5,6 +5,7 @@ Courseware views functions
import logging import logging
import urllib import urllib
import json import json
import cgi
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
...@@ -93,7 +94,7 @@ def user_groups(user): ...@@ -93,7 +94,7 @@ def user_groups(user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def courses(request): def courses(request):
""" """
Render "find courses" page. The course selection work is done in courseware.courses. Render "find courses" page. The course selection work is done in courseware.courses.
...@@ -713,7 +714,7 @@ def registered_for_course(course, user): ...@@ -713,7 +714,7 @@ def registered_for_course(course, user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def course_about(request, course_id): def course_about(request, course_id):
""" """
Display the course's about page. Display the course's about page.
...@@ -802,13 +803,10 @@ def course_about(request, course_id): ...@@ -802,13 +803,10 @@ def course_about(request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous('organization_full_name')
@ensure_valid_course_key @ensure_valid_course_key
def mktg_course_about(request, course_id): def mktg_course_about(request, course_id):
""" """This is the button that gets put into an iframe on the Drupal site."""
This is the button that gets put into an iframe on the Drupal site
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try: try:
...@@ -818,8 +816,7 @@ def mktg_course_about(request, course_id): ...@@ -818,8 +816,7 @@ def mktg_course_about(request, course_id):
) )
course = get_course_with_access(request.user, permission_name, course_key) course = get_course_with_access(request.user, permission_name, course_key)
except (ValueError, Http404): except (ValueError, Http404):
# if a course does not exist yet, display a coming # If a course does not exist yet, display a "Coming Soon" button
# soon button
return render_to_response( return render_to_response(
'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()} 'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()}
) )
...@@ -846,6 +843,12 @@ def mktg_course_about(request, course_id): ...@@ -846,6 +843,12 @@ def mktg_course_about(request, course_id):
'course_modes': course_modes, 'course_modes': course_modes,
} }
if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
# Drupal will pass the organization's full name as a GET parameter. If no full name
# is provided, the marketing iframe won't show the email opt-in checkbox.
organization_full_name = request.GET.get('organization_full_name')
context['organization_full_name'] = cgi.escape(organization_full_name) if organization_full_name else organization_full_name
# The edx.org marketing site currently displays only in English. # The edx.org marketing site currently displays only in English.
# To avoid displaying a different language in the register / access button, # To avoid displaying a different language in the register / access button,
# we force the language to English. # we force the language to English.
......
...@@ -30,7 +30,7 @@ def index(request, template): ...@@ -30,7 +30,7 @@ def index(request, template):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def render(request, template): def render(request, template):
""" """
This view function renders the template sent without checking that it This view function renders the template sent without checking that it
...@@ -43,7 +43,7 @@ def render(request, template): ...@@ -43,7 +43,7 @@ def render(request, template):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def render_press_release(request, slug): def render_press_release(request, slug):
""" """
Render a press release given a slug. Similar to the "render" function above, Render a press release given a slug. Similar to the "render" function above,
......
...@@ -283,6 +283,9 @@ FEATURES = { ...@@ -283,6 +283,9 @@ FEATURES = {
# Enable the combined login/registration form # Enable the combined login/registration form
'ENABLE_COMBINED_LOGIN_REGISTRATION': False, 'ENABLE_COMBINED_LOGIN_REGISTRATION': False,
# Enable organizational email opt-in
'ENABLE_MKTG_EMAIL_OPT_IN': False,
# Show a section in the membership tab of the instructor dashboard # Show a section in the membership tab of the instructor dashboard
# to allow an upload of a CSV file that contains a list of new accounts to create # to allow an upload of a CSV file that contains a list of new accounts to create
# and register for course. # and register for course.
......
...@@ -16,6 +16,14 @@ ...@@ -16,6 +16,14 @@
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
$(".register").click(function(event) { $(".register").click(function(event) {
if ( !$("#email-opt-in").prop("checked") ) {
$("input[name='email_opt_in']").val("false");
}
var email_opt_in = $("input[name='email_opt_in']").val(),
current_href = $("a.register").attr("href");
$("a.register").attr("href", current_href + "&email_opt_in=" + email_opt_in)
$("#class_enroll_form").submit(); $("#class_enroll_form").submit();
event.preventDefault(); event.preventDefault();
}); });
...@@ -29,7 +37,9 @@ ...@@ -29,7 +37,9 @@
window.top.location.href = "${reverse('dashboard')}"; window.top.location.href = "${reverse('dashboard')}";
} }
} else if (xhr.status == 403) { } else if (xhr.status == 403) {
window.top.location.href = $("a.register").attr("href") || "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll"; var email_opt_in = $("input[name='email_opt_in']").val();
## Ugh.
window.top.location.href = $("a.register").attr("href") || "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll&email_opt_in=" + email_opt_in;
} else { } else {
$('#register_error').html( $('#register_error').html(
(xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}") (xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}")
...@@ -71,6 +81,23 @@ ...@@ -71,6 +81,23 @@
</span> </span>
%endif %endif
</a> </a>
% if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
## We only display the email opt-in checkbox if we've been given a valid full name (i.e., not None)
% if organization_full_name:
<p class="form-field">
<input id="email-opt-in" type="checkbox" name="opt-in" class="email-opt-in" value="true" checked>
<label for="email-opt-in">
## Translators: This line appears next a checkbox which users can leave checked or uncheck in order
## to indicate whether they want to receive emails from the organization offering the course.
${_("I would like to receive email about other {organization_full_name} programs and offers.").format(
organization_full_name=organization_full_name
)}
</label>
</p>
% endif
% endif
%else: %else:
<div class="action registration-closed is-disabled">${_("Enrollment Is Closed")}</div> <div class="action registration-closed is-disabled">${_("Enrollment Is Closed")}</div>
%endif %endif
...@@ -83,6 +110,7 @@ ...@@ -83,6 +110,7 @@
<fieldset class="enroll_fieldset"> <fieldset class="enroll_fieldset">
<input name="course_id" type="hidden" value="${course.id | h}"> <input name="course_id" type="hidden" value="${course.id | h}">
<input name="enrollment_action" type="hidden" value="enroll"> <input name="enrollment_action" type="hidden" value="enroll">
<input name="email_opt_in" type="hidden" value="true">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
</fieldset> </fieldset>
<div class="submit"> <div class="submit">
......
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