forked from openedx/edx-platform
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added an app for sending progress emails to users (#509)
* feat: added an app for sending progress emails to users * feat: add cronjob for sending progress emails to users * make changes to only email course progress for specific sites * improve command * send emails on unit block completion post save signal instead of settings cronjob of django command * remove empty command * edit email template * improve command * filter all block completions instead of only verticals * change signal name * add signal to send course completion email to user * finalize email templates * convert floating digit to number * add space for arabic text * correct message condition * add red color for progress percentage * make percentage inline * correct inline issue for progress percentage --------- Co-authored-by: Ali Salman <[email protected]> Co-authored-by: Muhammad Faraz Maqsood <[email protected]> Co-authored-by: Muhammad Faraz Maqsood <[email protected]>
- Loading branch information
1 parent
36c31df
commit 62d4be1
Showing
26 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
""" | ||
Progress Updates App Config | ||
""" | ||
from django.apps import AppConfig | ||
from edx_django_utils.plugins import PluginURLs, PluginSettings | ||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType | ||
|
||
class CourseProgressConfig(AppConfig): | ||
name = 'openedx.features.sdaia_features.course_progress' | ||
|
||
plugin_app = { | ||
PluginSettings.CONFIG: { | ||
ProjectType.LMS: { | ||
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: 'settings.common'}, | ||
} | ||
} | ||
} | ||
|
||
def ready(self): | ||
from . import signals # pylint: disable=unused-import |
Empty file.
Empty file.
27 changes: 27 additions & 0 deletions
27
openedx/features/sdaia_features/course_progress/migrations/0001_initial.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Empty file.
8 changes: 8 additions & 0 deletions
8
openedx/features/sdaia_features/course_progress/settings/common.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
|
||
"""Settings""" | ||
|
||
|
||
def plugin_settings(settings): | ||
""" | ||
Required Common settings | ||
""" |
70 changes: 70 additions & 0 deletions
70
openedx/features/sdaia_features/course_progress/signals.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
""" | ||
Signal handlers for the course progress emails | ||
""" | ||
import logging | ||
|
||
from completion.models import BlockCompletion | ||
from django.db.models.signals import post_save | ||
from django.dispatch import receiver | ||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user | ||
from django.contrib.sites.models import Site | ||
|
||
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED | ||
from openedx.core.lib.celery.task_utils import emulate_http_request | ||
from openedx.features.sdaia_features.course_progress.models import CourseCompletionEmailHistory | ||
from openedx.features.sdaia_features.course_progress.tasks import send_user_course_progress_email, send_user_course_completion_email | ||
from openedx.features.sdaia_features.course_progress.utils import get_user_course_progress | ||
from xmodule.modulestore.django import modulestore | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@receiver(post_save, sender=BlockCompletion) | ||
def send_course_progress_milestones_achievement_emails(**kwargs): | ||
""" | ||
Receives the BlockCompletion signal and sends the email to | ||
the user if he completes a specific course progress threshold. | ||
""" | ||
logger.info(f"\n\n\n inside send_course_progress_milestones_achievement_emails \n\n\n") | ||
instance = kwargs['instance'] | ||
if not instance.context_key.is_course: | ||
return # Content in a library or some other thing that doesn't support milestones | ||
|
||
course_key = instance.context_key | ||
|
||
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: | ||
return | ||
|
||
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 {str(course_key)}") | ||
return | ||
|
||
user_id = instance.user_id | ||
user = User.objects.get(id=user_id) | ||
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 progress_last_email_sent_at == course_completion_percentages_for_emails[-1]: | ||
return | ||
|
||
site = Site.objects.first() or Site.objects.get_current() | ||
with emulate_http_request(site, user): | ||
user_completion_percentage = get_user_course_progress(user, course_key) | ||
|
||
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), user_id) | ||
|
||
|
||
@receiver(COURSE_GRADE_NOW_PASSED, dispatch_uid="course_completion") | ||
def send_course_completion_email(sender, user, course_id, **kwargs): # pylint: disable=unused-argument | ||
""" | ||
Listen for a signal indicating that the user has passed a course run. | ||
""" | ||
logger.info(f"\n\n\n inside send_course_completion_email \n\n\n") | ||
send_user_course_completion_email.delay(user.id, str(course_id)) |
138 changes: 138 additions & 0 deletions
138
openedx/features/sdaia_features/course_progress/tasks.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
""" | ||
celery tasks for the course progress emails | ||
""" | ||
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 edx_ace import ace | ||
from edx_ace.recipient import Recipient | ||
from opaque_keys.edx.keys import CourseKey | ||
|
||
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.djangoapps.content.block_structure.api import get_block_structure_manager | ||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY | ||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers | ||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference | ||
from openedx.core.lib.celery.task_utils import emulate_http_request | ||
from openedx.features.sdaia_features.course_progress.models import CourseCompletionEmailHistory | ||
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 Course Progress Email | ||
""" | ||
APP_LABEL = 'course_progress' | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.options['transactional'] = True | ||
|
||
|
||
class UserCourseCompletionEmail(BaseMessageType): | ||
""" | ||
Message Type Class for User Course Completion Email | ||
""" | ||
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, user_id): | ||
""" | ||
Sends User Activation Code Via Email | ||
""" | ||
user = User.objects.get(id=user_id) | ||
course_id = CourseKey.from_string(course_key) | ||
course = modulestore().get_course(course_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') | ||
platform_name = configuration_helpers.get_value_for_org( | ||
course.org, | ||
'PLATFORM_NAME', | ||
settings.PLATFORM_NAME | ||
) | ||
|
||
context={ | ||
'current_progress': int(current_progress), | ||
'progress_milestone_crossed': progress_last_email_sent_at, | ||
'course_key': course_key, | ||
'platform_name': platform_name, | ||
'course_name': course.display_name, | ||
'course_home_url': course_home_url, | ||
} | ||
message_context.update(context) | ||
user_language_pref = get_user_preference(user, LANGUAGE_KEY) or settings.LANGUAGE_CODE | ||
try: | ||
with emulate_http_request(site, user): | ||
msg = UserCourseProgressEmail(context=message_context).personalize( | ||
recipient=Recipient(0, user.email), | ||
language=user_language_pref, | ||
user_context={'full_name': user.profile.name} | ||
) | ||
ace.send(msg) | ||
logger.info('course progress 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 course progress email sent to user') | ||
return False | ||
|
||
|
||
@shared_task | ||
def send_user_course_completion_email(user_id, course_key): | ||
course_id = CourseKey.from_string(course_key) | ||
user = User.objects.get(id=user_id) | ||
collected_block_structure = get_block_structure_manager(course_id).get_collected() | ||
course_grade = CourseGradeFactory().read(user, collected_block_structure=collected_block_structure) | ||
passing_grade = int(course_grade.percent * 100) | ||
|
||
course = modulestore().get_course(course_id) | ||
site = Site.objects.first() or Site.objects.get_current() | ||
message_context = get_base_template_context(site) | ||
course_progress_url = get_learning_mfe_home_url(course_key=course_key, url_fragment='progress') | ||
platform_name = configuration_helpers.get_value_for_org( | ||
course.org, | ||
'PLATFORM_NAME', | ||
settings.PLATFORM_NAME | ||
) | ||
|
||
context={ | ||
'course_key': course_key, | ||
'platform_name': platform_name, | ||
'course_name': course.display_name, | ||
'course_progress_url': course_progress_url, | ||
'passing_grade': passing_grade, | ||
} | ||
message_context.update(context) | ||
user_language_pref = get_user_preference(user, LANGUAGE_KEY) or settings.LANGUAGE_CODE | ||
try: | ||
with emulate_http_request(site, user): | ||
msg = UserCourseCompletionEmail(context=message_context).personalize( | ||
recipient=Recipient(0, user.email), | ||
language=user_language_pref, | ||
user_context={'full_name': user.profile.name} | ||
) | ||
ace.send(msg) | ||
logger.info('course completion email sent to user:') | ||
return True | ||
except Exception as e: # pylint: disable=broad-except | ||
logger.exception(str(e)) | ||
logger.exception('Could not send course completion email sent to user') | ||
return False |
21 changes: 21 additions & 0 deletions
21
openedx/features/sdaia_features/course_progress/templates/course_progress/base.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
49 changes: 49 additions & 0 deletions
49
...urse_progress/templates/course_progress/edx_ace/usercoursecompletionemail/email/body.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<!-- {% 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 %}Congratulations!{% endblocktrans %} | ||
{% endautoescape %} | ||
<br /> | ||
</p> | ||
<p style="color: rgba(0,0,0,.75);"> | ||
{% autoescape off %} | ||
{# xss-lint: disable=django-blocktrans-missing-escape-filter #} | ||
{% blocktrans %}{{full_name}}{% endblocktrans %} | ||
{% endautoescape %} | ||
<br /> | ||
</p> | ||
<p style="color: rgba(0,0,0,.75);"> | ||
{% autoescape off %} | ||
{# xss-lint: disable=django-blocktrans-missing-escape-filter #} | ||
{% blocktrans %}You have successfully completed this course! {% endblocktrans %} | ||
{% endautoescape %} | ||
<br /> | ||
</p> | ||
<p style="color: rgba(0,0,0,.75);"> | ||
{% autoescape off %} | ||
{# xss-lint: disable=django-blocktrans-missing-escape-filter #} | ||
{% blocktrans %}Please click {% endblocktrans %}<a href="{{ course_progress_url }}">{% blocktrans %}here{% endblocktrans %}</a>{% blocktrans %} to view your accreditation. {% endblocktrans %} | ||
{% endautoescape %} | ||
<br /> | ||
<br /> | ||
</p> | ||
<p style="color: rgba(0,0,0,.75);"> | ||
{% autoescape off %} | ||
{# xss-lint: disable=django-blocktrans-missing-escape-filter #} | ||
{% blocktrans %}Thank you. {% endblocktrans %} | ||
{% endautoescape %} | ||
<br /> | ||
</p> | ||
<p style="color: rgba(0,0,0,.75);"> | ||
{% autoescape off %} | ||
{# xss-lint: disable=django-blocktrans-missing-escape-filter #} | ||
{% blocktrans %}SDAIA Academy{% endblocktrans %} | ||
{% endautoescape %} | ||
<br /> | ||
</p> | ||
{% endblock %} |
8 changes: 8 additions & 0 deletions
8
...ourse_progress/templates/course_progress/edx_ace/usercoursecompletionemail/email/body.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{% load i18n %}{% autoescape off %} | ||
{% blocktrans %}Congratulations!{% endblocktrans %} | ||
{% blocktrans %}{{ full_name }}{% endblocktrans %} | ||
{% blocktrans %}You have successfully completed this course! {% endblocktrans %} | ||
{% blocktrans %}Please click {% endblocktrans %}<a href="{{ course_progress_url }}">{% blocktrans %}here{% endblocktrans %}</a>{% blocktrans %} to view your accreditation. {% endblocktrans %} | ||
{% blocktrans %}Thank you. {% endblocktrans %} | ||
{% blocktrans %}SDAIA Academy{% endblocktrans %} | ||
{% endautoescape %} |
1 change: 1 addition & 0 deletions
1
..._progress/templates/course_progress/edx_ace/usercoursecompletionemail/email/from_name.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{{ platform_name }} |
1 change: 1 addition & 0 deletions
1
...urse_progress/templates/course_progress/edx_ace/usercoursecompletionemail/email/head.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<!-- {% extends 'ace_common/edx_ace/common/base_head.html' %} --> |
Oops, something went wrong.