Skip to content

Commit

Permalink
feat: add cronjob for sending progress emails to users
Browse files Browse the repository at this point in the history
  • Loading branch information
Muhammad Faraz Maqsood authored and Muhammad Faraz Maqsood committed Feb 19, 2024
1 parent cec09bf commit fb83bb7
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 3 deletions.
21 changes: 21 additions & 0 deletions openedx/features/sdaia_features/course_progress/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
"""
Admin Models
"""
"""
Django Admin page for SurveyReport.
"""


from django.contrib import admin
from .models import CourseCompletionEmailHistory


class CourseCompletionEmailHistoryAdmin(admin.ModelAdmin):
"""
Admin to manage Course Completion Email History.
"""
list_display = (
'id', 'user', 'course_key', 'last_progress_email_sent',
)
search_fields = (
'id', 'user__username', 'user__email', 'course_key',
)

admin.site.register(CourseCompletionEmailHistory, CourseCompletionEmailHistoryAdmin)
Empty file.
Original file line number Diff line number Diff line change
@@ -1,10 +1,98 @@
"""
Django admin command to send message email emails.
"""
from logging import getLogger
import json
import logging

from celery import shared_task
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
from edx_ace import ace
from edx_ace.recipient import Recipient

from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary
from lms.djangoapps.grades.api import CourseGradeFactory
from openedx.core.djangoapps.ace_common.message import BaseMessageType
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.lib.celery.task_utils import emulate_http_request
from openedx.features.sdaia_features.course_progress.models import CourseCompletionEmailHistory
from xmodule.course_block import CourseFields # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url

logger = logging.getLogger(__name__)


class UserCourseProgressEmail(BaseMessageType):
"""
Message Type Class for User Activation
"""
APP_LABEL = 'course_progress'

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options['transactional'] = True

@shared_task
def send_user_course_progress_email(current_progress, progress_last_email_sent_at, course_completion_percentages_for_email, course_key, course_name, user_id):
"""
Sends User Activation Code Via Email
"""
user = User.objects.get(id=user_id)

site = Site.objects.first() or Site.objects.get_current()
message_context = get_base_template_context(site)
course_home_url = get_learning_mfe_home_url(course_key=course_key, url_fragment='home')

context={
'current_progress': current_progress,
'progress_milestone_crossed': progress_last_email_sent_at,
'course_key': course_key,
'platform_name': "sdaia",
'course_name': course_name,
'course_home_url': course_home_url,
}
message_context.update(context)
try:
with emulate_http_request(site, user):
msg = UserCourseProgressEmail(context=message_context).personalize(
recipient=Recipient(0, user.email),
language=settings.LANGUAGE_CODE,
user_context={'full_name': user.profile.name}
)
ace.send(msg)
logger.info('Proctoring requirements email sent to user:')
user_completion_progress_email_history = CourseCompletionEmailHistory.objects.get(user=user, course_key=course_key)
user_completion_progress_email_history.last_progress_email_sent = course_completion_percentages_for_email
user_completion_progress_email_history.save()
return True
except Exception as e: # pylint: disable=broad-except
logger.exception(str(e))
logger.exception('Could not send email for proctoring requirements to user')
return False


def get_user_course_progress(user, course_key):
"""
Function to get the user's course completion percentage in a course.
:param user: The user object.
:param course_key: The course key (e.g., CourseKey.from_string("edX/DemoX/Demo_Course")).
:return: completion percentage.
"""
completion_summary = get_course_blocks_completion_summary(course_key, user)

complete_count = completion_summary.get('complete_count', 0)
incomplete_count = completion_summary.get('incomplete_count', 0)
locked_count = completion_summary.get('locked_count', 0)
total_count = complete_count + incomplete_count + locked_count

completion_percentage = (complete_count / total_count) * 100
return completion_percentage

log = getLogger(__name__)

class Command(BaseCommand):
"""
Expand All @@ -13,5 +101,47 @@ class Command(BaseCommand):
"""
help = 'Command to update users about their course progress'

def add_arguments(self, parser):
parser.add_argument(
'--skip-archived',
default=True,
help="If set to False, it'll send emails for archived courses also",
)

def handle(self, *args, **options):
print("Hello")
skip_archived = options.get('skip-archived')

course_ids = [course.id for course in modulestore().get_courses()]

for course_key in course_ids:
course = modulestore().get_course(course_key)

course_completion_percentages_for_emails = course.course_completion_percentages_for_emails
if not course.allow_course_completion_emails or not course_completion_percentages_for_emails:
continue

course_completion_percentages_for_emails = course_completion_percentages_for_emails.split(",")
try:
course_completion_percentages_for_emails = [int(entry.strip()) for entry in course_completion_percentages_for_emails]
except Exception as e:
log.info(f"invalid course_completion_percentages_for_emails for course {CourseKey.from_string(course_key)}")
continue

if skip_archived and course.has_ended():
continue

user_ids = CourseEnrollment.objects.filter(course_id=course_key, is_active=True).values_list('user_id', flat=True)
users = User.objects.filter(id__in=user_ids)
if not user_ids:
continue

for user in users:
user_completion_percentage = get_user_course_progress(user, course_key)
user_completion_progress_email_history, _ = CourseCompletionEmailHistory.objects.get_or_create(user=user, course_key=course_key)
progress_last_email_sent_at = user_completion_progress_email_history and user_completion_progress_email_history.last_progress_email_sent

if user_completion_percentage > progress_last_email_sent_at:
for course_completion_percentages_for_email in course_completion_percentages_for_emails:
if user_completion_percentage >= course_completion_percentages_for_email > progress_last_email_sent_at:
send_user_course_progress_email.delay(user_completion_percentage, progress_last_email_sent_at, course_completion_percentages_for_email, str(course_key), course.display_name, user.id)

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.20 on 2024-02-19 07:33

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import opaque_keys.edx.django.models


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='CourseCompletionEmailHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('course_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('last_progress_email_sent', models.IntegerField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
16 changes: 16 additions & 0 deletions openedx/features/sdaia_features/course_progress/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Models
"""
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db import models

from opaque_keys.edx.django.models import CourseKeyField


class CourseCompletionEmailHistory(models.Model):
"""
Keeps progress for a student for which he/she gets an email as he/she reaches at that particluar progress in a course.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255, db_index=True)
last_progress_email_sent = models.IntegerField(default=0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

{% comment %}
As the developer of this package, don't place anything here if you can help it
since this allows developers to have interoperability between your template
structure and their own.

Example: Developer melding the 2SoD pattern to fit inside with another pattern::

{% extends "base.html" %}
{% load static %}

<!-- Their site uses old school block layout -->
{% block extra_js %}

<!-- Your package using 2SoD block layout -->
{% block javascript %}
<script src="{% static 'js/ninja.js' %}" type="text/javascript"></script>
{% endblock javascript %}

{% endblock extra_js %}
{% endcomment %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!-- {% extends 'ace_common/edx_ace/common/base_body.html' %} -->

{% load i18n %}
{% load static %}
{% block content %}
<p style="color: rgba(0,0,0,.75);">
{% autoescape off %}
{# xss-lint: disable=django-blocktrans-missing-escape-filter #}
{% blocktrans %}Hi there,{% endblocktrans %}
{% endautoescape %}
<br />
</p>
<p style="color: rgba(0,0,0,.75);">
{% autoescape off %}
{# xss-lint: disable=django-blocktrans-missing-escape-filter #}
{% blocktrans %}Congrats on your progress! You're {{ current_progress }}% through the "{{ course_name }}". {% endblocktrans %}
{% endautoescape %}
</p>
<p style="color: rgba(0,0,0,.75);">
{% autoescape off %}
{# xss-lint: disable=django-blocktrans-missing-escape-filter #}
{% blocktrans %}Keep it up! Continue your journey {% endblocktrans %}<a href="{{ course_home_url }}">here</a>.
{% endautoescape %}
<br />
</p>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Hi there,{% endblocktrans %}
{% blocktrans %}Congrats on your progress! You're {{ current_progress }}% through the "{{ course_name }}".{% endblocktrans %}
{% blocktrans %}Keep it up! Continue your journey <a href="{{ course_home_url }}">here</a>.{% endblocktrans %}
{% trans "Enjoy your studies," %}
{% blocktrans %}The {{ platform_name }} Team {% endblocktrans %}
{% endautoescape %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ platform_name }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- {% extends 'ace_common/edx_ace/common/base_head.html' %} -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}{{ platform_name }} - {{ course_name }} | Course Progress 🚀 {% endblocktrans %}
{% endautoescape %}
12 changes: 12 additions & 0 deletions xmodule/course_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@ def to_json(self, value):


class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
allow_course_completion_emails = Boolean(
display_name=_("Allow Course Completion emails at different percentages"),
help=_("Enter true or false. When true, students will get email when they reach specific percentages mentioned in 'course completion percentages for emails'."),
default=False,
scope=Scope.settings
)
course_completion_percentages_for_emails = String(
display_name=_("Course Completion Percentages"),
help=_("set comma separated percentages at which instructors want to send course completion emails e.g. '60, 70'"),
default="60, 70",
scope=Scope.settings
)
lti_passports = List(
display_name=_("LTI Passports"),
help=_('Enter the passports for course LTI tools in the following format: "id:client_key:client_secret".'),
Expand Down

0 comments on commit fb83bb7

Please sign in to comment.