diff --git a/openedx/features/sdaia_features/course_progress/admin.py b/openedx/features/sdaia_features/course_progress/admin.py index 7a48b9299fd7..ee4595b556b3 100644 --- a/openedx/features/sdaia_features/course_progress/admin.py +++ b/openedx/features/sdaia_features/course_progress/admin.py @@ -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) diff --git a/openedx/features/sdaia_features/course_progress/management/commands/__init__.py b/openedx/features/sdaia_features/course_progress/management/commands/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/sdaia_features/course_progress/management/commands/send_progress_emails.py b/openedx/features/sdaia_features/course_progress/management/commands/send_progress_emails.py index 362f2c83bef2..cc4dac0a3af9 100644 --- a/openedx/features/sdaia_features/course_progress/management/commands/send_progress_emails.py +++ b/openedx/features/sdaia_features/course_progress/management/commands/send_progress_emails.py @@ -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): """ @@ -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) + diff --git a/openedx/features/sdaia_features/course_progress/migrations/0001_initial.py b/openedx/features/sdaia_features/course_progress/migrations/0001_initial.py new file mode 100644 index 000000000000..f5e53ef22b1d --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/openedx/features/sdaia_features/course_progress/migrations/__init__.py b/openedx/features/sdaia_features/course_progress/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/sdaia_features/course_progress/models.py b/openedx/features/sdaia_features/course_progress/models.py new file mode 100644 index 000000000000..7dfbb84d9d6b --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/models.py @@ -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) diff --git a/openedx/features/sdaia_features/course_progress/templates/course_progress/base.html b/openedx/features/sdaia_features/course_progress/templates/course_progress/base.html new file mode 100644 index 000000000000..09d26f38bf06 --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/templates/course_progress/base.html @@ -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 %} + + + {% block extra_js %} + + + {% block javascript %} + + {% endblock javascript %} + + {% endblock extra_js %} +{% endcomment %} diff --git a/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/body.html b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/body.html new file mode 100644 index 000000000000..03f93edf4c4f --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/body.html @@ -0,0 +1,26 @@ + + +{% load i18n %} +{% load static %} +{% block content %} +

+ {% autoescape off %} + {# xss-lint: disable=django-blocktrans-missing-escape-filter #} + {% blocktrans %}Hi there,{% endblocktrans %} + {% endautoescape %} +
+

+

+ {% 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 %} +

+

+ {% autoescape off %} + {# xss-lint: disable=django-blocktrans-missing-escape-filter #} + {% blocktrans %}Keep it up! Continue your journey {% endblocktrans %}here. + {% endautoescape %} +
+

+{% endblock %} \ No newline at end of file diff --git a/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/body.txt b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/body.txt new file mode 100644 index 000000000000..44555c88600a --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/body.txt @@ -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 here.{% endblocktrans %} +{% trans "Enjoy your studies," %} +{% blocktrans %}The {{ platform_name }} Team {% endblocktrans %} +{% endautoescape %} diff --git a/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/from_name.txt b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/from_name.txt new file mode 100644 index 000000000000..dcbc23c00480 --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/from_name.txt @@ -0,0 +1 @@ +{{ platform_name }} diff --git a/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/head.html b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/head.html new file mode 100644 index 000000000000..463307296e06 --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/head.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/subject.txt b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/subject.txt new file mode 100644 index 000000000000..24e2d603ec29 --- /dev/null +++ b/openedx/features/sdaia_features/course_progress/templates/course_progress/edx_ace/usercourseprogressemail/email/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}{{ platform_name }} - {{ course_name }} | Course Progress 🚀 {% endblocktrans %} +{% endautoescape %} \ No newline at end of file diff --git a/xmodule/course_block.py b/xmodule/course_block.py index b172b0059e1a..3cf3a1af3563 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -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".'),