feat: discussions in batches
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<div class="flex items-center mb-5">
|
<div v-if="!singleThread" class="flex items-center mb-5">
|
||||||
<Button variant="outline" @click="showTopics = true">
|
<Button variant="outline" @click="showTopics = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ChevronLeft class="w-5 h-5 stroke-1.5 text-gray-700" />
|
<ChevronLeft class="w-5 h-5 stroke-1.5 text-gray-700" />
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
:fixedMenu="reply.editable || false"
|
:fixedMenu="reply.editable || false"
|
||||||
:editorClass="
|
:editorClass="
|
||||||
reply.editable
|
reply.editable
|
||||||
? 'prose-sm max-w-none border-b border-x rounded-b-md py-1 px-2 min-h-[4rem]'
|
? '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'
|
||||||
: 'prose-sm'
|
: 'prose-sm'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
@change="(val) => (newReply = val)"
|
@change="(val) => (newReply = val)"
|
||||||
placeholder="Type your reply here..."
|
placeholder="Type your reply here..."
|
||||||
:fixedMenu="true"
|
:fixedMenu="true"
|
||||||
editorClass="prose-sm max-w-none min-h-[7rem] border-b border-x rounded-b-md py-1 px-2"
|
editorClass="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 border border-gray-300 rounded-b-md min-h-[7rem] py-1 px-2"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-between mt-2">
|
<div class="flex justify-between mt-2">
|
||||||
<span> </span>
|
<span> </span>
|
||||||
@@ -105,6 +105,10 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
singleThread: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -156,7 +160,7 @@ const postReply = () => {
|
|||||||
newReply.value = ''
|
newReply.value = ''
|
||||||
replies.reload()
|
replies.reload()
|
||||||
},
|
},
|
||||||
onError() {
|
onError(err) {
|
||||||
createToast({
|
createToast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
text: err.messages?.[0] || err,
|
text: err.messages?.[0] || err,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Button class="float-right" @click="openTopicModal()">
|
<Button v-if="!singleThread" class="float-right" @click="openTopicModal()">
|
||||||
{{ __('New {0}').format(title) }}
|
{{ __('New {0}').format(title) }}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="topics.data?.length">
|
<div v-if="topics.data?.length && !singleThread">
|
||||||
<div v-if="showTopics" v-for="(topic, index) in topics.data">
|
<div v-if="showTopics" v-for="(topic, index) in topics.data">
|
||||||
<div
|
<div
|
||||||
@click="showReplies(topic)"
|
@click="showReplies(topic)"
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="singleThread && topics.data">
|
||||||
|
<DiscussionReplies :topic="topics.data" :singleThread="singleThread" />
|
||||||
|
</div>
|
||||||
<div v-else class="flex justify-center border mt-5 p-5 rounded-md">
|
<div v-else class="flex justify-center border mt-5 p-5 rounded-md">
|
||||||
<MessageSquareIcon class="w-10 h-10 stroke-1.5 text-gray-800 mr-2" />
|
<MessageSquareIcon class="w-10 h-10 stroke-1.5 text-gray-800 mr-2" />
|
||||||
<div>
|
<div>
|
||||||
@@ -57,13 +60,13 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { createResource, Button, TextEditor } from 'frappe-ui'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { timeAgo } from '../utils'
|
import { timeAgo } from '../utils'
|
||||||
import { ref, onMounted, inject } from 'vue'
|
import { ref, onMounted, inject } from 'vue'
|
||||||
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
||||||
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
||||||
import { MessageSquareIcon, MessagesSquare } from 'lucide-vue-next'
|
import { MessageSquareIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
const showTopics = ref(true)
|
const showTopics = ref(true)
|
||||||
const currentTopic = ref(null)
|
const currentTopic = ref(null)
|
||||||
@@ -91,6 +94,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'Be the first to start a discussion',
|
default: 'Be the first to start a discussion',
|
||||||
},
|
},
|
||||||
|
singleThread: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -106,6 +113,7 @@ const topics = createResource({
|
|||||||
return {
|
return {
|
||||||
doctype: props.doctype,
|
doctype: props.doctype,
|
||||||
docname: props.docname,
|
docname: props.docname,
|
||||||
|
single_thread: props.singleThread,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-full">
|
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-full">
|
||||||
<div class="border-r-2">
|
<div class="border-r-2">
|
||||||
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
||||||
<template #tab="{ tab, selected }">
|
<template #tab="{ tab, selected }" class="overflow-x-hidden">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="group -mb-px flex items-center gap-1 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
class="group -mb-px flex items-center gap-1 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
||||||
@@ -80,6 +80,15 @@
|
|||||||
<div v-else-if="tab.label == 'Announcements'">
|
<div v-else-if="tab.label == 'Announcements'">
|
||||||
<Announcements :batch="batch.data.name" />
|
<Announcements :batch="batch.data.name" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tab.label == 'Discussions'">
|
||||||
|
<Discussions
|
||||||
|
doctype="LMS Batch"
|
||||||
|
:docname="batch.data.name"
|
||||||
|
title="Discussions"
|
||||||
|
:key="batch.data.name"
|
||||||
|
:singleThread="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -168,6 +177,7 @@ import {
|
|||||||
Contact2,
|
Contact2,
|
||||||
Mail,
|
Mail,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
|
MessageCircle,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { formatTime } from '@/utils'
|
import { formatTime } from '@/utils'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
@@ -177,6 +187,7 @@ import BatchStudents from '@/components/BatchStudents.vue'
|
|||||||
import Assessments from '@/components/Assessments.vue'
|
import Assessments from '@/components/Assessments.vue'
|
||||||
import Announcements from '@/components/Annoucements.vue'
|
import Announcements from '@/components/Annoucements.vue'
|
||||||
import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue'
|
import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue'
|
||||||
|
import Discussions from '@/components/Discussions.vue'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -199,17 +210,23 @@ const batch = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
return [
|
let crumbs = [{ label: 'All Batches', route: { name: 'Batches' } }]
|
||||||
{ label: 'All Batches', route: { name: 'Batches' } },
|
if (!isStudent.value) {
|
||||||
{
|
crumbs.push({
|
||||||
label: 'Batch Details',
|
label: batch.data?.title,
|
||||||
route: { name: 'BatchDetail', params: { batchName: props.batchName } },
|
route: {
|
||||||
|
name: 'BatchDetail',
|
||||||
|
params: {
|
||||||
|
batchName: batch.data?.name,
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
crumbs.push({
|
||||||
label: batch?.data?.title,
|
label: batch?.data?.title,
|
||||||
route: { name: 'Batch', params: { batchName: props.batchName } },
|
route: { name: 'Batch', params: { batchName: props.batchName } },
|
||||||
},
|
})
|
||||||
]
|
return crumbs
|
||||||
})
|
})
|
||||||
|
|
||||||
const isStudent = computed(() => {
|
const isStudent = computed(() => {
|
||||||
@@ -256,6 +273,11 @@ tabs.push({
|
|||||||
icon: Mail,
|
icon: Mail,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
label: 'Discussions',
|
||||||
|
icon: MessageCircle,
|
||||||
|
})
|
||||||
|
|
||||||
const courses = createResource({
|
const courses = createResource({
|
||||||
url: 'lms.lms.utils.get_batch_courses',
|
url: 'lms.lms.utils.get_batch_courses',
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -89,8 +89,11 @@ import { formatTime } from '../utils'
|
|||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import BatchOverlay from '@/components/BatchOverlay.vue'
|
import BatchOverlay from '@/components/BatchOverlay.vue'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
|
const user = inject('$user')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batchName: {
|
batchName: {
|
||||||
@@ -106,6 +109,16 @@ const batch = createResource({
|
|||||||
batch: props.batchName,
|
batch: props.batchName,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess(data) {
|
||||||
|
if (data.students?.includes(user.data.name)) {
|
||||||
|
router.push({
|
||||||
|
name: 'Batch',
|
||||||
|
params: {
|
||||||
|
batchName: props.batchName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const courses = createResource({
|
const courses = createResource({
|
||||||
|
|||||||
@@ -85,7 +85,9 @@
|
|||||||
{{ course.data.instructors.length - 1 }} others
|
{{ course.data.instructors.length - 1 }} others
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-content mt-6">
|
<div
|
||||||
|
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 mt-6"
|
||||||
|
>
|
||||||
<div v-if="lesson.data.youtube">
|
<div v-if="lesson.data.youtube">
|
||||||
<iframe
|
<iframe
|
||||||
class="youtube-video"
|
class="youtube-video"
|
||||||
|
|||||||
@@ -79,7 +79,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Breadcrumbs } from 'frappe-ui'
|
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { Line } from 'vue-chartjs'
|
|
||||||
import {
|
import {
|
||||||
BookOpen,
|
BookOpen,
|
||||||
LogIn,
|
LogIn,
|
||||||
|
|||||||
@@ -1619,7 +1619,17 @@ def get_batch_students(batch):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_discussion_topics(doctype, docname):
|
def get_discussion_topics(doctype, docname, single_thread):
|
||||||
|
if single_thread:
|
||||||
|
filters = {
|
||||||
|
"reference_doctype": doctype,
|
||||||
|
"reference_docname": docname,
|
||||||
|
}
|
||||||
|
topic = frappe.db.exists("Discussion Topic", filters)
|
||||||
|
if topic:
|
||||||
|
return frappe.db.get_value("Discussion Topic", topic, ["name"], as_dict=1)
|
||||||
|
else:
|
||||||
|
return create_discussion_topic(doctype, docname)
|
||||||
topics = frappe.get_all(
|
topics = frappe.get_all(
|
||||||
"Discussion Topic",
|
"Discussion Topic",
|
||||||
{
|
{
|
||||||
@@ -1638,6 +1648,18 @@ def get_discussion_topics(doctype, docname):
|
|||||||
return topics
|
return topics
|
||||||
|
|
||||||
|
|
||||||
|
def create_discussion_topic(doctype, docname):
|
||||||
|
doc = frappe.new_doc("Discussion Topic")
|
||||||
|
doc.update(
|
||||||
|
{
|
||||||
|
"reference_doctype": doctype,
|
||||||
|
"reference_docname": docname,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.insert()
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_discussion_replies(topic):
|
def get_discussion_replies(topic):
|
||||||
replies = frappe.get_all(
|
replies = frappe.get_all(
|
||||||
|
|||||||
@@ -83,3 +83,4 @@ lms.patches.v1_0.create_batch_source
|
|||||||
lms.patches.v1_0.batch_tabs_settings
|
lms.patches.v1_0.batch_tabs_settings
|
||||||
execute:frappe.delete_doc("Notification", "Assignment Submission Notification")
|
execute:frappe.delete_doc("Notification", "Assignment Submission Notification")
|
||||||
lms.patches.v1_0.change_jobs_url #17-01-2024
|
lms.patches.v1_0.change_jobs_url #17-01-2024
|
||||||
|
lms.patches.v1_0.custom_perm_for_discussions #14-01-2024
|
||||||
42
lms/patches/v1_0/custom_perm_for_discussions.py
Normal file
42
lms/patches/v1_0/custom_perm_for_discussions.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
roles = ["LMS Student", "Moderator", "Course Creator", "Class Evaluator"]
|
||||||
|
for role in roles:
|
||||||
|
add_perm_for_discussion_topic(role)
|
||||||
|
add_perm_for_discussion_reply(role)
|
||||||
|
|
||||||
|
|
||||||
|
def add_perm_for_discussion_topic(role):
|
||||||
|
topic_roles = frappe.permissions.get_doctype_roles("Discussion Topic")
|
||||||
|
if role in topic_roles:
|
||||||
|
return
|
||||||
|
|
||||||
|
topic_perm = frappe.new_doc("Custom DocPerm")
|
||||||
|
topic_perm.parent = "Discussion Topic"
|
||||||
|
topic_perm.role = role
|
||||||
|
topic_perm.if_owner = 1
|
||||||
|
topic_perm.read = 1
|
||||||
|
topic_perm.write = 1
|
||||||
|
topic_perm.create = 1
|
||||||
|
topic_perm.delete = 1
|
||||||
|
topic_perm.insert()
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def add_perm_for_discussion_reply(role):
|
||||||
|
reply_roles = frappe.permissions.get_doctype_roles("Discussion Reply")
|
||||||
|
if role in reply_roles:
|
||||||
|
return
|
||||||
|
|
||||||
|
reply_perm = frappe.new_doc("Custom DocPerm")
|
||||||
|
reply_perm.parent = "Discussion Reply"
|
||||||
|
reply_perm.role = role
|
||||||
|
reply_perm.if_owner = 1
|
||||||
|
reply_perm.read = 1
|
||||||
|
reply_perm.write = 1
|
||||||
|
reply_perm.create = 1
|
||||||
|
reply_perm.delete = 1
|
||||||
|
reply_perm.insert()
|
||||||
|
frappe.db.commit()
|
||||||
Reference in New Issue
Block a user