diff --git a/cms/envs/common.py b/cms/envs/common.py index 42e6607d6cd7..5f575e0cda1b 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1575,3 +1575,8 @@ # setting for the FileWrapper class used to iterate over the export file data. # See: https://docs.python.org/2/library/wsgiref.html#wsgiref.util.FileWrapper COURSE_EXPORT_DOWNLOAD_CHUNK_SIZE = 8192 + +############## Qverse Features ######################### + +# edX does not set the following variable in cms. But we have to set these, otherwise it gives an error. +PAID_COURSE_REGISTRATION_CURRENCY = ['usd', '$'] diff --git a/cms/envs/production.py b/cms/envs/production.py index ccfa74e1cc9c..5674b578e07f 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -624,3 +624,9 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + +############## Qverse Features ######################### + +# edX does not set the following variable in cms. But we have to set these, otherwise it gives an error. +PAID_COURSE_REGISTRATION_CURRENCY = ENV_TOKENS.get('PAID_COURSE_REGISTRATION_CURRENCY', + PAID_COURSE_REGISTRATION_CURRENCY) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 520958b47305..27f3828aabd4 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -33,6 +33,7 @@ 'bulk_sku', ]) +CURRENCY = settings.PAID_COURSE_REGISTRATION_CURRENCY[0] class CourseMode(models.Model): """ @@ -70,14 +71,14 @@ def course_id(self, value): # The 'pretty' name that can be translated and displayed mode_display_name = models.CharField(max_length=255, verbose_name=_("Display Name")) - # The price in USD that we would like to charge for this mode of the course + # The price in configured currency that we would like to charge for this mode of the course # Historical note: We used to allow users to choose from several prices, but later # switched to using a single price. Although this field is called `min_price`, it is # really just the price of the course. min_price = models.IntegerField(default=0, verbose_name=_("Price")) # the currency these prices are in, using lower case ISO currency codes - currency = models.CharField(default="usd", max_length=8) + currency = models.CharField(default=CURRENCY, max_length=8) # The datetime at which the course mode will expire. # This is used to implement "upgrade" deadlines. @@ -181,7 +182,7 @@ def course_id(self, value): # "honor" to "audit", we still need to have the shoppingcart # use "honor" DEFAULT_SHOPPINGCART_MODE_SLUG = HONOR - DEFAULT_SHOPPINGCART_MODE = Mode(HONOR, _('Honor'), 0, '', 'usd', None, None, None, None) + DEFAULT_SHOPPINGCART_MODE = Mode(HONOR, _('Honor'), 0, '', CURRENCY, None, None, None, None) CACHE_NAMESPACE = u"course_modes.CourseMode.cache." @@ -757,12 +758,12 @@ def get_course_prices(course, verified_only=False): if verified_only: registration_price = CourseMode.min_course_price_for_verified_for_currency( course.id, - settings.PAID_COURSE_REGISTRATION_CURRENCY[0] + CURRENCY ) else: registration_price = CourseMode.min_course_price_for_currency( course.id, - settings.PAID_COURSE_REGISTRATION_CURRENCY[0] + CURRENCY ) if registration_price > 0: @@ -813,7 +814,7 @@ class Meta(object): # The 'pretty' name that can be translated and displayed mode_display_name = models.CharField(max_length=255) - # minimum price in USD that we would like to charge for this mode of the course + # minimum price in configued currency that we would like to charge for this mode of the course min_price = models.IntegerField(default=0) # the suggested prices for this mode @@ -821,7 +822,7 @@ class Meta(object): validators=[validate_comma_separated_integer_list]) # the currency these prices are in, using lower case ISO currency codes - currency = models.CharField(default="usd", max_length=8) + currency = models.CharField(default=CURRENCY, max_length=8) # turn this mode off after the given expiration date expiration_date = models.DateField(default=None, null=True, blank=True) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 655f1378d2ed..3377553f3a61 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals """ Views for the course_mode module """ @@ -8,6 +9,7 @@ import waffle from babel.dates import format_datetime +from django.conf import settings from django.contrib.auth.decorators import login_required from django.db import transaction from django.http import HttpResponse, HttpResponseBadRequest @@ -219,6 +221,7 @@ def get(self, request, course_id, error=None): if x.strip() ] context["currency"] = verified_mode.currency.upper() + context["currency_symbol"] = settings.PAID_COURSE_REGISTRATION_CURRENCY[1] context["min_price"] = verified_mode.min_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description @@ -355,7 +358,7 @@ def create_mode(request, course_id): `sku` (str): The product SKU value. 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. + 'Honor Code', a minimum price of 0, no suggested prices, and using configured default currency as the currency. Args: request (`Request`): The Django Request object. @@ -369,7 +372,7 @@ def create_mode(request, course_id): 'mode_display_name': u'Honor Code Certificate', 'min_price': 0, 'suggested_prices': u'', - 'currency': u'usd', + 'currency': unicode(settings.PAID_COURSE_REGISTRATION_CURRENCY[0]), 'sku': None, } diff --git a/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx b/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx index 2b47d8fe393b..b04d00f7c815 100644 --- a/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx +++ b/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx @@ -49,6 +49,8 @@ export class PortfolioExperimentUpsellModal extends React.Component { ); + let upgradeText = `Upgrade (${this.props.currency_symbol}100 ${this.props.currency})`; + return ( {}} diff --git a/common/static/common/js/components/UpsellExperimentModal.jsx b/common/static/common/js/components/UpsellExperimentModal.jsx index d0d565277f64..528c305ad5c5 100644 --- a/common/static/common/js/components/UpsellExperimentModal.jsx +++ b/common/static/common/js/components/UpsellExperimentModal.jsx @@ -65,6 +65,7 @@ export class UpsellExperimentModal extends React.Component { ); const { buttonDestinationURL } = this.props; + let upgradeText = `Upgrade (${this.props.currency_symbol}100 ${this.props.currency})`; return ( window.location = buttonDestinationURL} @@ -88,4 +89,4 @@ export class UpsellExperimentModal extends React.Component { UpsellExperimentModal.propTypes = { buttonDestinationURL: PropTypes.string.isRequired, -}; \ No newline at end of file +}; diff --git a/common/test/acceptance/pages/lms/create_mode.py b/common/test/acceptance/pages/lms/create_mode.py index d50e4630a132..edc4ad1f5c26 100644 --- a/common/test/acceptance/pages/lms/create_mode.py +++ b/common/test/acceptance/pages/lms/create_mode.py @@ -20,7 +20,7 @@ def __init__(self, browser, course_id, mode_slug=None, mode_display_name=None, m """The mode creation page is an endpoint for HTTP GET requests. By default, it 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. + 'Honor Code', a minimum price of 0, no suggested prices, and using configured currency as the currency. Arguments: browser (Browser): The browser instance. diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 923f180d959a..679519c2a6bd 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -866,6 +866,8 @@ def course_about(request, course_id): reviews_fragment_view = CourseReviewsModuleFragmentView().render_to_fragment(request, course=course) context = { + 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], + 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'course': course, 'course_details': course_details, 'staff_access': staff_access, @@ -917,7 +919,11 @@ def program_marketing(request, program_uuid): skus = program.get('skus') ecommerce_service = EcommerceService() - context = {'program': program} + context = { + 'program': program, + 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], + 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], + } if program.get('is_learner_eligible_for_one_click_purchase') and skus: context['buy_button_href'] = ecommerce_service.get_checkout_page_url(*skus, program_uuid=program_uuid) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index b66424927166..e429f68b236b 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -90,6 +90,7 @@ # we need a tuple to represent the primary key of various OrderItem subclasses OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) +CURRENCY = settings.PAID_COURSE_REGISTRATION_CURRENCY[0] class OrderTypes(object): @@ -115,7 +116,7 @@ class Meta(object): app_label = "shoppingcart" user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE) - currency = models.CharField(default="usd", max_length=8) # lower case ISO currency codes + currency = models.CharField(default=CURRENCY, max_length=8) # lower case ISO currency codes status = models.CharField(max_length=32, default='cart', choices=ORDER_STATUSES) purchase_time = models.DateTimeField(null=True, blank=True) refunded_time = models.DateTimeField(null=True, blank=True) @@ -654,7 +655,7 @@ class Meta(object): unit_cost = models.DecimalField(default=0.0, decimal_places=2, max_digits=30) list_price = models.DecimalField(decimal_places=2, max_digits=30, null=True) line_desc = models.CharField(default="Misc. Item", max_length=1024) - currency = models.CharField(default="usd", max_length=8) # lower case ISO currency codes + currency = models.CharField(default=CURRENCY, max_length=8) # lower case ISO currency codes fulfilled_time = models.DateTimeField(null=True, db_index=True) refund_requested_time = models.DateTimeField(null=True, db_index=True) service_fee = models.DecimalField(default=0.0, decimal_places=2, max_digits=30) @@ -687,7 +688,7 @@ def add_to_order(cls, order, *args, **kwargs): # this is a validation step to verify that the currency of the item we # are adding is the same as the currency of the order we are adding it # to - currency = kwargs.get('currency', 'usd') + currency = kwargs.get('currency', CURRENCY) if order.currency != currency and order.orderitem_set.exists(): raise InvalidCartItem(_("Trying to add a different currency into the cart")) @@ -1009,7 +1010,7 @@ class Meta(object): ) ) currency = models.CharField( - default="usd", + default=CURRENCY, max_length=8, help_text=ugettext_lazy("Lower-case ISO currency codes") ) @@ -1104,7 +1105,7 @@ class Meta(object): help_text=ugettext_lazy("The price per item sold, including discounts.") ) currency = models.CharField( - default="usd", + default=CURRENCY, max_length=8, help_text=ugettext_lazy("Lower-case ISO currency codes") ) @@ -1921,7 +1922,7 @@ def refund_cert_callback(sender, course_enrollment=None, skip_refund=False, **kw @classmethod @transaction.atomic - def add_to_order(cls, order, course_id, cost, mode, currency='usd'): + def add_to_order(cls, order, course_id, cost, mode, currency=CURRENCY): """ Add a CertificateItem to an order @@ -2059,7 +2060,9 @@ def verified_certificates_contributing_more_than_minimum(cls, course_id): course_id=course_id, mode='verified', status='purchased', - unit_cost__gt=(CourseMode.min_course_price_for_verified_for_currency(course_id, 'usd')))).count() + unit_cost__gt=( + CourseMode.min_course_price_for_verified_for_currency(course_id, CURRENCY)) + )).count() def analytics_data(self): """Simple function used to construct analytics data for the OrderItem. @@ -2113,7 +2116,7 @@ class Meta(object): @classmethod @transaction.atomic - def add_to_order(cls, order, donation_amount, course_id=None, currency='usd'): + def add_to_order(cls, order, donation_amount, course_id=None, currency=CURRENCY): """Add a donation to an order. Args: diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource2.py b/lms/djangoapps/shoppingcart/processors/CyberSource2.py index e12d7aa12374..15a880aba9d8 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource2.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource2.py @@ -387,7 +387,7 @@ def _payment_accepted(order_id, auth_amount, currency, decision): return { 'accepted': False, 'amt_charged': 0, - 'currency': 'usd', + 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], 'order': order } diff --git a/lms/djangoapps/shoppingcart/reports.py b/lms/djangoapps/shoppingcart/reports.py index 8e8520b43810..7126437a0653 100644 --- a/lms/djangoapps/shoppingcart/reports.py +++ b/lms/djangoapps/shoppingcart/reports.py @@ -3,6 +3,7 @@ from decimal import Decimal import unicodecsv +from django.conf import settings from django.utils.translation import ugettext as _ from six import text_type @@ -164,6 +165,7 @@ def rows(self): total_enrolled = counts['total'] audit_enrolled = counts['audit'] honor_enrolled = counts['honor'] + currency = settings.PAID_COURSE_REGISTRATION_CURRENCY[0] if counts['verified'] == 0: verified_enrolled = 0 @@ -172,7 +174,7 @@ def rows(self): else: verified_enrolled = counts['verified'] gross_rev = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'purchased', 'unit_cost') - gross_rev_over_min = gross_rev - (CourseMode.min_course_price_for_verified_for_currency(course_id, 'usd') * verified_enrolled) + gross_rev_over_min = gross_rev - (CourseMode.min_course_price_for_verified_for_currency(course_id, currency) * verified_enrolled) num_verified_over_the_minimum = CertificateItem.verified_certificates_contributing_more_than_minimum(course_id) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 27d8e7c1d866..944a6d5420c6 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -411,6 +411,7 @@ def get( # Render the top-level page context = { + 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'contribution_amount': contribution_amount, 'course': course, 'course_key': unicode(course_key), diff --git a/lms/static/js/verify_student/pay_and_verify.js b/lms/static/js/verify_student/pay_and_verify.js index fd66d779ec51..913ba7a152e5 100644 --- a/lms/static/js/verify_student/pay_and_verify.js +++ b/lms/static/js/verify_student/pay_and_verify.js @@ -65,6 +65,7 @@ var edx = edx || {}; function(price) { return Boolean(price); } ), currency: $el.data('course-mode-currency'), + currencySymbol: $el.data('currency-symbol'), processors: $el.data('processors'), verificationDeadline: $el.data('verification-deadline'), courseModeSlug: $el.data('course-mode-slug'), diff --git a/lms/templates/learner_dashboard/program_details_view.underscore b/lms/templates/learner_dashboard/program_details_view.underscore index 55676fcbbfd9..a7b09c3b50b3 100644 --- a/lms/templates/learner_dashboard/program_details_view.underscore +++ b/lms/templates/learner_dashboard/program_details_view.underscore @@ -27,14 +27,14 @@ <% if (discount_data.is_discounted) { %> <%- StringUtils.interpolate( - gettext('${listPrice}'), {listPrice: discount_data.total_incl_tax_excl_discounts.toFixed(2)} + gettext('{currency_symbol}{listPrice}'), {currency_symbol: currency_symbol, listPrice: discount_data.total_incl_tax_excl_discounts.toFixed(2)} ) %> <% } %> <%- StringUtils.interpolate( - gettext(' ${price} {currency} )'), - {price: full_program_price.toFixed(2), currency: discount_data.currency} + gettext(' {currency_symbol}{price} {currency} )'), + {currency_symbol:currency_symbol, price: full_program_price.toFixed(2), currency: discount_data.currency} ) %> diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index 164e3034c2f4..c6454c6b1537 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -5,6 +5,7 @@ import uuid import pycountry +from django.conf import settings from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist from edx_rest_api_client.client import EdxRestApiClient @@ -24,6 +25,10 @@ logger = logging.getLogger(__name__) +CURRENCY_CODE = settings.PAID_COURSE_REGISTRATION_CURRENCY[0] +CURRENCY_SYMBOL = settings.PAID_COURSE_REGISTRATION_CURRENCY[1] + + def create_catalog_api_client(user, site=None): """Returns an API client which can be used to make Catalog API requests.""" jwt = create_jwt_for_user(user) @@ -230,13 +235,13 @@ def get_currency_data(): return [] -def format_price(price, symbol='$', code='USD'): +def format_price(price, symbol=CURRENCY_SYMBOL, code=CURRENCY_CODE): """ Format the price to have the appropriate currency and digits.. :param price: The price amount. - :param symbol: The symbol for the price (default: $) - :param code: The currency code to be appended to the price (default: USD) + :param symbol: The symbol for the price (default: configured currency symbol) + :param code: The currency code to be appended to the price (default: configured currency) :return: A formatted price string, i.e. '$10 USD', '$10.52 USD'. """ if int(price) == price: @@ -249,12 +254,12 @@ def get_localized_price_text(price, request): Returns the localized converted price as string (ex. '$150 USD') If the users location has been added to the request, this will return the given price based on conversion rate - from the Catalog service and return a localized string otherwise will return the default price in USD + from the Catalog service and return a localized string otherwise will return the default price in configured currency """ user_currency = { - 'symbol': '$', + 'symbol': CURRENCY_SYMBOL, 'rate': 1, - 'code': 'USD' + 'code': CURRENCY_CODE } # session.country_code is added via CountryMiddleware in the LMS diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 05400ca3bfea..44168ddf2013 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -692,7 +692,8 @@ def _collect_one_click_purchase_eligibility_data(self): self.data.update({ 'discount_data': discount_data, 'full_program_price': discount_data['total_incl_tax'], - 'variant': bundle_variant + 'variant': bundle_variant, + 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1] }) except (ConnectionError, SlumberBaseException, Timeout): log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus)) diff --git a/openedx/core/djangoapps/user_api/api.py b/openedx/core/djangoapps/user_api/api.py index 6596111a4f69..541826aac9f9 100644 --- a/openedx/core/djangoapps/user_api/api.py +++ b/openedx/core/djangoapps/user_api/api.py @@ -213,9 +213,8 @@ def _apply_third_party_auth_overrides(request, form_desc): class RegistrationFormFactory(object): """HTTP end-points for creating a new user. """ - # To remove full name field from the registration form shown to the user - # the "name" field has been removed from DEFAULT_FIELDS - DEFAULT_FIELDS = ["email", "username", "password"] + + DEFAULT_FIELDS = ["email", "name", "username", "password"] EXTRA_FIELDS = [ "confirm_email", diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index 46b7e08361e5..238bf4e8749c 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -146,8 +146,6 @@ def create_account_with_params(request, params): not do_external_auth or not eamap.external_domain.startswith(settings.SHIBBOLETH_DOMAIN_PREFIX) ) - if 'first_name' in params.keys() and 'surname' in params.keys(): - params['name'] = '{} {}'.format(params['first_name'], params['surname']) form = AccountCreationForm( data=params, extra_fields=extra_fields, diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py index 4fea2b163176..11ed9cc410a5 100644 --- a/openedx/features/content_type_gating/partitions.py +++ b/openedx/features/content_type_gating/partitions.py @@ -9,6 +9,7 @@ import crum from django.apps import apps +from django.conf import settings from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ from web_fragments.fragment import Fragment @@ -83,7 +84,9 @@ def access_denied_fragment(self, block, user, user_group, allowed_groups): frag = Fragment(render_to_string('content_type_gating/access_denied_message.html', { 'mobile_app': is_request_from_mobile_app(request), 'ecommerce_checkout_link': ecommerce_checkout_link, - 'min_price': str(verified_mode.min_price) + 'min_price': str(verified_mode.min_price), + 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], + 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1] })) return frag diff --git a/openedx/features/content_type_gating/templates/content_type_gating/access_denied_message.html b/openedx/features/content_type_gating/templates/content_type_gating/access_denied_message.html index b47de960f5f5..290ce04b1017 100644 --- a/openedx/features/content_type_gating/templates/content_type_gating/access_denied_message.html +++ b/openedx/features/content_type_gating/templates/content_type_gating/access_denied_message.html @@ -12,7 +12,7 @@

{% if not mobile_app and ecommerce_checkout_link %} - {% trans "Upgrade to unlock" %} (${{min_price}} USD) + {% trans "Upgrade to unlock" %} ({{currency_symbol}}{{min_price}} {{currency}}) {% endif %} diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html index c6dee98b27ac..f26a815c673f 100644 --- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html @@ -30,7 +30,10 @@ ${static.renderReact( component="UpsellExperimentModal", id="upsell-modal", - props={}, + props={ + "currency": settings.PAID_COURSE_REGISTRATION_CURRENCY[0], + "currency_symbol": settings.PAID_COURSE_REGISTRATION_CURRENCY[1] + }, )} % endif @@ -38,7 +41,9 @@ ${static.renderReact( component="PortfolioExperimentUpsellModal", id="portfolio-experiment-upsell-modal", - props={} + props={ + "currency": settings.PAID_COURSE_REGISTRATION_CURRENCY[0], + "currency_symbol": settings.PAID_COURSE_REGISTRATION_CURRENCY[1] } )} % endif diff --git a/openedx/features/qverse_features/registration/helpers.py b/openedx/features/qverse_features/registration/helpers.py new file mode 100644 index 000000000000..9498454c565d --- /dev/null +++ b/openedx/features/qverse_features/registration/helpers.py @@ -0,0 +1,61 @@ +""" +Contains helper functions for Qverse registration application. +""" +from csv import reader, Sniffer +import io +import logging + +LOGGER = logging.getLogger(__name__) + + +def get_file_header_row(file_content, encoding): + """ + Returns fields of header row of the file. + + Arguments: + file_content (str): File content that has been read from the file + encoding (str): File encoding format e.g: utf-8, utf-16 + + Returns: + header_row (list): List of fields of the header row of CSV file + """ + decoded_file = file_content.decode(encoding, 'ignore') + io_string = io.StringIO(decoded_file) + dialect = Sniffer().sniff(io_string.readline()) + io_string.seek(0) + header_row = reader(io_string, delimiter=dialect.delimiter).next() + return [heading.lower().strip() for heading in header_row] + + +def get_file_encoding(file_path): + """ + Returns the file encoding format. + + Arguments: + file_path (str): Path of the file whose encoding format will be returned + + Returns: + encoding (str): encoding format e.g: utf-8, utf-16, returns None if doesn't find + any encoding format + """ + try: + file = io.open(file_path, 'r', encoding='utf-8') + encoding = None + try: + _ = file.read() + encoding = 'utf-8' + except UnicodeDecodeError: + file.close() + file = io.open(file_path, 'r', encoding='utf-16') + try: + _ = file.read() + encoding = 'utf-16' + except UnicodeDecodeError: + LOGGER.exception('The file encoding format must be utf-8 or utf-16.') + + file.close() + return encoding + + except IOError as error: + LOGGER.exception('({}) --- {}'.format(error.filename, error.strerror)) + return None diff --git a/openedx/features/qverse_features/registration/signals.py b/openedx/features/qverse_features/registration/signals.py index eab6c36a254a..5dc666286af4 100644 --- a/openedx/features/qverse_features/registration/signals.py +++ b/openedx/features/qverse_features/registration/signals.py @@ -1,6 +1,7 @@ """ Signals for qverse registration application. """ +import io import logging import re from csv import DictReader, DictWriter, Error, Sniffer @@ -19,6 +20,7 @@ SURNAME_MAX_LENGTH, FIRST_NAME_MAX_LENGTH, MOBILE_NUMBER_MAX_LENGTH, OTHER_NAME_MAX_LENGTH, MAX_LEVEL_CHOICES, MAX_PROGRAMME_CHOICES) +from openedx.features.qverse_features.registration.helpers import get_file_encoding from openedx.features.qverse_features.registration.tasks import send_bulk_mail_to_newly_created_students from student.models import UserProfile @@ -51,8 +53,19 @@ def create_users_from_csv_file(sender, instance, created, **kwargs): csv_file = None dialect = None try: - csv_file = open(instance.admission_file.path, 'r') - dialect = Sniffer().sniff(csv_file.readline()) + encoding = get_file_encoding(instance.admission_file.path) + if not encoding: + LOGGER.exception('Because of invlid file encoding format, user creation process is aborted.') + return + + csv_file = io.open(instance.admission_file.path, 'r', encoding=encoding) + try: + dialect = Sniffer().sniff(csv_file.readline()) + except Error: + LOGGER.exception('Could not determine delimiter in the file.') + csv_file.close() + return + csv_file.seek(0) except IOError as error: LOGGER.exception('({}) --- {}'.format(error.filename, error.strerror)) @@ -65,6 +78,8 @@ def create_users_from_csv_file(sender, instance, created, **kwargs): # whitespaces. So, we will have to handle that case ourselves reader = (dict((k.strip().lower(), v.strip() if v else v) for k, v in row.items()) for row in dict_reader) output_file_rows = [] + users_with_updated_emails = set() + try: CsvRowValidator.prepare_csv_row_validator() for row in reader: @@ -78,7 +93,7 @@ def create_users_from_csv_file(sender, instance, created, **kwargs): # will be an atomic operation. If one operation fails the previous # successfull operations will be reverted and no changes will be applied. with transaction.atomic(): - edx_user = _create_or_update_edx_user(row) + edx_user = _create_or_update_edx_user(row, users_with_updated_emails) _create_or_update_edx_user_profile(edx_user) _create_or_update_qverse_user_profile(edx_user, row) except IntegrityError as error: @@ -98,22 +113,30 @@ def create_users_from_csv_file(sender, instance, created, **kwargs): .format(instance.admission_file.path, err)) csv_file.close() _write_status_on_csv_file(instance.admission_file.path, output_file_rows) - new_students = [student for student in output_file_rows if student.get('status') == USER_CREATED] + new_students = [student for student in output_file_rows + if student.get('status') == USER_CREATED or + (student.get('status') == USER_UPDATED and student.get('regno') in users_with_updated_emails)] site = get_current_site() protocol = 'https' if get_current_request().is_secure() else 'http' send_bulk_mail_to_newly_created_students.delay(new_students, site.id, protocol) -def _create_or_update_edx_user(user_info): +def _create_or_update_edx_user(user_info, users_with_updated_emails): """ Creates/Updates edx user from given information. Arguments: user_info (dict): A dict containing user information + users_with_updated_emails (set): A set containing registration numbers of students whose emails addresses + have been updated Returns: edx_user (User): A newly created/updated User object """ + is_new_email_address = False + if not User.objects.filter(email=user_info.get('email')).exists(): + is_new_email_address = True + user_data = { 'email': user_info.get('email'), 'first_name': user_info.get('firstname'), @@ -127,6 +150,11 @@ def _create_or_update_edx_user(user_info): edx_user.save() LOGGER.info('{} user has been created.'.format(edx_user.username)) else: + if is_new_email_address: + users_with_updated_emails.add(user_info.get('regno')) + # Setting new password will expire all the previous reset password links + edx_user.set_password(get_random_string()) + edx_user.save() LOGGER.info('{} user has been updated.'.format(edx_user.username)) return edx_user diff --git a/openedx/features/qverse_features/registration/validators.py b/openedx/features/qverse_features/registration/validators.py index 6d816988b238..1c22ac45db04 100644 --- a/openedx/features/qverse_features/registration/validators.py +++ b/openedx/features/qverse_features/registration/validators.py @@ -1,11 +1,12 @@ """ Validation utils for qverse registration application. """ -from csv import reader, Sniffer -import io +from csv import Error from django.core.exceptions import ValidationError +from openedx.features.qverse_features.registration.helpers import get_file_header_row + def validate_admission_file(file): """ @@ -22,18 +23,19 @@ def validate_admission_file(file): 'regno', 'firstname', 'surname', 'othername', 'levelid', 'programmeid', 'departmentid', 'mobile', 'email' ] - decoded_file = file.read().decode('utf-8') - io_string = io.StringIO(decoded_file) header_row = [] try: - dialect = Sniffer().sniff(io_string.readline()) - io_string.seek(0) - header_row = reader(io_string, delimiter=dialect.delimiter).next() - header_row = [heading.lower().strip() for heading in header_row] - except StopIteration: + file_content = file.read() + + try: + header_row = get_file_header_row(file_content, 'utf-8') + except Error: + header_row = get_file_header_row(file_content, 'utf-16') + + except Exception: raise ValidationError('', code='invalid') - if not all(field_name in header_row for field_name in FIELD_NAMES): + if not all([field_name in header_row for field_name in FIELD_NAMES]): raise ValidationError('', code='invalid') if 'error' in header_row: