Merge branch 'frappe:develop' into fix/persona

This commit is contained in:
Raizaaa
2026-03-05 13:49:12 +05:30
committed by GitHub
21 changed files with 251 additions and 334 deletions

View File

@@ -60,7 +60,7 @@ declare module 'vue' {
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
GoogleMeetAccountModal: typeof import('./src/components/Modals/GoogleMeetAccountModal.vue')['default']
GoogleMeetAccountModal: typeof import('./src/components/Settings/GoogleMeetAccountModal.vue')['default']
GoogleMeetSettings: typeof import('./src/components/Settings/GoogleMeetSettings.vue')['default']
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']

View File

@@ -3,13 +3,13 @@
<div class="space-y-2">
<div class="flex items-center text-sm font-medium space-x-2">
<span>
{{ __('What does include in preview mean?') }}
{{ __('What are Instructor Notes?') }}
</span>
</div>
<div class="text-xs text-ink-gray-5 mb-1 leading-5">
{{
__(
'If Include in Preview is enabled for a lesson then the lesson will also be accessible to non logged in users.'
'Instructor Notes are private notes that only instructors can see. They can be used to provide additional context or guidance for the lesson.'
)
}}
</div>

View File

@@ -16,7 +16,12 @@
>
<template #body-content>
<div class="space-y-4 text-base">
<FormControl label="Title" v-model="chapter.title" :required="true" />
<FormControl
label="Title"
v-model="chapter.title"
:required="true"
autocomplete="off"
/>
<Switch
size="sm"
:label="__('SCORM Package')"

View File

@@ -1,9 +1,14 @@
<template>
<div class="flex flex-col h-full">
<div class="flex flex-col h-full text-p-base">
<div>
<div class="flex items-center justify-between">
<div class="font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
<div class="space-y-2">
<div class="font-semibold text-xl text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<div class="space-x-2">
<Badge
@@ -21,9 +26,6 @@
</Button>
</div>
</div>
<div class="text-xs text-ink-gray-5">
{{ __(description) }}
</div>
</div>
<div class="overflow-y-auto">
<SettingFields :sections="sections" :data="branding.data" />

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
@@ -10,7 +10,7 @@
</div>
</div>
<div class="flex item-center space-x-2">
<Button variant="solid" @click="() => (showForm = !showForm)">
<Button @click="() => (showForm = !showForm)">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
@@ -10,7 +10,7 @@
</div>
</div>
<div class="flex item-center space-x-2">
<Button variant="solid" @click="() => (showForm = !showForm)">
<Button @click="() => (showForm = !showForm)">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">

View File

@@ -2,10 +2,13 @@
<div class="flex flex-col h-full text-base overflow-y-hidden">
<div class="">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<div class="flex flex-col space-y-2">
<div class="text-xl font-semibold leading-none text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<div class="space-x-2">
<Badge
@@ -19,9 +22,6 @@
</Button>
</div>
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<SettingFields :sections="sections" :data="data.doc" />

View File

@@ -269,41 +269,6 @@ const tabsStructure = computed(() => {
},
],
},
],
},
{
label: 'Lists',
hideLabel: false,
items: [
{
label: 'Members',
description:
'Add new members or manage roles and permissions of existing members',
icon: 'UserRoundPlus',
template: markRaw(Members),
},
{
label: 'Evaluators',
description: '',
icon: 'UserCheck',
description:
'Add new evaluators or check the slots existing evaluators',
template: markRaw(Evaluators),
},
{
label: 'Zoom Accounts',
description:
'Manage zoom accounts to conduct live classes from batches',
icon: 'Video',
template: markRaw(ZoomSettings),
},
{
label: 'Google Meet Accounts',
description:
'Manage Google Meet accounts to conduct live classes from batches',
icon: 'Video',
template: markRaw(GoogleMeetSettings),
},
{
label: 'Badges',
description:
@@ -325,6 +290,27 @@ const tabsStructure = computed(() => {
},
],
},
{
label: 'Users',
hideLabel: false,
items: [
{
label: 'Members',
description:
'Add new members or manage roles and permissions of existing members',
icon: 'User',
template: markRaw(Members),
},
{
label: 'Evaluators',
description: '',
icon: 'UserCircle2',
description:
'Add new evaluators or check the slots of existing evaluators',
template: markRaw(Evaluators),
},
],
},
{
label: 'Payment',
hideLabel: false,
@@ -395,6 +381,26 @@ const tabsStructure = computed(() => {
},
],
},
{
label: 'Conferencing',
hideLabel: false,
items: [
{
label: 'Zoom',
description:
'Manage zoom accounts to conduct live classes from batches',
icon: 'Video',
template: markRaw(ZoomSettings),
},
{
label: 'Google Meet',
description:
'Manage Google Meet accounts to conduct live classes from batches',
icon: 'Presentation',
template: markRaw(GoogleMeetSettings),
},
],
},
{
label: 'Customize',
hideLabel: false,
@@ -402,6 +408,8 @@ const tabsStructure = computed(() => {
{
label: 'Branding',
icon: 'Blocks',
description:
'Customize the brand name and logo to make the application your own',
template: markRaw(BrandSettings),
sections: [
{
@@ -490,6 +498,8 @@ const tabsStructure = computed(() => {
{
label: 'Signup',
icon: 'LogIn',
description:
'Manage the settings related to user signup and registration',
sections: [
{
columns: [
@@ -525,6 +535,8 @@ const tabsStructure = computed(() => {
{
label: 'SEO',
icon: 'Search',
description:
'Manage the SEO settings to improve your website ranking on search engines',
sections: [
{
columns: [

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">

View File

@@ -507,11 +507,11 @@ const conferencingOptions = computed(() => {
const mediumOptions = computed(() => {
return [
{
label: 'Online',
label: __('Online'),
value: 'Online',
},
{
label: 'Offline',
label: __('Offline'),
value: 'Offline',
},
]

View File

@@ -8,81 +8,86 @@
>
<template #body-content>
<div class="text-base">
<div class="grid grid-cols-2 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.title"
:label="__('Title')"
:required="true"
/>
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
:required="true"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
:required="true"
/>
<Link
doctype="LMS Category"
v-model="batch.category"
:label="__('Category')"
:allowCreate="true"
:onCreate="
() => {
openSettings('Categories')
show = false
}
"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
:required="true"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
:required="true"
/>
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
:required="true"
/>
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
:required="false"
/>
</div>
<div class="grid grid-cols-3 gap-5">
<FormControl
v-model="batch.title"
:label="__('Title')"
:required="true"
/>
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
:required="true"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
:required="true"
/>
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
:required="true"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
:required="true"
/>
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
:required="true"
/>
<Link
doctype="LMS Category"
v-model="batch.category"
:label="__('Category')"
:allowCreate="true"
:onCreate="
() => {
openSettings('Categories')
show = false
}
"
/>
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
:required="false"
/>
<FormControl
v-model="batch.medium"
type="select"
:options="mediumOptions"
:label="__('Medium')"
class="mb-4"
/>
</div>
<div class="space-y-5 border-t mt-5 pt-5">
<MultiSelect
v-model="batch.instructors"
doctype="Course Evaluator"
:label="__('Instructors')"
:required="true"
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
/>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
:required="true"
:rows="4"
/>
<div class="grid grid-cols-2 gap-5">
<MultiSelect
v-model="batch.instructors"
doctype="Course Evaluator"
:label="__('Instructors')"
:required="true"
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
/>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
:required="true"
:rows="4"
/>
</div>
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Batch Details') }}
@@ -93,7 +98,7 @@
@change="(val: string) => (batch.batch_details = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem]"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[14rem] overflow-auto"
/>
</div>
</div>
@@ -111,7 +116,7 @@
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { ref, inject, onMounted, onBeforeUnmount } from 'vue'
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { cleanError, openSettings, sanitizeHTML, escapeHTML } from '@/utils'
import Link from '@/components/Controls/Link.vue'
@@ -139,6 +144,7 @@ type Batch = {
instructors: string[]
category: string | null
seat_count: number
medium: string | null
}
const batch = ref<Batch>({
@@ -153,6 +159,7 @@ const batch = ref<Batch>({
instructors: [],
category: null,
seat_count: 0,
medium: null,
})
const validateFields = () => {
@@ -227,4 +234,17 @@ onBeforeUnmount(() => {
data: batch.value,
})
})
const mediumOptions = computed(() => {
return [
{
label: __('Online'),
value: 'Online',
},
{
label: __('Offline'),
value: 'Offline',
},
]
})
</script>

View File

@@ -13,6 +13,7 @@
v-model="course.title"
:label="__('Title')"
:required="true"
autocomplete="off"
/>
<Link
doctype="LMS Category"
@@ -57,7 +58,7 @@
@change="(val: string) => (course.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem]"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[17rem] overflow-auto"
/>
</div>
</div>
@@ -87,6 +88,7 @@ const router = useRouter()
const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const user = inject<any>('$user')
const courseCreated = ref(false)
const props = defineProps<{
courses: any
@@ -139,6 +141,7 @@ const saveCourse = (close: () => void = () => {}) => {
toast.success(__('Course created successfully'))
close()
capture('course_created')
courseCreated.value = true
router.push({
name: 'CourseDetail',
params: { courseName: data.name },
@@ -178,8 +181,10 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
capture('course_form_closed', {
data: course.value,
})
if (!courseCreated.value) {
capture('course_form_closed', {
data: course.value,
})
}
})
</script>

View File

@@ -15,17 +15,22 @@
</Button>
</header>
<div class="py-5">
<div class="w-5/6 mx-auto">
<div class="grid grid-cols-2 gap-5 w-5/6 mx-auto">
<FormControl
v-model="lesson.title"
label="Title"
:label="__('Title')"
class="mb-4"
:required="true"
autocomplete="off"
/>
<FormControl
<Switch
v-model="lesson.include_in_preview"
type="checkbox"
label="Include in Preview"
:label="__('Include in Preview')"
:description="
__(
'If enabled, the lesson will also be accessible to users who are not enrolled in the course.'
)
"
/>
</div>
<div class="border-t mt-4">
@@ -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 {

View File

@@ -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,

View File

@@ -1 +1 @@
__version__ = "2.44.0"
__version__ = "2.45.2"

View File

@@ -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()

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(

View File

@@ -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)

View File

@@ -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",