Files
edx-platform/lms/djangoapps/course_goals/models.py

175 lines
7.3 KiB
Python

"""
Course Goals Models
"""
import uuid
import logging
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import ugettext_lazy as _
from edx_django_utils.cache import TieredCache
from model_utils import Choices
from model_utils.models import TimeStampedModel
from opaque_keys.edx.django.models import CourseKeyField
from simple_history.models import HistoricalRecords
from lms.djangoapps.courseware.masquerade import is_masquerading
from lms.djangoapps.course_goals.toggles import COURSE_GOALS_NUMBER_OF_DAYS_GOALS
from lms.djangoapps.courseware.context_processor import get_user_timezone_or_last_seen_timezone_or_utc
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
# Each goal is represented by a goal key and a string description.
GOAL_KEY_CHOICES = Choices(
('certify', _('Earn a certificate')),
('complete', _('Complete the course')),
('explore', _('Explore the course')),
('unsure', _('Not sure yet')),
)
User = get_user_model()
log = logging.getLogger(__name__)
class CourseGoal(models.Model):
"""
Represents a course goal set by a user on the course home page.
.. no_pii:
"""
class Meta:
app_label = 'course_goals'
unique_together = ('user', 'course_key')
user = models.ForeignKey(User, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255, db_index=True)
# The goal a user has set for the number of days they want to learn per week
days_per_week = models.PositiveIntegerField(default=0)
# Controls whether a user will receive emails reminding them to stay on track with their learning goal
subscribed_to_reminders = models.BooleanField(default=False)
# With this token, anyone can unsubscribe this user from reminders. That's a mild enough action that we don't stress
# about the risk of keeping this key around long term in the database or bother using a higher-security generator
# than uuid4. The worst someone can do with this is unsubscribe us. And we want old tokens sitting in folks' email
# inboxes to still be valid as long as possible.
unsubscribe_token = models.UUIDField(null=True, blank=True, unique=True, editable=False, default=uuid.uuid4,
help_text='Used to validate unsubscribe requests without requiring a login')
goal_key = models.CharField(max_length=100, choices=GOAL_KEY_CHOICES, default=GOAL_KEY_CHOICES.unsure)
history = HistoricalRecords()
def __str__(self):
return 'CourseGoal: {user} set goal to {goal} days per week for course {course}'.format(
user=self.user.username,
goal=self.days_per_week,
course=self.course_key,
)
def save(self, **kwargs): # pylint: disable=arguments-differ
# Ensure we have an unsubscribe token (lazy migration from old goals, before this field was added)
if self.unsubscribe_token is None:
self.unsubscribe_token = uuid.uuid4()
super().save(**kwargs)
class CourseGoalReminderStatus(TimeStampedModel):
"""
Tracks whether we've sent a reminder about a particular goal this week.
See the management command goal_reminder_email for more detail about how this is used.
"""
class Meta:
verbose_name_plural = "Course goal reminder statuses"
goal = models.OneToOneField(CourseGoal, on_delete=models.CASCADE, related_name='reminder_status')
email_reminder_sent = models.BooleanField(
default=False, help_text='Tracks if the email reminder to complete the Course Goal has been sent this week.'
)
class UserActivity(models.Model):
"""
Tracks the date a user performs an activity in a course for goal purposes.
To be used in conjunction with the CourseGoal model to establish if a learner is hitting
their desired days_per_week.
To start, this model will only be tracking page views that count towards a learner's goal,
but could grow to tracking other types of goal achieving activities in the future.
.. no_pii:
"""
class Meta:
constraints = [models.UniqueConstraint(fields=['user', 'course_key', 'date'], name='unique_user_course_date')]
indexes = [models.Index(fields=['user', 'course_key'], name='user_course_index')]
verbose_name_plural = 'User activities'
id = models.BigAutoField(primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255)
date = models.DateField()
@classmethod
def record_user_activity(cls, user, course_key, request=None, only_if_mobile_app=False):
'''
Update the user activity table with a record for this activity.
Since we store one activity per date, we don't need to query the database
for every activity on a given date.
To avoid unnecessary queries, we store a record in a cache once we have an activity for the date,
which times out at the end of that date (in the user's timezone).
The request argument is only used to check if the request is coming from a mobile app.
Once the only_if_mobile_app argument is removed the request argument can be removed as well.
The return value is the id of the object that was created, or retrieved.
A return value of None signifies that a user activity record was not stored or retrieved
'''
if not COURSE_GOALS_NUMBER_OF_DAYS_GOALS.is_enabled(course_key):
return None
if not (user and user.id) or not course_key:
return None
if only_if_mobile_app and request and not is_request_from_mobile_app(request):
return None
if is_masquerading(user, course_key):
return None
timezone = get_user_timezone_or_last_seen_timezone_or_utc(user)
now = datetime.now(timezone)
date = now.date()
cache_key = 'goals_user_activity_{}_{}_{}'.format(str(user.id), str(course_key), str(date))
cached_value = TieredCache.get_cached_response(cache_key)
if cached_value.is_found:
# Temporary debugging log for testing mobile app connection
if request:
log.info(
'Retrieved cached value with request {} for user and course combination {} {}'.format(
str(request.build_absolute_uri()), str(user.id), str(course_key)
)
)
return cached_value.value, False
activity_object, __ = cls.objects.get_or_create(user=user, course_key=course_key, date=date)
# Cache result until the end of the day to avoid unnecessary database requests
tomorrow = now + timedelta(days=1)
midnight = datetime(year=tomorrow.year, month=tomorrow.month,
day=tomorrow.day, hour=0, minute=0, second=0, tzinfo=timezone)
seconds_until_midnight = (midnight - now).seconds
TieredCache.set_all_tiers(cache_key, activity_object.id, seconds_until_midnight)
# Temporary debugging log for testing mobile app connection
if request:
log.info(
'Set cached value with request {} for user and course combination {} {}'.format(
str(request.build_absolute_uri()), str(user.id), str(course_key)
)
)
return activity_object.id