views.py 10.7 KB
Newer Older
1 2 3 4
"""
Views for the course_mode module
"""

5
import decimal
6 7
from ipware.ip import get_ip

8
from django.core.urlresolvers import reverse
9
from django.http import HttpResponse, HttpResponseBadRequest
10
from django.shortcuts import redirect
11
from django.views.generic.base import View
12
from django.utils.translation import ugettext as _
13 14
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
15

David Baumgold committed
16
from edxmako.shortcuts import render_to_response
17 18

from course_modes.models import CourseMode
19
from courseware.access import has_access
Julia Hansbrough committed
20
from student.models import CourseEnrollment
21
from opaque_keys.edx.locations import SlashSeparatedCourseKey
22
from opaque_keys.edx.keys import CourseKey
23
from util.db import commit_on_success_with_read_committed
24
from xmodule.modulestore.django import modulestore
25

26 27
from embargo import api as embargo_api

28

29
class ChooseModeView(View):
30
    """View used when the user is asked to pick a mode.
31 32

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

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

38
    """
39

40
    @method_decorator(login_required)
41
    def get(self, request, course_id, error=None):
42
        """Displays the course mode choice page.
43 44 45 46 47 48 49 50

        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.
51

52 53 54 55
        Returns:
            Response

        """
56 57
        course_key = CourseKey.from_string(course_id)

58 59 60 61 62 63 64 65 66 67 68
        # 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_ip(request),
            url=request.path
        )
        if embargo_redirect:
            return redirect(embargo_redirect)

69
        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
70
        modes = CourseMode.modes_for_course_dict(course_key)
71 72 73

        # We assume that, if 'professional' is one of the modes, it is the *only* mode.
        # If we offer more modes alongside 'professional' in the future, this will need to route
74 75 76
        # to the usual "choose your track" page same is true for no-id-professional mode.
        has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
77 78 79 80
            return redirect(
                reverse(
                    'verify_student_start_flow',
                    kwargs={'course_id': unicode(course_key)}
81
                )
82
            )
83

84 85 86 87 88 89 90 91
        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  The user has almost certainly been auto-registered
        # in the "honor" track by this point, so we send the user
        # to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return redirect(reverse('dashboard'))

        # If a user has already paid, redirect them to the dashboard.
92
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]):
93 94
            return redirect(reverse('dashboard'))

95
        donation_for_course = request.session.get("donation_for_course", {})
96
        chosen_price = donation_for_course.get(unicode(course_key), None)
97

98
        course = modulestore().get_course(course_key)
99 100 101 102 103 104 105 106 107 108 109 110 111 112

        # 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 univerity 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)
        )

113
        context = {
114
            "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}),
115
            "modes": modes,
116
            "has_credit_upsell": has_credit_upsell,
117
            "course_name": course.display_name_with_default,
118 119
            "course_org": course.display_org_with_default,
            "course_num": course.display_number_with_default,
120
            "chosen_price": chosen_price,
121
            "error": error,
122 123
            "responsive": True,
            "nav_hidden": True,
124
        }
125
        if "verified" in modes:
126 127 128 129 130
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in modes["verified"].suggested_prices.split(",")
                if x.strip()
            ]
131
            context["currency"] = modes["verified"].currency.upper()
132
            context["min_price"] = modes["verified"].min_price
133
            context["verified_name"] = modes["verified"].name
134
            context["verified_description"] = modes["verified"].description
135

136 137
        return render_to_response("course_modes/choose.html", context)

138
    @method_decorator(login_required)
139
    @method_decorator(commit_on_success_with_read_committed)
140
    def post(self, request, course_id):
141 142 143 144 145 146 147 148 149 150 151 152 153
        """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.

        """
154
        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
155 156
        user = request.user

Sarina Canelake committed
157
        # This is a bit redundant with logic in student.views.change_enrollment,
158
        # but I don't really have the time to refactor it more nicely and test.
159
        course = modulestore().get_course(course_key)
160
        if not has_access(user, 'enroll', course):
161
            error_msg = _("Enrollment is closed")
162
            return self.get(request, course_id, error=error_msg)
163

164
        requested_mode = self._get_requested_mode(request.POST)
165

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

170
        if requested_mode == 'honor':
171 172 173
            # The user will have already been enrolled in the honor mode at this
            # point, so we just redirect them to the dashboard, thereby avoiding
            # hitting the database a second time attempting to enroll them.
174
            return redirect(reverse('dashboard'))
175

176 177
        mode_info = allowed_modes[requested_mode]

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

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

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

194
            donation_for_course = request.session.get("donation_for_course", {})
195
            donation_for_course[unicode(course_key)] = amount_value
196 197
            request.session["donation_for_course"] = donation_for_course

198 199 200 201
            return redirect(
                reverse(
                    'verify_student_start_flow',
                    kwargs={'course_id': unicode(course_key)}
202
                )
203
            )
204

205 206 207 208 209 210 211 212 213 214
    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.

215
        """
216
        if 'verified_mode' in request_dict:
217
            return 'verified'
218 219 220 221
        if 'honor_mode' in request_dict:
            return 'honor'
        else:
            return None
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240


def create_mode(request, course_id):
    """Add a mode to the course corresponding to the given course ID.

    Only available when settings.FEATURES['MODE_CREATION_FOR_TESTING'] is True.

    Attempts to use the following querystring parameters from the request:
        `mode_slug` (str): The mode to add, either 'honor', 'verified', or 'professional'
        `mode_display_name` (str): Describes the new course mode
        `min_price` (int): The minimum price a user must pay to enroll in the new course mode
        `suggested_prices` (str): Comma-separated prices to suggest to the user.
        `currency` (str): The currency in which to list prices.

    By default, this endpoint will create an 'honor' mode for the given course with display name
    'Honor Code', a minimum price of 0, no suggested prices, and using USD as the currency.

    Args:
        request (`Request`): The Django Request object.
241
        course_id (unicode): A course ID.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262

    Returns:
        Response
    """
    PARAMETERS = {
        'mode_slug': u'honor',
        'mode_display_name': u'Honor Code Certificate',
        'min_price': 0,
        'suggested_prices': u'',
        'currency': u'usd',
    }

    # Try pulling querystring parameters out of the request
    for parameter, default in PARAMETERS.iteritems():
        PARAMETERS[parameter] = request.GET.get(parameter, default)

    # Attempt to create the new mode for the given course
    course_key = CourseKey.from_string(course_id)
    CourseMode.objects.get_or_create(course_id=course_key, **PARAMETERS)

    # Return a success message and a 200 response
263
    return HttpResponse("Mode '{mode_slug}' created for '{course}'.".format(
264
        mode_slug=PARAMETERS['mode_slug'],
265
        course=course_id
266
    ))