109
frontend/src/components/Modals/EditCoverImage.vue
Normal file
109
frontend/src/components/Modals/EditCoverImage.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<Popover transition="default">
|
||||||
|
<template #target="{ isOpen, togglePopover }" class="flex w-full">
|
||||||
|
<slot v-bind="{ isOpen, togglePopover }"></slot>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="absolute left-1/2 mt-3 w-96 max-w-lg -translate-x-1/2 transform rounded-lg bg-white px-4 sm:px-0 lg:max-w-3xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overflow-hidden rounded-lg p-3 shadow-2xl ring-1 ring-black ring-opacity-5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center space-x-2">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
placeholder="search by keyword"
|
||||||
|
v-model="search"
|
||||||
|
:debounce="300"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<FileUploader
|
||||||
|
:fileTypes="['image/*']"
|
||||||
|
:validateFile="validateFile"
|
||||||
|
@success="(file) => saveImage(file)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
<Button @click="openFileSelector" :loading="uploading">
|
||||||
|
{{ uploading ? `Uploading ${progress}%` : 'Upload Image' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative mt-2 grid w-[25.5rem] gap-2 bg-white lg:grid-cols-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="image in images.data"
|
||||||
|
:key="image.id"
|
||||||
|
class="h-[50px] w-[200px] overflow-hidden rounded hover:opacity-80"
|
||||||
|
@click="$emit('select', image.urls.raw)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="
|
||||||
|
image.urls.raw +
|
||||||
|
'&w=200&h=50&fit=crop&crop=entropy,faces,focalpoint'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="images.data"
|
||||||
|
class="mt-2 text-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ __('Image search powered by') }}
|
||||||
|
<a class="underline" target="_blank" href="https://unsplash.com">
|
||||||
|
{{ __('Unsplash') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
TextInput,
|
||||||
|
FileUploader,
|
||||||
|
Button,
|
||||||
|
createResource,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const search = ref(null)
|
||||||
|
const emit = defineEmits(['select'])
|
||||||
|
|
||||||
|
const images = createResource({
|
||||||
|
url: 'lms.lms.api.get_unsplash_photos',
|
||||||
|
makeParams: () => {
|
||||||
|
return {
|
||||||
|
keyword: search.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
debounce: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => search.value,
|
||||||
|
() => {
|
||||||
|
images.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveImage = (file) => {
|
||||||
|
emit('select', file.file_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFile = (file) => {
|
||||||
|
let extension = file.name.split('.').pop().toLowerCase()
|
||||||
|
if (!['jpg', 'jpeg', 'png'].includes(extension)) {
|
||||||
|
return 'Only image file is allowed.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
178
frontend/src/components/Modals/EditProfile.vue
Normal file
178
frontend/src/components/Modals/EditProfile.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:options="{
|
||||||
|
title: 'Edit your profile',
|
||||||
|
size: 'xl',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Save',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: (close) => saveProfile(close),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<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="text-xs text-gray-600 mb-1">
|
||||||
|
{{ __('Profile Image') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="border rounded-md p-2 mr-2">
|
||||||
|
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="text-base flex flex-col">
|
||||||
|
<span>
|
||||||
|
{{ profile.image.file_name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ getFileSize(profile.image.file_size) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<X
|
||||||
|
@click="removeImage()"
|
||||||
|
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="profile.first_name"
|
||||||
|
:label="__('First Name')"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="profile.last_name"
|
||||||
|
:label="__('Last Name')"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="profile.headline"
|
||||||
|
:label="__('Headline')"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<FormControl type="textarea" v-model="profile.bio" :label="__('Bio')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
FormControl,
|
||||||
|
FileUploader,
|
||||||
|
Button,
|
||||||
|
createResource,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { reactive, watch, defineModel } from 'vue'
|
||||||
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
|
import { getFileSize, showToast } from '@/utils'
|
||||||
|
|
||||||
|
const reloadProfile = defineModel('reloadProfile')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const profile = reactive({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
headline: '',
|
||||||
|
bio: '',
|
||||||
|
image: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageResource = createResource({
|
||||||
|
url: 'lms.lms.api.get_file_info',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
file_url: values.image,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
onSuccess(data) {
|
||||||
|
profile.image = data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateProfile = createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'User',
|
||||||
|
name: props.profile.data.name,
|
||||||
|
fieldname: {
|
||||||
|
user_image: profile.image.file_url,
|
||||||
|
...profile,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
props.profile.data = data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveProfile = (close) => {
|
||||||
|
updateProfile.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
close()
|
||||||
|
reloadProfile.value.reload()
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFile = (file) => {
|
||||||
|
let extension = file.name.split('.').pop().toLowerCase()
|
||||||
|
if (!['jpg', 'jpeg', 'png'].includes(extension)) {
|
||||||
|
return 'Only image file is allowed.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveImage = (file) => {
|
||||||
|
profile.image = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeImage = () => {
|
||||||
|
profile.image = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.profile.data,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
profile.first_name = newVal.first_name
|
||||||
|
profile.last_name = newVal.last_name
|
||||||
|
profile.headline = newVal.headline
|
||||||
|
profile.bio = newVal.bio
|
||||||
|
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="mb-1.5 text-sm text-gray-600">
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
{{ __('Date') }}
|
{{ __('Date') }}
|
||||||
</div>
|
</div>
|
||||||
<DatePicker v-model="evaluation.date" />
|
<FormControl type="date" v-model="evaluation.date" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slots.data?.length">
|
<div v-if="slots.data?.length">
|
||||||
<div class="mb-1.5 text-sm text-gray-600">
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
@@ -46,7 +46,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-red-600">
|
<div
|
||||||
|
v-else-if="evaluation.course && evaluation.date"
|
||||||
|
class="text-sm italic text-red-600"
|
||||||
|
>
|
||||||
{{ __('No slots available for this date.') }}
|
{{ __('No slots available for this date.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createResource, Select, DatePicker } from 'frappe-ui'
|
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch, inject } from 'vue'
|
import { defineModel, reactive, watch, inject } from 'vue'
|
||||||
import { createToast, formatTime } from '@/utils/'
|
import { createToast, formatTime } from '@/utils/'
|
||||||
|
|
||||||
@@ -165,7 +168,7 @@ watch(
|
|||||||
() => evaluation.date,
|
() => evaluation.date,
|
||||||
(date) => {
|
(date) => {
|
||||||
evaluation.start_time = ''
|
evaluation.start_time = ''
|
||||||
if (date) {
|
if (date && evaluation.course) {
|
||||||
slots.submit(evaluation)
|
slots.submit(evaluation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
frontend/src/components/NoPermission.vue
Normal file
42
frontend/src/components/NoPermission.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="border rounded-md w-1/3 mx-auto my-32">
|
||||||
|
<div class="border-b px-5 py-3 font-medium">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center before:bg-red-600 before:w-2 before:h-2 before:rounded-md before:mr-2"
|
||||||
|
></span>
|
||||||
|
{{ __('Not Permitted') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="user.data" class="px-5 py-3">
|
||||||
|
<div>
|
||||||
|
{{ __('You do not have permission to access this page.') }}
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'Courses',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button variant="solid" class="mt-2">
|
||||||
|
{{ __('Checkout Courses') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 py-3">
|
||||||
|
<div>
|
||||||
|
{{ __('Please login to access this page.') }}
|
||||||
|
</div>
|
||||||
|
<Button variant="solid" @click="redirectToLogin()" class="mt-2">
|
||||||
|
{{ __('Login') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { Button } from 'frappe-ui'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
|
||||||
|
const redirectToLogin = () => {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
90
frontend/src/components/UnsplashImageBrowser.vue
Normal file
90
frontend/src/components/UnsplashImageBrowser.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<Popover transition="default">
|
||||||
|
<template #target="{ isOpen, togglePopover }" class="flex w-full">
|
||||||
|
<slot v-bind="{ isOpen, togglePopover }"></slot>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="absolute left-1/2 mt-3 max-w-sm -translate-x-1/2 transform rounded-lg bg-white px-4 sm:px-0 lg:max-w-3xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overflow-hidden rounded-lg p-3 shadow-2xl ring-1 ring-black ring-opacity-5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
placeholder="search by keyword"
|
||||||
|
v-model="search"
|
||||||
|
:debounce="300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FileUploader @success="(file) => $emit('select', file.file_url)">
|
||||||
|
<template
|
||||||
|
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||||
|
>
|
||||||
|
<div class="w-full text-center">
|
||||||
|
<Button @click="openFileSelector" :loading="uploading">
|
||||||
|
{{ uploading ? `Uploading ${progress}%` : 'Upload Image' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative mt-2 grid w-[25.5rem] gap-2 bg-white lg:grid-cols-2"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-for="image in $resources.images.data"
|
||||||
|
:key="image.id"
|
||||||
|
class="h-[50px] w-[200px] overflow-hidden rounded hover:opacity-80"
|
||||||
|
@click="$emit('select', image.urls.raw)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="
|
||||||
|
image.urls.raw +
|
||||||
|
'&w=200&h=50&fit=crop&crop=entropy,faces,focalpoint'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-center text-sm text-gray-500">
|
||||||
|
{{ __('Image search powered by') }}
|
||||||
|
<a class="underline" target="_blank" href="https://unsplash.com">
|
||||||
|
{{ __('Unsplash') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||||
|
import { Popover, FileUploader, Button } from 'frappe-ui'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UnsplashImageBrowser',
|
||||||
|
components: {
|
||||||
|
Popover,
|
||||||
|
FileUploader,
|
||||||
|
},
|
||||||
|
emits: ['select'],
|
||||||
|
resources: {
|
||||||
|
images() {
|
||||||
|
return {
|
||||||
|
url: 'gameplan.api.get_unsplash_photos',
|
||||||
|
params: { keyword: this.search },
|
||||||
|
auto: true,
|
||||||
|
debounce: 500,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -31,8 +31,11 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else> Learning </span>
|
<span v-else> Learning </span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="user" class="mt-1 text-sm text-gray-700 leading-none">
|
<div
|
||||||
{{ convertToTitleCase(user.split('@')[0]) }}
|
v-if="userResource"
|
||||||
|
class="mt-1 text-sm text-gray-700 leading-none"
|
||||||
|
>
|
||||||
|
{{ convertToTitleCase(userResource.data?.email.split('@')[0]) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -57,7 +60,8 @@ import { Dropdown, createResource } from 'frappe-ui'
|
|||||||
import { ChevronDown, LogIn, LogOut, User } from 'lucide-vue-next'
|
import { ChevronDown, LogIn, LogOut, User } from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { convertToTitleCase } from '../utils'
|
import { convertToTitleCase } from '../utils'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted, inject } from 'vue'
|
||||||
|
import { usersStore } from '@/stores/user'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -76,19 +80,21 @@ const branding = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { logout, user } = sessionStore()
|
const { logout } = sessionStore()
|
||||||
|
let { userResource } = usersStore()
|
||||||
|
|
||||||
let { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
const userDropdownOptions = [
|
const userDropdownOptions = [
|
||||||
/* {
|
{
|
||||||
icon: User,
|
icon: User,
|
||||||
label: 'My Profile',
|
label: 'My Profile',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push(`/user/${user.data?.username}`)
|
router.push(`/user/${userResource.data?.username}`)
|
||||||
},
|
},
|
||||||
condition: () => {
|
condition: () => {
|
||||||
return isLoggedIn
|
return isLoggedIn
|
||||||
},
|
},
|
||||||
}, */
|
},
|
||||||
{
|
{
|
||||||
icon: LogOut,
|
icon: LogOut,
|
||||||
label: 'Log out',
|
label: 'Log out',
|
||||||
|
|||||||
@@ -1 +1,198 @@
|
|||||||
<template></template>
|
<template>
|
||||||
|
<NoPermission v-if="!$user.data" />
|
||||||
|
<div v-else-if="profile.data">
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
</header>
|
||||||
|
<div class="group relative h-[130px] w-full">
|
||||||
|
<img
|
||||||
|
v-if="profile.data.cover_image"
|
||||||
|
:src="profile.data.cover_image"
|
||||||
|
class="h-[130px] w-full object-cover object-center"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="{ 'bg-gray-100': !profile.data.cover_image }"
|
||||||
|
class="h-[130px] w-full"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-0 left-1/2 mb-4 flex -translate-x-1/2 space-x-2 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
|
||||||
|
v-if="isSessionUser()"
|
||||||
|
>
|
||||||
|
<EditCoverImage
|
||||||
|
@select="(imageUrl) => coverImage.submit({ url: imageUrl })"
|
||||||
|
>
|
||||||
|
<template v-slot="{ togglePopover }">
|
||||||
|
<Button variant="outline" @click="togglePopover()">
|
||||||
|
<template #prefix>
|
||||||
|
<Edit class="w-4 h-4 stroke-1.5 text-gray-700" />
|
||||||
|
</template>
|
||||||
|
{{ __('Edit') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</EditCoverImage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto -mt-4 max-w-4xl translate-x-0 sm:px-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
v-if="profile.data.user_image"
|
||||||
|
:src="profile.data.user_image"
|
||||||
|
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
|
||||||
|
/>
|
||||||
|
<UserAvatar
|
||||||
|
v-else
|
||||||
|
:user="profile.data"
|
||||||
|
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-6">
|
||||||
|
<h2 class="mt-2 text-3xl font-semibold text-gray-900">
|
||||||
|
{{ profile.data.full_name }}
|
||||||
|
</h2>
|
||||||
|
<div class="mt-2 text-base text-gray-700">
|
||||||
|
{{ profile.data.headline }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button v-if="isSessionUser()" class="ml-auto" @click="editProfile()">
|
||||||
|
<template #prefix>
|
||||||
|
<Edit class="w-4 h-4 stroke-1.5 text-gray-700" />
|
||||||
|
</template>
|
||||||
|
{{ __('Edit Profile') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 mt-6">
|
||||||
|
<TabButtons
|
||||||
|
class="inline-block"
|
||||||
|
:buttons="getTabButtons()"
|
||||||
|
v-model="activeTab"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<router-view :profile="profile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EditProfile
|
||||||
|
v-model="showProfileModal"
|
||||||
|
v-model:reloadProfile="profile"
|
||||||
|
:profile="profile"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Breadcrumbs, createResource, Button, TabButtons } from 'frappe-ui'
|
||||||
|
import { computed, inject, reactive, ref, onMounted, watchEffect } from 'vue'
|
||||||
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import { Edit } from 'lucide-vue-next'
|
||||||
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import NoPermission from '@/components/NoPermission.vue'
|
||||||
|
import { convertToTitleCase } from '@/utils'
|
||||||
|
import EditProfile from '@/components/Modals/EditProfile.vue'
|
||||||
|
import EditCoverImage from '@/components/Modals/EditCoverImage.vue'
|
||||||
|
|
||||||
|
const { user } = sessionStore()
|
||||||
|
const $user = inject('$user')
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const activeTab = ref('')
|
||||||
|
const showProfileModal = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if ($user.data) profile.reload()
|
||||||
|
|
||||||
|
setActiveTab()
|
||||||
|
})
|
||||||
|
|
||||||
|
const profile = createResource({
|
||||||
|
url: 'frappe.client.get',
|
||||||
|
params: {
|
||||||
|
doctype: 'User',
|
||||||
|
filters: {
|
||||||
|
username: props.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const coverImage = createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'User',
|
||||||
|
name: profile.data?.name,
|
||||||
|
fieldname: 'cover_image',
|
||||||
|
value: values.url,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
profile.reload()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const setActiveTab = () => {
|
||||||
|
let fragments = route.path.split('/')
|
||||||
|
let sections = ['certificates', 'roles', 'evaluations']
|
||||||
|
sections.forEach((section) => {
|
||||||
|
if (fragments.includes(section)) {
|
||||||
|
activeTab.value = convertToTitleCase(section)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!activeTab.value) activeTab.value = 'About'
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (activeTab.value) {
|
||||||
|
let route = {
|
||||||
|
About: { name: 'ProfileAbout' },
|
||||||
|
Certificates: { name: 'ProfileCertificates' },
|
||||||
|
Roles: { name: 'ProfileRoles' },
|
||||||
|
Evaluations: { name: 'ProfileEvaluator' },
|
||||||
|
}[activeTab.value]
|
||||||
|
router.push(route)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const editProfile = () => {
|
||||||
|
showProfileModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSessionUser = () => {
|
||||||
|
return $user.data?.email === profile.data?.email
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTabButtons = () => {
|
||||||
|
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
|
||||||
|
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
|
||||||
|
if (isSessionUser() && $user.data?.is_evaluator)
|
||||||
|
buttons.push({ label: 'Evaluations' })
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
let crumbs = [
|
||||||
|
{
|
||||||
|
label: 'People',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: profile.data?.full_name,
|
||||||
|
route: {
|
||||||
|
name: 'Profile',
|
||||||
|
params: {
|
||||||
|
username: user.doc?.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return crumbs
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
23
frontend/src/pages/ProfileAbout.vue
Normal file
23
frontend/src/pages/ProfileAbout.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-7">
|
||||||
|
<h2 class="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
{{ __('About') }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
v-if="profile.data.bio"
|
||||||
|
v-html="profile.data.bio"
|
||||||
|
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal"
|
||||||
|
></div>
|
||||||
|
<div v-else class="text-gray-700 text-sm italic">
|
||||||
|
{{ __('No introduction') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
46
frontend/src/pages/ProfileCertificates.vue
Normal file
46
frontend/src/pages/ProfileCertificates.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-7">
|
||||||
|
<h2 class="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
{{ __('Certificates') }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="certificate in certificates.data"
|
||||||
|
:key="certificate.name"
|
||||||
|
class="bg-white shadow rounded-lg p-3 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div class="font-medium leading-5">
|
||||||
|
{{ certificate.course_title }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="text-xs text-gray-700"> {{ __('issued on') }}: </span>
|
||||||
|
{{ dayjs(certificate.issue_date).format('DD MMM YYYY') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { createResource } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
const props = defineProps({
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const certificates = createResource({
|
||||||
|
url: 'frappe.client.get_list',
|
||||||
|
params: {
|
||||||
|
doctype: 'LMS Certificate',
|
||||||
|
fields: ['name', 'course', 'course_title', 'issue_date'],
|
||||||
|
filters: {
|
||||||
|
member: props.profile.data.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
318
frontend/src/pages/ProfileEvaluator.vue
Normal file
318
frontend/src/pages/ProfileEvaluator.vue
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-7 mb-20">
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-gray-900">
|
||||||
|
{{ __('My availability') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<div class="grid grid-cols-4 gap-4 text-sm text-gray-700 mb-4">
|
||||||
|
<div>
|
||||||
|
{{ __('Day') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ __('Start Time') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ __('End Time') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="evaluator.data"
|
||||||
|
v-for="slot in evaluator.data.slots.schedule"
|
||||||
|
class="grid grid-cols-4 gap-4 mb-4 group"
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
:options="days"
|
||||||
|
v-model="slot.day"
|
||||||
|
@focusout.stop="update(slot.name, 'day', slot.day)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="time"
|
||||||
|
v-model="slot.start_time"
|
||||||
|
@focusout.stop="update(slot.name, 'start_time', slot.start_time)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="time"
|
||||||
|
v-model="slot.end_time"
|
||||||
|
@focusout.stop="update(slot.name, 'end_time', slot.end_time)"
|
||||||
|
/>
|
||||||
|
<X
|
||||||
|
@click="deleteRow(slot.name)"
|
||||||
|
class="w-6 h-auto stroke-1.5 text-red-900 rounded-md cursor-pointer p-1 bg-red-100 hidden group-hover:block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 gap-4 mb-4" v-show="showSlotsTemplate">
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
:options="days"
|
||||||
|
v-model="newSlot.day"
|
||||||
|
@focusout.stop="add()"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="time"
|
||||||
|
v-model="newSlot.start_time"
|
||||||
|
@focusout.stop="add()"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="time"
|
||||||
|
v-model="newSlot.end_time"
|
||||||
|
@focusout.stop="add()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button @click="showSlotsTemplate = 1">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="w-4 h-4 stroke-1.5 text-gray-700" />
|
||||||
|
</template>
|
||||||
|
{{ __('Add Slot') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="my-10">
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-gray-900">
|
||||||
|
{{ __('I am unavailable') }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
<FormControl
|
||||||
|
type="date"
|
||||||
|
:label="__('From')"
|
||||||
|
v-model="from"
|
||||||
|
@change.stop="
|
||||||
|
() => {
|
||||||
|
updateUnavailability.submit({
|
||||||
|
field: 'unavailable_from',
|
||||||
|
value: from,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="date"
|
||||||
|
:label="__('To')"
|
||||||
|
v-model="to"
|
||||||
|
@change.stop="
|
||||||
|
() => {
|
||||||
|
updateUnavailability.submit({
|
||||||
|
field: 'unavailable_to',
|
||||||
|
value: to,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-gray-900">
|
||||||
|
{{ __('My calendar') }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
v-if="evaluator.data?.calendar && evaluator.data?.is_authorized"
|
||||||
|
class="flex items-center bg-green-100 text-green-900 text-sm p-1 rounded-md mb-4 w-fit"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4 stroke-1.5 mr-2" />
|
||||||
|
{{ __('Your calendar is set.') }}
|
||||||
|
</div>
|
||||||
|
<Button @click="() => authorizeCalendar.submit()">
|
||||||
|
{{ __('Authorize Google Calendar Access') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { createResource, FormControl, Button } from 'frappe-ui'
|
||||||
|
import { computed, reactive, ref, onMounted, inject } from 'vue'
|
||||||
|
import { showToast, convertToTitleCase } from '@/utils'
|
||||||
|
import { Plus, X, Check } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (user.data?.name !== props.profile.data?.name) {
|
||||||
|
window.location.href = `/user/${props.profile.data?.username}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const showSlotsTemplate = ref(0)
|
||||||
|
const from = ref(null)
|
||||||
|
const to = ref(null)
|
||||||
|
|
||||||
|
const newSlot = reactive({
|
||||||
|
day: '',
|
||||||
|
start_time: '',
|
||||||
|
end_time: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const evaluator = createResource({
|
||||||
|
url: 'lms.lms.api.get_evaluator_details',
|
||||||
|
params: {
|
||||||
|
evaluator: props.profile.data?.name,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
onSuccess(data) {
|
||||||
|
if (data.slots.unavailable_from) from.value = data.slots.unavailable_from
|
||||||
|
if (data.slots.unavailable_to) to.value = data.slots.unavailable_to
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createSlot = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'Evaluator Schedule',
|
||||||
|
parent: evaluator.data?.slots.name,
|
||||||
|
parentfield: 'schedule',
|
||||||
|
parenttype: 'Course Evaluator',
|
||||||
|
...newSlot,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showToast('Success', 'Slot added successfully', 'check')
|
||||||
|
evaluator.reload()
|
||||||
|
showSlotsTemplate.value = 0
|
||||||
|
newSlot.day = ''
|
||||||
|
newSlot.start_time = ''
|
||||||
|
newSlot.end_time = ''
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSlot = createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'Evaluator Schedule',
|
||||||
|
name: values.name,
|
||||||
|
fieldname: values.field,
|
||||||
|
value: values.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showToast('Success', 'Availability updated successfully', 'check')
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteSlot = createResource({
|
||||||
|
url: 'frappe.client.delete',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'Evaluator Schedule',
|
||||||
|
name: values.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showToast('Success', 'Slot deleted successfully', 'check')
|
||||||
|
evaluator.reload()
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateUnavailability = createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'Course Evaluator',
|
||||||
|
name: evaluator.data?.slots.name,
|
||||||
|
fieldname: values.field,
|
||||||
|
value: values.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showToast('Success', 'Unavailability updated successfully', 'check')
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const update = (name, field, value) => {
|
||||||
|
updateSlot.submit(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validate() {
|
||||||
|
if (!value) {
|
||||||
|
return `Please enter a value for ${convertToTitleCase(field)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = () => {
|
||||||
|
if (!newSlot.day || !newSlot.start_time || !newSlot.end_time) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
createSlot.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRow = (name) => {
|
||||||
|
deleteSlot.submit({ name })
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorizeCalendar = createResource({
|
||||||
|
url: 'frappe.integrations.doctype.google_calendar.google_calendar.authorize_access',
|
||||||
|
makeParams() {
|
||||||
|
return {
|
||||||
|
g_calendar: evaluator.data?.calendar,
|
||||||
|
reauthorize: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
window.open(data.url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const days = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Monday',
|
||||||
|
value: 'Monday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tuesday',
|
||||||
|
value: 'Tuesday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Wednesday',
|
||||||
|
value: 'Wednesday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Thursday',
|
||||||
|
value: 'Thursday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Friday',
|
||||||
|
value: 'Friday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saturday',
|
||||||
|
value: 'Saturday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sunday',
|
||||||
|
value: 'Sunday',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
96
frontend/src/pages/ProfileRoles.vue
Normal file
96
frontend/src/pages/ProfileRoles.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-7">
|
||||||
|
<h2 class="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
{{ __('Settings') }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex justify-between w-3/4 mt-5">
|
||||||
|
<FormControl
|
||||||
|
:label="__('Moderator')"
|
||||||
|
v-model="moderator"
|
||||||
|
type="checkbox"
|
||||||
|
@change.stop="changeRole('moderator')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
:label="__('Course Creator')"
|
||||||
|
v-model="course_creator"
|
||||||
|
type="checkbox"
|
||||||
|
@change.stop="changeRole('course_creator')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
:label="__('Evaluator')"
|
||||||
|
v-model="batch_evaluator"
|
||||||
|
type="checkbox"
|
||||||
|
@change.stop="changeRole('batch_evaluator')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
:label="__('Student')"
|
||||||
|
v-model="lms_student"
|
||||||
|
type="checkbox"
|
||||||
|
@change.stop="changeRole('lms_student')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { FormControl, createResource } from 'frappe-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { showToast, convertToTitleCase } from '@/utils'
|
||||||
|
|
||||||
|
const moderator = ref(false)
|
||||||
|
const course_creator = ref(false)
|
||||||
|
const batch_evaluator = ref(false)
|
||||||
|
const lms_student = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const roles = createResource({
|
||||||
|
url: 'lms.lms.utils.get_roles',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
name: props.profile.data?.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
onSuccess(data) {
|
||||||
|
let roles = [
|
||||||
|
'moderator',
|
||||||
|
'course_creator',
|
||||||
|
'batch_evaluator',
|
||||||
|
'lms_student',
|
||||||
|
]
|
||||||
|
for (let role of roles) {
|
||||||
|
if (data[role]) eval(role).value = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateRole = createResource({
|
||||||
|
url: 'lms.overrides.user.save_role',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
user: props.profile.data?.name,
|
||||||
|
role: values.role,
|
||||||
|
value: values.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeRole = (role) => {
|
||||||
|
updateRole.submit(
|
||||||
|
{
|
||||||
|
role: convertToTitleCase(role.split('_').join(' ')),
|
||||||
|
value: eval(role).value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
showToast('Success', 'Role updated successfully', 'check')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -56,10 +56,33 @@ const routes = [
|
|||||||
component: () => import('@/pages/Statistics.vue'),
|
component: () => import('@/pages/Statistics.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user/:userName',
|
path: '/user/:username',
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
component: () => import('@/pages/Profile.vue'),
|
component: () => import('@/pages/Profile.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
|
redirect: { name: 'ProfileAbout' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'ProfileAbout',
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/pages/ProfileAbout.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ProfileCertificates',
|
||||||
|
path: 'certificates',
|
||||||
|
component: () => import('@/pages/ProfileCertificates.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ProfileRoles',
|
||||||
|
path: 'roles',
|
||||||
|
component: () => import('@/pages/ProfileRoles.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ProfileEvaluator',
|
||||||
|
path: 'evaluations',
|
||||||
|
component: () => import('@/pages/ProfileEvaluator.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/job-openings',
|
path: '/job-openings',
|
||||||
|
|||||||
@@ -81,7 +81,18 @@ export function showToast(title, text, icon) {
|
|||||||
? 'bg-green-600 text-white rounded-md p-px'
|
? 'bg-green-600 text-white rounded-md p-px'
|
||||||
: 'bg-red-600 text-white rounded-md p-px',
|
: 'bg-red-600 text-white rounded-md p-px',
|
||||||
position: icon == 'check' ? 'bottom-right' : 'top-center',
|
position: icon == 'check' ? 'bottom-right' : 'top-center',
|
||||||
timeout: icon == 'check' ? 5 : 10,
|
timeout: 5,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImgDimensions(imgSrc) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let img = new Image()
|
||||||
|
img.onload = function () {
|
||||||
|
let { width, height } = img
|
||||||
|
resolve({ width, height, ratio: width / height })
|
||||||
|
}
|
||||||
|
img.src = imgSrc
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2062
frontend/yarn.lock
2062
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -90,11 +90,11 @@ def create_moderator_role():
|
|||||||
|
|
||||||
|
|
||||||
def create_evaluator_role():
|
def create_evaluator_role():
|
||||||
if not frappe.db.exists("Role", "Class Evaluator"):
|
if not frappe.db.exists("Role", "Batch Evaluator"):
|
||||||
role = frappe.new_doc("Role")
|
role = frappe.new_doc("Role")
|
||||||
role.update(
|
role.update(
|
||||||
{
|
{
|
||||||
"role_name": "Class Evaluator",
|
"role_name": "Batch Evaluator",
|
||||||
"home_page": "",
|
"home_page": "",
|
||||||
"desk_access": 0,
|
"desk_access": 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ def get_user_info():
|
|||||||
user = frappe.db.get_value(
|
user = frappe.db.get_value(
|
||||||
"User",
|
"User",
|
||||||
frappe.session.user,
|
frappe.session.user,
|
||||||
["name", "email", "enabled", "user_image", "full_name", "user_type"],
|
["name", "email", "enabled", "user_image", "full_name", "user_type", "username"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
user["roles"] = frappe.get_roles(user.name)
|
user["roles"] = frappe.get_roles(user.name)
|
||||||
@@ -288,3 +288,40 @@ def get_branding():
|
|||||||
"brand_html": frappe.db.get_single_value("Website Settings", "brand_html"),
|
"brand_html": frappe.db.get_single_value("Website Settings", "brand_html"),
|
||||||
"favicon": frappe.db.get_single_value("Website Settings", "favicon"),
|
"favicon": frappe.db.get_single_value("Website Settings", "favicon"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_unsplash_photos(keyword=None):
|
||||||
|
from lms.unsplash import get_list, get_by_keyword
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
return get_by_keyword(keyword)
|
||||||
|
|
||||||
|
return frappe.cache().get_value("unsplash_photos", generator=get_list)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_evaluator_details(evaluator):
|
||||||
|
frappe.only_for("Batch Evaluator")
|
||||||
|
|
||||||
|
if not frappe.db.exists("Google Calendar", {"user": evaluator}):
|
||||||
|
calendar = frappe.new_doc("Google Calendar")
|
||||||
|
calendar.update({"user": evaluator, "calendar_name": evaluator})
|
||||||
|
calendar.insert()
|
||||||
|
else:
|
||||||
|
calendar = frappe.db.get_value(
|
||||||
|
"Google Calendar", {"user": evaluator}, ["name", "authorization_code"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if frappe.db.exists("Course Evaluator", {"evaluator": evaluator}):
|
||||||
|
doc = frappe.get_doc("Course Evaluator", evaluator, as_dict=1)
|
||||||
|
else:
|
||||||
|
doc = frappe.new_doc("Course Evaluator")
|
||||||
|
doc.evaluator = evaluator
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"slots": doc.as_dict(),
|
||||||
|
"calendar": calendar.name,
|
||||||
|
"is_authorised": calendar.authorization_code,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"evaluator",
|
"evaluator",
|
||||||
"schedule"
|
"schedule",
|
||||||
|
"unavailability_section",
|
||||||
|
"unavailable_from",
|
||||||
|
"column_break_ahzi",
|
||||||
|
"unavailable_to"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -23,11 +27,30 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Schedule",
|
"label": "Schedule",
|
||||||
"options": "Evaluator Schedule"
|
"options": "Evaluator Schedule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "unavailability_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Unavailability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ahzi",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "unavailable_from",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "unavailable_to",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "To"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-13 11:30:22.641076",
|
"modified": "2024-04-15 18:45:08.614466",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Evaluator",
|
"name": "Course Evaluator",
|
||||||
@@ -66,7 +89,7 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Class Evaluator",
|
"role": "Batch Evaluator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,27 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from lms.lms.utils import get_evaluator
|
from lms.lms.utils import get_evaluator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from frappe.utils import get_time
|
||||||
|
|
||||||
|
|
||||||
class CourseEvaluator(Document):
|
class CourseEvaluator(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_time_slots()
|
self.validate_time_slots()
|
||||||
|
self.validate_unavailability()
|
||||||
|
|
||||||
|
def validate_unavailability(self):
|
||||||
|
if self.unavailable_from and not self.unavailable_to:
|
||||||
|
frappe.throw(_("Unavailable To Date is mandatory if Unavailable From Date is set"))
|
||||||
|
|
||||||
|
if self.unavailable_to and not self.unavailable_from:
|
||||||
|
frappe.throw(_("Unavailable From Date is mandatory if Unavailable To Date is set"))
|
||||||
|
|
||||||
|
if self.unavailable_from >= self.unavailable_to:
|
||||||
|
frappe.throw(_("Unavailable From Date cannot be greater than Unavailable To Date"))
|
||||||
|
|
||||||
def validate_time_slots(self):
|
def validate_time_slots(self):
|
||||||
for schedule in self.schedule:
|
for schedule in self.schedule:
|
||||||
if schedule.start_time >= schedule.end_time:
|
if get_time(schedule.start_time) >= get_time(schedule.end_time):
|
||||||
frappe.throw(_("Start Time cannot be greater than End Time"))
|
frappe.throw(_("Start Time cannot be greater than End Time"))
|
||||||
|
|
||||||
self.validate_overlaps(schedule)
|
self.validate_overlaps(schedule)
|
||||||
@@ -26,11 +38,21 @@ class CourseEvaluator(Document):
|
|||||||
overlap = False
|
overlap = False
|
||||||
|
|
||||||
for slot in same_day_slots:
|
for slot in same_day_slots:
|
||||||
if schedule.start_time <= slot.start_time < schedule.end_time:
|
if (
|
||||||
|
get_time(schedule.start_time)
|
||||||
|
<= get_time(slot.start_time)
|
||||||
|
< get_time(schedule.end_time)
|
||||||
|
):
|
||||||
overlap = True
|
overlap = True
|
||||||
if schedule.start_time < slot.end_time <= schedule.end_time:
|
if (
|
||||||
|
get_time(schedule.start_time)
|
||||||
|
< get_time(slot.end_time)
|
||||||
|
<= get_time(schedule.end_time)
|
||||||
|
):
|
||||||
overlap = True
|
overlap = True
|
||||||
if slot.start_time < schedule.start_time and schedule.end_time < slot.end_time:
|
if get_time(slot.start_time) < get_time(schedule.start_time) and get_time(
|
||||||
|
schedule.end_time
|
||||||
|
) < get_time(slot.end_time):
|
||||||
overlap = True
|
overlap = True
|
||||||
|
|
||||||
if overlap:
|
if overlap:
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"course",
|
"course",
|
||||||
|
"course_title",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"template",
|
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
|
"template",
|
||||||
"issue_date",
|
"issue_date",
|
||||||
"expiry_date",
|
"expiry_date",
|
||||||
"batch_name",
|
"batch_name",
|
||||||
@@ -75,11 +76,18 @@
|
|||||||
"label": "Template",
|
"label": "Template",
|
||||||
"options": "Print Format",
|
"options": "Print Format",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "course.title",
|
||||||
|
"fieldname": "course_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Course Title",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-25 12:20:56.091979",
|
"modified": "2024-04-09 13:42:18.350028",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-18 20:03:27.040073",
|
"modified": "2024-04-15 11:22:43.189908",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Evaluation",
|
"name": "LMS Certificate Evaluation",
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Class Evaluator",
|
"role": "Batch Evaluator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,6 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "course.evaluator",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "evaluator",
|
"fieldname": "evaluator",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Evaluator",
|
"label": "Evaluator",
|
||||||
@@ -109,7 +107,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-09 10:05:13.918890",
|
"modified": "2024-04-16 11:01:28.336807",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
@@ -128,18 +126,6 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Class Evaluator",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
@@ -161,6 +147,18 @@
|
|||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "LMS Student",
|
"role": "LMS Student",
|
||||||
"share": 1
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Batch Evaluator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -11,10 +11,35 @@ from lms.lms.utils import get_evaluator
|
|||||||
|
|
||||||
class LMSCertificateRequest(Document):
|
class LMSCertificateRequest(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.set_evaluator()
|
||||||
|
self.validate_unavailability()
|
||||||
self.validate_slot()
|
self.validate_slot()
|
||||||
self.validate_if_existing_requests()
|
self.validate_if_existing_requests()
|
||||||
self.validate_evaluation_end_date()
|
self.validate_evaluation_end_date()
|
||||||
|
|
||||||
|
def set_evaluator(self):
|
||||||
|
if not self.evaluator:
|
||||||
|
self.evaluator = get_evaluator(self.course, self.batch_name)
|
||||||
|
|
||||||
|
def validate_unavailability(self):
|
||||||
|
unavailable = frappe.db.get_value(
|
||||||
|
"Course Evaluator", self.evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
unavailable.unavailable_from
|
||||||
|
and unavailable.unavailable_to
|
||||||
|
and getdate(self.date) >= unavailable.unavailable_from
|
||||||
|
and getdate(self.date) <= unavailable.unavailable_to
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Evaluator is unavailable from {0} to {1}. Please select a date after {1}"
|
||||||
|
).format(
|
||||||
|
format_date(unavailable.unavailable_from, "medium"),
|
||||||
|
format_date(unavailable.unavailable_to, "medium"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_slot(self):
|
def validate_slot(self):
|
||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
"LMS Certificate Request",
|
"LMS Certificate Request",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"force_profile_completion",
|
"force_profile_completion",
|
||||||
"is_onboarding_complete",
|
"is_onboarding_complete",
|
||||||
"column_break_zdel",
|
"column_break_zdel",
|
||||||
|
"unsplash_access_key",
|
||||||
"livecode_url",
|
"livecode_url",
|
||||||
"course_settings_section",
|
"course_settings_section",
|
||||||
"search_placeholder",
|
"search_placeholder",
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "force_profile_completion",
|
"fieldname": "force_profile_completion",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Force users to complete their Profile"
|
"label": "Force users to complete their Profile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -348,12 +350,17 @@
|
|||||||
"fieldname": "show_day_view",
|
"fieldname": "show_day_view",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Day View in Timetable"
|
"label": "Show Day View in Timetable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "unsplash_access_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Unsplash Access Key"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-12 10:32:13.638368",
|
"modified": "2024-04-16 12:18:14.670978",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
|
|||||||
@@ -577,7 +577,15 @@ def has_course_moderator_role(member=None):
|
|||||||
def has_course_evaluator_role(member=None):
|
def has_course_evaluator_role(member=None):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Has Role",
|
"Has Role",
|
||||||
{"parent": member or frappe.session.user, "role": "Class Evaluator"},
|
{"parent": member or frappe.session.user, "role": "Batch Evaluator"},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_student_role(member=None):
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Has Role",
|
||||||
|
{"parent": member or frappe.session.user, "role": "LMS Student"},
|
||||||
"name",
|
"name",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1782,3 +1790,14 @@ def get_lesson_creation_details(course, chapter, lesson):
|
|||||||
),
|
),
|
||||||
"lesson": lesson_details if lesson_name else None,
|
"lesson": lesson_details if lesson_name else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_roles(name):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
|
return {
|
||||||
|
"moderator": has_course_moderator_role(name),
|
||||||
|
"course_creator": has_course_instructor_role(name),
|
||||||
|
"batch_evaluator": has_course_evaluator_role(name),
|
||||||
|
"lms_student": has_student_role(name),
|
||||||
|
}
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ def get_users(or_filters, start, page_length):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_role(user, role, value):
|
def save_role(user, role, value):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
if cint(value):
|
if cint(value):
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
<meta name="twitter:title" content="{{ meta.title }}" />
|
<meta name="twitter:title" content="{{ meta.title }}" />
|
||||||
<meta name="twitter:image" content="{{ meta.image }}" />
|
<meta name="twitter:image" content="{{ meta.image }}" />
|
||||||
<meta name="twitter:description" content="{{ meta.description }}" />
|
<meta name="twitter:description" content="{{ meta.description }}" />
|
||||||
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-BVArU1VY.js"></script>
|
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-B0I4dIsL.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-h_6W7zSS.js">
|
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-BlL1CpdE.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css">
|
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-B1gEXx4C.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-B5SBTRQU.css">
|
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-wBsCm0D8.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|||||||
43
lms/unsplash.py
Normal file
43
lms/unsplash.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
base_url = "https://api.unsplash.com"
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_keyword(keyword):
|
||||||
|
data = make_unsplash_request(f"/search/photos?query={keyword}")
|
||||||
|
return data.get("results")
|
||||||
|
|
||||||
|
|
||||||
|
def get_list():
|
||||||
|
return make_unsplash_request("/photos")
|
||||||
|
|
||||||
|
|
||||||
|
def get_random(params=None):
|
||||||
|
query_string = ""
|
||||||
|
for key, value in params.items():
|
||||||
|
query_string += f"{key}={value}&"
|
||||||
|
return make_unsplash_request(f"/photos/random?{query_string}")
|
||||||
|
|
||||||
|
|
||||||
|
def make_unsplash_request(path):
|
||||||
|
unsplash_access_key = frappe.db.get_single_value("LMS Settings", "unsplash_access_key")
|
||||||
|
if not unsplash_access_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = f"{base_url}{path}"
|
||||||
|
print(url)
|
||||||
|
res = requests.get(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"Accept-Version": "v1",
|
||||||
|
"Authorization": f"Client-ID {unsplash_access_key}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
res.raise_for_status()
|
||||||
|
data = res.json()
|
||||||
|
return data
|
||||||
Reference in New Issue
Block a user