Merge branch 'develop' into develop

This commit is contained in:
Om vataliya
2026-01-14 11:35:38 +05:30
committed by GitHub
55 changed files with 9056 additions and 5859 deletions

View File

@@ -132,6 +132,7 @@ scheduler_events = {
"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_live_class.lms_live_class.send_live_class_reminder",
"lms.lms.doctype.lms_course.lms_course.send_notification_for_published_courses",
],
}

View File

@@ -29,7 +29,13 @@ from frappe.utils import (
from frappe.utils.response import Response
from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import get_average_rating, get_batch_details, get_course_details, get_lesson_count
from lms.lms.utils import (
get_average_rating,
get_batch_details,
get_course_details,
get_instructors,
get_lesson_count,
)
@frappe.whitelist(allow_guest=True)
@@ -1197,17 +1203,79 @@ def get_notifications(filters):
notifications = frappe.get_all(
"Notification Log",
filters,
["subject", "from_user", "link", "read", "name"],
[
"subject",
"from_user",
"link",
"read",
"name",
"creation",
"document_type",
"document_name",
"type",
"email_content",
],
order_by="creation desc",
)
for notification in notifications:
notification = update_document_details(notification)
notification = update_user_details(notification)
return notifications
def update_user_details(notification):
if (
notification.document_details
and len(notification.document_details.get("instructors", []))
and not is_mention(notification)
):
from_user_details = notification.document_details["instructors"][0]
else:
from_user_details = frappe.db.get_value(
"User", notification.from_user, ["full_name", "user_image"], as_dict=1
)
notification.update(from_user_details)
notification["from_user_details"] = from_user_details
return notification
return notifications
def is_mention(notification):
if notification.type == "Mention":
return True
if "mentioned you" in notification.subject.lower():
return True
return False
def update_document_details(notification):
if notification.document_type == "LMS Course":
details = frappe.db.get_value(
"LMS Course", notification.document_name, ["title", "video_link", "short_introduction"], as_dict=1
)
instructors = get_instructors("LMS Course", notification.document_name)
details["instructors"] = instructors
notification["document_details"] = details
elif notification.document_type == "LMS Batch":
details = frappe.db.get_value(
"LMS Batch",
notification.document_name,
[
"title",
"description as short_introduction",
"video_link",
"start_date",
"end_date",
"start_time",
"timezone",
],
as_dict=1,
)
instructors = get_instructors("LMS Batch", notification.document_name)
details["instructors"] = instructors
notification["document_details"] = details
return notification
@frappe.whitelist(allow_guest=True)

View File

@@ -64,16 +64,21 @@ class LMSAssignmentSubmission(Document):
def trigger_update_notification(self):
notification = frappe._dict(
{
"subject": _("There has been an update on your submission for assignment {0}").format(
self.assignment_title
"subject": _("The instructor has left a comment on your assignment {0}").format(
frappe.bold(self.assignment_title)
),
"email_content": self.comments,
"document_type": self.doctype,
"document_name": self.name,
"for_user": self.owner,
"from_user": self.evaluator,
"type": "Alert",
"link": f"/lms/assignment-submission/{self.assignment}/{self.name}",
"link": f"/lms/assignment-submission/{self.assignment}/{self.name}",
}
)
make_notification_logs(notification, [self.member])
@frappe.whitelist()
def upload_assignment(
assignment_attachment=None,
answer=None,

View File

@@ -34,6 +34,10 @@
"column_break_flwy",
"seat_count",
"evaluation_end_date",
"notification_sent",
"section_break_jedp",
"video_link",
"column_break_kpct",
"meta_image",
"section_break_khcn",
"batch_details",
@@ -361,6 +365,26 @@
"fieldtype": "Link",
"label": "Zoom Account",
"options": "LMS Zoom Settings"
},
{
"fieldname": "video_link",
"fieldtype": "Attach",
"label": "Preview Video"
},
{
"default": "0",
"fieldname": "notification_sent",
"fieldtype": "Check",
"label": "Notification Sent",
"read_only": 1
},
{
"fieldname": "section_break_jedp",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_kpct",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -383,7 +407,7 @@
"link_fieldname": "payment_for_document"
}
],
"modified": "2025-12-23 11:27:00.424331",
"modified": "2026-01-13 18:50:27.420712",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Batch",

View File

@@ -8,12 +8,14 @@ from datetime import timedelta
import frappe
import requests
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.model.document import Document
from frappe.utils import add_days, cint, format_datetime, get_time, nowdate
from lms.lms.utils import (
generate_slug,
get_assignment_details,
get_instructors,
get_lesson_index,
get_lesson_url,
get_quiz_details,
@@ -33,6 +35,10 @@ class LMSBatch(Document):
self.validate_timetable()
self.validate_evaluation_end_date()
def on_update(self):
if self.has_value_changed("published") and self.published:
frappe.enqueue(send_notification_for_published_batch, batch=self, now=True)
def autoname(self):
if not self.name:
self.name = generate_slug(self.title, "LMS Batch")
@@ -123,6 +129,77 @@ class LMSBatch(Document):
update_payment_record("LMS Batch", self.name)
def send_notification_for_published_batch(batch):
send_notification = frappe.db.get_single_value("LMS Settings", "send_notification_for_published_batches")
if not send_notification:
return
if not batch.published:
return
if batch.notification_sent:
return
if send_notification == "Email":
send_email_notification_for_published_batch(batch)
else:
send_system_notification_for_published_batch(batch)
def send_email_notification_for_published_batch(batch):
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_batch_notification"
students = frappe.get_all("User", {"enabled": 1}, pluck="name")
instructors = get_instructors("LMS Batch", batch.name)
args = {
"brand_logo": brand_logo,
"brand_name": brand_name,
"title": batch.title,
"short_introduction": batch.description,
"start_date": batch.start_date,
"end_date": batch.end_date,
"start_time": batch.start_time,
"medium": batch.medium,
"timezone": batch.timezone,
"instructors": instructors,
"batch_url": f"{frappe.utils.get_url()}/lms/batches/details/{batch.name}",
}
frappe.sendmail(
recipients=instructors,
bcc=students,
subject=subject,
template=template,
args=args,
)
frappe.db.set_value("LMS Batch", batch.name, "notification_sent", 1)
def send_system_notification_for_published_batch(batch):
students = frappe.get_all("User", {"enabled": 1}, pluck="name")
instructors = frappe.get_all("Course Instructor", {"parent": batch.name}, pluck="instructor")
instructor_name = frappe.db.get_value("User", instructors[0], "full_name")
notification = frappe._dict(
{
"subject": _("{0} has published a new batch {1}").format(
frappe.bold(instructor_name), frappe.bold(batch.title)
),
"email_content": _(
"A new batch '{0}' has been published that might interest you. Check it out!"
).format(batch.title),
"document_type": "LMS Batch",
"document_name": batch.name,
"from_user": instructors[0] if instructors else None,
"type": "Alert",
"link": f"/lms/batches/details/{batch.name}",
}
)
make_notification_logs(notification, students)
frappe.db.set_value("LMS Batch", batch.name, "notification_sent", 1)
@frappe.whitelist()
def create_live_class(
batch_name,

View File

@@ -48,7 +48,8 @@
"statistics_section",
"enrollments",
"lessons",
"rating"
"rating",
"notification_sent"
],
"fields": [
{
@@ -288,6 +289,13 @@
"fieldname": "timezone",
"fieldtype": "Data",
"label": "Timezone"
},
{
"default": "0",
"fieldname": "notification_sent",
"fieldtype": "Check",
"label": "Notification Sent",
"read_only": 1
}
],
"is_published_field": "published",
@@ -306,7 +314,7 @@
}
],
"make_attachments_public": 1,
"modified": "2025-12-15 15:15:42.226098",
"modified": "2026-01-13 18:48:56.069280",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Course",

View File

@@ -1,15 +1,15 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import json
import random
import frappe
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.model.document import Document
from frappe.utils import cint, today
from ...utils import generate_slug, update_payment_record, validate_image
from ...utils import generate_slug, get_instructors, update_payment_record, validate_image
class LMSCourse(Document):
@@ -131,3 +131,79 @@ class LMSCourse(Document):
def __repr__(self):
return f"<Course#{self.name}>"
def send_notification_for_published_courses():
send_notification = frappe.db.get_single_value("LMS Settings", "send_notification_for_published_courses")
if not send_notification:
return
courses_published_today = frappe.get_all(
"LMS Course",
{
"published_on": today(),
"notification_sent": 0,
},
["name", "title", "short_introduction"],
)
if not courses_published_today:
return
if send_notification == "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:
instructors = get_instructors("LMS Course", course.name)
args = {
"brand_logo": brand_logo,
"brand_name": brand_name,
"title": course.title,
"short_introduction": course.short_introduction,
"instructors": instructors,
"course_url": f"{frappe.utils.get_url()}/lms/courses/{course.name}",
}
frappe.sendmail(
recipients=instructors,
bcc=students,
subject=subject,
template=template,
args=args,
)
frappe.db.set_value("LMS Course", course.name, "notification_sent", 1)
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")
instructor_name = frappe.db.get_value("User", instructors[0], "full_name")
notification = frappe._dict(
{
"subject": _("{0} has published a new course {1}").format(
frappe.bold(instructor_name), frappe.bold(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, students)
frappe.db.set_value("LMS Course", course.name, "notification_sent", 1)

View File

@@ -54,15 +54,15 @@ class LMSQuizSubmission(Document):
notification = frappe._dict(
{
"subject": _("You have got a score of {0} for the quiz {1}").format(
self.score, self.quiz_title
(frappe.bold(self.score)), frappe.bold(self.quiz_title)
),
"email_content": _(
"There has been an update on your submission. You have got a score of {0} for the quiz {1}"
).format(self.score, self.quiz_title),
).format(frappe.bold(self.score), frappe.bold(self.quiz_title)),
"document_type": self.doctype,
"document_name": self.name,
"for_user": self.member,
"from_user": "Administrator",
"from_user": frappe.session.user,
"type": "Alert",
"link": "",
}

View File

@@ -6,16 +6,20 @@
"engine": "InnoDB",
"field_order": [
"general_tab",
"default_home",
"send_calendar_invite_for_evaluations",
"persona_captured",
"column_break_zdel",
"allow_guest_access",
"prevent_skipping_videos",
"send_calendar_invite_for_evaluations",
"column_break_zdel",
"disable_pwa",
"persona_captured",
"default_home",
"column_break_bjis",
"unsplash_access_key",
"livecode_url",
"notifications_section",
"send_notification_for_published_courses",
"column_break_dtns",
"send_notification_for_published_batches",
"section_break_szgq",
"show_day_view",
"column_break_2",
@@ -446,13 +450,34 @@
"fieldname": "disable_pwa",
"fieldtype": "Check",
"label": "Disable PWA"
},
{
"fieldname": "send_notification_for_published_courses",
"fieldtype": "Select",
"label": "Send Notification for Published Courses",
"options": "\nEmail\nIn-app"
},
{
"fieldname": "send_notification_for_published_batches",
"fieldtype": "Select",
"label": "Send Notification for Published Batches",
"options": "\nEmail\nIn-app"
},
{
"fieldname": "notifications_section",
"fieldtype": "Section Break",
"label": "Notifications"
},
{
"fieldname": "column_break_dtns",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-12-22 11:30:13.868031",
"modified": "2026-01-01 19:36:54.443390",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Settings",

View File

@@ -398,26 +398,47 @@ def handle_notifications(doc, method):
notify_mentions_via_email(doc, topic)
def create_notification_log(doc, topic):
def get_course_details_for_notification(topic):
users = []
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
course_title = frappe.db.get_value("LMS Course", course, "title")
instructors = frappe.db.get_all(
"Course Instructor", {"parent": course, "parenttype": "LMS Course"}, pluck="instructor"
)
users.append(topic.owner)
users += instructors
subject = _("New reply on the topic {0} in course {1}").format(topic.title, course_title)
link = get_lesson_url(course, get_lesson_index(topic.reference_docname))
return subject, link, users
def get_batch_details_for_notification(topic):
users = []
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
subject = _("New comment in batch {0}").format(batch_title)
link = f"/lms/batches/{topic.reference_docname}"
instructors = frappe.db.get_all(
"Course Instructor",
{"parenttype": "LMS Batch", "parent": topic.reference_docname},
pluck="instructor",
)
students = frappe.db.get_all("LMS Batch Enrollment", {"batch": topic.reference_docname}, pluck="member")
users += instructors
users += students
return subject, link, users
def create_notification_log(doc, topic):
if topic.reference_doctype == "Course Lesson":
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
course_title = frappe.db.get_value("LMS Course", course, "title")
instructors = frappe.db.get_all("Course Instructor", {"parent": course}, pluck="instructor")
if doc.owner != topic.owner:
users.append(topic.owner)
users += instructors
subject = _("New reply on the topic {0} in course {1}").format(topic.title, course_title)
link = get_lesson_url(course, get_lesson_index(topic.reference_docname))
subject, link, users = get_course_details_for_notification(topic)
else:
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
subject = _("New comment in batch {0}").format(batch_title)
link = f"/batches/{topic.reference_docname}"
moderators = frappe.get_all("Has Role", {"role": "Moderator"}, pluck="parent")
users += moderators
subject, link, users = get_batch_details_for_notification(topic)
if doc.owner in users:
users.remove(doc.owner)
notification = frappe._dict(
{
@@ -425,7 +446,6 @@ def create_notification_log(doc, topic):
"email_content": doc.reply,
"document_type": topic.reference_doctype,
"document_name": topic.reference_docname,
"for_user": topic.owner,
"from_user": doc.owner,
"type": "Alert",
"link": link,
@@ -444,12 +464,16 @@ def notify_mentions_on_portal(doc, topic):
if topic.reference_doctype == "Course Lesson":
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
subject = _("{0} mentioned you in a comment in {1}").format(from_user_name, topic.title)
subject = _("{0} mentioned you in a comment in {1}").format(
frappe.bold(from_user_name), frappe.bold(topic.title)
)
link = get_lesson_url(course, get_lesson_index(topic.reference_docname))
else:
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
subject = _("{0} mentioned you in a comment in {1}").format(from_user_name, batch_title)
link = f"/batches/{topic.reference_docname}"
subject = _("{0} mentioned you in a comment in {1}").format(
frappe.bold(from_user_name), frappe.bold(batch_title)
)
link = f"/lms/batches/{topic.reference_docname}#discussions"
for user in mentions:
notification = frappe._dict(
@@ -460,7 +484,7 @@ def notify_mentions_on_portal(doc, topic):
"document_name": topic.reference_docname,
"for_user": user,
"from_user": doc.owner,
"type": "Alert",
"type": "Mention",
"link": link,
}
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
<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 batch 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; font-size: 15px;">
{{ title }}
</div>
<div>
{{ short_introduction }}
</div>
<div style="margin-top: 20px; font-size: 13px;">
{% if end_date %}
<span>
{{ _("From ") }} {{ frappe.utils.format_date(start_date, "dd MMM YYYY") }} {{ _(" to ") }} {{ frappe.utils.format_date(end_date, "dd MMM YYYY") }}
</span>
{% else %}
<span>
{{ frappe.utils.format_date(start_date, "dd MMM YYYY") }}
</span>
{% endif %}
</div>
<div style="color: #525252; margin-top: 4px; font-size: 13px;">
<span>
{{ _("Time: ") }} {{ frappe.utils.format_time(start_time, "HH:mm a") }} {{ timezone }}
</span>
</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="{{ batch_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 batch") }}
</a>
</div>

View 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>