+
-
@@ -83,6 +88,7 @@ import {
Button,
createResource,
FormControl,
+ Switch,
usePageMeta,
toast,
} from 'frappe-ui'
@@ -708,8 +714,8 @@ iframe {
height: 15px;
}
-.ce-popover--opened > .ce-popover__container {
- max-height: unset;
+.ce-popover--opened {
+ max-height: unset !important;
}
.cdx-search-field__icon svg {
diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js
index 989f0f2a..01026cfd 100644
--- a/frontend/src/utils/index.js
+++ b/frontend/src/utils/index.js
@@ -126,6 +126,7 @@ export function getEditorTools() {
defaultStyle: 'ordered',
},
},
+ upload: Upload,
table: {
class: Table,
inlineToolbar: true,
@@ -133,7 +134,6 @@ export function getEditorTools() {
quiz: Quiz,
assignment: Assignment,
program: Program,
- upload: Upload,
markdown: {
class: Markdown,
inlineToolbar: true,
diff --git a/lms/__init__.py b/lms/__init__.py
index a6a5b127..79387d42 100644
--- a/lms/__init__.py
+++ b/lms/__init__.py
@@ -1 +1 @@
-__version__ = "2.44.0"
+__version__ = "2.45.2"
diff --git a/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py b/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py
index 69b99356..4376fe6a 100644
--- a/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py
+++ b/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py
@@ -199,8 +199,6 @@ def create_event(evaluation: dict):
"subject": f"Evaluation of {evaluation.member_name}",
"starts_on": f"{evaluation.date} {evaluation.start_time}",
"ends_on": f"{evaluation.date} {evaluation.end_time}",
- "reference_doctype": "LMS Certificate Request",
- "reference_docname": evaluation.name,
}
)
event.save()
diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.py b/lms/lms/doctype/lms_live_class/lms_live_class.py
index 6d1554ac..cea7d79d 100644
--- a/lms/lms/doctype/lms_live_class/lms_live_class.py
+++ b/lms/lms/doctype/lms_live_class/lms_live_class.py
@@ -14,10 +14,7 @@ from lms.lms.doctype.lms_batch.lms_batch import authenticate
class LMSLiveClass(Document):
def after_insert(self):
- if self.conferencing_provider == "Google Meet":
- self._create_google_meet_event()
- else:
- self._create_calendar_event()
+ self.create_calendar_event()
def on_update(self):
if not self.event:
@@ -33,13 +30,11 @@ class LMSLiveClass(Document):
self._update_linked_event()
- def on_trash(self):
- if self.event and frappe.db.exists("Event", self.event):
- event_name = self.event
- frappe.db.set_value("LMS Live Class", self.name, "event", "")
- frappe.delete_doc("Event", event_name, ignore_permissions=True)
+ def after_delete(self):
+ if self.event:
+ frappe.delete_doc("Event", self.event, force=True)
- def _get_participants(self):
+ def get_participants(self):
participants = frappe.get_all("LMS Batch Enrollment", {"batch": self.batch_name}, pluck="member")
instructors = frappe.get_all(
"Course Instructor", {"parenttype": "LMS Batch", "parent": self.batch_name}, pluck="instructor"
@@ -48,12 +43,12 @@ class LMSLiveClass(Document):
participants.extend(instructors)
return list(set(participants))
- def _build_event_description(self):
+ def build_event_description(self):
description = f"A Live Class has been scheduled on {format_date(self.date, 'medium')} at {format_time(self.time, 'hh:mm a')}."
if self.join_url:
- description += f" Click on this link to join. {self.join_url}."
+ description += f" Click on this link to join. {self.join_url}. \n\n"
if self.description:
- description += f" {self.description}"
+ description += f"{self.description}"
return description
def _update_linked_event(self):
@@ -63,99 +58,41 @@ class LMSLiveClass(Document):
event.subject = f"Live Class on {self.title}"
event.starts_on = start
event.ends_on = get_datetime(start) + timedelta(minutes=cint(self.duration))
- event.description = self._build_event_description()
+ event.description = self.build_event_description()
event.save(ignore_permissions=True)
- def _create_calendar_event(self):
- calendar = frappe.db.get_value("Google Calendar", {"user": frappe.session.user, "enable": 1}, "name")
+ def create_calendar_event(self):
+ if self.conferencing_provider == "Google Meet":
+ calendar = frappe.db.get_value(
+ "LMS Google Meet Settings", self.google_meet_account, "google_calendar"
+ )
+ else:
+ calendar = frappe.db.get_value(
+ "Google Calendar", {"user": frappe.session.user, "enable": 1}, "name"
+ )
+
+ if not calendar:
+ frappe.throw(
+ _(
+ "No calendar is configured for the conferencing provider. Please set up a calendar to create events."
+ )
+ )
if calendar:
event = self.create_event()
- self.add_event_participants(event, calendar)
frappe.db.set_value(self.doctype, self.name, "event", event.name)
+ self.add_event_participants(event, calendar)
+ self.sync_with_google_calendar(event, calendar)
- def _create_google_meet_event(self):
- google_meet_settings = frappe.get_doc("LMS Google Meet Settings", self.google_meet_account)
- calendar = google_meet_settings.google_calendar
-
- if not calendar:
- frappe.throw(_("Google Calendar is not configured for this Google Meet account."))
-
- event = self.create_event()
-
- event.reload()
- event.update(
- {
- "sync_with_google_calendar": 1,
- "add_video_conferencing": 1,
- "google_calendar": calendar,
- "description": self._build_event_description(),
- }
- )
- event.save()
- event.reload()
-
- meet_link = event.google_meet_link
- frappe.db.set_value(
- self.doctype,
- self.name,
- {
- "event": event.name,
- "join_url": meet_link or "",
- "start_url": meet_link or "",
- },
- )
-
- if not meet_link:
- frappe.msgprint(
- _(
- "The Meet link is not yet available. It will be generated once Google Calendar syncs the event. Please refresh the page after a few moments."
- ),
- indicator="orange",
- alert=True,
- )
-
- self._add_google_meet_participants(event, calendar)
-
- def _add_google_meet_participants(self, event, calendar):
- from frappe.integrations.doctype.google_calendar.google_calendar import get_google_calendar_object
-
- attendees = []
- for participant in self._get_participants():
- email = frappe.db.get_value("User", participant, "email")
- if not email:
- continue
- attendees.append({"email": email})
-
- if not attendees:
- return
-
- try:
- google_calendar_api, account = get_google_calendar_object(calendar)
- google_calendar_api.events().patch(
- calendarId=event.google_calendar_id,
- eventId=event.google_calendar_event_id,
- body={
- "attendees": attendees,
- "guestsCanSeeOtherGuests": False,
- },
- sendUpdates="all",
- ).execute()
- except Exception:
- frappe.log_error(title=_("Google Meet - Failed to add participants to calendar event"))
- frappe.msgprint(
- _(
- "Live class was created but calendar invites could not be sent to participants. You may need to share the Meet link manually."
- ),
- indicator="orange",
- alert=True,
- )
+ if self.conferencing_provider == "Google Meet":
+ self.add_video_conferencing_to_event(event)
def create_event(self):
start = f"{self.date} {self.time}"
- event = frappe.get_doc(
+ event = frappe.new_doc("Event")
+ event.update(
{
"doctype": "Event",
"subject": f"Live Class on {self.title}",
@@ -164,11 +101,12 @@ class LMSLiveClass(Document):
"ends_on": get_datetime(start) + timedelta(minutes=cint(self.duration)),
}
)
+
event.save()
return event
def add_event_participants(self, event, calendar, add_video_conferencing=False):
- for participant in self._get_participants():
+ for participant in self.get_participants():
frappe.get_doc(
{
"doctype": "Event Participants",
@@ -181,20 +119,36 @@ class LMSLiveClass(Document):
}
).save()
+ def sync_with_google_calendar(self, event, calendar):
event.reload()
-
update_data = {
"sync_with_google_calendar": 1,
"google_calendar": calendar,
- "description": self._build_event_description(),
+ "description": self.build_event_description(),
}
-
- if add_video_conferencing:
- update_data["add_video_conferencing"] = 1
-
event.update(update_data)
event.save()
+ def add_video_conferencing_to_event(self, event):
+ event.reload()
+ event.update(
+ {
+ "add_video_conferencing": 1,
+ }
+ )
+ event.save()
+ event.reload()
+ google_meet_link = event.google_meet_link
+ if google_meet_link:
+ frappe.db.set_value(
+ self.doctype,
+ self.name,
+ {
+ "start_url": google_meet_link,
+ "join_url": google_meet_link,
+ },
+ )
+
def send_live_class_reminder():
classes = frappe.get_all(
diff --git a/lms/lms/doctype/lms_live_class/test_lms_live_class.py b/lms/lms/doctype/lms_live_class/test_lms_live_class.py
index 5351db79..269b8f94 100644
--- a/lms/lms/doctype/lms_live_class/test_lms_live_class.py
+++ b/lms/lms/doctype/lms_live_class/test_lms_live_class.py
@@ -129,13 +129,6 @@ class TestLMSLiveClass(BaseTestUtils):
self.assertEqual(event.sync_with_google_calendar, 1)
self.assertEqual(event.add_video_conferencing, 1)
self.assertEqual(event.google_calendar, self.google_calendar.name)
-
- def test_google_meet_live_class_event_has_correct_times(self):
- """The linked Event should have correct start and end times."""
- live_class = self._create_live_class()
- live_class.reload()
-
- event = frappe.get_doc("Event", live_class.event)
self.assertIn("10:00", str(event.starts_on))
self.assertIn("11:00", str(event.ends_on))
@@ -183,31 +176,6 @@ class TestLMSLiveClass(BaseTestUtils):
self.google_meet_settings.google_calendar = old_calendar
self.google_meet_settings.save()
- def test_zoom_live_class_not_affected(self):
- """Creating a Zoom-style live class should still work (regression test)."""
- live_class = frappe.get_doc(
- {
- "doctype": "LMS Live Class",
- "title": f"Zoom Class {frappe.generate_hash(length=6)}",
- "host": "Administrator",
- "date": add_days(nowdate(), 1),
- "time": "14:00:00",
- "duration": 45,
- "timezone": "Asia/Kolkata",
- "batch_name": self.batch.name,
- "conferencing_provider": "Zoom",
- "join_url": "https://zoom.us/j/123456",
- "start_url": "https://zoom.us/s/123456",
- }
- )
- live_class.insert(ignore_permissions=True)
- self.cleanup_items.append(("LMS Live Class", live_class.name))
-
- self.assertTrue(frappe.db.exists("LMS Live Class", live_class.name))
- self.assertEqual(live_class.join_url, "https://zoom.us/j/123456")
-
- # --- T10: Unit tests for event update and cancellation sync ---
-
def test_update_live_class_date_updates_event(self):
"""Rescheduling a live class should update the linked Event."""
live_class = self._create_live_class()
@@ -270,40 +238,8 @@ class TestLMSLiveClass(BaseTestUtils):
(t, n) for t, n in self.cleanup_items if not (t == "LMS Live Class" and n == live_class.name)
]
frappe.delete_doc("LMS Live Class", live_class.name, force=True)
-
self.assertFalse(frappe.db.exists("Event", event_name))
- def test_delete_zoom_live_class_with_event(self):
- """Deleting a Zoom live class with a linked event should also delete the event (regression)."""
- live_class = self._create_live_class(provider="Zoom")
- # Zoom classes created via direct insert won't have an event from calendar flow,
- # but if one is set manually, on_trash should clean it up
- event = frappe.get_doc(
- {
- "doctype": "Event",
- "subject": "Test Zoom Event",
- "event_type": "Public",
- "starts_on": f"{add_days(nowdate(), 1)} 14:00:00",
- "ends_on": f"{add_days(nowdate(), 1)} 15:00:00",
- }
- )
- event.insert(ignore_permissions=True)
- self.cleanup_items.append(("Event", event.name))
-
- frappe.db.set_value("LMS Live Class", live_class.name, "event", event.name)
- live_class.reload()
-
- self.cleanup_items = [
- (t, n) for t, n in self.cleanup_items if not (t == "LMS Live Class" and n == live_class.name)
- ]
- # Remove event from cleanup too since on_trash will delete it
- self.cleanup_items = [(t, n) for t, n in self.cleanup_items if not (t == "Event" and n == event.name)]
- frappe.delete_doc("LMS Live Class", live_class.name, force=True)
-
- self.assertFalse(frappe.db.exists("Event", event.name))
-
- # --- T11: Integration tests for end-to-end workflow ---
-
def test_batch_validation_google_meet_without_account(self):
"""Saving a batch with Google Meet provider but no account should fail."""
self.batch.conferencing_provider = "Google Meet"
@@ -311,7 +247,6 @@ class TestLMSLiveClass(BaseTestUtils):
with self.assertRaises(frappe.exceptions.ValidationError):
self.batch.save()
- # Reset
self.batch.reload()
def test_batch_validation_google_meet_with_valid_account(self):
@@ -324,7 +259,6 @@ class TestLMSLiveClass(BaseTestUtils):
self.assertEqual(self.batch.conferencing_provider, "Google Meet")
self.assertEqual(self.batch.google_meet_account, self.google_meet_settings.name)
- # Reset
self.batch.conferencing_provider = ""
self.batch.google_meet_account = ""
self.batch.save()
@@ -335,23 +269,4 @@ class TestLMSLiveClass(BaseTestUtils):
self.batch.zoom_account = ""
with self.assertRaises(frappe.exceptions.ValidationError):
self.batch.save()
-
- # Reset
self.batch.reload()
-
- def test_update_attendance_skips_google_meet(self):
- """The Zoom attendance scheduler should skip Google Meet classes."""
- live_class = self._create_live_class()
- live_class.reload()
-
- # The update_attendance function uses conferencing_provider != "Google Meet"
- # to filter out Google Meet classes from Zoom attendance processing.
- # Verify a Google Meet class is excluded by that filter.
- past_classes = frappe.get_all(
- "LMS Live Class",
- {
- "conferencing_provider": ["!=", "Google Meet"],
- },
- pluck="name",
- )
- self.assertNotIn(live_class.name, past_classes)
diff --git a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json
index de030560..86771e04 100644
--- a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json
+++ b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json
@@ -118,7 +118,7 @@
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-25 12:36:40.110346",
- "modified_by": "sayali@frappe.io",
+ "modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Submission",
"owner": "Administrator",