From b20c8f9248e9761d5e685ac8a529cf659ac449cf Mon Sep 17 00:00:00 2001
From: jsa <jsa@edx.org>
Date: Fri, 14 Mar 2014 11:02:36 -0400
Subject: [PATCH] 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
---
 .../management/commands/create_user.py        | 15 ++---
 common/djangoapps/student/models.py           | 10 +--
 .../student/tests/test_create_account.py      | 46 ++++++++++++-
 common/djangoapps/student/views.py            | 64 +++++++++++--------
 lms/lib/comment_client/user.py                | 11 +++-
 5 files changed, 103 insertions(+), 43 deletions(-)

diff --git a/common/djangoapps/student/management/commands/create_user.py b/common/djangoapps/student/management/commands/create_user.py
index d375dedc54a..cacc5dea8ea 100644
--- a/common/djangoapps/student/management/commands/create_user.py
+++ b/common/djangoapps/student/management/commands/create_user.py
@@ -1,7 +1,7 @@
 from optparse import make_option
 from django.core.management.base import BaseCommand
-from student.models import CourseEnrollment, Registration
-from student.views import _do_create_account
+from student.models import CourseEnrollment, Registration, create_comments_service_user
+from student.views import _do_create_account, AccountValidationError
 from django.contrib.auth.models import User
 
 from track.management.tracked_command import TrackedCommand
@@ -74,17 +74,16 @@ class Command(TrackedCommand):
             'honor_code': u'true',
             'terms_of_service': u'true',
         }
-        create_account = _do_create_account(post_data)
-        if isinstance(create_account, tuple):
-            user = create_account[0]
+        try:
+            user, profile, reg = _do_create_account(post_data)
             if options['staff']:
                 user.is_staff = True
                 user.save()
-            reg = Registration.objects.get(user=user)
             reg.activate()
             reg.save()
-        else:
-            print create_account
+            create_comments_service_user(user)
+        except AccountValidationError as e:
+            print e.message
             user = User.objects.get(email=options['email'])
         if options['course']:
             CourseEnrollment.enroll(user, options['course'], mode=options['mode'])
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index c2fcdea3547..8f2952fdfe4 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -831,18 +831,18 @@ def add_user_to_default_group(user, group):
     utg.save()
 
 
-@receiver(post_save, sender=User)
-def update_user_information(sender, instance, created, **kwargs):
+def create_comments_service_user(user):
     if not settings.FEATURES['ENABLE_DISCUSSION_SERVICE']:
         # Don't try--it won't work, and it will fill the logs with lots of errors
         return
     try:
-        cc_user = cc.User.from_django_user(instance)
+        cc_user = cc.User.from_django_user(user)
         cc_user.save()
     except Exception as e:
         log = logging.getLogger("edx.discussion")
-        log.error(unicode(e))
-        log.error("update user info to discussion failed for user with id: " + str(instance.id))
+        log.error(
+            "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,
 # so that they are more likely to be loaded when a Studio user brings up the Studio admin
diff --git a/common/djangoapps/student/tests/test_create_account.py b/common/djangoapps/student/tests/test_create_account.py
index 1f5f4943063..bc9503d1d05 100644
--- a/common/djangoapps/student/tests/test_create_account.py
+++ b/common/djangoapps/student/tests/test_create_account.py
@@ -3,13 +3,17 @@
 import ddt
 from django.contrib.auth.models import User
 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
 import mock
 
 from user_api.models import UserPreference
 from lang_pref import LANGUAGE_KEY
 
+import student
+
+TEST_CS_URL = 'https://comments.service.test:123/'
 
 @ddt.ddt
 class TestCreateAccount(TestCase):
@@ -41,3 +45,43 @@ class TestCreateAccount(TestCase):
         self.assertEqual(response.status_code, 200)
         user = User.objects.get(username=self.username)
         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)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 61d2aa22d6c..54f3e2bfb3d 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -38,7 +38,8 @@ from course_modes.models import CourseMode
 from student.models import (
     Registration, UserProfile, PendingNameChange,
     PendingEmailChange, CourseEnrollment, unique_id_for_user,
-    CourseEnrollmentAllowed, UserStanding, LoginFailures
+    CourseEnrollmentAllowed, UserStanding, LoginFailures,
+    create_comments_service_user
 )
 from student.forms import PasswordResetFormNoActive
 from student.firebase_token_generator import create_token
@@ -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):
     """
     Given cleaned post variables, create the User and UserProfile objects, as well as the
@@ -970,19 +976,19 @@ def _do_create_account(post_vars):
     try:
         user.save()
     except IntegrityError:
-        js = {'success': False}
         # Figure out the cause of the integrity error
         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'])
-            js['field'] = 'username'
-            return JsonResponse(js, status=400)
-
-        if len(User.objects.filter(email=post_vars['email'])) > 0:
-            js['value'] = _("An account with the Email '{email}' already exists.").format(email=post_vars['email'])
-            js['field'] = 'email'
-            return JsonResponse(js, status=400)
-
-        raise
+            raise AccountValidationError(
+                _("An account with the Public Username '{username}' already exists.").format(username=post_vars['username']),
+                field="username"
+                )
+        elif len(User.objects.filter(email=post_vars['email'])) > 0:
+            raise AccountValidationError(
+                _("An account with the Email '{email}' already exists.").format(email=post_vars['email']),
+                field="email"
+                )
+        else:
+            raise
 
     registration.register(user)
 
@@ -1005,6 +1011,7 @@ def _do_create_account(post_vars):
         profile.save()
     except Exception:
         log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
+        raise
 
     UserPreference.set_preference(user, LANGUAGE_KEY, get_language())
 
@@ -1150,11 +1157,17 @@ def create_account(request, post_override=None):
             return JsonResponse(js, status=400)
 
     # Ok, looks like everything is legit.  Create the account.
-    ret = _do_create_account(post_vars)
-    if isinstance(ret, HttpResponse):  # if there was an error then return that
-        return ret
+    try:
+        with transaction.commit_on_success():
+            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
 
+    dog_stats_api.increment("common.student.account_created")
+    create_comments_service_user(user)
+
     context = {
         'name': post_vars['name'],
         'key': registration.activation_key,
@@ -1213,8 +1226,6 @@ def create_account(request, post_override=None):
             login_user.save()
             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({
         'success': True,
         'redirect_url': try_change_enrollment(request),
@@ -1283,20 +1294,16 @@ def auto_auth(request):
 
     # Attempt to create the account.
     # If successful, this will return a tuple containing
-    # the new user object; otherwise it will return an error
-    # message.
-    result = _do_create_account(post_data)
-
-    if isinstance(result, tuple):
-        user = result[0]
-
-    # If we did not create a new account, the user might already
-    # exist.  Attempt to retrieve it.
-    else:
+    # the new user object.
+    try:
+        user, profile, reg = _do_create_account(post_data)
+    except AccountValidationError:
+        # Attempt to retrieve the existing user.
         user = User.objects.get(username=username)
         user.email = email
         user.set_password(password)
         user.save()
+        reg = Registration.objects.get(user=user)
 
     # Set the user's global staff bit
     if is_staff is not None:
@@ -1304,7 +1311,6 @@ def auto_auth(request):
         user.save()
 
     # Activate the user
-    reg = Registration.objects.get(user=user)
     reg.activate()
     reg.save()
 
@@ -1321,6 +1327,8 @@ def auto_auth(request):
     user = authenticate(username=username, password=password)
     login(request, user)
 
+    create_comments_service_user(user)
+
     # Provide the user with a valid CSRF token
     # then return a 200 response
     success_msg = u"Logged in user {0} ({1}) with password {2} and user_id {3}".format(
diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py
index 014fe0a810e..77e8b0d0116 100644
--- a/lms/lib/comment_client/user.py
+++ b/lms/lib/comment_client/user.py
@@ -80,7 +80,16 @@ class User(models.Model):
         retrieve_params = self.default_retrieve_params
         if self.attributes.get('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)
 
 
-- 
GitLab