Merge branch 'frappe:develop' into fix/payment

This commit is contained in:
Raizaaa
2026-03-05 02:07:41 +05:30
committed by GitHub
21 changed files with 251 additions and 334 deletions
+1 -1
View File
@@ -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()
+55 -101
View File
@@ -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",