Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added an app for sending progress emails to users #509

Merged
merged 18 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
24 changes: 24 additions & 0 deletions openedx/features/sdaia_features/course_progress/admin.py
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)
20 changes: 20 additions & 0 deletions openedx/features/sdaia_features/course_progress/apps.py
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.
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)
Empty file.
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 openedx/features/sdaia_features/course_progress/signals.py
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 openedx/features/sdaia_features/course_progress/tasks.py
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
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,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 %}
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 %}
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' %} -->
Loading
Loading