<commit_before>"""
Resolvers used to find users for course_duration_limit message
"""

import logging
from datetime import datetime, timedelta

from course_modes.models import CourseMode
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.db.models import Q
from django.utils.timesince import timeuntil
from django.utils.translation import ugettext as _
from eventtracking import tracker
from lms.djangoapps.experiments.utils import stable_bucketing_hash_group
from openedx.core.djangoapps.catalog.utils import get_course_run_details
from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid

from openedx.core.djangoapps.schedules.resolvers import (
    BinnedSchedulesBaseResolver,
    InvalidContextError,
    _get_trackable_course_home_url,
)
from track import segment

from .access import MAX_DURATION, MIN_DURATION, get_user_course_expiration_date
from .models import CourseDurationLimitConfig

LOG = logging.getLogger(__name__)

DEFAULT_NUM_BINS = 24
EXPIRY_REMINDER_NUM_BINS = 1
EXPIRY_REMINDER_LOG_PREFIX = 'FBE Expiry Reminder'


class ExpiryReminderResolver(BinnedSchedulesBaseResolver):
    """
    Send a message to all users whose course duration limit expiration date
    is at ``self.current_date`` + ``day_offset``.
    """
    log_prefix = EXPIRY_REMINDER_LOG_PREFIX
    schedule_date_field = 'start'
    num_bins = EXPIRY_REMINDER_NUM_BINS

    def __init__(
        self,
        async_send_task,
        site,
        course_key,
        target_datetime,
        day_offset,
        override_recipient_email=None
    ):
        access_duration = MIN_DURATION
        discovery_course_details = get_course_run_details(course_key, ['weeks_to_complete'])
        expected_weeks = discovery_course_details.get('weeks_to_complete')
        if expected_weeks:
            access_duration = timedelta(weeks=expected_weeks)

        access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration))

        self.course_key = course_key

        super(ExpiryReminderResolver, self).__init__(
            async_send_task,
            site,
            target_datetime - access_duration,
            day_offset - access_duration.days,
            0,
            override_recipient_email,
        )

    # TODO: This isn't named well, given the purpose we're using it for. That's ok for now,
    # this is just a test.
    @property
    def experience_filter(self):
        return Q(enrollment__course_id=self.course_key, enrollment__mode=CourseMode.AUDIT)

    def get_template_context(self, user, user_schedules):
        course_id_strs = []
        course_links = []
        first_valid_upsell_context = None
        first_schedule = None
        first_expiration_date = None

        self.log_info(u"Found %s schedules for %s", len(user_schedules), user.username)

        for schedule in user_schedules:
            upsell_context = _get_upsell_information_for_schedule(user, schedule)
            if not upsell_context['show_upsell']:
                self.log_info(u"No upsell available for %r", schedule.enrollment)
                continue

            if not CourseDurationLimitConfig.enabled_for_enrollment(enrollment=schedule.enrollment):
                self.log_info(u"course duration limits not enabled for %r", schedule.enrollment)
                continue

            expiration_date = get_user_course_expiration_date(user, schedule.enrollment.course)
            if expiration_date is None:
                self.log_info(u"No course expiration date for %r", schedule.enrollment.course)
                continue

            if first_valid_upsell_context is None:
                first_schedule = schedule
                first_valid_upsell_context = upsell_context
                first_expiration_date = expiration_date
            course_id_str = str(schedule.enrollment.course_id)
            course_id_strs.append(course_id_str)
            course_links.append({
                'url': _get_trackable_course_home_url(schedule.enrollment.course_id),
                'name': schedule.enrollment.course.display_name
            })

        if first_schedule is None:
            self.log_info(u'No courses eligible for upgrade for user %s.', user.username)
            raise InvalidContextError()

        # Experiment code: Skip users who are in the control bucket
        hash_bucket = stable_bucketing_hash_group('fbe_access_expiry_reminder', 2, user.username)
        properties = {
            'site': self.site.domain,  # pylint: disable=no-member
            'app_label': 'course_duration_limits',
            'nonInteraction': 1,
            'bucket': hash_bucket,
            'experiment': 'REVMI-95',
        }
        course_ids = course_id_strs
        properties['num_courses'] = len(course_ids)
        if course_ids:
            properties['course_ids'] = course_ids[:10]
            properties['primary_course_id'] = course_ids[0]

        tracking_context = {
            'host': self.site.domain,  # pylint: disable=no-member
            'path': '/',  # make up a value, in order to allow the host to be passed along.
        }
        # I wonder if the user of this event should be the recipient, as they are not the ones
        # who took an action.  Rather, the system is acting, and they are the object.
        # Admittedly that may be what 'nonInteraction' is meant to address.  But sessionization may
        # get confused by these events if they're attributed in this way, because there's no way for
        # this event to get context that would match with what the user might be doing at the moment.
        # But the events do show up in GA being joined up with existing sessions (i.e. within a half
        # hour in the past), so they don't always break sessions.  Not sure what happens after these.
        # We can put the recipient_user_id into the properties, and then export as a custom dimension.
        with tracker.get_tracker().context('course_duration_limits', tracking_context):
            segment.track(
                user_id=user.id,
                event_name='edx.bi.experiment.user.bucketed',
                properties=properties,
            )
        if hash_bucket == 0:
            raise InvalidContextError()

        context = {
            'course_links': course_links,
            'first_course_name': first_schedule.enrollment.course.display_name,
            'cert_image': static('course_experience/images/verified-cert.png'),
            'course_ids': course_id_strs,
            'first_course_expiration_date': first_expiration_date.strftime(_(u"%b. %d, %Y")),
            'time_until_expiration': timeuntil(first_expiration_date.date(), now=datetime.utcnow().date())
        }
        context.update(first_valid_upsell_context)
        return context


def _get_verified_upgrade_link(user, schedule):
    enrollment = schedule.enrollment
    if verified_upgrade_link_is_valid(enrollment):
        return verified_upgrade_deadline_link(user, enrollment.course)


def _get_upsell_information_for_schedule(user, schedule):
    """
    Return upsell variables for inclusion in a message template being sent to this user.
    """
    template_context = {}

    verified_upgrade_link = _get_verified_upgrade_link(user, schedule)
    has_verified_upgrade_link = verified_upgrade_link is not None

    if has_verified_upgrade_link:
        template_context['upsell_link'] = verified_upgrade_link

    template_context['show_upsell'] = has_verified_upgrade_link
    return template_context<commit_msg>Don't check the schedule upgrade_deadline before sending Access Expiry Email<commit_after>"""
Resolvers used to find users for course_duration_limit message
"""

import logging
from datetime import datetime, timedelta

from course_modes.models import CourseMode
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.db.models import Q
from django.utils.timesince import timeuntil
from django.utils.translation import ugettext as _
from eventtracking import tracker
from lms.djangoapps.experiments.utils import stable_bucketing_hash_group
from openedx.core.djangoapps.catalog.utils import get_course_run_details
from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid

from openedx.core.djangoapps.schedules.resolvers import (
    BinnedSchedulesBaseResolver,
    InvalidContextError,
    _get_trackable_course_home_url,
)
from track import segment

from .access import MAX_DURATION, MIN_DURATION, get_user_course_expiration_date
from .models import CourseDurationLimitConfig

LOG = logging.getLogger(__name__)

DEFAULT_NUM_BINS = 24
EXPIRY_REMINDER_NUM_BINS = 1
EXPIRY_REMINDER_LOG_PREFIX = 'FBE Expiry Reminder'


class ExpiryReminderResolver(BinnedSchedulesBaseResolver):
    """
    Send a message to all users whose course duration limit expiration date
    is at ``self.current_date`` + ``day_offset``.
    """
    log_prefix = EXPIRY_REMINDER_LOG_PREFIX
    schedule_date_field = 'start'
    num_bins = EXPIRY_REMINDER_NUM_BINS

    def __init__(
        self,
        async_send_task,
        site,
        course_key,
        target_datetime,
        day_offset,
        override_recipient_email=None
    ):
        access_duration = MIN_DURATION
        discovery_course_details = get_course_run_details(course_key, ['weeks_to_complete'])
        expected_weeks = discovery_course_details.get('weeks_to_complete')
        if expected_weeks:
            access_duration = timedelta(weeks=expected_weeks)

        access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration))

        self.course_key = course_key

        super(ExpiryReminderResolver, self).__init__(
            async_send_task,
            site,
            target_datetime - access_duration,
            day_offset - access_duration.days,
            0,
            override_recipient_email,
        )

    # TODO: This isn't named well, given the purpose we're using it for. That's ok for now,
    # this is just a test.
    @property
    def experience_filter(self):
        return Q(enrollment__course_id=self.course_key, enrollment__mode=CourseMode.AUDIT)

    def get_template_context(self, user, user_schedules):
        course_id_strs = []
        course_links = []
        first_valid_upsell_context = None
        first_schedule = None
        first_expiration_date = None

        self.log_info(u"Found %s schedules for %s", len(user_schedules), user.username)

        for schedule in user_schedules:
            upsell_context = _get_upsell_information_for_schedule(user, schedule)
            if not upsell_context['show_upsell']:
                self.log_info(u"No upsell available for %r", schedule.enrollment)
                continue

            if not CourseDurationLimitConfig.enabled_for_enrollment(enrollment=schedule.enrollment):
                self.log_info(u"course duration limits not enabled for %r", schedule.enrollment)
                continue

            expiration_date = get_user_course_expiration_date(user, schedule.enrollment.course)
            if expiration_date is None:
                self.log_info(u"No course expiration date for %r", schedule.enrollment.course)
                continue

            if first_valid_upsell_context is None:
                first_schedule = schedule
                first_valid_upsell_context = upsell_context
                first_expiration_date = expiration_date
            course_id_str = str(schedule.enrollment.course_id)
            course_id_strs.append(course_id_str)
            course_links.append({
                'url': _get_trackable_course_home_url(schedule.enrollment.course_id),
                'name': schedule.enrollment.course.display_name
            })

        if first_schedule is None:
            self.log_info(u'No courses eligible for upgrade for user %s.', user.username)
            raise InvalidContextError()

        # Experiment code: Skip users who are in the control bucket
        hash_bucket = stable_bucketing_hash_group('fbe_access_expiry_reminder', 2, user.username)
        properties = {
            'site': self.site.domain,  # pylint: disable=no-member
            'app_label': 'course_duration_limits',
            'nonInteraction': 1,
            'bucket': hash_bucket,
            'experiment': 'REVMI-95',
        }
        course_ids = course_id_strs
        properties['num_courses'] = len(course_ids)
        if course_ids:
            properties['course_ids'] = course_ids[:10]
            properties['primary_course_id'] = course_ids[0]

        tracking_context = {
            'host': self.site.domain,  # pylint: disable=no-member
            'path': '/',  # make up a value, in order to allow the host to be passed along.
        }
        # I wonder if the user of this event should be the recipient, as they are not the ones
        # who took an action.  Rather, the system is acting, and they are the object.
        # Admittedly that may be what 'nonInteraction' is meant to address.  But sessionization may
        # get confused by these events if they're attributed in this way, because there's no way for
        # this event to get context that would match with what the user might be doing at the moment.
        # But the events do show up in GA being joined up with existing sessions (i.e. within a half
        # hour in the past), so they don't always break sessions.  Not sure what happens after these.
        # We can put the recipient_user_id into the properties, and then export as a custom dimension.
        with tracker.get_tracker().context('course_duration_limits', tracking_context):
            segment.track(
                user_id=user.id,
                event_name='edx.bi.experiment.user.bucketed',
                properties=properties,
            )
        if hash_bucket == 0:
            raise InvalidContextError()

        context = {
            'course_links': course_links,
            'first_course_name': first_schedule.enrollment.course.display_name,
            'cert_image': static('course_experience/images/verified-cert.png'),
            'course_ids': course_id_strs,
            'first_course_expiration_date': first_expiration_date.strftime(_(u"%b. %d, %Y")),
            'time_until_expiration': timeuntil(first_expiration_date.date(), now=datetime.utcnow().date())
        }
        context.update(first_valid_upsell_context)
        return context


def _get_verified_upgrade_link(user, schedule):
    enrollment = schedule.enrollment
    return verified_upgrade_deadline_link(user, enrollment.course)


def _get_upsell_information_for_schedule(user, schedule):
    """
    Return upsell variables for inclusion in a message template being sent to this user.
    """
    template_context = {}

    verified_upgrade_link = _get_verified_upgrade_link(user, schedule)
    has_verified_upgrade_link = verified_upgrade_link is not None

    if has_verified_upgrade_link:
        template_context['upsell_link'] = verified_upgrade_link

    template_context['show_upsell'] = has_verified_upgrade_link
    return template_context
{'fraction_non_alphanumeric': 0.06974900924702775, 'fraction_numerical': 0.001585204755614267, 'mean_word_length': 2.947080291970803, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7386527', 'n_tokens_mistral': 4368, 'n_tokens_neox': 4190, 'n_words': 1060}