mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge branch 'frappe:develop' into fix/payment
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "2.44.0"
|
||||
__version__ = "2.45.2"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user