feat: zoom attendance
This commit is contained in:
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -65,6 +65,7 @@ declare module 'vue' {
|
|||||||
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
|
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
|
||||||
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
||||||
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
|
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
|
||||||
|
LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default']
|
||||||
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
|
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
|
||||||
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
|
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
|
||||||
Members: typeof import('./src/components/Settings/Members.vue')['default']
|
Members: typeof import('./src/components/Settings/Members.vue')['default']
|
||||||
|
|||||||
@@ -22,10 +22,18 @@
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="liveClasses.data?.length" class="grid grid-cols-2 gap-5 mt-5">
|
<div v-if="liveClasses.data?.length" class="grid grid-cols-3 gap-5 mt-5">
|
||||||
<div
|
<div
|
||||||
v-for="cls in liveClasses.data"
|
v-for="cls in liveClasses.data"
|
||||||
class="flex flex-col border rounded-md h-full text-ink-gray-7 hover:border-outline-gray-3 p-3"
|
class="flex flex-col border rounded-md h-full text-ink-gray-7 hover:border-outline-gray-3 p-3"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer': hasPermission() && cls.attendees > 0,
|
||||||
|
}"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
openAttendanceModal(cls)
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||||
{{ cls.title }}
|
{{ cls.title }}
|
||||||
@@ -33,7 +41,7 @@
|
|||||||
<div class="short-introduction">
|
<div class="short-introduction">
|
||||||
{{ cls.description }}
|
{{ cls.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="mt-auto space-y-3">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Calendar class="w-4 h-4 stroke-1.5" />
|
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
@@ -43,11 +51,12 @@
|
|||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Clock class="w-4 h-4 stroke-1.5" />
|
<Clock class="w-4 h-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatTime(cls.time) }}
|
{{ formatTime(cls.time) }} -
|
||||||
|
{{ dayjs(getClassEnd(cls)).format('HH:mm') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="cls.date >= dayjs().format('YYYY-MM-DD')"
|
v-if="canAccessClass(cls)"
|
||||||
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -69,12 +78,18 @@
|
|||||||
{{ __('Join') }}
|
{{ __('Join') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center space-x-2 text-yellow-700">
|
<Tooltip
|
||||||
<Info class="w-4 h-4 stroke-1.5" />
|
v-else-if="hasClassEnded(cls)"
|
||||||
<span>
|
:text="__('This class has ended')"
|
||||||
{{ __('This class has ended') }}
|
placement="right"
|
||||||
</span>
|
>
|
||||||
</div>
|
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
|
||||||
|
<Info class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ __('Ended') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,9 +103,11 @@
|
|||||||
v-model="showLiveClassModal"
|
v-model="showLiveClassModal"
|
||||||
v-model:reloadLiveClasses="liveClasses"
|
v-model:reloadLiveClasses="liveClasses"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LiveClassAttendance v-model="showAttendance" :live_class="attendanceFor" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createListResource, Button } from 'frappe-ui'
|
import { createListResource, Button, Tooltip } from 'frappe-ui'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -101,13 +118,16 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
|
|
||||||
import { formatTime } from '@/utils/'
|
import { formatTime } from '@/utils/'
|
||||||
|
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
|
||||||
|
import LiveClassAttendance from '@/components/Modals/LiveClassAttendance.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showLiveClassModal = ref(false)
|
const showLiveClassModal = ref(false)
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
|
const showAttendance = ref(false)
|
||||||
|
const attendanceFor = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
@@ -127,6 +147,8 @@ const liveClasses = createListResource({
|
|||||||
'description',
|
'description',
|
||||||
'time',
|
'time',
|
||||||
'date',
|
'date',
|
||||||
|
'duration',
|
||||||
|
'attendees',
|
||||||
'start_url',
|
'start_url',
|
||||||
'join_url',
|
'join_url',
|
||||||
'owner',
|
'owner',
|
||||||
@@ -148,6 +170,31 @@ const canCreateClass = () => {
|
|||||||
const hasPermission = () => {
|
const hasPermission = () => {
|
||||||
return user.data?.is_moderator || user.data?.is_evaluator
|
return user.data?.is_moderator || user.data?.is_evaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canAccessClass = (cls) => {
|
||||||
|
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
|
||||||
|
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
|
||||||
|
if (hasClassEnded(cls)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getClassEnd = (cls) => {
|
||||||
|
const classStart = new Date(`${cls.date}T${cls.time}`)
|
||||||
|
return new Date(classStart.getTime() + cls.duration * 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasClassEnded = (cls) => {
|
||||||
|
const classEnd = getClassEnd(cls)
|
||||||
|
const now = new Date()
|
||||||
|
return now > classEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAttendanceModal = (cls) => {
|
||||||
|
if (!hasPermission()) return
|
||||||
|
if (cls.attendees <= 0) return
|
||||||
|
showAttendance.value = true
|
||||||
|
attendanceFor.value = cls
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.short-introduction {
|
.short-introduction {
|
||||||
|
|||||||
91
frontend/src/components/Modals/LiveClassAttendance.vue
Normal file
91
frontend/src/components/Modals/LiveClassAttendance.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Attendance for Class - {0}').format(live_class?.title),
|
||||||
|
size: 'xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div
|
||||||
|
v-for="participant in participants.data"
|
||||||
|
@click="redirectToProfile(participant.member_username)"
|
||||||
|
class="cursor-pointer text-base w-fit"
|
||||||
|
>
|
||||||
|
<Tooltip placement="right">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Avatar
|
||||||
|
:image="participant.member_image"
|
||||||
|
:label="participant.member_name"
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ participant.member_name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ participant.member }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="max-w-[30ch] rounded bg-surface-gray-7 px-2 py-1 text-p-xs text-ink-white leading-5 shadow-xl"
|
||||||
|
>
|
||||||
|
{{ dayjs(participant.joined_at).format('HH:mm a') }} -
|
||||||
|
{{ dayjs(participant.left_at).format('HH:mm a') }}
|
||||||
|
<br />
|
||||||
|
{{ __('attended for') }} {{ participant.duration }}
|
||||||
|
{{ __('minutes') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Avatar, createListResource, Dialog, Tooltip } from 'frappe-ui'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const router = useRouter()
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
interface LiveClass {
|
||||||
|
name: String
|
||||||
|
title: String
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
live_class: LiveClass | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const participants = createListResource({
|
||||||
|
doctype: 'LMS Live Class Participant',
|
||||||
|
filter: {
|
||||||
|
live_class: props.live_class?.name,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'member',
|
||||||
|
'member_name',
|
||||||
|
'member_image',
|
||||||
|
'member_username',
|
||||||
|
'joined_at',
|
||||||
|
'left_at',
|
||||||
|
'duration',
|
||||||
|
],
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirectToProfile = (username: string) => {
|
||||||
|
router.push({
|
||||||
|
name: 'Profile',
|
||||||
|
params: { username },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,14 +16,29 @@
|
|||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div class="space-y-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
v-model="liveClass.title"
|
v-model="liveClass.title"
|
||||||
:label="__('Title')"
|
:label="__('Title')"
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="liveClass.date"
|
||||||
|
type="date"
|
||||||
|
:label="__('Date')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Tooltip :text="__('Duration of the live class in minutes')">
|
||||||
|
<FormControl
|
||||||
|
type="number"
|
||||||
|
v-model="liveClass.duration"
|
||||||
|
:label="__('Duration')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
:text="
|
:text="
|
||||||
__(
|
__(
|
||||||
@@ -35,7 +50,6 @@
|
|||||||
v-model="liveClass.time"
|
v-model="liveClass.time"
|
||||||
type="time"
|
type="time"
|
||||||
:label="__('Time')"
|
:label="__('Time')"
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -52,24 +66,6 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
|
||||||
v-model="liveClass.date"
|
|
||||||
type="date"
|
|
||||||
class="mb-4"
|
|
||||||
:label="__('Date')"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
<Tooltip :text="__('Duration of the live class in minutes')">
|
|
||||||
<FormControl
|
|
||||||
type="number"
|
|
||||||
v-model="liveClass.duration"
|
|
||||||
:label="__('Duration')"
|
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="liveClass.auto_recording"
|
v-model="liveClass.auto_recording"
|
||||||
type="select"
|
type="select"
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ scheduler_events = {
|
|||||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||||
"lms.lms.api.update_course_statistics",
|
"lms.lms.api.update_course_statistics",
|
||||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.mark_eval_as_completed",
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.mark_eval_as_completed",
|
||||||
|
"lms.lms.doctype.lms_live_class.lms_live_class.update_attendance",
|
||||||
],
|
],
|
||||||
"daily": [
|
"daily": [
|
||||||
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ def create_live_class(
|
|||||||
"doctype": "LMS Live Class",
|
"doctype": "LMS Live Class",
|
||||||
"start_url": data.get("start_url"),
|
"start_url": data.get("start_url"),
|
||||||
"join_url": data.get("join_url"),
|
"join_url": data.get("join_url"),
|
||||||
|
"meeting_id": data.get("id"),
|
||||||
|
"uuid": data.get("uuid"),
|
||||||
"title": title,
|
"title": title,
|
||||||
"host": frappe.session.user,
|
"host": frappe.session.user,
|
||||||
"date": date,
|
"date": date,
|
||||||
|
|||||||
@@ -20,8 +20,13 @@
|
|||||||
"description",
|
"description",
|
||||||
"column_break_spvt",
|
"column_break_spvt",
|
||||||
"event",
|
"event",
|
||||||
"password",
|
|
||||||
"auto_recording",
|
"auto_recording",
|
||||||
|
"section_break_fhet",
|
||||||
|
"meeting_id",
|
||||||
|
"uuid",
|
||||||
|
"column_break_aony",
|
||||||
|
"attendees",
|
||||||
|
"password",
|
||||||
"section_break_yrpq",
|
"section_break_yrpq",
|
||||||
"start_url",
|
"start_url",
|
||||||
"column_break_yokr",
|
"column_break_yokr",
|
||||||
@@ -137,13 +142,42 @@
|
|||||||
"label": "Zoom Account",
|
"label": "Zoom Account",
|
||||||
"options": "LMS Zoom Settings",
|
"options": "LMS Zoom Settings",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "meeting_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Meeting ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "attendees",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Attendees",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fhet",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uuid",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "UUID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_aony",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2025-05-26 13:23:38.209187",
|
{
|
||||||
|
"link_doctype": "LMS Live Class Participant",
|
||||||
|
"link_fieldname": "live_class"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2025-05-27 14:44:35.679712",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Live Class",
|
"name": "LMS Live Class",
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
||||||
|
from lms.lms.doctype.lms_batch.lms_batch import authenticate
|
||||||
|
|
||||||
|
|
||||||
class LMSLiveClass(Document):
|
class LMSLiveClass(Document):
|
||||||
@@ -102,3 +105,56 @@ def send_mail(live_class, student):
|
|||||||
args=args,
|
args=args,
|
||||||
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_attendance():
|
||||||
|
past_live_classes = frappe.get_all(
|
||||||
|
"LMS Live Class",
|
||||||
|
{
|
||||||
|
"uuid": ["is", "set"],
|
||||||
|
"attendees": ["is", "not set"],
|
||||||
|
},
|
||||||
|
["name", "uuid", "zoom_account"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for live_class in past_live_classes:
|
||||||
|
attendance_data = get_attendance(live_class)
|
||||||
|
create_attendance(live_class, attendance_data)
|
||||||
|
update_attendees_count(live_class, attendance_data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_attendance(live_class):
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer " + authenticate(live_class.zoom_account),
|
||||||
|
"content-type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded_uuid = requests.utils.quote(live_class.uuid, safe="")
|
||||||
|
response = requests.get(
|
||||||
|
f"https://api.zoom.us/v2/past_meetings/{encoded_uuid}/participants", headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
frappe.throw(
|
||||||
|
_("Failed to fetch attendance data from Zoom for class {0}: {1}").format(
|
||||||
|
live_class, response.text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
return data.get("participants", [])
|
||||||
|
|
||||||
|
|
||||||
|
def create_attendance(live_class, data):
|
||||||
|
for participant in data:
|
||||||
|
doc = frappe.new_doc("LMS Live Class Participant")
|
||||||
|
doc.live_class = live_class.name
|
||||||
|
doc.member = participant.get("user_email")
|
||||||
|
doc.joined_at = participant.get("join_time")
|
||||||
|
doc.left_at = participant.get("leave_time")
|
||||||
|
doc.duration = participant.get("duration")
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def update_attendees_count(live_class, data):
|
||||||
|
frappe.db.set_value("LMS Live Class", live_class.name, "attendees", len(data))
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2025, Frappe and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("LMS Live Class Participant", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-05-27 12:09:57.712221",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"live_class",
|
||||||
|
"joined_at",
|
||||||
|
"column_break_dwbm",
|
||||||
|
"duration",
|
||||||
|
"left_at",
|
||||||
|
"section_break_xczy",
|
||||||
|
"member",
|
||||||
|
"member_name",
|
||||||
|
"column_break_bpjn",
|
||||||
|
"member_image",
|
||||||
|
"member_username"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "live_class",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Live Class",
|
||||||
|
"options": "LMS Live Class",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "member",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Member",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.full_name",
|
||||||
|
"fieldname": "member_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Member Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_dwbm",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "duration",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Duration",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "joined_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Joined At",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "left_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Left At",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xczy",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bpjn",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.user_image",
|
||||||
|
"fieldname": "member_image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Member Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.username",
|
||||||
|
"fieldname": "member_username",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Member Username"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-05-27 22:32:24.196643",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Live Class Participant",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "member_name"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2025, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LMSLiveClassParticipant(Document):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2025, Frappe and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestLMSLiveClassParticipant(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for LMSLiveClassParticipant.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestLMSLiveClassParticipant(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for LMSLiveClassParticipant.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user