Commit bdf38ae0 by Harry Rein

Implemented an upgrade verification sock.

This sock sits at the bottom of both the home and the course content pages. It allows the user to click a 'Learn More' button to open a panel that allows the user to navigate to the upgrade checkout page. The sock is only shown for users that have not yet upgraded in a course that has a verification upgrade date that has not yet passed. Python tests cover the various course mode and upgrade dates.
parent 0b90c60b
......@@ -10,6 +10,8 @@
// ----------------------------
%btn-shims {
display: inline-block;
background-color: transparent;
background-image: none;
border-style: $btn-border-style;
border-radius: $btn-border-radius;
border-width: $btn-border-size;
......
......@@ -118,6 +118,14 @@ class DateSummary(object):
return datetime.now(utc) <= self.date
return False
def deadline_has_passed(self):
"""
Return True if a deadline (the date) exists, and has already passed.
Returns False otherwise.
"""
deadline = self.date
return deadline is not None and deadline <= datetime.now(utc)
def __repr__(self):
return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
title=self.title,
......@@ -313,13 +321,6 @@ class VerificationDeadlineDate(DateSummary):
"""Return the verification status for this user."""
return SoftwareSecurePhotoVerification.user_status(self.user)[0]
def deadline_has_passed(self):
"""
Return True if a verification deadline exists, and has already passed.
"""
deadline = self.date
return deadline is not None and deadline <= datetime.now(utc)
def must_retry(self):
"""Return True if the user must re-submit verification, False otherwise."""
return self.verification_status == 'must_reverify'
......@@ -209,8 +209,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20
@ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 147),
(ModuleStoreEnum.Type.split, 4, 147),
(ModuleStoreEnum.Type.mongo, 10, 149),
(ModuleStoreEnum.Type.split, 4, 149),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
......
......@@ -33,6 +33,7 @@ from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG, default_course_url_name
from openedx.features.enterprise_support.api import data_sharing_consent_required
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
from request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode
from student.views import is_course_blocked
......@@ -367,6 +368,9 @@ class CoursewareIndex(View):
table_of_contents['chapters'],
)
courseware_context['course_sock_fragment'] = CourseSockFragmentView().render_to_fragment(
request, course=self.course)
# entrance exam data
self._add_entrance_exam_to_context(courseware_context)
......
......@@ -68,3 +68,6 @@
// responsive
@import 'base/layouts'; // temporary spot for responsive course
// features
@import 'features/course-sock';
......@@ -27,3 +27,4 @@
@import 'features/bookmarks';
@import 'features/course-experience';
@import 'features/course-search';
@import 'features/course-sock';
......@@ -107,6 +107,10 @@
border: 1px solid $lms-active-color;
}
}
&:last-child {
border-bottom: none;
}
}
}
}
......@@ -186,4 +190,3 @@
}
}
}
.verification-sock {
display: inline-block;
position: relative;
width: 100%;
margin-top: $baseline;
max-width: $lms-max-width;
margin: $baseline auto 0;
-webkit-transition: all 0.4s ease-out;
-moz-transition: all 0.4s ease-out;
-o-transition: all 0.4s ease-out;
-ms-transition: all 0.4s ease-out;
transition: all 0.4s ease-out;
.action-toggle-verification-sock {
@include left(50%);
@include margin-left(-1 * $baseline * 15/2);
position: absolute;
top: (-1 * $baseline);
width: ($baseline * 15);
color: $button-bg-hover-color;
background-color: $success-color;
border-color: $success-color;
background-image: none;
box-shadow: none;
-webkit-transition: background-color 0.5s;
transition: background-color 0.5s;
&.active {
color: $success-color;
background-color: $button-bg-hover-color;
border-color: $success-color;
background-image: none;
box-shadow: none;
&:hover {
color: $button-bg-hover-color;
background-color: $success-color-hover;
border-color: $success-color-hover;
background-image: none;
box-shadow: none;
}
}
&:hover {
color: $button-bg-hover-color;
background-color: $success-color-hover;
border-color: $success-color-hover;
background-image: none;
box-shadow: none;
}
}
.verification-main-panel {
display: none;
overflow: hidden;
border-top: 1px solid $lms-border-color;
padding: ($baseline * 5/2) ($baseline * 2);
-webkit-transition: height ease-out;
transition: height ease-out;
.verification-desc-panel {
color: $black-t3;
position: relative;
@media (max-width: 960px) {
.mini-cert {
display: none;
border: 1px solid $black-t0;
}
}
.mini-cert {
@include right($baseline);
position: absolute;
top: $baseline;
width: ($baseline * 13);
}
h2 {
font-size: 1.5rem;
font-weight: 700;
}
h4 {
font-size: 1.25rem;
font-weight: 600;
}
.learner-story-container {
display: flex;
max-width: 630px;
.student-image {
margin: ($baseline / 4) $baseline 0 0;
height: ($baseline * 5/2);
width: ($baseline * 5/2);
}
.story-quote > .author{
display: block;
margin-top: ($baseline / 4);
font-weight: 600;
}
&:not(:first-child) {
margin-top: ($baseline * 2);
}
}
.action-upgrade-certificate {
position: absolute;
right: $baseline;
background-color: $success-color;
border-color: $success-color;
background-image: none;
box-shadow: none;
@media (max-width: 960px) {
& {
position: relative;
margin-top: ($baseline * 2);
}
}
@media (min-width: 960px) {
&.stuck-top {
bottom: auto;
top: $baseline * (52 / 5);
}
&.stuck-bottom {
top: auto;
bottom: $baseline * (-1 * 3/2);
}
&.attached {
@include right($baseline);
position: fixed;
bottom: $baseline;
top: auto;
}
}
&:hover {
background-color: $success-color-hover;
border-color: $success-color-hover;
}
}
}
}
}
// Overrides for the courseware page.
.view-courseware {
.verification-sock {
margin-top: 0;
border-top: none;
border-bottom: none;
.action-toggle-verification-sock {
top: (-1 * $baseline * 5/4);
&:not(.active) {
color: $button-bg-hover-color;
background-color: $success-color;
box-shadow: none;
border: 1px solid $success-color;
&:hover {
background-color: $success-color-hover;
}
}
}
.verification-main-panel {
border-top: 0;
border-bottom: 1px solid $lms-border-color;
}
}
}
......@@ -36,7 +36,7 @@ $fg-gutter: $gw-gutter !default;
$fg-max-columns: 12 !default;
$fg-max-width: 1400px !default;
$fg-min-width: 810px !default;
$lms-max-width: 1180px !default;
// ----------------------------
// #COLORS
......@@ -218,7 +218,7 @@ $active-color: $blue !default;
$highlight-color: rgb(255,255,0) !default;
$alert-color: rgb(212, 64, 64) !default;
$success-color: rgb(0, 155, 0) !default;
$success-color-hover: rgb(0, 129, 0) !default;
// ----------------------------
// #COLORS- EDX-SPECIFIC
......
......@@ -9,27 +9,38 @@
// ----------------------------
// #GRID
// ----------------------------
$lms-max-width: 1180px;
$lms-max-width: 1180px !default;
// ----------------------------
// #COLORS
// ----------------------------
$lms-gray: palette(grayscale, base);
$lms-background-color: palette(grayscale, x-back);
$lms-container-background-color: $white;
$lms-border-color: palette(grayscale, back);
$lms-label-color: palette(grayscale, black);
$lms-active-color: palette(primary, base);
$lms-preview-menu-color: #c8c8c8;
$white-transparent: rgba(255, 255, 255, 0);
$white-opacity-40: rgba(255, 255, 255, 0.4);
$white-opacity-60: rgba(255, 255, 255, 0.6);
$white-opacity-70: rgba(255, 255, 255, 0.7);
$white-opacity-80: rgba(255, 255, 255, 0.8);
$lms-gray: palette(grayscale, base) !default;
$lms-background-color: palette(grayscale, x-back) !default;
$lms-container-background-color: $white !default;
$lms-border-color: palette(grayscale, back) !default;
$lms-label-color: palette(grayscale, black) !default;
$lms-active-color: palette(primary, base) !default;
$lms-preview-menu-color: #c8c8c8 !default;
$success-color: palette(success, accent) !default;
$success-color-hover: palette(success, text) !default;
$light-grey-transparent: rgba(200,200,200, 0);
$light-grey-solid: rgba(200,200,200, 1);
$button-bg-hover-color: $white !default;
$white-transparent: rgba(255, 255, 255, 0) !default;
$white-opacity-40: rgba(255, 255, 255, 0.4) !default;
$white-opacity-60: rgba(255, 255, 255, 0.6) !default;
$white-opacity-70: rgba(255, 255, 255, 0.7) !default;
$white-opacity-80: rgba(255, 255, 255, 0.8) !default;
$black: rgb(0,0,0) !default;
$black-t0: rgba($black, 0.125) !default;
$black-t1: rgba($black, 0.25) !default;
$black-t2: rgba($black, 0.5) !default;
$black-t3: rgba($black, 0.75) !default;
$light-grey-transparent: rgba(200,200,200, 0) !default;
$light-grey-solid: rgba(200,200,200, 1) !default;
// ----------------------------
// #TYPOGRAPHY
......@@ -42,9 +53,10 @@ $font-bold: 700 !default;
// ----------------------------
// #ICONS
// ----------------------------
$lms-dark-icon-color: $white;
$lms-dark-icon-background-color: palette(grayscale, black);
// Icons
$lms-dark-icon-color: $white !default;
$lms-dark-icon-background-color: palette(grayscale, black) !default;
$site-status-color: rgb(182,37,103);
$site-status-color: rgb(182,37,103) !default;
$shadow-l1: rgba(0,0,0,0.1) !default;
......@@ -226,6 +226,7 @@ ${HTML(fragment.foot_html())}
</section>
</div>
${HTML(course_sock_fragment.body_html())}
</div>
<div class="container-footer">
% if settings.FEATURES.get("LICENSING", False):
......
......@@ -19,6 +19,9 @@ WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
# Waffle flag to enable a single unified "Course" tab.
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
# Waffle flag to enable the sock on the footer of the home and courseware pages
DISPLAY_COURSE_SOCK = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'display_course_sock')
def course_home_page_title(course): # pylint: disable=unused-argument
"""
......
/* globals Logger */
export class CourseSock { // eslint-disable-line import/prefer-default-export
constructor() {
const $toggleActionButton = $('.action-toggle-verification-sock');
const $verificationSock = $('.verification-sock .verification-main-panel');
const $upgradeToVerifiedButton = $('.verification-sock .action-upgrade-certificate');
const pageLocation = window.location.href.indexOf('courseware') > -1
? 'Course Content Page' : 'Home Page';
// Behavior to fix button to bottom of screen on scroll
const fixUpgradeButton = () => {
if (!$upgradeToVerifiedButton.is(':visible')) return;
// Grab the current scroll location
const documentBottom = $(window).scrollTop() + $(window).height();
// Establish a sliding window in which the button is fixed
const startFixed = $verificationSock.offset().top + 320;
const endFixed = (startFixed + $verificationSock.height()) - 220;
// Assure update button stays in sock even when max-width is exceeded
const distLeft = ($verificationSock.offset().left + $verificationSock.width())
- ($upgradeToVerifiedButton.width() + 22);
// Update positioning when scrolling is in fixed window and screen width is sufficient
if ((documentBottom > startFixed && documentBottom < endFixed)
|| $(window).width() < 960) {
$upgradeToVerifiedButton.addClass('attached');
$upgradeToVerifiedButton.css('left', `${distLeft}px`);
} else {
// If outside sliding window, reset to un-attached state
$upgradeToVerifiedButton.removeClass('attached');
$upgradeToVerifiedButton.css('left', 'auto');
// Add class to define absolute location
if (documentBottom < startFixed) {
$upgradeToVerifiedButton.addClass('stuck-top');
$upgradeToVerifiedButton.removeClass('stuck-bottom');
} else if (documentBottom > endFixed) {
$upgradeToVerifiedButton.addClass('stuck-bottom');
$upgradeToVerifiedButton.removeClass('stuck-top');
}
}
};
// Fix the sock to the screen on scroll and resize events
if ($upgradeToVerifiedButton.length) {
$(window).scroll(fixUpgradeButton).resize(fixUpgradeButton);
}
// Open the sock when user clicks to Learn More
$toggleActionButton.on('click', () => {
const toggleSpeed = 400;
$toggleActionButton.toggleClass('active').toggleClass('aria-expanded');
$verificationSock.slideToggle(toggleSpeed, fixUpgradeButton);
// Log open and close events
const isOpening = $toggleActionButton.hasClass('active');
const logMessage = isOpening ? 'User opened the verification sock.'
: 'User closed the verification sock.';
Logger.log(
logMessage,
{
from_page: pageLocation,
},
);
});
$upgradeToVerifiedButton.on('click', () => {
Logger.log(
'User clicked the upgrade button in the verification sock.',
{
from_page: pageLocation,
},
);
});
}
}
......@@ -97,5 +97,6 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
</aside>
</div>
</div>
${HTML(course_sock_fragment.body_html())}
</div>
</%block>
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import DISPLAY_COURSE_SOCK
%>
<%block name="content">
% if show_course_sock and DISPLAY_COURSE_SOCK.is_enabled(course_id):
<div class="verification-sock">
<button type="button" class="btn btn-brand focusable action-toggle-verification-sock">
Learn About Verified Certificate
</button>
<div class="verification-main-panel">
<div class="verification-desc-panel content-main">
<h2>edX Verified Certificate</h2>
<h4>Why upgrade?</h4>
<ul>
<li>Official proof of completion</li>
<li>Easily shareable certificate</li>
<li>Proven motivator to complete the course</li>
<li>Certificate purchases help edX continue to offer free courses</li>
</ul>
<h4>How it works</h4>
<ul>
<li>Pay the Verified Certificate upgrade fee</li>
<li>Verify your identity with a webcam and government-issued ID</li>
<li>Study hard and pass the course</li>
<li>Share your certificate with friends, employers, and others</li>
</ul>
<h4>edX Learner Stories</h4>
<div class="learner-story-container">
<img class="student-image" alt="Student Image" src="${static.url('course_experience/images/learner-quote.png')}" />
<div class="story-quote">
My certificate has helped me showcase my knowledge on my
resume - I feel like this certificate could really help me land
my dream job!
<span class="author">- Christina Fong, edX Learner</span>
</div>
</div>
<div class="learner-story-container">
<img class="student-image" alt="Student Image" src="${static.url('course_experience/images/learner-quote2.png')}" />
<div class="story-quote">
I wanted to include a verified certificate on my resume and my profile to
illustrate that I am working towards this goal I have and that I have
achieved something while I was unemployed.</br>
<span class="author">- Cheryl Troell, edX Learner</span>
</div>
</div>
<img class="mini-cert" src="${static.url('course_experience/images/verified-cert.png')}"/>
<a href="/verify_student/upgrade/${course_id}/">
<button type="button" class="btn btn-brand stuck-top focusable action-upgrade-certificate">
Upgrade Now (${HTML(course_price)})
</button>
</a>
</div>
</div>
</div>
% endif
</%block>
<%static:webpack entry="CourseSock">
new CourseSock({
el:'.verification-sock'
});
</%static:webpack>
......@@ -89,7 +89,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
course_home_url(self.course)
# Fetch the view and verify the query counts
with self.assertNumQueries(45):
with self.assertNumQueries(47):
with check_mongo_calls(5):
url = course_home_url(self.course)
self.client.get(url)
"""
Tests for course verification sock
"""
import datetime
import ddt
from course_modes.models import CourseMode
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import DISPLAY_COURSE_SOCK
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .test_course_home import course_home_url
TEST_PASSWORD = 'test'
TEST_VERIFICATION_SOCK_LOCATOR = '<div class="verification-sock">'
TEST_COURSE_PRICE = 50
@ddt.ddt
class TestCourseSockView(SharedModuleStoreTestCase):
"""
Tests for the course verification sock fragment view.
"""
@classmethod
def setUpClass(cls):
super(TestCourseSockView, cls).setUpClass()
# Create four courses
cls.standard_course = CourseFactory.create()
cls.verified_course = CourseFactory.create()
cls.verified_course_update_expired = CourseFactory.create()
cls.verified_course_already_enrolled = CourseFactory.create()
# Assign each verifiable course a upgrade deadline
cls._add_course_mode(cls.verified_course, upgrade_deadline_expired=False)
cls._add_course_mode(cls.verified_course_update_expired, upgrade_deadline_expired=True)
cls._add_course_mode(cls.verified_course_already_enrolled, upgrade_deadline_expired=False)
def setUp(self):
super(TestCourseSockView, self).setUp()
self.user = UserFactory.create()
# Enroll the user in the four courses
CourseEnrollmentFactory.create(user=self.user, course_id=self.standard_course.id)
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course.id)
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course_update_expired.id)
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course_already_enrolled.id, mode=CourseMode.VERIFIED)
# Log the user in
self.client.login(username=self.user.username, password=TEST_PASSWORD)
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
def test_standard_course(self):
"""
Assure that a course that cannot be verified does
not have a visible verification sock.
"""
response = self.client.get(course_home_url(self.standard_course))
self.assertEqual(self.is_verified_sock_visible(response), False,
'Student should not be able to see sock in a unverifiable course.')
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
def test_verified_course(self):
"""
Assure that a course that can be verified has a
visible verification sock.
"""
response = self.client.get(course_home_url(self.verified_course))
self.assertEqual(self.is_verified_sock_visible(response), True,
'Student should be able to see sock in a verifiable course.')
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
def test_verified_course_updated_expired(self):
"""
Assure that a course that has an expired upgrade
date does not display the verification sock.
"""
response = self.client.get(course_home_url(self.verified_course_update_expired))
self.assertEqual(self.is_verified_sock_visible(response), False,
'Student should be able to see sock in a verifiable course if the update expiration date has passed.')
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
def test_verified_course_user_already_upgraded(self):
"""
Assure that a user that has already upgraded to a
verified status cannot see the verification sock.
"""
response = self.client.get(course_home_url(self.verified_course_already_enrolled))
self.assertEqual(self.is_verified_sock_visible(response), False,
'Student should be able to see sock if they have already upgraded to verified mode.')
@classmethod
def is_verified_sock_visible(cls, response):
return TEST_VERIFICATION_SOCK_LOCATOR in response.content
@classmethod
def _add_course_mode(cls, course, upgrade_deadline_expired=False):
"""
Adds a course mode to the test course.
"""
upgrade_exp_date = datetime.datetime.now()
if upgrade_deadline_expired:
upgrade_exp_date = upgrade_exp_date - datetime.timedelta(days=21)
else:
upgrade_exp_date = upgrade_exp_date + datetime.timedelta(days=21)
CourseMode(
course_id=course.id,
mode_slug=CourseMode.VERIFIED,
mode_display_name="Verified Certificate",
min_price=TEST_COURSE_PRICE,
_expiration_datetime=upgrade_exp_date, # pylint: disable=protected-access
).save()
......@@ -8,6 +8,7 @@ from views.course_home import CourseHomeFragmentView, CourseHomeView
from views.course_outline import CourseOutlineFragmentView
from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
from views.welcome_message import WelcomeMessageFragmentView
from views.course_sock import CourseSockFragmentView
urlpatterns = [
url(
......@@ -40,4 +41,9 @@ urlpatterns = [
WelcomeMessageFragmentView.as_view(),
name='openedx.course_experience.welcome_message_fragment_view',
),
url(
r'course_sock_fragment$',
CourseSockFragmentView.as_view(),
name='openedx.course_experience.course_sock_fragment_view',
),
]
......@@ -20,6 +20,7 @@ from ..utils import get_course_outline_block_tree
from .course_dates import CourseDatesFragmentView
from .course_outline import CourseOutlineFragmentView
from .welcome_message import WelcomeMessageFragmentView
from .course_sock import CourseSockFragmentView
class CourseHomeView(CourseTabView):
......@@ -105,6 +106,9 @@ class CourseHomeFragmentView(EdxFragmentView):
# TODO: Use get_course_overview_with_access and blocks api
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
# Render the verification sock as a fragment
course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
# Get the handouts
handouts_html = get_course_info_section(request, request.user, course, 'handouts')
......@@ -119,6 +123,7 @@ class CourseHomeFragmentView(EdxFragmentView):
'resume_course_url': resume_course_url,
'dates_fragment': dates_fragment,
'welcome_message_fragment': welcome_message_fragment,
'course_sock_fragment': course_sock_fragment,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
......
"""
Fragment for rendering the course's sock and associated toggle button.
"""
from datetime import datetime
from django.conf import settings
from django.template.loader import render_to_string
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
from student.models import CourseEnrollment
from course_modes.models import CourseMode
from courseware.date_summary import VerifiedUpgradeDeadlineDate
from courseware.courses import get_course_with_access
from courseware.views.views import get_course_prices
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
class CourseSockFragmentView(EdxFragmentView):
"""
A fragment to provide extra functionality in a dropdown sock.
"""
def render_to_fragment(self, request, course, **kwargs):
"""
Render the course's sock fragment.
"""
context = self.get_verification_context(request, course)
html = render_to_string('course_experience/course-sock-fragment.html', context)
return Fragment(html)
def get_verification_context(self, request, course):
course_key = CourseKey.from_string(unicode(course.id))
# Establish whether the course has a verified mode
available_modes = CourseMode.modes_for_course_dict(unicode(course.id))
has_verified_mode = CourseMode.has_verified_mode(available_modes)
# Establish whether the user is already enrolled
is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user.id, course_key)
# Establish whether the verification deadline has already passed
verification_deadline = VerifiedUpgradeDeadlineDate(course, request.user)
deadline_has_passed = verification_deadline.deadline_has_passed()
show_course_sock = has_verified_mode and not is_already_verified and not deadline_has_passed
# Get the price of the course and format correctly
course_prices = get_course_prices(course)
context = {
'show_course_sock': show_course_sock,
'course_price': course_prices[1],
'course_id': course.id
}
return context
......@@ -19,6 +19,7 @@ var wpconfig = {
entry: {
CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js',
CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
Import: './cms/static/js/features/import/factories/import.js'
},
......
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