Files
edx-platform/openedx/features/discounts/utils.py
Tarun Tak 18d5abb2f6 chore: Replace pytz with zoneinfo for UTC handling - Part 1 (#37523)
First PR to replace pytz with zoneinfo for UTC handling across codebase.

This PR migrates all UTC timezone handling from pytz to Python’s standard
library zoneinfo. The pytz library is now deprecated, and its documentation
recommends using zoneinfo for all new code. This update modernizes our
codebase, removes legacy pytz usage, and ensures compatibility with
current best practices for timezone management in Python 3.9+. No functional
changes to timezone logic - just a direct replacement for UTC handling.

https://github.com/openedx/edx-platform/issues/33980
2025-10-28 16:23:22 -04:00

165 lines
6.0 KiB
Python

"""
Utility functions for working with discounts and discounted pricing.
"""
from datetime import datetime
from zoneinfo import ZoneInfo
from django.conf import settings
from django.utils.translation import get_language
from django.utils.translation import gettext as _
from edx_django_utils.plugins import pluggable_override
from common.djangoapps.course_modes.models import format_course_price, get_course_prices
from lms.djangoapps.experiments.models import ExperimentData
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
from openedx.core.djangolib.markup import HTML
from openedx.features.discounts.applicability import (
FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG,
REV1008_EXPERIMENT_ID,
can_receive_discount,
discount_percentage,
get_discount_expiration_date
)
def offer_banner_wrapper(user, block, view, frag, context): # pylint: disable=W0613
"""
A wrapper that prepends the First Purchase Discount banner if
the user hasn't upgraded yet.
"""
if block.category != 'vertical':
return frag
offer_banner_fragment = None
if not offer_banner_fragment:
return frag
# Course content must be escaped to render correctly due to the way the
# way the XBlock rendering works. Transforming the safe markup to unicode
# escapes correctly.
offer_banner_fragment.content = str(offer_banner_fragment.content)
offer_banner_fragment.add_content(frag.content)
offer_banner_fragment.add_fragment_resources(frag)
return offer_banner_fragment
def _get_discount_prices(user, course, assume_discount=False):
"""
Return a tuple of (original, discounted, percentage)
If assume_discount is True, we do not check if a discount applies and just go ahead with discount math anyway.
Each returned price is a string with appropriate currency formatting added already.
discounted and percentage will be returned as None if no discount is applicable.
"""
base_price = get_course_prices(course, verified_only=True)[0]
can_discount = assume_discount or can_receive_discount(user, course)
if can_discount:
percentage = discount_percentage(course)
discounted_price = base_price * ((100.0 - percentage) / 100)
if discounted_price: # leave 0 prices alone, as format_course_price below will adjust to 'Free'
if discounted_price == int(discounted_price):
discounted_price = f'{discounted_price:0.0f}'
else:
discounted_price = f'{discounted_price:0.2f}'
return format_course_price(base_price), format_course_price(discounted_price), percentage
else:
return format_course_price(base_price), None, None
@pluggable_override("OVERRIDE_GENERATE_OFFER_DATA")
def generate_offer_data(user, course):
"""
Create a dictionary of information about the current discount offer.
Used by serializers to pass onto frontends and by the LMS locally to generate HTML for template rendering.
Returns a dictionary of data, or None if no offer is applicable.
"""
if not user or not course or user.is_anonymous:
return None
ExperimentData.objects.get_or_create(
user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(course),
defaults={
'value': datetime.now(tz=ZoneInfo("UTC")).strftime('%Y-%m-%d %H:%M:%S%z'),
},
)
expiration_date = get_discount_expiration_date(user, course)
if not expiration_date:
return None
if not can_receive_discount(user, course, discount_expiration_date=expiration_date):
return None
original, discounted, percentage = _get_discount_prices(user, course, assume_discount=True)
# Override the First Purchase Discount to another code only if flag is enabled
first_purchase_discount_code = 'BIENVENIDOAEDX' if get_language() == 'es-419' else 'EDXWELCOME'
if FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG.is_enabled():
first_purchase_discount_code = getattr(
settings,
'FIRST_PURCHASE_DISCOUNT_OVERRIDE_CODE',
first_purchase_discount_code
)
return {
'code': first_purchase_discount_code,
'expiration_date': expiration_date,
'original_price': original,
'discounted_price': discounted,
'percentage': percentage,
'upgrade_url': verified_upgrade_deadline_link(user, course=course),
}
def _format_discounted_price(original_price, discount_price):
"""Helper method that returns HTML containing a strikeout price with discount."""
# Separate out this string because it has a lot of syntax but no actual information for
# translators to translate
formatted_discount_price = HTML(
'{s_dp}{discount_price}{e_p} {s_st}{s_op}{original_price}{e_p}{e_st}'
).format(
original_price=original_price,
discount_price=discount_price,
s_op=HTML("<span class='price original'>"),
s_dp=HTML("<span class='price discount'>"),
s_st=HTML("<del aria-hidden='true'>"),
e_p=HTML('</span>'),
e_st=HTML('</del>'),
)
return (
HTML(_(
'{s_sr}Original price: {s_op}{original_price}{e_p}, discount price: {e_sr}{formatted_discount_price}'
)).format(
original_price=original_price,
formatted_discount_price=formatted_discount_price,
s_sr=HTML("<span class='sr-only'>"),
s_op=HTML("<span class='price original'>"),
e_p=HTML('</span>'),
e_sr=HTML('</span>'),
)
)
def format_strikeout_price(user, course):
"""
Return a formatted price, including a struck-out original price if a discount applies, and also
whether a discount was applied, as the tuple (formatted_price, has_discount).
"""
original_price, discounted_price, _ = _get_discount_prices(user, course)
if discounted_price is None:
return HTML("<span class='price'>{}</span>").format(original_price), False
else:
return _format_discounted_price(original_price, discounted_price), True