Commit b20c8f92 by jsa

Ensure account creation succeeds before creating comments service user

Also improves handling in cases where account creation did succeed, but
comments service user creation did not.

Depends on commit 88abdd8a0b20bc9816f487b25abdea1953f5661d in https://github.com/edx/cs_comments_service

JIRA: FOR-522
parent 16523b99
from optparse import make_option from optparse import make_option
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from student.models import CourseEnrollment, Registration from student.models import CourseEnrollment, Registration, create_comments_service_user
from student.views import _do_create_account from student.views import _do_create_account, AccountValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from track.management.tracked_command import TrackedCommand from track.management.tracked_command import TrackedCommand
...@@ -74,17 +74,16 @@ class Command(TrackedCommand): ...@@ -74,17 +74,16 @@ class Command(TrackedCommand):
'honor_code': u'true', 'honor_code': u'true',
'terms_of_service': u'true', 'terms_of_service': u'true',
} }
create_account = _do_create_account(post_data) try:
if isinstance(create_account, tuple): user, profile, reg = _do_create_account(post_data)
user = create_account[0]
if options['staff']: if options['staff']:
user.is_staff = True user.is_staff = True
user.save() user.save()
reg = Registration.objects.get(user=user)
reg.activate() reg.activate()
reg.save() reg.save()
else: create_comments_service_user(user)
print create_account except AccountValidationError as e:
print e.message
user = User.objects.get(email=options['email']) user = User.objects.get(email=options['email'])
if options['course']: if options['course']:
CourseEnrollment.enroll(user, options['course'], mode=options['mode']) CourseEnrollment.enroll(user, options['course'], mode=options['mode'])
...@@ -831,18 +831,18 @@ def add_user_to_default_group(user, group): ...@@ -831,18 +831,18 @@ def add_user_to_default_group(user, group):
utg.save() utg.save()
@receiver(post_save, sender=User) def create_comments_service_user(user):
def update_user_information(sender, instance, created, **kwargs):
if not settings.FEATURES['ENABLE_DISCUSSION_SERVICE']: if not settings.FEATURES['ENABLE_DISCUSSION_SERVICE']:
# Don't try--it won't work, and it will fill the logs with lots of errors # Don't try--it won't work, and it will fill the logs with lots of errors
return return
try: try:
cc_user = cc.User.from_django_user(instance) cc_user = cc.User.from_django_user(user)
cc_user.save() cc_user.save()
except Exception as e: except Exception as e:
log = logging.getLogger("edx.discussion") log = logging.getLogger("edx.discussion")
log.error(unicode(e)) log.error(
log.error("update user info to discussion failed for user with id: " + str(instance.id)) "Could not create comments service user with id {}".format(user.id),
exc_info=True)
# Define login and logout handlers here in the models file, instead of the views file, # Define login and logout handlers here in the models file, instead of the views file,
# so that they are more likely to be loaded when a Studio user brings up the Studio admin # so that they are more likely to be loaded when a Studio user brings up the Studio admin
......
...@@ -3,13 +3,17 @@ ...@@ -3,13 +3,17 @@
import ddt import ddt
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.db.transaction import rollback
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
import mock import mock
from user_api.models import UserPreference from user_api.models import UserPreference
from lang_pref import LANGUAGE_KEY from lang_pref import LANGUAGE_KEY
import student
TEST_CS_URL = 'https://comments.service.test:123/'
@ddt.ddt @ddt.ddt
class TestCreateAccount(TestCase): class TestCreateAccount(TestCase):
...@@ -41,3 +45,43 @@ class TestCreateAccount(TestCase): ...@@ -41,3 +45,43 @@ class TestCreateAccount(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
user = User.objects.get(username=self.username) user = User.objects.get(username=self.username)
self.assertEqual(UserPreference.get_preference(user, LANGUAGE_KEY), lang) self.assertEqual(UserPreference.get_preference(user, LANGUAGE_KEY), lang)
@mock.patch.dict("student.models.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
@mock.patch("lms.lib.comment_client.User.base_url", TEST_CS_URL)
@mock.patch("lms.lib.comment_client.utils.requests.request", return_value=mock.Mock(status_code=200, text='{}'))
class TestCreateCommentsServiceUser(TransactionTestCase):
def setUp(self):
self.username = "test_user"
self.url = reverse("create_account")
self.params = {
"username": self.username,
"email": "test@example.org",
"password": "testpass",
"name": "Test User",
"honor_code": "true",
"terms_of_service": "true",
}
def test_cs_user_created(self, request):
"If user account creation succeeds, we should create a comments service user"
response = self.client.post(self.url, self.params)
self.assertEqual(response.status_code, 200)
self.assertTrue(request.called)
args, kwargs = request.call_args
self.assertEqual(args[0], 'put')
self.assertTrue(args[1].startswith(TEST_CS_URL))
self.assertEqual(kwargs['data']['username'], self.params['username'])
@mock.patch("student.models.Registration.register", side_effect=Exception)
def test_cs_user_not_created(self, register, request):
"If user account creation fails, we should not create a comments service user"
try:
response = self.client.post(self.url, self.params)
except:
pass
with self.assertRaises(User.DoesNotExist):
User.objects.get(username=self.username)
self.assertTrue(register.called)
self.assertFalse(request.called)
...@@ -38,7 +38,8 @@ from course_modes.models import CourseMode ...@@ -38,7 +38,8 @@ from course_modes.models import CourseMode
from student.models import ( from student.models import (
Registration, UserProfile, PendingNameChange, Registration, UserProfile, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user, PendingEmailChange, CourseEnrollment, unique_id_for_user,
CourseEnrollmentAllowed, UserStanding, LoginFailures CourseEnrollmentAllowed, UserStanding, LoginFailures,
create_comments_service_user
) )
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
from student.firebase_token_generator import create_token from student.firebase_token_generator import create_token
...@@ -951,6 +952,11 @@ def change_setting(request): ...@@ -951,6 +952,11 @@ def change_setting(request):
}) })
class AccountValidationError(Exception):
def __init__(self, message, field):
super(AccountValidationError, self).__init__(message)
self.field = field
def _do_create_account(post_vars): def _do_create_account(post_vars):
""" """
Given cleaned post variables, create the User and UserProfile objects, as well as the Given cleaned post variables, create the User and UserProfile objects, as well as the
...@@ -970,19 +976,19 @@ def _do_create_account(post_vars): ...@@ -970,19 +976,19 @@ def _do_create_account(post_vars):
try: try:
user.save() user.save()
except IntegrityError: except IntegrityError:
js = {'success': False}
# Figure out the cause of the integrity error # Figure out the cause of the integrity error
if len(User.objects.filter(username=post_vars['username'])) > 0: if len(User.objects.filter(username=post_vars['username'])) > 0:
js['value'] = _("An account with the Public Username '{username}' already exists.").format(username=post_vars['username']) raise AccountValidationError(
js['field'] = 'username' _("An account with the Public Username '{username}' already exists.").format(username=post_vars['username']),
return JsonResponse(js, status=400) field="username"
)
if len(User.objects.filter(email=post_vars['email'])) > 0: elif len(User.objects.filter(email=post_vars['email'])) > 0:
js['value'] = _("An account with the Email '{email}' already exists.").format(email=post_vars['email']) raise AccountValidationError(
js['field'] = 'email' _("An account with the Email '{email}' already exists.").format(email=post_vars['email']),
return JsonResponse(js, status=400) field="email"
)
raise else:
raise
registration.register(user) registration.register(user)
...@@ -1005,6 +1011,7 @@ def _do_create_account(post_vars): ...@@ -1005,6 +1011,7 @@ def _do_create_account(post_vars):
profile.save() profile.save()
except Exception: except Exception:
log.exception("UserProfile creation failed for user {id}.".format(id=user.id)) log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
raise
UserPreference.set_preference(user, LANGUAGE_KEY, get_language()) UserPreference.set_preference(user, LANGUAGE_KEY, get_language())
...@@ -1150,11 +1157,17 @@ def create_account(request, post_override=None): ...@@ -1150,11 +1157,17 @@ def create_account(request, post_override=None):
return JsonResponse(js, status=400) return JsonResponse(js, status=400)
# Ok, looks like everything is legit. Create the account. # Ok, looks like everything is legit. Create the account.
ret = _do_create_account(post_vars) try:
if isinstance(ret, HttpResponse): # if there was an error then return that with transaction.commit_on_success():
return ret ret = _do_create_account(post_vars)
except AccountValidationError as e:
return JsonResponse({'success': False, 'value': e.message, 'field': e.field}, status=400)
(user, profile, registration) = ret (user, profile, registration) = ret
dog_stats_api.increment("common.student.account_created")
create_comments_service_user(user)
context = { context = {
'name': post_vars['name'], 'name': post_vars['name'],
'key': registration.activation_key, 'key': registration.activation_key,
...@@ -1213,8 +1226,6 @@ def create_account(request, post_override=None): ...@@ -1213,8 +1226,6 @@ def create_account(request, post_override=None):
login_user.save() login_user.save()
AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(login_user.username, login_user.email)) AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(login_user.username, login_user.email))
dog_stats_api.increment("common.student.account_created")
response = JsonResponse({ response = JsonResponse({
'success': True, 'success': True,
'redirect_url': try_change_enrollment(request), 'redirect_url': try_change_enrollment(request),
...@@ -1283,20 +1294,16 @@ def auto_auth(request): ...@@ -1283,20 +1294,16 @@ def auto_auth(request):
# Attempt to create the account. # Attempt to create the account.
# If successful, this will return a tuple containing # If successful, this will return a tuple containing
# the new user object; otherwise it will return an error # the new user object.
# message. try:
result = _do_create_account(post_data) user, profile, reg = _do_create_account(post_data)
except AccountValidationError:
if isinstance(result, tuple): # Attempt to retrieve the existing user.
user = result[0]
# If we did not create a new account, the user might already
# exist. Attempt to retrieve it.
else:
user = User.objects.get(username=username) user = User.objects.get(username=username)
user.email = email user.email = email
user.set_password(password) user.set_password(password)
user.save() user.save()
reg = Registration.objects.get(user=user)
# Set the user's global staff bit # Set the user's global staff bit
if is_staff is not None: if is_staff is not None:
...@@ -1304,7 +1311,6 @@ def auto_auth(request): ...@@ -1304,7 +1311,6 @@ def auto_auth(request):
user.save() user.save()
# Activate the user # Activate the user
reg = Registration.objects.get(user=user)
reg.activate() reg.activate()
reg.save() reg.save()
...@@ -1321,6 +1327,8 @@ def auto_auth(request): ...@@ -1321,6 +1327,8 @@ def auto_auth(request):
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
login(request, user) login(request, user)
create_comments_service_user(user)
# Provide the user with a valid CSRF token # Provide the user with a valid CSRF token
# then return a 200 response # then return a 200 response
success_msg = u"Logged in user {0} ({1}) with password {2} and user_id {3}".format( success_msg = u"Logged in user {0} ({1}) with password {2} and user_id {3}".format(
......
...@@ -80,7 +80,16 @@ class User(models.Model): ...@@ -80,7 +80,16 @@ class User(models.Model):
retrieve_params = self.default_retrieve_params retrieve_params = self.default_retrieve_params
if self.attributes.get('course_id'): if self.attributes.get('course_id'):
retrieve_params['course_id'] = self.course_id retrieve_params['course_id'] = self.course_id
response = perform_request('get', url, retrieve_params) try:
response = perform_request('get', url, retrieve_params)
except CommentClientRequestError as e:
if e.status_code == 404:
# attempt to gracefully recover from a previous failure
# to sync this user to the comments service.
self.save()
response = perform_request('get', url, retrieve_params)
else:
raise
self.update_attributes(**response) self.update_attributes(**response)
......
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