feat: read only mode

This commit is contained in:
Jannat Patel
2025-04-30 18:03:00 +05:30
parent 0d32c2a9d9
commit 603e80fd26
26 changed files with 427 additions and 302 deletions

View File

@@ -26,13 +26,8 @@
<a href="{{ meta.link }}">Know More</a>
</div>
</div>
<div id="modals"></div>
<div id="popovers"></div>
<script>
document.getElementById('seo-content').style.display = 'none';
window.csrf_token = '{{ csrf_token }}'
window.read_only_mode = '{{ read_only }}'
</script>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -39,7 +39,11 @@
{{ __('More') }}
</span>
</div>
<Button v-if="isModerator" variant="ghost" @click="openPageModal()">
<Button
v-if="isModerator && !readOnlyMode"
variant="ghost"
@click="openPageModal()"
>
<template #icon>
<Plus class="h-4 w-4 text-ink-gray-7 stroke-1.5" />
</template>
@@ -63,6 +67,16 @@
</div>
</div>
<div class="m-2 flex flex-col gap-1">
<div
v-if="readOnlyMode && !sidebarStore.isSidebarCollapsed"
class="z-10 m-2 bg-surface-modal py-2.5 px-3 text-xs text-ink-gray-7 leading-5 rounded-md"
>
{{
__(
'This site is being updated. You will not be able to make any changes. Full access will be restored shortly.'
)
}}
</div>
<TrialBanner
v-if="
userResource.data?.is_system_manager && userResource.data?.is_fc_site
@@ -89,15 +103,31 @@
: 'flex-row space-x-3'
"
>
<Tooltip v-if="readOnlyMode && sidebarStore.isSidebarCollapsed">
<CircleAlert
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
/>
<template #body>
<div
class="max-w-[30ch] rounded bg-surface-gray-7 px-2 py-1 text-center text-p-xs text-ink-white shadow-xl"
>
{{
__(
'This site is being updated. You will not be able to make any changes. Full access will be restored shortly.'
)
}}
</div>
</template>
</Tooltip>
<Tooltip :text="__('Powered by Learning')">
<Zap
class="size-4 stroke-1.5 text-gray-700 cursor-pointer"
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="redirectToWebsite()"
/>
</Tooltip>
<Tooltip :text="__('Help')">
<CircleHelp
class="size-4 stroke-1.5 text-gray-700 cursor-pointer"
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="
() => {
showHelpModal = minimize ? true : !showHelpModal
@@ -113,7 +143,7 @@
"
>
<CollapseSidebar
class="size-4 text-gray-700 duration-300 stroke-1.5 ease-in-out cursor-pointer"
class="size-4 text-ink-gray-7 duration-300 stroke-1.5 ease-in-out cursor-pointer"
:class="{
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
}"
@@ -166,6 +196,7 @@ import { useRouter } from 'vue-router'
import InviteIcon from './Icons/InviteIcon.vue'
import {
BookOpen,
CircleAlert,
ChevronRight,
Plus,
CircleHelp,
@@ -203,6 +234,7 @@ const currentStep = ref({})
const router = useRouter()
let onboardingDetails
let isOnboardingStepsCompleted = false
const readOnlyMode = window.read_only_mode
const iconProps = {
strokeWidth: 1.5,
width: 16,

View File

@@ -4,7 +4,7 @@
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Assessments') }}
</div>
<Button v-if="canSeeAddButton()" @click="showModal = true">
<Button v-if="canAddAssessments()" @click="showModal = true">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
@@ -100,6 +100,7 @@ import { Plus, Trash2 } from 'lucide-vue-next'
const user = inject('$user')
const showModal = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
batch: {
@@ -181,7 +182,8 @@ const getRowRoute = (row) => {
}
}
const canSeeAddButton = () => {
const canAddAssessments = () => {
if (readOnlyMode) return false
return user.data?.is_moderator || user.data?.is_evaluator
}

View File

@@ -89,6 +89,7 @@ import {
} from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils'
const readOnlyMode = window.read_only_mode
const showCourseModal = ref(false)
const user = inject('$user')
@@ -159,6 +160,9 @@ const removeCourses = (selections, unselectAll) => {
}
const canSeeAddButton = () => {
if (readOnlyMode) {
return false
}
return user.data?.is_moderator || user.data?.is_evaluator
}
</script>

View File

@@ -111,7 +111,6 @@ import {
FormControl,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,

View File

@@ -49,68 +49,70 @@
{{ batch.data.timezone }}
</span>
</div>
<router-link
v-if="isModerator || isStudent"
:to="{
name: 'Batch',
params: {
batchName: batch.data.name,
},
}"
>
<Button variant="solid" class="w-full mt-4">
<span>
{{ isModerator ? __('Manage Batch') : __('Visit Batch') }}
</span>
<div v-if="!readOnlyMode">
<router-link
v-if="isModerator || isStudent"
:to="{
name: 'Batch',
params: {
batchName: batch.data.name,
},
}"
>
<Button variant="solid" class="w-full mt-4">
<span>
{{ isModerator ? __('Manage Batch') : __('Visit Batch') }}
</span>
</Button>
</router-link>
<router-link
:to="{
name: 'Billing',
params: {
type: 'batch',
name: batch.data.name,
},
}"
v-else-if="
batch.data.paid_batch &&
batch.data.seats_left > 0 &&
batch.data.accept_enrollments
"
>
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
<span>
{{ __('Register Now') }}
</span>
</Button>
</router-link>
<Button
variant="solid"
class="w-full mt-2"
v-else-if="
batch.data.allow_self_enrollment &&
batch.data.seats_left &&
batch.data.accept_enrollments
"
@click="enrollInBatch()"
>
{{ __('Enroll Now') }}
</Button>
</router-link>
<router-link
:to="{
name: 'Billing',
params: {
type: 'batch',
name: batch.data.name,
},
}"
v-else-if="
batch.data.paid_batch &&
batch.data.seats_left > 0 &&
batch.data.accept_enrollments
"
>
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
<span>
{{ __('Register Now') }}
</span>
</Button>
</router-link>
<Button
variant="solid"
class="w-full mt-2"
v-else-if="
batch.data.allow_self_enrollment &&
batch.data.seats_left &&
batch.data.accept_enrollments
"
@click="enrollInBatch()"
>
{{ __('Enroll Now') }}
</Button>
<router-link
v-if="isModerator"
:to="{
name: 'BatchForm',
params: {
batchName: batch.data.name,
},
}"
>
<Button class="w-full mt-2">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
<router-link
v-if="isModerator"
:to="{
name: 'BatchForm',
params: {
batchName: batch.data.name,
},
}"
>
<Button class="w-full mt-2">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
</div>
</div>
</template>
<script setup>
@@ -123,7 +125,7 @@ import { useRouter } from 'vue-router'
const router = useRouter()
const user = inject('$user')
const dayjs = inject('$dayjs')
const readOnlyMode = window.read_only_mode
const props = defineProps({
batch: {

View File

@@ -110,7 +110,7 @@
<div class="text-ink-gray-7 font-medium">
{{ __('Students') }}
</div>
<Button @click="openStudentModal()">
<Button v-if="!readOnlyMode" @click="openStudentModal()">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
@@ -247,6 +247,7 @@ const chartData = ref(null)
const chartOptions = ref(null)
const showProgressChart = ref(false)
const assessmentCount = ref(0)
const readOnlyMode = window.read_only_mode
const props = defineProps({
batch: {

View File

@@ -9,89 +9,94 @@
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
{{ course.data.price }}
</div>
<div v-if="course.data.membership" class="space-y-2">
<div v-if="!readOnlyMode">
<div v-if="course.data.membership" class="space-y-2">
<router-link
:to="{
name: 'Lesson',
params: {
courseName: course.name,
chapterNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[0]
: 1,
lessonNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[1]
: 1,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Continue Learning') }}
</span>
</Button>
</router-link>
<CertificationLinks :courseName="course.data.name" class="w-full" />
</div>
<router-link
v-else-if="course.data.paid_course"
:to="{
name: 'Lesson',
name: 'Billing',
params: {
courseName: course.name,
chapterNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[0]
: 1,
lessonNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[1]
: 1,
type: 'course',
name: course.data.name,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Continue Learning') }}
{{ __('Buy this course') }}
</span>
</Button>
</router-link>
<Badge
v-else-if="course.data.disable_self_learning"
theme="blue"
size="lg"
>
{{ __('Contact the Administrator to enroll for this course.') }}
</Badge>
<Button
v-else
@click="enrollStudent()"
variant="solid"
class="w-full"
size="md"
>
<span>
{{ __('Start Learning') }}
</span>
</Button>
<Button
v-if="canGetCertificate"
@click="fetchCertificate()"
variant="subtle"
class="w-full mt-2"
size="md"
>
{{ __('Get Certificate') }}
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
name: 'CourseForm',
params: {
courseName: course.data.name,
},
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
<CertificationLinks :courseName="course.data.name" class="w-full" />
</div>
<router-link
v-else-if="course.data.paid_course"
:to="{
name: 'Billing',
params: {
type: 'course',
name: course.data.name,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Buy this course') }}
</span>
</Button>
</router-link>
<Badge
v-else-if="course.data.disable_self_learning"
theme="blue"
size="lg"
>
{{ __('Contact the Administrator to enroll for this course.') }}
</Badge>
<Button
v-else
@click="enrollStudent()"
variant="solid"
class="w-full"
size="md"
>
<span>
{{ __('Start Learning') }}
</span>
</Button>
<Button
v-if="canGetCertificate"
@click="fetchCertificate()"
variant="subtle"
class="w-full mt-2"
size="md"
>
{{ __('Get Certificate') }}
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
name: 'CourseForm',
params: {
courseName: course.data.name,
},
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
<div class="space-y-4">
<div class="mt-8 font-medium text-ink-gray-9">
<div
class="font-medium text-ink-gray-9"
:class="{ 'mt-8': !readOnlyMode }"
>
{{ __('This course has:') }}
</div>
<div class="flex items-center text-ink-gray-9">
@@ -149,6 +154,7 @@ import CertificationLinks from '@/components/CertificationLinks.vue'
const router = useRouter()
const user = inject('$user')
const readOnlyMode = window.read_only_mode
const props = defineProps({
course: {

View File

@@ -8,18 +8,6 @@
<AppSidebar />
</div>
<div class="w-full overflow-auto" id="scrollContainer">
<div
v-if="readOnlyMode"
class="right-0 top-0 mb-3 bg-surface-gray-2 py-3 text-sm text-ink-gray-5"
>
<div class="mx-auto px-10">
{{
__(
'This site is running in read-only mode. Full functionality will be restored soon.'
)
}}
</div>
</div>
<slot />
</div>
</div>
@@ -28,6 +16,4 @@
</template>
<script setup>
import AppSidebar from './AppSidebar.vue'
const readOnlyMode = window.read_only_mode
</script>

View File

@@ -27,7 +27,9 @@
</span>
</div>
<Dropdown
v-if="user.data.name == reply.owner && !reply.editable"
v-if="
user.data.name == reply.owner && !reply.editable && !readOnlyMode
"
:options="[
{
label: 'Edit',
@@ -71,7 +73,7 @@
</div>
<TextEditor
v-if="renderEditor"
v-if="renderEditor && !readOnlyMode"
class="mt-5"
:content="newReply"
:mentions="mentionUsers"
@@ -80,7 +82,7 @@
:fixedMenu="true"
editorClass="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none border border-outline-gray-2 rounded-b-md min-h-[7rem] py-1 px-2"
/>
<div class="flex justify-between mt-2">
<div v-if="!readOnlyMode" class="flex justify-between mt-2">
<span> </span>
<Button @click="postReply()">
<span>
@@ -105,6 +107,7 @@ const user = inject('$user')
const allUsers = inject('$allUsers')
const mentionUsers = ref([])
const renderEditor = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
topic: {

View File

@@ -1,6 +1,10 @@
<template>
<div>
<Button v-if="!singleThread" class="float-right" @click="openTopicModal()">
<Button
v-if="!singleThread && !readOnlyMode"
class="float-right"
@click="openTopicModal()"
>
{{ __('New {0}').format(singularize(title)) }}
</Button>
<div class="text-xl font-semibold text-ink-gray-9">
@@ -77,6 +81,7 @@ const currentTopic = ref(null)
const socket = inject('$socket')
const user = inject('$user')
const showTopicModal = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
title: {

View File

@@ -3,7 +3,7 @@
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Live Class') }}
</div>
<Button v-if="user.data.is_moderator" @click="openLiveClassModal">
<Button v-if="canCreateClass()" @click="openLiveClassModal">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
@@ -87,6 +87,7 @@ import { formatTime } from '@/utils/'
const user = inject('$user')
const showLiveClassModal = ref(false)
const dayjs = inject('$dayjs')
const readOnlyMode = window.read_only_mode
const props = defineProps({
batch: {
@@ -116,6 +117,11 @@ const liveClasses = createListResource({
const openLiveClassModal = () => {
showLiveClassModal.value = true
}
const canCreateClass = () => {
if (readOnlyMode) return false
return user.data?.is_moderator || user.data?.is_evaluator
}
</script>
<style>
.short-introduction {

View File

@@ -29,4 +29,3 @@ app.provide('$allUsers', allUsers)
app.config.globalProperties.$user = userResource
app.config.globalProperties.$dialog = createDialog
app.config.globalProperties.readOnlyMode = window.read_only_mode

View File

@@ -4,6 +4,7 @@
>
<Breadcrumbs :items="breadcrumbs" />
<Button
v-if="!readOnlyMode"
variant="solid"
@click="
() => {
@@ -38,6 +39,7 @@
showTooltip: false,
selectable: false,
onRowClick: (row) => {
if (readOnlyMode) return
assignmentID = row.name
showAssignmentForm = true
},
@@ -98,6 +100,7 @@ const showAssignmentForm = ref(false)
const assignmentID = ref('new')
const { brand } = sessionStore()
const router = useRouter()
const readOnlyMode = window.read_only_mode
onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {

View File

@@ -11,7 +11,7 @@
>
{{ __('Generate Certificates') }}
</Button>
<Button v-if="user.data?.is_moderator" @click="openAnnouncementModal()">
<Button v-if="canMakeAnnouncement()" @click="openAnnouncementModal()">
<span>
{{ __('Make an Announcement') }}
</span>
@@ -242,6 +242,7 @@ const route = useRoute()
const router = useRouter()
const { brand } = sessionStore()
const tabIndex = ref(0)
const readOnlyMode = window.read_only_mode
const tabs = computed(() => {
let batchTabs = []
@@ -354,6 +355,11 @@ watch(tabIndex, () => {
}
})
const canMakeAnnouncement = () => {
if (readOnlyMode) return false
return user.data?.is_moderator || user.data?.is_evaluator
}
usePageMeta(() => {
return {
title: batch?.data?.title,

View File

@@ -4,7 +4,7 @@
>
<Breadcrumbs :items="breadcrumbs" />
<router-link
v-if="user.data?.is_moderator"
v-if="canCreateBatch()"
:to="{
name: 'BatchForm',
params: { batchName: 'new' },
@@ -124,6 +124,7 @@ const filters = ref({})
const is_student = computed(() => user.data?.is_student)
const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
const orderBy = ref('start_date')
const readOnlyMode = window.read_only_mode
onMounted(() => {
setFiltersFromQuery()
@@ -299,6 +300,12 @@ const batchTabs = computed(() => {
return tabs
})
const canCreateBatch = () => {
if (readOnlyMode) return false
if (user.data?.is_moderator || user.data?.is_instructor) return true
return false
}
const breadcrumbs = computed(() => [
{
label: __('Batches'),

View File

@@ -16,7 +16,10 @@
},
]"
/>
<div v-if="user.data?.name" class="flex items-center space-x-2">
<div
v-if="user.data?.name && !readOnlyMode"
class="flex items-center space-x-2"
>
<router-link
v-if="user.data.name == job.data?.owner"
:to="{
@@ -54,7 +57,7 @@
{{ __('You have applied') }}
</Badge>
</div>
<div v-else>
<div v-else-if="!readOnlyMode">
<Button @click="redirectToLogin(job.data?.name)">
<span>
{{ __('Login to apply') }}
@@ -181,6 +184,7 @@ const user = inject('$user')
const dayjs = inject('$dayjs')
const { brand } = sessionStore()
const showApplicationModal = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
job: {

View File

@@ -16,7 +16,7 @@
},
}"
>
<Button variant="solid">
<Button v-if="!readOnlyMode" variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
@@ -120,6 +120,7 @@ const country = ref(null)
const filters = ref({})
const orFilters = ref({})
const jobCount = ref(0)
const readOnlyMode = window.read_only_mode
onMounted(() => {
let queries = new URLSearchParams(location.search)

View File

@@ -5,7 +5,7 @@
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2">
<Tooltip v-if="lesson.data?.membership" :text="__('Zen Mode')">
<Tooltip v-if="canGoZen()" :text="__('Zen Mode')">
<Button @click="goFullScreen()">
<template #icon>
<Focus class="w-4 h-4 stroke-2" />
@@ -539,6 +539,7 @@ const checkIfDiscussionsAllowed = () => {
}
const allowEdit = () => {
if (window.read_only_mode) return false
if (user.data?.is_moderator) return true
if (lesson.data?.instructors?.includes(user.data?.name)) return true
return false
@@ -574,6 +575,17 @@ const enrollStudent = () => {
)
}
const canGoZen = () => {
if (
user.data?.is_moderator ||
user.data?.is_instructor ||
user.data?.is_evaluator
)
return false
if (lesson.data?.membership) return true
return false
}
const goFullScreen = () => {
if (lessonContainer.value.requestFullscreen) {
lessonContainer.value.requestFullscreen()

View File

@@ -25,7 +25,11 @@
@select="(imageUrl) => coverImage.submit({ url: imageUrl })"
>
<template v-slot="{ togglePopover }">
<Button variant="outline" @click="togglePopover()">
<Button
v-if="!readOnlyMode"
variant="outline"
@click="togglePopover()"
>
<template #prefix>
<Edit class="w-4 h-4 stroke-1.5 text-ink-gray-7" />
</template>
@@ -58,7 +62,7 @@
</div>
</div>
<Button
v-if="isSessionUser()"
v-if="isSessionUser() && !readOnlyMode"
class="mt-3 sm:mt-0 md:ml-auto"
@click="editProfile()"
>
@@ -95,7 +99,7 @@ import {
} from 'frappe-ui'
import { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
import { sessionStore } from '@/stores/session'
import { Edit, icons } from 'lucide-vue-next'
import { Edit } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { useRoute, useRouter } from 'vue-router'
import NoPermission from '@/components/NoPermission.vue'
@@ -109,6 +113,7 @@ const route = useRoute()
const router = useRouter()
const activeTab = ref('')
const showProfileModal = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
username: {

View File

@@ -4,134 +4,150 @@
{{ __('My availability') }}
</h2>
<div class="">
<div
class="grid grid-cols-3 md:grid-cols-4 gap-4 text-sm text-ink-gray-7 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-3 md: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-surface-red-2 hidden group-hover:block"
/>
</div>
<div
class="grid grid-cols-3 md: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-ink-gray-7" />
</template>
{{ __('Add Slot') }}
</Button>
<div
v-if="readOnlyMode"
class="flex items-center space-x-2 text-sm text-ink-gray-7 bg-surface-gray-1 px-3 py-2 rounded-md w-full text-center"
>
<CircleAlert class="size-4 stroke-1.5" />
<span>
{{
__(
'You cannot change the availability when the site is being updated.'
)
}}
</span>
</div>
<div class="my-10">
<h2 class="mb-4 text-lg font-semibold text-ink-gray-9">
{{ __('I am unavailable') }}
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<FormControl
type="date"
:label="__('From')"
v-model="from"
@blur="
() => {
updateUnavailability.submit({
field: 'unavailable_from',
value: from,
})
}
"
/>
<FormControl
type="date"
:label="__('To')"
v-model="to"
@blur="
() => {
updateUnavailability.submit({
field: 'unavailable_to',
value: to,
})
}
"
/>
<div v-else>
<div>
<div
class="grid grid-cols-3 md:grid-cols-4 gap-4 text-sm text-ink-gray-7 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-3 md: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-surface-red-2 hidden group-hover:block"
/>
</div>
<div
class="grid grid-cols-3 md: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-ink-gray-7" />
</template>
{{ __('Add Slot') }}
</Button>
</div>
</div>
<div>
<h2 class="mb-4 text-lg font-semibold text-ink-gray-9">
{{ __('My calendar') }}
</h2>
<div
v-if="evaluator.data?.calendar && evaluator.data?.is_authorized"
class="flex items-center bg-surface-green-2 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 class="my-10">
<h2 class="mb-4 text-lg font-semibold text-ink-gray-9">
{{ __('I am unavailable') }}
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<FormControl
type="date"
:label="__('From')"
v-model="from"
@blur="
() => {
updateUnavailability.submit({
field: 'unavailable_from',
value: from,
})
}
"
/>
<FormControl
type="date"
:label="__('To')"
v-model="to"
@blur="
() => {
updateUnavailability.submit({
field: 'unavailable_to',
value: to,
})
}
"
/>
</div>
</div>
<div>
<h2 class="mb-4 text-lg font-semibold text-ink-gray-9">
{{ __('My calendar') }}
</h2>
<div
v-if="evaluator.data?.calendar && evaluator.data?.is_authorized"
class="flex items-center bg-surface-green-2 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>
<Button @click="() => authorizeCalendar.submit()">
{{ __('Authorize Google Calendar Access') }}
</Button>
</div>
</div>
</template>
<script setup>
import { createResource, FormControl, Button } from 'frappe-ui'
import { createResource, FormControl, Button, Badge } from 'frappe-ui'
import { computed, reactive, ref, onMounted, inject } from 'vue'
import { showToast, convertToTitleCase } from '@/utils'
import { Plus, X, Check } from 'lucide-vue-next'
import { Plus, X, Check, CircleAlert } from 'lucide-vue-next'
const user = inject('$user')
const readOnlyMode = window.read_only_mode
const props = defineProps({
profile: {

View File

@@ -4,6 +4,16 @@
{{ __('Settings') }}
</h2>
<div
v-if="readOnlyMode"
class="flex items-center space-x-2 text-sm text-ink-gray-7 bg-surface-gray-1 px-3 py-2 rounded-md w-full text-center"
>
<CircleAlert class="size-4 stroke-1.5" />
<span>
{{ __('You cannot change the roles in read-only mode.') }}
</span>
</div>
<div
v-else
class="flex flex-col md:flex-row gap-4 md:gap-0 justify-between w-3/4 mt-5"
>
<FormControl
@@ -37,11 +47,13 @@
import { FormControl, createResource } from 'frappe-ui'
import { ref } from 'vue'
import { showToast, convertToTitleCase } from '@/utils'
import { CircleAlert } from 'lucide-vue-next'
const moderator = ref(false)
const course_creator = ref(false)
const batch_evaluator = ref(false)
const lms_student = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
profile: {

View File

@@ -4,7 +4,7 @@
>
<Breadcrumbs :items="breadbrumbs" />
<Button
v-if="user.data?.is_moderator || user.data?.is_instructor"
v-if="canCreateProgram()"
@click="showDialog = true"
variant="solid"
>
@@ -46,7 +46,7 @@
params: { programName: program.name },
}"
>
<Button>
<Button v-if="!readOnlyMode">
<template #prefix>
<Edit class="h-4 w-4 stroke-1.5" />
</template>
@@ -142,6 +142,7 @@ const showDialog = ref(false)
const router = useRouter()
const title = ref('')
const settings = useSettings()
const readOnlyMode = window.read_only_mode
onMounted(() => {
if (
@@ -208,6 +209,12 @@ const lockCourse = (course) => {
return true
}
const canCreateProgram = () => {
if (readOnlyMode) return false
if (user.data?.is_moderator || user.data?.is_instructor) return true
return false
}
const breadbrumbs = computed(() => [
{
label: 'Programs',

View File

@@ -3,7 +3,7 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
<div class="space-x-2">
<div v-if="!readOnlyMode" class="space-x-2">
<router-link
v-if="quizDetails.data?.name"
:to="{
@@ -116,7 +116,7 @@
<div class="font-semibold">
{{ __('Questions') }}
</div>
<Button @click="openQuestionModal()">
<Button v-if="!readOnlyMode" @click="openQuestionModal()">
<template #prefix>
<Plus class="w-4 h-4" />
</template>
@@ -223,6 +223,7 @@ const currentQuestion = reactive({
})
const user = inject('$user')
const router = useRouter()
const readOnlyMode = window.read_only_mode
const props = defineProps({
quizID: {

View File

@@ -4,6 +4,7 @@
>
<Breadcrumbs :items="breadcrumbs" />
<router-link
v-if="!readOnlyMode"
:to="{
name: 'QuizForm',
params: {
@@ -89,6 +90,7 @@ import { sessionStore } from '@/stores/session'
const { brand } = sessionStore()
const user = inject('$user')
const router = useRouter()
const readOnlyMode = window.read_only_mode
onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {

View File

@@ -8,6 +8,10 @@ no_cache = 1
def get_context():
context = frappe._dict()
context.boot = get_boot()
frappe.db.commit()
app_path = frappe.form_dict.get("app_path")
favicon = (
frappe.db.get_single_value("Website Settings", "favicon")
@@ -15,19 +19,24 @@ def get_context():
)
title = frappe.db.get_single_value("Website Settings", "app_name") or "Frappe Learning"
csrf_token = frappe.sessions.get_csrf_token()
frappe.db.commit()
context = frappe._dict()
context.csrf_token = csrf_token
context.read_only = frappe.flags.read_only
context.meta = get_meta(app_path, title, favicon)
capture("active_site", "lms")
context.title = title
context.favicon = favicon
capture("active_site", "lms")
return context
def get_boot():
return frappe._dict(
{
"frappe_version": frappe.__version__,
"read_only_mode": frappe.flags.read_only,
"csrf_token": frappe.sessions.get_csrf_token(),
}
)
def get_meta(app_path, title, favicon):
meta = frappe._dict()
if app_path: