feat: email notifications for published courses
This commit is contained in:
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-ink-red-3">
|
<div v-else-if class="text-ink-red-3">
|
||||||
{{ __('No slots available for the selected course.') }}
|
{{ __('No slots available for the selected course.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,23 +65,9 @@ const saveSettings = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
let fieldsToSave = {}
|
|
||||||
let imageFields = ['favicon', 'banner_image']
|
|
||||||
props.fields.forEach((f) => {
|
|
||||||
if (imageFields.includes(f.name)) {
|
|
||||||
fieldsToSave[f.name] =
|
|
||||||
branding.data[f.name] && branding.data[f.name].file_url
|
|
||||||
? branding.data[f.name].file_url
|
|
||||||
: null
|
|
||||||
} else {
|
|
||||||
fieldsToSave[f.name] = branding.data[f.name]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fieldsToSave['app_logo'] = fieldsToSave['banner_image']
|
|
||||||
saveSettings.submit(
|
saveSettings.submit(
|
||||||
{
|
{
|
||||||
fields: fieldsToSave,
|
fields: getFieldsToSave(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
@@ -91,18 +77,36 @@ const update = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(branding, (updatedDoc) => {
|
const getFieldsToSave = () => {
|
||||||
let textFields = []
|
let imageFields = ['favicon', 'banner_image']
|
||||||
let imageFields = []
|
let fieldsToSave = {}
|
||||||
|
|
||||||
props.fields.forEach((f) => {
|
props.sections.forEach((section) => {
|
||||||
if (f.type === 'Upload') {
|
section.columns.forEach((column) => {
|
||||||
imageFields.push(f.name)
|
column.fields.forEach((field) => {
|
||||||
} else {
|
if (imageFields.includes(field.name)) {
|
||||||
textFields.push(f.name)
|
fieldsToSave[field.name] =
|
||||||
}
|
branding.data[field.name] && branding.data[field.name].file_url
|
||||||
|
? branding.data[field.name].file_url
|
||||||
|
: null
|
||||||
|
} else {
|
||||||
|
fieldsToSave[field.name] = branding.data[field.name]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fieldsToSave['app_logo'] = fieldsToSave['banner_image']
|
||||||
|
return fieldsToSave
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(branding, (updatedDoc) => {
|
||||||
|
updateDirtyState(updatedDoc)
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDirtyState = (updatedDoc) => {
|
||||||
|
const { textFields, imageFields } = segregateFields()
|
||||||
|
|
||||||
textFields.forEach((field) => {
|
textFields.forEach((field) => {
|
||||||
if (updatedDoc.data[field] != updatedDoc.previousData[field]) {
|
if (updatedDoc.data[field] != updatedDoc.previousData[field]) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
@@ -111,11 +115,28 @@ watch(branding, (updatedDoc) => {
|
|||||||
|
|
||||||
imageFields.forEach((field) => {
|
imageFields.forEach((field) => {
|
||||||
if (
|
if (
|
||||||
updatedDoc.data[field] &&
|
updatedDoc.data[field]?.file_url != updatedDoc.previousData[field]?.file_url
|
||||||
updatedDoc.data[field].file_url != updatedDoc.previousData[field].file_url
|
|
||||||
) {
|
) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const segregateFields = () => {
|
||||||
|
let textFields = []
|
||||||
|
let imageFields = []
|
||||||
|
|
||||||
|
props.sections.forEach(section => {
|
||||||
|
section.columns.forEach(column => {
|
||||||
|
column.fields.forEach(field => {
|
||||||
|
if (field.type === 'Upload') {
|
||||||
|
imageFields.push(field.name)
|
||||||
|
} else {
|
||||||
|
textFields.push(field.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return { textFields, imageFields }
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
5611
frontend/yarn.lock
Normal file
5611
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -132,6 +132,7 @@ scheduler_events = {
|
|||||||
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
||||||
"lms.lms.doctype.lms_batch.lms_batch.send_batch_start_reminder",
|
"lms.lms.doctype.lms_batch.lms_batch.send_batch_start_reminder",
|
||||||
"lms.lms.doctype.lms_live_class.lms_live_class.send_live_class_reminder",
|
"lms.lms.doctype.lms_live_class.lms_live_class.send_live_class_reminder",
|
||||||
|
"lms.lms.doctype.lms_course.lms_course.send_notification_for_published_courses",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
# Copyright (c) 2021, Frappe and contributors
|
# Copyright (c) 2021, Frappe and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import json
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, today
|
from frappe.utils import cint, today
|
||||||
|
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||||
from ...utils import generate_slug, update_payment_record, validate_image
|
from ...utils import generate_slug, update_payment_record, validate_image
|
||||||
|
|
||||||
|
|
||||||
@@ -131,3 +129,67 @@ class LMSCourse(Document):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Course#{self.name}>"
|
return f"<Course#{self.name}>"
|
||||||
|
|
||||||
|
def send_notification_for_published_courses():
|
||||||
|
send_notification_for_published_courses = frappe.db.get_single_value("LMS Settings", "send_notification_for_published_courses")
|
||||||
|
if not send_notification_for_published_courses:
|
||||||
|
return
|
||||||
|
|
||||||
|
courses_published_today = frappe.get_all("LMS Course", {
|
||||||
|
"published_on": today(),
|
||||||
|
}, ["name", "title", "video_link", "short_introduction"])
|
||||||
|
|
||||||
|
if not courses_published_today:
|
||||||
|
return
|
||||||
|
|
||||||
|
if send_notification_for_published_courses == "Email":
|
||||||
|
send_email_notification_for_published_courses(courses_published_today)
|
||||||
|
else:
|
||||||
|
send_system_notification_for_published_courses(courses_published_today)
|
||||||
|
|
||||||
|
def send_email_notification_for_published_courses(courses):
|
||||||
|
brand_name = frappe.db.get_single_value("Website Settings", "app_name")
|
||||||
|
brand_logo = frappe.db.get_single_value("Website Settings", "banner_image")
|
||||||
|
subject = _("A new course has been published on {0}").format(brand_name)
|
||||||
|
template = "published_course_notification"
|
||||||
|
students = frappe.get_all("User", { "enabled": 1 }, pluck="name")
|
||||||
|
|
||||||
|
for course in courses:
|
||||||
|
instructor_details = []
|
||||||
|
instructors = frappe.get_all("Course Instructor", { "parent": course.name }, pluck="instructor")
|
||||||
|
for instructor in instructors:
|
||||||
|
details = frappe.db.get_value("User", instructor, ["full_name", "email", "user_image"], as_dict=True)
|
||||||
|
instructor_details.append(details)
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"brand_logo": brand_logo,
|
||||||
|
"brand_name": brand_name,
|
||||||
|
"preview_video": f"https://www.youtube.com/embed/{course.video_link}",
|
||||||
|
"title": course.title,
|
||||||
|
"short_introduction": course.short_introduction,
|
||||||
|
"instructors": instructor_details,
|
||||||
|
"course_url": f"{frappe.utils.get_url()}/lms/courses/{course.name}",
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=instructors,
|
||||||
|
bcc=students,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_system_notification_for_published_courses(courses):
|
||||||
|
for course in courses:
|
||||||
|
students = frappe.get_all("User", { "enabled": 1 }, pluck="name")
|
||||||
|
instructors = frappe.get_all("Course Instructor", { "parent": course.name }, pluck="instructor")
|
||||||
|
notification = frappe._dict({
|
||||||
|
"subject": course.title,
|
||||||
|
"email_content": _("A new course '{0}' has been published that might interest you. Check it out!").format(course.title),
|
||||||
|
"document_type": "LMS Course",
|
||||||
|
"document_name": course.name,
|
||||||
|
"from_user": instructors[0] if instructors else None,
|
||||||
|
"type": "Alert",
|
||||||
|
"link": f"/lms/courses/{course.name}",
|
||||||
|
})
|
||||||
|
make_notification_logs(notification, ["ash@ipp.com"])
|
||||||
38
lms/templates/emails/published_course_notification.html
Normal file
38
lms/templates/emails/published_course_notification.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<div style="width: 70%; margin: 0 auto;">
|
||||||
|
<img src="{{ brand_logo }}" style="width: 30px; height: 30px;" />
|
||||||
|
<p style="font-size: 16px; font-weight: 600;">
|
||||||
|
{{ _("Hello Learner") }},
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ _("A new course has been published on ")}} {{ brand_name }} {{ _("that might interest you!") }} {{ _("Here are the details:") }}
|
||||||
|
</p>
|
||||||
|
<div style="background-color: #F8F8F8; border-radius: 12px; padding: 12px; margin-bottom: 6px;">
|
||||||
|
<div style="font-weight: 600; margin-bottom: 6px;">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ short_introduction }}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
{% for instructor in instructors %}
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
{% if instructor.user_image %}
|
||||||
|
<img src="{{ instructor.user_image }}" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 5px;" />
|
||||||
|
{% else %}
|
||||||
|
<div style="width: 20px; height: 20px; border-radius: 50%; background-color: #ccc; display: flex; align-items: center; justify-content: center; margin-right: 5px;">
|
||||||
|
<span style="font-size: 12px; color: #fff;">
|
||||||
|
{{ instructor.full_name.split("")[0] | upper }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
{{ instructor.full_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ course_url }}" style="display: inline-block; padding: 4px 8px; background-color: #171717; color: #fff; text-decoration: none; cursor: pointer; border-radius: 8px; margin-top: 10px;">
|
||||||
|
{{ _("Checkout the course") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user