Merge pull request #1479 from pateljannat/read-only

feat: read only mode
This commit is contained in:
Jannat Patel
2025-04-30 18:33:22 +05:30
committed by GitHub
27 changed files with 437 additions and 288 deletions

View File

@@ -26,12 +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 }}'
</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,6 +49,7 @@
{{ batch.data.timezone }}
</span>
</div>
<div v-if="!readOnlyMode">
<router-link
v-if="isModerator || isStudent"
:to="{
@@ -112,6 +113,7 @@
</Button>
</router-link>
</div>
</div>
</template>
<script setup>
import { inject, computed } from 'vue'
@@ -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,6 +9,7 @@
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
{{ course.data.price }}
</div>
<div v-if="!readOnlyMode">
<div v-if="course.data.membership" class="space-y-2">
<router-link
:to="{
@@ -90,8 +91,12 @@
</span>
</Button>
</router-link>
</div>
<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

@@ -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

@@ -26,5 +26,6 @@ app.mount('#app')
const { userResource, allUsers } = usersStore()
app.provide('$user', userResource)
app.provide('$allUsers', allUsers)
app.config.globalProperties.$user = userResource
app.config.globalProperties.$dialog = createDialog

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

@@ -121,6 +121,7 @@ const certification = ref(false)
const filters = ref({})
const currentTab = ref('Live')
const { brand } = sessionStore()
const readOnlyMode = window.read_only_mode
onMounted(() => {
identifyUserPersona()

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" />
@@ -95,7 +95,7 @@
</div>
<div class="flex items-center space-x-2 mt-2 md:mt-0">
<Button v-if="zenModeEnabled" @click="showDiscussionsInZenMode">
<Button v-if="zenModeEnabled" @click="showDiscussionsInZenMode()">
<template #icon>
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" />
</template>
@@ -424,7 +424,7 @@ const setupLesson = (data) => {
if (!editor.value && data.body) {
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
hasQuiz.value = quizRegex.test(data.body)
if (!hasQuiz.value) allowDiscussions.value = true
if (!hasQuiz.value && !zenModeEnabled) allowDiscussions.value = true
}
}
@@ -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,7 +4,21 @@
{{ __('My availability') }}
</h2>
<div class="">
<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 v-else>
<div>
<div
class="grid grid-cols-3 md:grid-cols-4 gap-4 text-sm text-ink-gray-7 mb-4"
>
@@ -124,14 +138,16 @@
</Button>
</div>
</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

@@ -16,6 +16,8 @@ import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table'
import { usersStore } from '../stores/user'
const readOnlyMode = window.read_only_mode
export function createToast(options) {
toast({
position: 'bottom-right',
@@ -543,5 +545,8 @@ export const escapeHTML = (text) => {
export const canCreateCourse = () => {
const { userResource } = usersStore()
return userResource.data?.is_instructor || userResource.data?.is_moderator
return (
!readOnlyMode &&
(userResource.data?.is_instructor || userResource.data?.is_moderator)
)
}

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,18 +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.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: