test: certified participants data
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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') || ''
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user