chore: merged conflicts

This commit is contained in:
Jannat Patel
2025-01-10 11:03:44 +05:30
22 changed files with 383 additions and 93 deletions

View File

@@ -11,7 +11,7 @@
{{ __('Add') }} {{ __('Add') }}
</Button> </Button>
</div> </div>
<div v-if="assessments.data?.length"> <div v-if="assessments.data?.length" class="text-sm">
<ListView <ListView
:columns="getAssessmentColumns()" :columns="getAssessmentColumns()"
:rows="assessments.data" :rows="assessments.data"

View File

@@ -1,17 +1,18 @@
<template> <template>
<div> <div class="space-y-10">
<UpcomingEvaluations <UpcomingEvaluations
:batch="batch.data.name" :batch="batch.data.name"
:endDate="batch.data.evaluation_end_date" :endDate="batch.data.evaluation_end_date"
:courses="batch.data.courses" :courses="batch.data.courses"
:isStudent="isStudent"
/> />
<Assessments :batch="batch.data.name" /> <Assessments :batch="batch.data.name" />
<StudentHeatmap />
</div> </div>
</template> </template>
<script setup> <script setup>
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue' import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
import Assessments from '@/components/Assessments.vue' import Assessments from '@/components/Assessments.vue'
import StudentHeatmap from '@/components/StudentHeatmap.vue'
const props = defineProps({ const props = defineProps({
batch: { batch: {

View File

@@ -8,10 +8,10 @@
<div class="grid grid-cols-3 gap-5 mb-8"> <div class="grid grid-cols-3 gap-5 mb-8">
<div class="flex items-center shadow py-2 px-3 rounded-md"> <div class="flex items-center shadow py-2 px-3 rounded-md">
<div class="p-2 rounded-md bg-gray-100 mr-3"> <div class="p-2 rounded-md bg-gray-100 mr-3">
<User class="w-18 h-18 stroke-1.5 text-gray-700" /> <User class="w-5 h-5 stroke-1.5 text-gray-700" />
</div> </div>
<div class="flex flex-col"> <div class="flex items-center space-x-2">
<span class="text-xl font-semibold mb-1"> <span class="font-semibold">
{{ students.data?.length }} {{ students.data?.length }}
</span> </span>
<span class="text-gray-700"> <span class="text-gray-700">
@@ -22,10 +22,10 @@
<div class="flex items-center shadow py-2 px-3 rounded-md"> <div class="flex items-center shadow py-2 px-3 rounded-md">
<div class="p-2 rounded-md bg-gray-100 mr-3"> <div class="p-2 rounded-md bg-gray-100 mr-3">
<BookOpen class="w-18 h-18 stroke-1.5 text-gray-700" /> <BookOpen class="w-5 h-5 stroke-1.5 text-gray-700" />
</div> </div>
<div class="flex flex-col"> <div class="flex items-center space-x-2">
<span class="text-xl font-semibold mb-1"> <span class="font-semibold">
{{ batch.courses?.length }} {{ batch.courses?.length }}
</span> </span>
<span class="text-gray-700"> <span class="text-gray-700">
@@ -36,10 +36,10 @@
<div class="flex items-center shadow py-2 px-3 rounded-md"> <div class="flex items-center shadow py-2 px-3 rounded-md">
<div class="p-2 rounded-md bg-gray-100 mr-3"> <div class="p-2 rounded-md bg-gray-100 mr-3">
<ShieldCheck class="w-18 h-18 stroke-1.5 text-gray-700" /> <ShieldCheck class="w-5 h-5 stroke-1.5 text-gray-700" />
</div> </div>
<div class="flex flex-col"> <div class="flex items-center space-x-2">
<span class="text-xl font-semibold mb-1"> <span class="font-semibold">
{{ assessmentCount }} {{ assessmentCount }}
</span> </span>
<span class="text-gray-700"> <span class="text-gray-700">
@@ -48,28 +48,33 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-8"> <div v-if="showProgressChart" class="mb-8">
<div class="text-gray-600 font-medium"> <div class="text-gray-600 font-medium">
{{ __('Progress') }} {{ __('Progress') }}
</div> </div>
<ApexChart <ApexChart
v-if="showProgressChart"
:options="chartOptions" :options="chartOptions"
:series="chartData" :series="chartData"
type="bar" type="bar"
height="350" height="200"
/> />
<div <div
class="flex items-center justify-center text-sm text-gray-700 space-x-4" class="flex items-center justify-center text-sm text-gray-700 space-x-4"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="w-3 h-3" style="background-color: #0f736b"></div> <div
class="w-3 h-3 rounded-sm"
:style="{ 'background-color': theme.colors.green[600] }"
></div>
<div> <div>
{{ __('Courses') }} {{ __('Courses') }}
</div> </div>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="w-3 h-3" style="background-color: #0070cc"></div> <div
class="w-3 h-3 rounded-sm"
:style="{ 'background-color': theme.colors.blue[600] }"
></div>
<div> <div>
{{ __('Assessments') }} {{ __('Assessments') }}
</div> </div>
@@ -147,16 +152,6 @@
<ProgressBar :progress="row[column.key]" size="sm" /> <ProgressBar :progress="row[column.key]" size="sm" />
<div class="text-xs">{{ row[column.key] }}%</div> <div class="text-xs">{{ row[column.key] }}%</div>
</div> </div>
<div
v-else-if="column.key == 'copy'"
class="invisible group-hover:visible"
>
<Button variant="ghost" @click="copyEmail(row)">
<template #icon>
<Clipboard class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
<div v-else> <div v-else>
{{ row[column.key] }} {{ row[column.key] }}
</div> </div>
@@ -221,6 +216,7 @@ import { showToast } from '@/utils'
import ProgressBar from '@/components/ProgressBar.vue' import ProgressBar from '@/components/ProgressBar.vue'
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue' import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
import ApexChart from 'vue3-apexcharts' import ApexChart from 'vue3-apexcharts'
import { theme } from '@/utils/theme'
const showStudentModal = ref(false) const showStudentModal = ref(false)
const showStudentProgressModal = ref(false) const showStudentProgressModal = ref(false)
@@ -271,10 +267,6 @@ const getStudentColumns = () => {
align: 'center', align: 'center',
icon: 'clock', icon: 'clock',
}, },
{
label: '',
key: 'copy',
},
] ]
return columns return columns
@@ -357,8 +349,8 @@ const getChartData = () => {
} }
const getChartOptions = (categories) => { const getChartOptions = (categories) => {
const courseColor = '#0F736B' const courseColor = theme.colors.green[700]
const assessmentColor = '#0070CC' const assessmentColor = theme.colors.blue[700]
const maxY = const maxY =
students.data?.length % 5 students.data?.length % 5
? students.data?.length + (5 - (students.data?.length % 5)) ? students.data?.length + (5 - (students.data?.length % 5))
@@ -367,7 +359,6 @@ const getChartOptions = (categories) => {
return { return {
chart: { chart: {
type: 'bar', type: 'bar',
height: 50,
toolbar: { toolbar: {
show: false, show: false,
}, },
@@ -375,9 +366,10 @@ const getChartOptions = (categories) => {
plotOptions: { plotOptions: {
bar: { bar: {
distributed: true, distributed: true,
borderRadius: 0, borderRadius: 3,
borderRadiusApplication: 'end',
horizontal: true, horizontal: true,
barHeight: '30%', barHeight: '40%',
}, },
}, },
colors: Object.values(categories).map((item) => colors: Object.values(categories).map((item) =>
@@ -391,7 +383,7 @@ const getChartOptions = (categories) => {
}, },
rotate: 0, rotate: 0,
formatter: function (value) { formatter: function (value) {
return value.length > 20 ? `${value.substring(0, 20)}...` : value return value.length > 30 ? `${value.substring(0, 30)}...` : value
}, },
}, },
}, },
@@ -400,15 +392,11 @@ const getChartOptions = (categories) => {
min: 0, min: 0,
stepSize: 10, stepSize: 10,
tickAmount: maxY / 5, tickAmount: maxY / 5,
/* reversed: true */
}, },
} }
} }
const copyEmail = (row) => {
navigator.clipboard.writeText(row.email)
showToast(__('Success'), __('Email copied to clipboard'), 'check')
}
watch(students, () => { watch(students, () => {
if (students.data?.length) { if (students.data?.length) {
assessmentCount.value = Object.keys(students.data?.[0].assessments).length assessmentCount.value = Object.keys(students.data?.[0].assessments).length

View File

@@ -1,7 +1,12 @@
<template> <template>
<Dialog v-model="show" :options="{}"> <Dialog
v-model="show"
:options="{
size: 'xl',
}"
>
<template #body> <template #body>
<div class="p-5 space-y-8 text-base"> <div class="p-5 space-y-10 text-base">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Avatar :image="student.user_image" size="3xl" /> <Avatar :image="student.user_image" size="3xl" />
<div class="space-y-1"> <div class="space-y-1">
@@ -19,13 +24,11 @@
</div> </div>
</div> </div>
<!-- Assessments --> <div class="space-y-8">
<div> <!-- Assessments -->
<div> <div class="space-y-2 text-sm">
<div <div class="flex items-center border-b pb-1 font-medium">
class="grid grid-cols-[70%,30%] border-b pl-2 pb-1 mb-2 text-xs text-gray-700 font-medium" <span class="flex-1">
>
<span>
{{ __('Assessment') }} {{ __('Assessment') }}
</span> </span>
<span> <span>
@@ -34,9 +37,9 @@
</div> </div>
<div <div
v-for="assessment in Object.keys(student.assessments)" v-for="assessment in Object.keys(student.assessments)"
class="grid grid-cols-[70%,30%] pl-2 mb-2 text-gray-700 font-medium" class="flex items-center text-gray-700 font-medium"
> >
<span> <span class="flex-1">
{{ assessment }} {{ assessment }}
</span> </span>
<span v-if="isAssignment(student.assessments[assessment])"> <span v-if="isAssignment(student.assessments[assessment])">
@@ -49,15 +52,11 @@
</span> </span>
</div> </div>
</div> </div>
</div>
<!-- Courses --> <!-- Courses -->
<div> <div class="space-y-2 text-sm">
<div> <div class="flex items-center border-b pb-1 font-medium">
<div <span class="flex-1">
class="grid grid-cols-[70%,30%] mb-2 text-xs text-gray-700 border-b pl-2 pb-1 font-medium"
>
<span>
{{ __('Courses') }} {{ __('Courses') }}
</span> </span>
<span> <span>
@@ -66,9 +65,9 @@
</div> </div>
<div <div
v-for="course in Object.keys(student.courses)" v-for="course in Object.keys(student.courses)"
class="grid grid-cols-[70%,30%] pl-2 mb-2 text-gray-700 font-medium" class="flex items-center text-gray-700 font-medium"
> >
<span> <span class="flex-1">
{{ course }} {{ course }}
</span> </span>
<span> <span>
@@ -77,13 +76,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Heatmap -->
<StudentHeatmap :member="student.email" :base_days="120" />
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Avatar, Badge, Dialog } from 'frappe-ui' import { Avatar, Badge, Dialog } from 'frappe-ui'
import ProgressBar from '@/components/ProgressBar.vue' import StudentHeatmap from '@/components/StudentHeatmap.vue'
const show = defineModel() const show = defineModel()
const props = defineProps({ const props = defineProps({

View File

@@ -0,0 +1,138 @@
<template>
<div v-if="heatmap.data">
<div class="text-lg font-semibold mb-2">
{{ heatmap.data.total_activities }}
{{
heatmap.data.total_activities > 1 ? __('activities') : __('activity')
}}
{{ __('in the last') }}
{{ heatmap.data.weeks }}
{{ __('weeks') }}
</div>
<ApexChart :options="chartOptions" :series="chartSeries" height="240" />
</div>
</template>
<script setup>
import { createResource } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue'
import ApexChart from 'vue3-apexcharts'
import { theme } from '@/utils/theme'
const user = inject('$user')
const labels = ref([])
const memberName = ref(null)
const props = defineProps({
member: {
type: String,
},
base_days: {
type: Number,
default: 200,
},
})
onMounted(() => {
memberName.value = props.member || user.data?.name
})
const heatmap = createResource({
url: 'lms.lms.api.get_heatmap_data',
makeParams(values) {
return {
member: values.member,
base_days: props.base_days,
}
},
auto: false,
cache: ['heatmap', memberName.value],
})
watch(memberName, (newVal) => {
heatmap.reload(
{
member: newVal,
},
{
onSuccess(data) {
labels.value = data.labels
},
}
)
})
const chartOptions = computed(() => {
return {
chart: {
type: 'heatmap',
toolbar: {
show: false,
},
},
highlightOnHover: false,
grid: {
show: false,
},
plotOptions: {
heatmap: {
radius: 8,
shadeIntensity: 0.2,
enableShades: true,
colorScale: {
ranges: [
{ from: 0, to: 0, color: theme.colors.gray[400] },
{ from: 1, to: 5, color: theme.colors.green[200] },
{ from: 6, to: 15, color: theme.colors.green[500] },
{ from: 16, to: 30, color: theme.colors.green[700] },
{ from: 31, to: 100, color: theme.colors.green[800] },
],
},
},
},
dataLabels: {
enabled: false,
},
xaxis: {
type: 'category',
categories: labels.value,
position: 'top',
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
tooltip: {
enabled: false,
},
},
yaxis: {
type: 'category',
categories: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
reversed: true,
tooltip: {
enabled: false,
},
},
tooltip: {
custom: ({ series, seriesIndex, dataPointIndex, w }) => {
return `<div class="text-xs bg-gray-900 text-white font-medium p-1">
<div class="text-center">${heatmap.data.heatmap_data[seriesIndex].data[dataPointIndex].label}</div>
</div>`
},
},
}
})
const chartSeries = computed(() => {
if (!heatmap.data) return []
let series = heatmap.data.heatmap_data.map((row) => {
return {
name: row.name,
data: row.data.map((value) => value.count),
}
})
return series
})
</script>

View File

@@ -1,10 +1,12 @@
<template> <template>
<div class="mb-10"> <div>
<Button v-if="isStudent" @click="openEvalModal" class="float-right"> <div class="flex items-center justify-between mb-4">
{{ __('Schedule Evaluation') }} <div class="text-lg font-semibold">
</Button> {{ __('Upcoming Evaluations') }}
<div class="text-lg font-semibold mb-4"> </div>
{{ __('Upcoming Evaluations') }} <Button @click="openEvalModal">
{{ __('Schedule Evaluation') }}
</Button>
</div> </div>
<div v-if="upcoming_evals.data?.length"> <div v-if="upcoming_evals.data?.length">
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -67,10 +69,6 @@ const props = defineProps({
type: Array, type: Array,
default: [], default: [],
}, },
isStudent: {
type: Boolean,
default: false,
},
endDate: { endDate: {
type: String, type: String,
default: null, default: null,

View File

@@ -92,10 +92,10 @@
</Tabs> </Tabs>
</div> </div>
<div class="p-5"> <div class="p-5">
<div class="text-xl font-semibold mb-2"> <div class="text-gray-700 font-semibold mb-4">
{{ batch.data.title }} {{ __('About this batch') }}:
</div> </div>
<div v-html="batch.data.description" class="leading-5 mb-2"></div> <div v-html="batch.data.description" class="leading-5 mb-4"></div>
<div class="flex items-center avatar-group overlap mb-5"> <div class="flex items-center avatar-group overlap mb-5">
<div <div

View File

@@ -163,7 +163,7 @@ onMounted(() => {
const batches = createResource({ const batches = createResource({
doctype: 'LMS Batch', doctype: 'LMS Batch',
url: 'lms.lms.utils.get_batches', url: 'lms.lms.utils.get_batches',
cache: ['batches', user.data?.email], cache: ['batches', user.data?.email || ''],
auto: true, auto: true,
}) })

View File

@@ -71,7 +71,7 @@
<template #default="{ tab }"> <template #default="{ tab }">
<div <div
v-if="tab.courses && tab.courses.value.length" v-if="tab.courses && tab.courses.value.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 my-5 mx-5" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-7 my-5 mx-5"
> >
<router-link <router-link
v-for="course in tab.courses.value" v-for="course in tab.courses.value"

View File

@@ -475,7 +475,8 @@ updateDocumentTitle(pageMeta)
font-weight: 500; font-weight: 500;
} }
.embed-tool__caption { .embed-tool__caption,
.cdx-simple-image__caption {
display: none; display: none;
} }
@@ -585,4 +586,8 @@ iframe {
border-top: 3px solid theme('colors.gray.700'); border-top: 3px solid theme('colors.gray.700');
border-bottom: 3px solid theme('colors.gray.700'); border-bottom: 3px solid theme('colors.gray.700');
} }
.tc-table {
border-left: 1px solid #e8e8eb;
}
</style> </style>

View File

@@ -619,4 +619,8 @@ iframe {
border-top: 3px solid theme('colors.gray.700'); border-top: 3px solid theme('colors.gray.700');
border-bottom: 3px solid theme('colors.gray.700'); border-bottom: 3px solid theme('colors.gray.700');
} }
.tc-table {
border-left: 1px solid #e8e8eb;
}
</style> </style>

View File

@@ -28,9 +28,7 @@
size="lg" size="lg"
> >
{{ program.members }} {{ program.members }}
{{ {{ program.members == 1 ? __('member') : __('members') }}
program.members == 1 ? __(singularize('members')) : __('members')
}}
</Badge> </Badge>
<Badge <Badge
v-if="program.progress" v-if="program.progress"
@@ -133,7 +131,7 @@ import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next' import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, singularize } from '@/utils' import { showToast } from '@/utils'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
const user = inject('$user') const user = inject('$user')

View File

@@ -1,8 +1,10 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { sessionStore } from './session'
export const useSettings = defineStore('settings', () => { export const useSettings = defineStore('settings', () => {
const { isLoggedIn } = sessionStore()
const isSettingsOpen = ref(false) const isSettingsOpen = ref(false)
const activeTab = ref(null) const activeTab = ref(null)
const learningPaths = createResource({ const learningPaths = createResource({
@@ -13,13 +15,13 @@ export const useSettings = defineStore('settings', () => {
field: 'enable_learning_paths', field: 'enable_learning_paths',
} }
}, },
auto: true, auto: isLoggedIn ? true : false,
cache: ['learningPaths'], cache: ['learningPaths'],
}) })
const onboardingDetails = createResource({ const onboardingDetails = createResource({
url: 'lms.lms.utils.is_onboarding_complete', url: 'lms.lms.utils.is_onboarding_complete',
auto: true, auto: isLoggedIn ? true : false,
cache: ['onboardingDetails'], cache: ['onboardingDetails'],
}) })

View File

@@ -66,6 +66,7 @@ export class Assignment {
return return
} }
const app = createApp(AssessmentPlugin, { const app = createApp(AssessmentPlugin, {
type: 'assignment',
onAddition: (assignment) => { onAddition: (assignment) => {
this.data.assignment = assignment this.data.assignment = assignment
this.renderAssignment(assignment) this.renderAssignment(assignment)

View File

@@ -160,7 +160,10 @@ export function getEditorTools() {
upload: Upload, upload: Upload,
markdown: Markdown, markdown: Markdown,
image: SimpleImage, image: SimpleImage,
table: Table, table: {
class: Table,
inlineToolbar: true,
},
paragraph: { paragraph: {
class: Paragraph, class: Paragraph,
inlineToolbar: true, inlineToolbar: true,
@@ -179,6 +182,7 @@ export function getEditorTools() {
}, },
list: { list: {
class: NestedList, class: NestedList,
inlineToolbar: true,
config: { config: {
defaultStyle: 'ordered', defaultStyle: 'ordered',
}, },

View File

@@ -64,6 +64,7 @@ export class Quiz {
return return
} }
const app = createApp(AssessmentPlugin, { const app = createApp(AssessmentPlugin, {
type: 'quiz',
onAddition: (quiz) => { onAddition: (quiz) => {
this.data.quiz = quiz this.data.quiz = quiz
this.renderQuiz(quiz) this.renderQuiz(quiz)

View File

@@ -0,0 +1,5 @@
import resolveConfig from 'tailwindcss/resolveConfig'
import tailwindConfig from 'tailwind.config.js'
export const config = resolveConfig(tailwindConfig)
export const theme = config.theme

View File

@@ -11,6 +11,10 @@ module.exports = {
strokeWidth: { strokeWidth: {
1.5: '1.5', 1.5: '1.5',
}, },
screens: {
'2xl': '1536px',
'3xl': '1920px',
},
}, },
}, },
plugins: [], plugins: [],

View File

@@ -17,6 +17,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, 'src'), '@': path.resolve(__dirname, 'src'),
'tailwind.config.js': path.resolve(__dirname, 'tailwind.config.js'),
}, },
}, },
build: { build: {
@@ -36,6 +37,11 @@ export default defineConfig({
}, },
}, },
optimizeDeps: { optimizeDeps: {
include: ['frappe-ui > feather-icons', 'showdown', 'engine.io-client'], include: [
'feather-icons',
'showdown',
'engine.io-client',
'tailwind.config.js',
],
}, },
}) })

View File

@@ -1 +1 @@
__version__ = "2.18.0" __version__ = "2.19.0"

View File

@@ -13,7 +13,17 @@ from frappe.translate import get_all_translations
from frappe import _ from frappe import _
from frappe.query_builder import DocType from frappe.query_builder import DocType
from frappe.query_builder.functions import Count from frappe.query_builder.functions import Count
from frappe.utils import time_diff, now_datetime, get_datetime, flt from frappe.utils import (
time_diff,
now_datetime,
get_datetime,
cint,
flt,
now,
add_days,
format_date,
date_diff,
)
from typing import Optional from typing import Optional
from lms.lms.utils import get_average_rating, get_lesson_count from lms.lms.utils import get_average_rating, get_lesson_count
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
@@ -593,9 +603,13 @@ def get_categories(doctype, filters):
@frappe.whitelist() @frappe.whitelist()
def get_members(start=0, search=""): def get_members(start=0, search=""):
"""Get members for the given search term and start index. """Get members for the given search term and start index.
Args: start (int): Start index for the query. Args: start (int): Start index for the query.
search (str): Search term to filter the results. <<<<<<< HEAD
Returns: List of members. search (str): Search term to filter the results.
=======
search (str): Search term to filter the results.
>>>>>>> 4869bba7bbb2fb38477d6fc29fb3b5838e075577
Returns: List of members.
""" """
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]} filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
@@ -1043,3 +1057,121 @@ def mark_lesson_progress(course, chapter_number, lesson_number):
"Lesson Reference", {"parent": chapter_name, "idx": lesson_number}, "lesson" "Lesson Reference", {"parent": chapter_name, "idx": lesson_number}, "lesson"
) )
save_progress(lesson_name, course) save_progress(lesson_name, course)
@frappe.whitelist()
def get_heatmap_data(member=None, base_days=200):
if not member:
member = frappe.session.user
base_date, start_date, number_of_days, days = calculate_date_ranges(base_days)
date_count = initialize_date_count(days)
lesson_completions, quiz_submissions, assignment_submissions = fetch_activity_data(
member, start_date
)
count_dates(lesson_completions, date_count)
count_dates(quiz_submissions, date_count)
count_dates(assignment_submissions, date_count)
heatmap_data, labels, total_activities, weeks = prepare_heatmap_data(
start_date, number_of_days, date_count
)
return {
"heatmap_data": heatmap_data,
"labels": labels,
"total_activities": total_activities,
"weeks": weeks,
}
def calculate_date_ranges(base_days):
today = format_date(now(), "YYYY-MM-dd")
day_today = get_datetime(today).strftime("%w")
padding_end = 6 - cint(day_today)
base_date = add_days(today, -base_days)
day_of_base_date = cint(get_datetime(base_date).strftime("%w"))
start_date = add_days(base_date, -day_of_base_date)
number_of_days = base_days + day_of_base_date + padding_end
days = [add_days(start_date, i) for i in range(number_of_days + 1)]
return base_date, start_date, number_of_days, days
def initialize_date_count(days):
return {format_date(day, "YYYY-MM-dd"): 0 for day in days}
def fetch_activity_data(member, start_date):
lesson_completions = frappe.get_all(
"LMS Course Progress",
fields=["creation"],
filters={"member": member, "creation": [">=", start_date]},
)
quiz_submissions = frappe.get_all(
"LMS Quiz Submission",
fields=["creation"],
filters={"member": member, "creation": [">=", start_date]},
)
assignment_submissions = frappe.get_all(
"LMS Assignment Submission",
fields=["creation"],
filters={"member": member, "creation": [">=", start_date]},
)
return lesson_completions, quiz_submissions, assignment_submissions
def count_dates(data, date_count):
for entry in data:
date = format_date(entry.creation, "YYYY-MM-dd")
if date in date_count:
date_count[date] += 1
def prepare_heatmap_data(start_date, number_of_days, date_count):
days_of_week = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
heatmap_data = {day: [] for day in days_of_week}
week_count = -(number_of_days // -7)
labels = [None] * week_count
last_seen_month = None
sorted_dates = sorted(date_count.keys())
for date in sorted_dates:
activity_count = date_count[date]
day_of_week = get_datetime(date).strftime("%a")
current_month = get_datetime(date).strftime("%b")
column_index = get_week_difference(start_date, date)
if 0 <= column_index < week_count:
heatmap_data[day_of_week].append(
{
"date": date,
"count": activity_count,
"label": f"{activity_count} activities on {format_date(date, 'dd MMM')}",
}
)
if last_seen_month != current_month:
labels[column_index] = current_month
last_seen_month = current_month
for (index, label) in enumerate(labels):
if not label:
labels[index] = ""
formatted_heatmap_data = [
{"name": day, "data": heatmap_data[day]} for day in days_of_week
]
total_activities = sum(date_count.values())
return formatted_heatmap_data, labels, total_activities, week_count
def get_week_difference(start_date, current_date):
diff_in_days = date_diff(current_date, start_date)
return diff_in_days // 7

View File

@@ -1244,6 +1244,7 @@ def get_batch_card_details(batchname):
"end_time", "end_time",
"timezone", "timezone",
"published", "published",
"category",
], ],
as_dict=True, as_dict=True,
) )