test: certified participants data

This commit is contained in:
Jannat Patel
2025-12-29 16:02:38 +05:30
parent 3151854bfd
commit 5053b4e45f
5 changed files with 120 additions and 71 deletions

View File

@@ -1,79 +1,70 @@
<template> <template>
<Dialog <Dialog
:options="{ :options="{
title: 'Edit your profile',
size: '3xl', size: '3xl',
}" }"
> >
<template #body-header>
<div class="flex items-center mb-5">
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Edit Profile') }}
</div>
<Badge v-if="isDirty" class="ml-4" theme="orange">
{{ __('Not Saved') }}
</Badge>
</div>
</template>
<template #body-content> <template #body-content>
<div class="text-base"> <div class="text-base">
<div class="grid grid-cols-2 gap-10">
<div>
<div class="text-xs text-ink-gray-5 mb-1">
{{ __('Profile Image') }}
</div>
<FileUploader
v-if="!profile.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? `Uploading ${progress}%`
: 'Upload a profile image'
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="profile.image?.file_url"
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
/>
<div class="text-base flex flex-col ml-2">
<span>
{{ profile.image?.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
{{ getFileSize(profile.image?.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div>
</div>
</div>
<FormControl
v-model="profile.open_to"
type="select"
:options="[' ', 'Opportunities', 'Hiring']"
:label="__('Open to')"
:placeholder="__('Looking for new work or hiring talent?')"
/>
<!-- <Switch
v-model="profile.open_to"
:label="__('Open to Opportunities')"
:description="
__('Show recruiters and others that you are open to work.')
"
class="!px-0"
/> -->
</div>
<div class="grid grid-cols-2 gap-10"> <div class="grid grid-cols-2 gap-10">
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-4"> <div class="space-y-4">
<div>
<div class="text-xs text-ink-gray-5 mb-1">
{{ __('Profile Image') }}
</div>
<FileUploader
v-if="!profile.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? `Uploading ${progress}%`
: 'Upload a profile image'
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="profile.image?.file_url"
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
/>
<div class="text-base flex flex-col ml-2">
<span>
{{ profile.image?.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
{{ getFileSize(profile.image?.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div>
</div>
</div>
<FormControl <FormControl
v-model="profile.first_name" v-model="profile.first_name"
:label="__('First Name')" :label="__('First Name')"
@@ -96,6 +87,13 @@
</div> </div>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<FormControl
v-model="profile.open_to"
type="select"
:options="[' ', 'Opportunities', 'Hiring']"
:label="__('Open to')"
:placeholder="__('Looking for new work or hiring talent?')"
/>
<Link <Link
:label="__('Language')" :label="__('Language')"
v-model="profile.language" v-model="profile.language"
@@ -110,7 +108,7 @@
@change="(val) => (profile.bio = val)" @change="(val) => (profile.bio = val)"
:content="profile.bio" :content="profile.bio"
:rows="15" :rows="15"
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3" editorClass="prose-sm py-2 px-2 min-h-[280px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
/> />
</div> </div>
</div> </div>
@@ -128,12 +126,12 @@
</template> </template>
<script setup> <script setup>
import { import {
Badge,
Button, Button,
createResource, createResource,
Dialog, Dialog,
FormControl, FormControl,
FileUploader, FileUploader,
Switch,
TextEditor, TextEditor,
toast, toast,
} from 'frappe-ui' } from 'frappe-ui'
@@ -144,6 +142,7 @@ import Link from '@/components/Controls/Link.vue'
const reloadProfile = defineModel('reloadProfile') const reloadProfile = defineModel('reloadProfile')
const hasLanguageChanged = ref(false) const hasLanguageChanged = ref(false)
const isDirty = ref(false)
const props = defineProps({ const props = defineProps({
profile: { profile: {
@@ -229,6 +228,27 @@ const removeImage = () => {
profile.image = null profile.image = null
} }
watch(
() => profile,
(newVal) => {
if (!props.profile.data) return
let keys = Object.keys(newVal)
keys.splice(keys.indexOf('image'), 1)
for (let key of keys) {
if (newVal[key] !== props.profile.data[key]) {
isDirty.value = true
return
}
}
if (profile.image?.file_url !== props.profile.data.user_image) {
isDirty.value = true
return
}
isDirty.value = false
},
{ deep: true }
)
watch( watch(
() => props.profile.data, () => props.profile.data,
(newVal) => { (newVal) => {
@@ -243,6 +263,7 @@ watch(
profile.github = newVal.github profile.github = newVal.github
profile.twitter = newVal.twitter profile.twitter = newVal.twitter
if (newVal.user_image) imageResource.submit({ image: newVal.user_image }) if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
isDirty.value = false
} }
} }
) )

View File

@@ -31,11 +31,20 @@
</template> </template>
<script setup> <script setup>
import { inject } from 'vue' import { inject } from 'vue'
import { Button } from 'frappe-ui' import { Button, usePageMeta } from 'frappe-ui'
import { sessionStore } from '../stores/session'
const user = inject('$user') const user = inject('$user')
const { brand } = sessionStore()
const redirectToLogin = () => { const redirectToLogin = () => {
window.location.href = '/login' window.location.href = '/login'
} }
usePageMeta(() => {
return {
title: __('Not Permitted'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -204,10 +204,12 @@ const setQueryParams = () => {
let filterKeys = { let filterKeys = {
category: currentCategory.value, category: currentCategory.value,
name: nameFilter.value, name: nameFilter.value,
'open-to-opportunities': openToOpportunities.value,
hiring: hiring.value,
} }
Object.keys(filterKeys).forEach((key) => { Object.keys(filterKeys).forEach((key) => {
if (filterKeys[key] && filterKeys[key].trim() !== '') { if (filterKeys[key] && hasValue(filterKeys[key])) {
queries.set(key, filterKeys[key]) queries.set(key, filterKeys[key])
} else { } else {
queries.delete(key) queries.delete(key)
@@ -220,6 +222,13 @@ const setQueryParams = () => {
) )
} }
const hasValue = (value) => {
if (typeof value === 'string') {
return value.trim() !== ''
}
return true
}
const setFiltersFromQuery = () => { const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search) let queries = new URLSearchParams(location.search)
nameFilter.value = queries.get('name') || '' nameFilter.value = queries.get('name') || ''

View File

@@ -287,7 +287,7 @@ def get_certified_participants(filters=None, start=0, page_length=100):
"LMS Certificate", "LMS Certificate",
filters=filters, filters=filters,
or_filters=or_filters, or_filters=or_filters,
fields=["member", "issue_date"], fields=["member", "issue_date", "batch_name", "course", "name"],
group_by="member", group_by="member",
order_by="issue_date desc", order_by="issue_date desc",
start=start, start=start,
@@ -309,7 +309,6 @@ def update_certification_filters(filters):
or_filters = {} or_filters = {}
if not filters: if not filters:
filters = {} filters = {}
filters.update({"published": 1}) filters.update({"published": 1})
category = filters.get("category") category = filters.get("category")

View File

@@ -2,6 +2,7 @@ import frappe
from frappe.tests import UnitTestCase from frappe.tests import UnitTestCase
from frappe.utils import add_days, nowdate from frappe.utils import add_days, nowdate
from lms.lms.api import get_certified_participants
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified
from .utils import ( from .utils import (
@@ -147,6 +148,7 @@ class TestUtils(UnitTestCase):
certificate.member = member certificate.member = member
certificate.issue_date = frappe.utils.nowdate() certificate.issue_date = frappe.utils.nowdate()
certificate.template = get_default_certificate_template() certificate.template = get_default_certificate_template()
certificate.published = 1
certificate.save() certificate.save()
return certificate return certificate
@@ -265,6 +267,15 @@ class TestUtils(UnitTestCase):
self.assertIsNone(is_certified(self.course.name)) self.assertIsNone(is_certified(self.course.name))
frappe.session.user = "Administrator" frappe.session.user = "Administrator"
def test_certified_participants(self):
filters = {"category": "Utility Course"}
certified_participants = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants), 1)
self.assertEqual(certified_participants[0].member, self.student1.email)
filters = {"category": "Nonexistent Category"}
certified_participants_no_match = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_no_match), 0)
def test_rating_validation(self): def test_rating_validation(self):
student3 = self.create_user("student3@example.com", "Emily", "Cooper", ["LMS Student"]) student3 = self.create_user("student3@example.com", "Emily", "Cooper", ["LMS Student"])
with self.assertRaises(frappe.exceptions.ValidationError): with self.assertRaises(frappe.exceptions.ValidationError):