Merge pull request #29747 from openedx/ddumesnil/goal-celly-modal-aa-1138

feat: AA-1138: Adds ability to have Weekly Goal Celebration Modal in MFE

Adds celebrate_weekly_goal to the CourseEnrollmentCelebration and includes logic
for only returning True if the db field is true and the learner has hit their goal
this week. Adds ability to set to false via the API already used by the frontend.
Default db value is False, but all new enrollments after merge will be set to True.
This commit is contained in:
Dillon Dumesnil
2022-01-18 06:13:43 -08:00
committed by GitHub
9 changed files with 99 additions and 21 deletions

View File

@@ -524,7 +524,7 @@ class AllowedAuthUserAdmin(admin.ModelAdmin):
class CourseEnrollmentCelebrationAdmin(DisableEnrollmentAdminMixin, admin.ModelAdmin):
"""Admin interface for the CourseEnrollmentCelebration model. """
raw_id_fields = ('enrollment',)
list_display = ('id', 'course', 'user', 'celebrate_first_section')
list_display = ('id', 'course', 'user', 'celebrate_first_section', 'celebrate_weekly_goal',)
search_fields = ('enrollment__course__id', 'enrollment__user__username')
class Meta:

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.11 on 2022-01-11 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('student', '0043_remove_userprofile_allow_certificate'),
]
operations = [
migrations.AddField(
model_name='courseenrollmentcelebration',
name='celebrate_weekly_goal',
field=models.BooleanField(default=False),
),
]

View File

@@ -18,7 +18,7 @@ import json # lint-amnesty, pylint: disable=wrong-import-order
import logging # lint-amnesty, pylint: disable=wrong-import-order
import uuid # lint-amnesty, pylint: disable=wrong-import-order
from collections import defaultdict, namedtuple # lint-amnesty, pylint: disable=wrong-import-order
from datetime import datetime, timedelta # lint-amnesty, pylint: disable=wrong-import-order
from datetime import date, datetime, timedelta # lint-amnesty, pylint: disable=wrong-import-order
from functools import total_ordering # lint-amnesty, pylint: disable=wrong-import-order
from importlib import import_module # lint-amnesty, pylint: disable=wrong-import-order
from urllib.parse import urlencode # lint-amnesty, pylint: disable=wrong-import-order
@@ -683,11 +683,11 @@ class UserProfile(models.Model):
self.set_meta(meta)
self.save()
def requires_parental_consent(self, date=None, age_limit=None, default_requires_consent=True):
def requires_parental_consent(self, year=None, age_limit=None, default_requires_consent=True):
"""Returns true if this user requires parental consent.
Args:
date (Date): The date for which consent needs to be tested (defaults to now).
year (int): The year for which consent needs to be tested (defaults to now).
age_limit (int): The age limit at which parental consent is no longer required.
This defaults to the value of the setting 'PARENTAL_CONTROL_AGE_LIMIT'.
default_requires_consent (bool): True if users require parental consent if they
@@ -712,10 +712,10 @@ class UserProfile(models.Model):
if year_of_birth is None:
return default_requires_consent
if date is None:
if year is None:
age = self.age
else:
age = self._calculate_age(date.year, year_of_birth)
age = self._calculate_age(year, year_of_birth)
return age < age_limit
@@ -3445,15 +3445,22 @@ class CourseEnrollmentCelebration(TimeStampedModel):
"""
enrollment = models.OneToOneField(CourseEnrollment, models.CASCADE, related_name='celebration')
celebrate_first_section = models.BooleanField(default=False)
celebrate_weekly_goal = models.BooleanField(default=False)
def __str__(self):
return (
'[CourseEnrollmentCelebration] course: {}; user: {}; first_section: {};'
).format(self.enrollment.course.id, self.enrollment.user.username, self.celebrate_first_section)
'[CourseEnrollmentCelebration] course: {}; user: {}'
).format(self.enrollment.course.id, self.enrollment.user.username)
@staticmethod
def should_celebrate_first_section(enrollment):
""" Returns the celebration value for first_section with appropriate fallback if it doesn't exist """
"""
Returns the celebration value for first_section with appropriate fallback if it doesn't exist.
The frontend will use this result and additional information calculated to actually determine
if the first section celebration will render. In other words, the value returned here is
NOT the final value used.
"""
if not enrollment:
return False
try:
@@ -3461,6 +3468,36 @@ class CourseEnrollmentCelebration(TimeStampedModel):
except CourseEnrollmentCelebration.DoesNotExist:
return False
@staticmethod
def should_celebrate_weekly_goal(enrollment):
"""
Returns the celebration value for weekly_goal with appropriate fallback if it doesn't exist.
The frontend will use this result directly to determine if the weekly goal celebration
should be rendered. The value returned here IS the final value used.
"""
# Avoiding circular import
from lms.djangoapps.course_goals.models import CourseGoal, UserActivity
try:
if not enrollment or not enrollment.celebration.celebrate_weekly_goal:
return False
except CourseEnrollmentCelebration.DoesNotExist:
return False
try:
goal = CourseGoal.objects.get(user=enrollment.user, course_key=enrollment.course.id)
if not goal.days_per_week:
return False
today = date.today()
monday_date = today - timedelta(days=today.weekday())
week_activity_count = UserActivity.objects.filter(
user=enrollment.user, course_key=enrollment.course.id, date__gte=monday_date,
).count()
return week_activity_count == goal.days_per_week
except CourseGoal.DoesNotExist:
return False
class UserPasswordToggleHistory(TimeStampedModel):
"""

View File

@@ -74,6 +74,7 @@ def create_course_enrollment_celebration(sender, instance, created, **kwargs):
CourseEnrollmentCelebration.objects.create(
enrollment=instance,
celebrate_first_section=True,
celebrate_weekly_goal=True,
)
except IntegrityError:
# A celebration object was already created. Shouldn't happen, but ignore it if it does.

View File

@@ -1,8 +1,6 @@
"""Unit tests for parental controls."""
import datetime
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.timezone import now
@@ -60,13 +58,13 @@ class ProfileParentalControlsTest(TestCase):
# Verify for a child born 13 years agp
self.set_year_of_birth(current_year - 13)
assert self.profile.requires_parental_consent()
assert self.profile.requires_parental_consent(date=datetime.date(current_year, 12, 31))
assert not self.profile.requires_parental_consent(date=datetime.date((current_year + 1), 1, 1))
assert self.profile.requires_parental_consent(year=current_year)
assert not self.profile.requires_parental_consent(year=(current_year + 1))
# Verify for a child born 14 years ago
self.set_year_of_birth(current_year - 14)
assert not self.profile.requires_parental_consent()
assert not self.profile.requires_parental_consent(date=datetime.date(current_year, 1, 1))
assert not self.profile.requires_parental_consent(year=current_year)
def test_profile_image(self):
"""Verify that a profile's image obeys parental controls."""

View File

@@ -28,7 +28,9 @@ class ReceiversTest(SharedModuleStoreTestCase):
# Test initial creation upon an enrollment being made
enrollment = CourseEnrollmentFactory()
assert CourseEnrollmentCelebration.objects.count() == 1
celebration = CourseEnrollmentCelebration.objects.get(enrollment=enrollment, celebrate_first_section=True)
celebration = CourseEnrollmentCelebration.objects.get(
enrollment=enrollment, celebrate_first_section=True, celebrate_weekly_goal=True
)
# Test nothing changes if we update that enrollment
celebration.celebrate_first_section = False
@@ -36,7 +38,9 @@ class ReceiversTest(SharedModuleStoreTestCase):
enrollment.mode = 'test-mode'
enrollment.save()
assert CourseEnrollmentCelebration.objects.count() == 1
CourseEnrollmentCelebration.objects.get(enrollment=enrollment, celebrate_first_section=False)
CourseEnrollmentCelebration.objects.get(
enrollment=enrollment, celebrate_first_section=False, celebrate_weekly_goal=True
)
def test_celebration_gated_by_waffle(self):
""" Test we don't make a celebration if the MFE redirect waffle flag is off """