Esempio n. 1
0
    def test_outer_atomic_nesting(self):
        """
        Test that outer_atomic raises an error if it is nested inside
        another atomic.
        """
        if connection.vendor != 'mysql':
            raise unittest.SkipTest('Only works on MySQL.')

        outer_atomic()(do_nothing)()

        with atomic():
            atomic()(do_nothing)()

        with outer_atomic():
            atomic()(do_nothing)()

        with self.assertRaisesRegex(TransactionManagementError,
                                    'Cannot be inside an atomic block.'):
            with atomic():
                outer_atomic()(do_nothing)()

        with self.assertRaisesRegex(TransactionManagementError,
                                    'Cannot be inside an atomic block.'):
            with outer_atomic():
                outer_atomic()(do_nothing)()
Esempio n. 2
0
def submit_task(request, task_type, task_class, course_key, task_input,
                task_key):
    """
    Helper method to submit a task.

    Reserves the requested task, based on the `course_key`, `task_type`, and `task_key`,
    checking to see if the task is already running.  The `task_input` is also passed so that
    it can be stored in the resulting InstructorTask entry.  Arguments are extracted from
    the `request` provided by the originating server request.  Then the task is submitted to run
    asynchronously, using the specified `task_class` and using the task_id constructed for it.

    Cannot be inside an atomic block.

    `AlreadyRunningError` is raised if the task is already running.
    """
    with outer_atomic():
        # check to see if task is already running, and reserve it otherwise:
        instructor_task = _reserve_task(course_key, task_type, task_key,
                                        task_input, request.user)

    # make sure all data has been committed before handing off task to celery.

    task_id = instructor_task.task_id
    task_args = [
        instructor_task.id,
        _get_xmodule_instance_args(request, task_id)
    ]
    try:
        task_class.apply_async(task_args, task_id=task_id)

    except Exception as error:  # lint-amnesty, pylint: disable=broad-except
        _handle_instructor_task_failure(instructor_task, error)

    return instructor_task
Esempio n. 3
0
    def test_outer_atomic_nesting(self):
        """
        Test that outer_atomic raises an error if it is nested inside
        another atomic.
        """
        outer_atomic()(do_nothing)()  # pylint: disable=not-callable

        with atomic():
            atomic()(do_nothing)()  # pylint: disable=not-callable

        with outer_atomic():
            atomic()(do_nothing)()  # pylint: disable=not-callable

        with self.assertRaisesRegex(TransactionManagementError,
                                    'Cannot be inside an atomic block.'):
            with atomic():
                outer_atomic()(do_nothing)()  # pylint: disable=not-callable

        with self.assertRaisesRegex(TransactionManagementError,
                                    'Cannot be inside an atomic block.'):
            with outer_atomic():
                outer_atomic()(do_nothing)()  # pylint: disable=not-callable
Esempio n. 4
0
class ChooseModeView(View):
    """View used when the user is asked to pick a mode.

    When a get request is used, shows the selection page.

    When a post request is used, assumes that it is a form submission
    from the selection page, parses the response, and then sends user
    to the next step in the flow.

    """
    @method_decorator(transaction.non_atomic_requests)
    def dispatch(self, *args, **kwargs):
        """Disable atomicity for the view.

        Otherwise, we'd be unable to commit to the database until the
        request had concluded; Django will refuse to commit when an
        atomic() block is active, since that would break atomicity.

        """
        return super().dispatch(*args, **kwargs)

    @method_decorator(login_required)
    @method_decorator(transaction.atomic)
    def get(self, request, course_id, error=None):  # lint-amnesty, pylint: disable=too-many-statements
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_client_ip(request)[0],
            url=request.path)
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)

        increment('track-selection.{}.{}'.format(
            enrollment_mode, 'active' if is_active else 'inactive'))
        increment('track-selection.views')

        if enrollment_mode is None:
            LOG.info(
                'Rendering track selection for unenrolled user, referred by %s',
                request.META.get('HTTP_REFERER'))

        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to 'no-id-professional'.
        has_enrolled_professional = (
            CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(
                modes) and not has_enrolled_professional:
            purchase_workflow = request.GET.get("purchase_workflow", "single")
            redirect_url = IDVerificationService.get_verify_location(
                course_id=course_key)
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(
                    CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(
                        CourseMode.PROFESSIONAL)
                if purchase_workflow == "single" and professional_mode.sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(
                        professional_mode.sku)
                if purchase_workflow == "bulk" and professional_mode.bulk_sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(
                        professional_mode.bulk_sku)
            return redirect(redirect_url)

        course = modulestore().get_course(course_key)

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  Send the user to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return self._redirect_to_course_or_dashboard(
                course, course_key, request.user)

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES +
                          [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            return self._redirect_to_course_or_dashboard(
                course, course_key, request.user)

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(str(course_key), None)

        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end,
                                                  'short',
                                                  locale=locale)
            params = six.moves.urllib.parse.urlencode(
                {'course_closed': enrollment_end_date})
            return redirect('{}?{}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for university credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode)
            for mode in CourseMode.modes_for_course(course_key,
                                                    only_selectable=False))
        course_id = str(course_key)
        gated_content = ContentTypeGatingConfig.enabled_for_enrollment(
            user=request.user, course_key=course_key)
        context = {
            "course_modes_choose_url":
            reverse("course_modes_choose", kwargs={'course_id': course_id}),
            "modes":
            modes,
            "has_credit_upsell":
            has_credit_upsell,
            "course_name":
            course.display_name_with_default,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "chosen_price":
            chosen_price,
            "error":
            error,
            "responsive":
            True,
            "nav_hidden":
            True,
            "content_gating_enabled":
            gated_content,
            "course_duration_limit_enabled":
            CourseDurationLimitConfig.enabled_for_enrollment(
                request.user, course),
        }
        context.update(
            get_experiment_user_metadata_context(
                course,
                request.user,
            ))

        title_content = ''
        if enrollment_mode:
            title_content = _(
                "Congratulations!  You are now enrolled in {course_name}"
            ).format(course_name=course.display_name_with_default)

        context["title_content"] = title_content

        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            price_before_discount = verified_mode.min_price
            course_price = price_before_discount
            enterprise_customer = enterprise_customer_for_request(request)
            LOG.info(
                '[e-commerce calculate API] Going to hit the API for user [%s] linked to [%s] enterprise',
                request.user.username,
                enterprise_customer.get('name') if isinstance(
                    enterprise_customer, dict) else None  # Test Purpose
            )
            if enterprise_customer and verified_mode.sku:
                course_price = get_course_final_price(request.user,
                                                      verified_mode.sku,
                                                      price_before_discount)

            context["currency"] = verified_mode.currency.upper()
            context["currency_symbol"] = get_currency_symbol(
                verified_mode.currency.upper())
            context["min_price"] = course_price
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description
            # if course_price is equal to price_before_discount then user doesn't entitle to any discount.
            if course_price != price_before_discount:
                context["price_before_discount"] = price_before_discount

            if verified_mode.sku:
                context[
                    "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(
                        request.user)
                context[
                    "ecommerce_payment_page"] = ecommerce_service.payment_page_url(
                    )
                context["sku"] = verified_mode.sku
                context["bulk_sku"] = verified_mode.bulk_sku

        context['currency_data'] = []
        if waffle.switch_is_active('local_currency'):
            if 'edx-price-l10n' not in request.COOKIES:
                currency_data = get_currency_data()
                try:
                    context['currency_data'] = json.dumps(currency_data)
                except TypeError:
                    pass

        language = get_language()
        context['track_links'] = get_verified_track_links(language)

        duration = get_user_course_duration(request.user, course)
        deadline = duration and get_user_course_expiration_date(
            request.user, course)
        if deadline:
            formatted_audit_access_date = strftime_localized_html(
                deadline, 'SHORT_DATE')
            context['audit_access_deadline'] = formatted_audit_access_date
        fbe_is_on = deadline and gated_content

        # Route to correct Track Selection page.
        # REV-2133 TODO Value Prop: remove waffle flag after testing is completed
        # and happy path version is ready to be rolled out to all users.
        if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled():
            if not error:  # TODO: Remove by executing REV-2355
                if not enterprise_customer_for_request(
                        request):  # TODO: Remove by executing REV-2342
                    if fbe_is_on:
                        return render_to_response("course_modes/fbe.html",
                                                  context)
                    else:
                        return render_to_response("course_modes/unfbe.html",
                                                  context)

        # If error or enterprise_customer, failover to old choose.html page
        return render_to_response("course_modes/choose.html", context)

    @method_decorator(transaction.non_atomic_requests)
    @method_decorator(login_required)
    @method_decorator(outer_atomic())
    def post(self, request, course_id):
        """Takes the form submission from the page and parses it.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Returns:
            Status code 400 when the requested mode is unsupported. When the honor mode
            is selected, redirects to the dashboard. When the verified mode is selected,
            returns error messages if the indicated contribution amount is invalid or
            below the minimum, otherwise redirects to the verification flow.

        """
        course_key = CourseKey.from_string(course_id)
        user = request.user

        # This is a bit redundant with logic in student.views.change_enrollment,
        # but I don't really have the time to refactor it more nicely and test.
        course = modulestore().get_course(course_key)
        if not user.has_perm(ENROLL_IN_COURSE, course):
            error_msg = _("Enrollment is closed")
            return self.get(request, course_id, error=error_msg)

        requested_mode = self._get_requested_mode(request.POST)

        allowed_modes = CourseMode.modes_for_course_dict(course_key)
        if requested_mode not in allowed_modes:
            return HttpResponseBadRequest(_("Enrollment mode not supported"))

        if requested_mode == 'audit':
            # If the learner has arrived at this screen via the traditional enrollment workflow,
            # then they should already be enrolled in an audit mode for the course, assuming one has
            # been configured.  However, alternative enrollment workflows have been introduced into the
            # system, such as third-party discovery.  These workflows result in learners arriving
            # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode.
            CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT)
            return self._redirect_to_course_or_dashboard(
                course, course_key, user)

        if requested_mode == 'honor':
            CourseEnrollment.enroll(user, course_key, mode=requested_mode)
            return self._redirect_to_course_or_dashboard(
                course, course_key, user)

        mode_info = allowed_modes[requested_mode]

        if requested_mode == 'verified':
            amount = request.POST.get("contribution") or \
                request.POST.get("contribution-other-amt") or 0

            try:
                # Validate the amount passed in and force it into two digits
                amount_value = decimal.Decimal(amount).quantize(
                    decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
            except decimal.InvalidOperation:
                error_msg = _("Invalid amount selected.")
                return self.get(request, course_id, error=error_msg)

            # Check for minimum pricing
            if amount_value < mode_info.min_price:
                error_msg = _(
                    "No selected price or selected price is too low.")
                return self.get(request, course_id, error=error_msg)

            donation_for_course = request.session.get("donation_for_course",
                                                      {})
            donation_for_course[str(course_key)] = amount_value
            request.session["donation_for_course"] = donation_for_course

            verify_url = IDVerificationService.get_verify_location(
                course_id=course_key)
            return redirect(verify_url)

    def _get_requested_mode(self, request_dict):
        """Get the user's requested mode

        Args:
            request_dict (`QueryDict`): A dictionary-like object containing all given HTTP POST parameters.

        Returns:
            The course mode slug corresponding to the choice in the POST parameters,
            None if the choice in the POST parameters is missing or is an unsupported mode.

        """
        if 'verified_mode' in request_dict:
            return 'verified'
        if 'honor_mode' in request_dict:
            return 'honor'
        if 'audit_mode' in request_dict:
            return 'audit'
        else:
            return None

    def _redirect_to_course_or_dashboard(self, course, course_key, user):
        """Perform a redirect to the course if the user is able to access the course.

        If the user is not able to access the course, redirect the user to the dashboard.

        Args:
            course: modulestore object for course
            course_key: course_id converted to a course_key
            user: request.user, the current user for the request

        Returns:
            302 to the course if possible or the dashboard if not.
        """
        if course.has_started() or user.is_staff:
            return redirect(
                reverse('openedx.course_experience.course_home',
                        kwargs={'course_id': course_key}))
        else:
            return redirect(reverse('dashboard'))
Esempio n. 5
0
class TransactionManagersTestCase(TransactionTestCase):
    """
    Tests outer_atomic.

    Note: This TestCase only works with MySQL.

    To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db"
    """
    DECORATORS = {
        'outer_atomic': outer_atomic(),
        'outer_atomic_read_committed': outer_atomic(read_committed=True),
    }

    @ddt.data(
        ('outer_atomic', IntegrityError, None, True),
        ('outer_atomic_read_committed', type(None), False, True),
    )
    @ddt.unpack
    def test_concurrent_requests(self, transaction_decorator_name, exception_class, created_in_1, created_in_2):
        """
        Test that when isolation level is set to READ COMMITTED get_or_create()
        for the same row in concurrent requests does not raise an IntegrityError.
        """
        transaction_decorator = self.DECORATORS[transaction_decorator_name]
        if connection.vendor != 'mysql':
            raise unittest.SkipTest('Only works on MySQL.')

        class RequestThread(threading.Thread):
            """ A thread which runs a dummy view."""
            def __init__(self, delay, **kwargs):
                super().__init__(**kwargs)
                self.delay = delay
                self.status = {}

            @transaction_decorator
            def run(self):
                """A dummy view."""
                try:
                    try:
                        User.objects.get(username='******', email='*****@*****.**')
                    except User.DoesNotExist:
                        pass
                    else:
                        raise AssertionError('Did not raise User.DoesNotExist.')

                    if self.delay > 0:
                        time.sleep(self.delay)

                    __, created = User.objects.get_or_create(username='******', email='*****@*****.**')
                except Exception as exception:  # pylint: disable=broad-except
                    self.status['exception'] = exception
                else:
                    self.status['created'] = created

        thread1 = RequestThread(delay=1)
        thread2 = RequestThread(delay=0)

        thread1.start()
        thread2.start()
        thread2.join()
        thread1.join()

        assert isinstance(thread1.status.get('exception'), exception_class)
        assert thread1.status.get('created') == created_in_1

        assert thread2.status.get('exception') is None
        assert thread2.status.get('created') == created_in_2

    def test_outer_atomic_nesting(self):
        """
        Test that outer_atomic raises an error if it is nested inside
        another atomic.
        """
        if connection.vendor != 'mysql':
            raise unittest.SkipTest('Only works on MySQL.')

        outer_atomic()(do_nothing)()  # pylint: disable=not-callable

        with atomic():
            atomic()(do_nothing)()  # pylint: disable=not-callable

        with outer_atomic():
            atomic()(do_nothing)()  # pylint: disable=not-callable

        with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
            with atomic():
                outer_atomic()(do_nothing)()  # pylint: disable=not-callable

        with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
            with outer_atomic():
                outer_atomic()(do_nothing)()  # pylint: disable=not-callable

    def test_named_outer_atomic_nesting(self):
        """
        Test that a named outer_atomic raises an error only if nested in
        enable_named_outer_atomic and inside another atomic.
        """
        if connection.vendor != 'mysql':
            raise unittest.SkipTest('Only works on MySQL.')

        outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with atomic():
            outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with enable_named_outer_atomic('abc'):

            outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable  # Not nested.

            with atomic():
                outer_atomic(name='pqr')(do_nothing)()  # pylint: disable=not-callable  # Not enabled.

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with enable_named_outer_atomic('abc', 'def'):

            outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable  # Not nested.

            with atomic():
                outer_atomic(name='pqr')(do_nothing)()  # pylint: disable=not-callable  # Not enabled.

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with outer_atomic():
                    outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with outer_atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable
Esempio n. 6
0
    def test_named_outer_atomic_nesting(self):
        """
        Test that a named outer_atomic raises an error only if nested in
        enable_named_outer_atomic and inside another atomic.
        """
        if connection.vendor != 'mysql':
            raise unittest.SkipTest('Only works on MySQL.')

        outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with atomic():
            outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with enable_named_outer_atomic('abc'):

            outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable  # Not nested.

            with atomic():
                outer_atomic(name='pqr')(do_nothing)()  # pylint: disable=not-callable  # Not enabled.

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with enable_named_outer_atomic('abc', 'def'):

            outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable  # Not nested.

            with atomic():
                outer_atomic(name='pqr')(do_nothing)()  # pylint: disable=not-callable  # Not enabled.

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with outer_atomic():
                    outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError, 'Cannot be inside an atomic block.'):
                with outer_atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable
Esempio n. 7
0
def create_account_with_params(request, params):
    """
    Given a request and a dict of parameters (which may or may not have come
    from the request), create an account for the requesting user, including
    creating a comments service user object and sending an activation email.
    This also takes external/third-party auth into account, updates that as
    necessary, and authenticates the user for the request's session.

    Does not return anything.

    Raises AccountValidationError if an account with the username or email
    specified by params already exists, or ValidationError if any of the given
    parameters is invalid for any other reason.

    Issues with this code:
    * It is non-transactional except where explicitly wrapped in atomic to
      alleviate deadlocks and improve performance. This means failures at
      different places in registration can leave users in inconsistent
      states.
    * Third-party auth passwords are not verified. There is a comment that
      they are unused, but it would be helpful to have a sanity check that
      they are sane.
    * The user-facing text is rather unfriendly (e.g. "Username must be a
      minimum of two characters long" rather than "Please use a username of
      at least two characters").
    * Duplicate email raises a ValidationError (rather than the expected
      AccountValidationError). Duplicate username returns an inconsistent
      user message (i.e. "An account with the Public Username '{username}'
      already exists." rather than "It looks like {username} belongs to an
      existing account. Try again with a different username.") The two checks
      occur at different places in the code; as a result, registering with
      both a duplicate username and email raises only a ValidationError for
      email only.
    """
    # Copy params so we can modify it; we can't just do dict(params) because if
    # params is request.POST, that results in a dict containing lists of values
    params = dict(list(params.items()))

    # allow to define custom set of required/optional/hidden fields via configuration
    extra_fields = configuration_helpers.get_value(
        'REGISTRATION_EXTRA_FIELDS',
        getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})
    )
    if is_registration_api_v1(request):
        if 'confirm_email' in extra_fields:
            del extra_fields['confirm_email']

    # registration via third party (Google, Facebook) using mobile application
    # doesn't use social auth pipeline (no redirect uri(s) etc involved).
    # In this case all related info (required for account linking)
    # is sent in params.
    # `third_party_auth_credentials_in_api` essentially means 'request
    # is made from mobile application'
    third_party_auth_credentials_in_api = 'provider' in params
    is_third_party_auth_enabled = third_party_auth.is_enabled()

    if is_third_party_auth_enabled and (pipeline.running(request) or third_party_auth_credentials_in_api):
        params["password"] = generate_password()

    # in case user is registering via third party (Google, Facebook) and pipeline has expired, show appropriate
    # error message
    if is_third_party_auth_enabled and ('social_auth_provider' in params and not pipeline.running(request)):
        raise ValidationError(
            {
                'session_expired': [
                    _("Registration using {provider} has timed out.").format(
                        provider=params.get('social_auth_provider'))
                ],
                'error_code': 'tpa-session-expired',
            }
        )

    if is_third_party_auth_enabled:
        set_custom_attribute('register_user_tpa', pipeline.running(request))
    extended_profile_fields = configuration_helpers.get_value('extended_profile_fields', [])
    # Can't have terms of service for certain SHIB users, like at Stanford
    registration_fields = getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})
    tos_required = (
        registration_fields.get('terms_of_service') != 'hidden' or
        registration_fields.get('honor_code') != 'hidden'
    )

    form = AccountCreationForm(
        data=params,
        extra_fields=extra_fields,
        extended_profile_fields=extended_profile_fields,
        do_third_party_auth=False,
        tos_required=tos_required,
    )
    custom_form = get_registration_extension_form(data=params)

    # Perform operations within a transaction that are critical to account creation
    with outer_atomic():
        # first, create the account
        (user, profile, registration) = do_create_account(form, custom_form)

        third_party_provider, running_pipeline = _link_user_to_third_party_provider(
            is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params,
        )

        new_user = authenticate_new_user(request, user.username, form.cleaned_data['password'])
        django_login(request, new_user)
        request.session.set_expiry(0)

    # Sites using multiple languages need to record the language used during registration.
    # If not, compose_and_send_activation_email will be sent in site's default language only.
    create_or_set_user_attribute_created_on_site(user, request.site)

    # Only add a default user preference if user does not already has one.
    if not preferences_api.has_user_preference(user, LANGUAGE_KEY):
        preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language())

    # Check if system is configured to skip activation email for the current user.
    skip_email = _skip_activation_email(
        user, running_pipeline, third_party_provider,
    )

    if skip_email:
        registration.activate()
    else:
        redirect_to, root_url = get_next_url_for_login_page(request, include_host=True)
        redirect_url = get_redirect_url_with_host(root_url, redirect_to)
        compose_and_send_activation_email(user, profile, registration, redirect_url)

    if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'):
        try:
            enable_notifications(user)
        except Exception:  # pylint: disable=broad-except
            log.exception(f"Enable discussion notifications failed for user {user.id}.")

    _track_user_registration(user, profile, params, third_party_provider, registration)

    # Announce registration
    REGISTER_USER.send(sender=None, user=user, registration=registration)

    STUDENT_REGISTRATION_COMPLETED.send_event(
        user=UserData(
            pii=UserPersonalData(
                username=user.username,
                email=user.email,
                name=user.profile.name,
            ),
            id=user.id,
            is_active=user.is_active,
        ),
    )

    create_comments_service_user(user)

    try:
        _record_registration_attributions(request, new_user)
        _record_marketing_emails_opt_in_attribute(params.get('marketing_emails_opt_in'), new_user)
    # Don't prevent a user from registering due to attribution errors.
    except Exception:   # pylint: disable=broad-except
        log.exception('Error while attributing cookies to user registration.')

    # TODO: there is no error checking here to see that the user actually logged in successfully,
    # and is not yet an active user.
    is_new_user(request, new_user)
    return new_user
Esempio n. 8
0
def queue_subtasks_for_query(
    entry,
    action_name,
    create_subtask_fcn,
    item_querysets,
    item_fields,
    items_per_task,
    total_num_items,
):
    """
    Generates and queues subtasks to each execute a chunk of "items" generated by a queryset.

    Arguments:
        `entry` : the InstructorTask object for which subtasks are being queued.
        `action_name` : a past-tense verb that can be used for constructing readable status messages.
        `create_subtask_fcn` : a function of two arguments that constructs the desired kind of subtask object.
            Arguments are the list of items to be processed by this subtask, and a SubtaskStatus
            object reflecting initial status (and containing the subtask's id).
        `item_querysets` : a list of query sets that define the "items" that should be passed to subtasks.
        `item_fields` : the fields that should be included in the dict that is returned.
            These are in addition to the 'pk' field.
        `items_per_task` : maximum size of chunks to break each query chunk into for use by a subtask.
        `total_num_items` : total amount of items that will be put into subtasks

    Returns:  the task progress as stored in the InstructorTask object.

    """
    task_id = entry.task_id

    # Calculate the number of tasks that will be created, and create a list of ids for each task.
    total_num_subtasks = _get_number_of_subtasks(total_num_items, items_per_task)
    subtask_id_list = [str(uuid4()) for _ in range(total_num_subtasks)]

    # Update the InstructorTask  with information about the subtasks we've defined.
    TASK_LOG.info(
        u"Task %s: updating InstructorTask %s with subtask info for %s subtasks to process %s items.",
        task_id,
        entry.id,
        total_num_subtasks,
        total_num_items,
    )
    # Make sure this is committed to database before handing off subtasks to celery.
    with outer_atomic():
        progress = initialize_subtask_info(entry, action_name, total_num_items, subtask_id_list)

    # Construct a generator that will return the recipients to use for each subtask.
    # Pass in the desired fields to fetch for each recipient.
    item_list_generator = _generate_items_for_subtask(
        item_querysets,
        item_fields,
        total_num_items,
        items_per_task,
        total_num_subtasks,
        entry.course_id,
    )

    # Now create the subtasks, and start them running.
    TASK_LOG.info(
        u"Task %s: creating %s subtasks to process %s items.",
        task_id,
        total_num_subtasks,
        total_num_items,
    )
    num_subtasks = 0
    for item_list in item_list_generator:
        subtask_id = subtask_id_list[num_subtasks]
        num_subtasks += 1
        subtask_status = SubtaskStatus.create(subtask_id)
        new_subtask = create_subtask_fcn(item_list, subtask_status)
        TASK_LOG.info(
            u"Queueing BulkEmail Task: %s Subtask: %s at timestamp: %s",
            task_id, subtask_id, datetime.now()
        )
        new_subtask.apply_async()

    # Subtasks have been queued so no exceptions should be raised after this point.

    # Return the task progress as stored in the InstructorTask object.
    return progress
Esempio n. 9
0
class SubmitPhotosView(View):
    """
    End-point for submitting photos for verification.
    """
    @method_decorator(transaction.non_atomic_requests)
    def dispatch(self, request, *args, **kwargs):
        return super(SubmitPhotosView, self).dispatch(request, *args, **kwargs)

    @method_decorator(login_required)
    @method_decorator(outer_atomic(read_committed=True))
    def post(self, request):
        """
        Submit photos for verification.

        This end-point is used for the following cases:

        * Initial verification through the pay-and-verify flow.
        * Initial verification initiated from a checkpoint within a course.
        * Re-verification initiated from a checkpoint within a course.

        POST Parameters:

            face_image (str): base64-encoded image data of the user's face.
            photo_id_image (str): base64-encoded image data of the user's photo ID.
            full_name (str): The user's full name, if the user is requesting a name change as well.
            course_key (str): Identifier for the course, if initiated from a checkpoint.
            checkpoint (str): Location of the checkpoint in the course.

        """
        log.info((
            u"User {user_id} is submitting photos for ID verification").format(
                user_id=request.user.id))

        # If the user already has an initial verification attempt, we can re-use the photo ID
        # the user submitted with the initial attempt.
        initial_verification = SoftwareSecurePhotoVerification.get_initial_verification(
            request.user)

        # Validate the POST parameters
        params, response = self._validate_parameters(
            request, bool(initial_verification))
        if response is not None:
            return response

        # If necessary, update the user's full name
        if "full_name" in params:
            response = self._update_full_name(request, params["full_name"])
            if response is not None:
                return response

        # Retrieve the image data
        # Validation ensures that we'll have a face image, but we may not have
        # a photo ID image if this is a re-verification.
        face_image, photo_id_image, response = self._decode_image_data(
            request, params["face_image"], params.get("photo_id_image"))

        # If we have a photo_id we do not want use the initial verification image.
        if photo_id_image is not None:
            initial_verification = None

        if response is not None:
            return response

        # Submit the attempt
        self._submit_attempt(request.user, face_image, photo_id_image,
                             initial_verification)

        self._fire_event(request.user, "edx.bi.verify.submitted",
                         {"category": "verification"})
        self._send_confirmation_email(request.user)
        return JsonResponse({})

    def _validate_parameters(self, request, has_initial_verification):
        """
        Check that the POST parameters are valid.

        Arguments:
            request (HttpRequest): The request object.
            has_initial_verification (bool): Whether the user has an initial verification attempt.

        Returns:
            HttpResponse or None

        """
        # Pull out the parameters we care about.
        params = {
            param_name: request.POST[param_name]
            for param_name in
            ["face_image", "photo_id_image", "course_key", "full_name"]
            if param_name in request.POST
        }

        # If the user already has an initial verification attempt, then we don't
        # require the user to submit a photo ID image, since we can re-use the photo ID
        # image from the initial attempt.
        # If we don't have an initial verification OR a photo ID image, something has gone
        # terribly wrong in the JavaScript.  Log this as an error so we can track it down.
        if "photo_id_image" not in params and not has_initial_verification:
            log.error(
                (u"User %s does not have an initial verification attempt "
                 "and no photo ID image data was provided. "
                 "This most likely means that the JavaScript client is not "
                 "correctly constructing the request to submit photos."),
                request.user.id)
            return None, HttpResponseBadRequest(
                _("Photo ID image is required if the user does not have an initial verification attempt."
                  ))

        # The face image is always required.
        if "face_image" not in params:
            msg = _("Missing required parameter face_image")
            log.error((u"User {user_id} missing required parameter face_image"
                       ).format(user_id=request.user.id))
            return None, HttpResponseBadRequest(msg)

        # If provided, parse the course key and checkpoint location
        if "course_key" in params:
            try:
                params["course_key"] = CourseKey.from_string(
                    params["course_key"])
            except InvalidKeyError:
                log.error(
                    (u"User {user_id} provided invalid course_key").format(
                        user_id=request.user.id))
                return None, HttpResponseBadRequest(_("Invalid course key"))

        return params, None

    def _update_full_name(self, request, full_name):
        """
        Update the user's full name.

        Arguments:
            user (User): The user to update.
            full_name (unicode): The user's updated full name.

        Returns:
            HttpResponse or None

        """
        try:
            update_account_settings(request.user, {"name": full_name})
        except UserNotFound:
            log.error((u"No profile found for user {user_id}").format(
                user_id=request.user.id))
            return HttpResponseBadRequest(_("No profile found for user"))
        except AccountValidationError:
            msg = _(u"Name must be at least {min_length} character long."
                    ).format(min_length=NAME_MIN_LENGTH)
            log.error((
                u"User {user_id} provided an account name less than {min_length} characters"
            ).format(user_id=request.user.id, min_length=NAME_MIN_LENGTH))
            return HttpResponseBadRequest(msg)

    def _decode_image_data(self, request, face_data, photo_id_data=None):
        """
        Decode image data sent with the request.

        Arguments:
            face_data (str): base64-encoded face image data.

        Keyword Arguments:
            photo_id_data (str): base64-encoded photo ID image data.

        Returns:
            tuple of (str, str, HttpResponse)

        """
        try:
            # Decode face image data (used for both an initial and re-verification)
            face_image = decode_image_data(face_data)

            # Decode the photo ID image data if it's provided
            photo_id_image = (decode_image_data(photo_id_data)
                              if photo_id_data is not None else None)

            return face_image, photo_id_image, None

        except InvalidImageData:
            msg = _("Image data is not valid.")
            log.error((u"Image data for user {user_id} is not valid").format(
                user_id=request.user.id))
            return None, None, HttpResponseBadRequest(msg)

    def _submit_attempt(self,
                        user,
                        face_image,
                        photo_id_image=None,
                        initial_verification=None):
        """
        Submit a verification attempt.

        Arguments:
            user (User): The user making the attempt.
            face_image (str): Decoded face image data.

        Keyword Arguments:
            photo_id_image (str or None): Decoded photo ID image data.
            initial_verification (SoftwareSecurePhotoVerification): The initial verification attempt.
        """
        attempt = SoftwareSecurePhotoVerification(user=user)

        # We will always have face image data, so upload the face image
        attempt.upload_face_image(face_image)

        # If an ID photo wasn't submitted, re-use the ID photo from the initial attempt.
        # Earlier validation rules ensure that at least one of these is available.
        if photo_id_image is not None:
            attempt.upload_photo_id_image(photo_id_image)
        elif initial_verification is None:
            # Earlier validation should ensure that we never get here.
            log.error(
                "Neither a photo ID image or initial verification attempt provided. "
                "Parameter validation in the view should prevent this from happening!"
            )

        # Submit the attempt
        attempt.mark_ready()
        attempt.submit(copy_id_photo_from=initial_verification)

        return attempt

    def _send_confirmation_email(self, user):
        """
        Send an email confirming that the user submitted photos
        for initial verification.
        """
        lms_root_url = configuration_helpers.get_value('LMS_ROOT_URL',
                                                       settings.LMS_ROOT_URL)
        context = {
            'user': user,
            'dashboard_link': '{}{}'.format(lms_root_url, reverse('dashboard'))
        }
        return send_verification_confirmation_email(context)

    def _fire_event(self, user, event_name, parameters):
        """
        Fire an analytics event.

        Arguments:
            user (User): The user who submitted photos.
            event_name (str): Name of the analytics event.
            parameters (dict): Event parameters.

        Returns: None

        """
        segment.track(user.id, event_name, parameters)
def run_main_task(entry_id, task_fcn, action_name):
    """
    Applies the `task_fcn` to the arguments defined in `entry_id` InstructorTask.

    Arguments passed to `task_fcn` are:

     `entry_id` : the primary key for the InstructorTask entry representing the task.
     `course_id` : the id for the course.
     `task_input` : dict containing task-specific arguments, JSON-decoded from InstructorTask's task_input.
     `action_name` : past-tense verb to use for constructing status messages.

    If no exceptions are raised, the `task_fcn` should return a dict containing
    the task's result with the following keys:

          'attempted': number of attempts made
          'succeeded': number of attempts that "succeeded"
          'skipped': number of attempts that "skipped"
          'failed': number of attempts that "failed"
          'total': number of possible subtasks to attempt
          'action_name': user-visible verb to use in status messages.
              Should be past-tense.  Pass-through of input `action_name`.
          'duration_ms': how long the task has (or had) been running.

    """

    # Get the InstructorTask to be updated. If this fails then let the exception return to Celery.
    # There's no point in catching it here.
    with outer_atomic():
        entry = InstructorTask.objects.get(pk=entry_id)
        entry.task_state = PROGRESS
        entry.save_now()

    # Get inputs to use in this task from the entry
    task_id = entry.task_id
    course_id = entry.course_id
    task_input = json.loads(entry.task_input)

    # Construct log message
    fmt = 'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
    task_info_string = fmt.format(task_id=task_id,
                                  entry_id=entry_id,
                                  course_id=course_id,
                                  task_input=task_input)
    TASK_LOG.info('%s, Starting update (nothing %s yet)', task_info_string,
                  action_name)

    # Check that the task_id submitted in the InstructorTask matches the current task
    # that is running.
    request_task_id = _get_current_task().request.id
    if task_id != request_task_id:
        fmt = '{task_info}, Requested task did not match actual task "{actual_id}"'
        message = fmt.format(task_info=task_info_string,
                             actual_id=request_task_id)
        TASK_LOG.error(message)
        raise ValueError(message)

    # Now do the work
    task_progress = task_fcn(entry_id, course_id, task_input, action_name)

    # Release any queries that the connection has been hanging onto
    reset_queries()

    # Log and exit, returning task_progress info as task result
    TASK_LOG.info('%s, Task type: %s, Finishing task: %s', task_info_string,
                  action_name, task_progress)
    return task_progress
Esempio n. 11
0
    def test_named_outer_atomic_nesting(self):
        """
        Test that a named outer_atomic raises an error only if nested in
        enable_named_outer_atomic and inside another atomic.
        """
        outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with atomic():
            outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with enable_named_outer_atomic('abc'):

            outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable  # Not nested.

            with atomic():
                outer_atomic(name='pqr')(do_nothing)()  # pylint: disable=not-callable  # Not enabled.

            with self.assertRaisesRegex(TransactionManagementError,
                                        'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

        with enable_named_outer_atomic('abc', 'def'):

            outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable  # Not nested.

            with atomic():
                outer_atomic(name='pqr')(do_nothing)()  # pylint: disable=not-callable  # Not enabled.

            with self.assertRaisesRegex(TransactionManagementError,
                                        'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError,
                                        'Cannot be inside an atomic block.'):
                with outer_atomic():
                    outer_atomic(name='def')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError,
                                        'Cannot be inside an atomic block.'):
                with atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable

            with self.assertRaisesRegex(TransactionManagementError,
                                        'Cannot be inside an atomic block.'):
                with outer_atomic():
                    outer_atomic(name='abc')(do_nothing)()  # pylint: disable=not-callable