Merge pull request #1459 from pateljannat/user-persona
chore: identify user persona
This commit is contained in:
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
@@ -100,6 +100,7 @@ jobs:
|
|||||||
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
|
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
|
||||||
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
|
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
|
||||||
bench --site lms.test set-password frappe@example.com admin
|
bench --site lms.test set-password frappe@example.com admin
|
||||||
|
bench --site lms.test execute lms.lms.utils.persona_captured
|
||||||
|
|
||||||
- name: cypress pre-requisites
|
- name: cypress pre-requisites
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://testui:8000",
|
baseUrl: "http://pertest:8000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ describe("Course Creation", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
cy.fixture("profile.png", "base64").then((fileContent) => {
|
cy.fixture("profile.png", "base64").then((fileContent) => {
|
||||||
/* cy.get('input[type="file"]').should("be.hidden").attachFile({
|
|
||||||
fileContent,
|
|
||||||
fileName: "profile.png",
|
|
||||||
mimeType: "image/png",
|
|
||||||
encoding: "base64",
|
|
||||||
}); */
|
|
||||||
|
|
||||||
cy.get("div")
|
cy.get("div")
|
||||||
.contains("Course Image")
|
.contains("Course Image")
|
||||||
.siblings("div")
|
.siblings("div")
|
||||||
|
|||||||
Submodule frappe-ui updated: 29307e4fff...8cd9b06a5e
@@ -24,7 +24,7 @@ const router = useRouter()
|
|||||||
const noSidebar = ref(false)
|
const noSidebar = ref(false)
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (to.query.fromLesson) {
|
if (to.query.fromLesson || to.path === '/persona') {
|
||||||
noSidebar.value = true
|
noSidebar.value = true
|
||||||
} else {
|
} else {
|
||||||
noSidebar.value = false
|
noSidebar.value = false
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center"
|
class="flex items-center mt-4"
|
||||||
:class="
|
:class="
|
||||||
sidebarStore.isSidebarCollapsed ? 'flex-col space-y-3' : 'flex-row'
|
sidebarStore.isSidebarCollapsed ? 'flex-col space-y-3' : 'flex-row'
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -322,11 +322,11 @@ const tabsStructure = computed(() => {
|
|||||||
icon: 'LogIn',
|
icon: 'LogIn',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Identify User Persona',
|
label: 'Identify User Category',
|
||||||
name: 'user_category',
|
name: 'user_category',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
description:
|
description:
|
||||||
'Enable this option to identify the user persona during signup.',
|
'Enable this option to identify the user category during signup.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Disable signup',
|
label: 'Disable signup',
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
|
call,
|
||||||
createListResource,
|
createListResource,
|
||||||
FormControl,
|
FormControl,
|
||||||
Select,
|
Select,
|
||||||
@@ -107,6 +108,7 @@ import { BookOpen, Plus } from 'lucide-vue-next'
|
|||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { canCreateCourse } from '@/utils'
|
import { canCreateCourse } from '@/utils'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
@@ -121,6 +123,7 @@ const currentTab = ref('Live')
|
|||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
identifyUserPersona()
|
||||||
setFiltersFromQuery()
|
setFiltersFromQuery()
|
||||||
updateCourses()
|
updateCourses()
|
||||||
categories.value = [
|
categories.value = [
|
||||||
@@ -145,6 +148,11 @@ const courses = createListResource({
|
|||||||
pageLength: pageLength.value,
|
pageLength: pageLength.value,
|
||||||
start: start.value,
|
start: start.value,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
setCategories(data)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const setCategories = (data) => {
|
||||||
let allCategories = data.map((course) => course.category)
|
let allCategories = data.map((course) => course.category)
|
||||||
allCategories = allCategories.filter(
|
allCategories = allCategories.filter(
|
||||||
(category, index) => allCategories.indexOf(category) === index && category
|
(category, index) => allCategories.indexOf(category) === index && category
|
||||||
@@ -152,8 +160,39 @@ const courses = createListResource({
|
|||||||
if (categories.value.length <= allCategories.length) {
|
if (categories.value.length <= allCategories.length) {
|
||||||
updateCategories(data)
|
updateCategories(data)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
const isPersonaCaptured = async () => {
|
||||||
|
let persona = await call('frappe.client.get_single_value', {
|
||||||
|
doctype: 'LMS Settings',
|
||||||
|
field: 'persona_captured',
|
||||||
|
})
|
||||||
|
return persona
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifyUserPersona = async () => {
|
||||||
|
let personaCaptured = await isPersonaCaptured()
|
||||||
|
debugger
|
||||||
|
console.log('personaCaptured', personaCaptured)
|
||||||
|
console.log('user.data?.is_system_manager', user.data?.is_system_manager)
|
||||||
|
console.log('user.data?.developer_mode', user.data?.developer_mode)
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.data?.is_system_manager &&
|
||||||
|
!user.data?.developer_mode &&
|
||||||
|
!personaCaptured
|
||||||
|
) {
|
||||||
|
call('frappe.client.get_count', {
|
||||||
|
doctype: 'LMS Course',
|
||||||
|
}).then((data) => {
|
||||||
|
if (!data) {
|
||||||
|
router.push({
|
||||||
|
name: 'PersonaForm',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateCourses = () => {
|
const updateCourses = () => {
|
||||||
updateFilters()
|
updateFilters()
|
||||||
|
|||||||
199
frontend/src/pages/PersonaForm.vue
Normal file
199
frontend/src/pages/PersonaForm.vue
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-screen overflow-hidden sm:bg-gray-50">
|
||||||
|
<div class="relative h-full z-10 mx-auto pt-8 sm:w-max sm:pt-32">
|
||||||
|
<div class="mx-auto flex items-center justify-center space-x-2">
|
||||||
|
<LMSLogo class="size-7" />
|
||||||
|
<span
|
||||||
|
class="select-none text-xl font-semibold tracking-tight text-gray-900"
|
||||||
|
>
|
||||||
|
Learning
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx-auto space-y-5 w-full h-fit bg-white px-4 py-8 sm:mt-6 sm:w-96 sm:rounded-lg sm:px-8 sm:shadow-xl"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-700 mb-2">
|
||||||
|
{{ __('1. What best describes your role?') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="persona.role"
|
||||||
|
type="select"
|
||||||
|
:options="roleOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-700 mb-2">
|
||||||
|
{{ __('2. How many students are you planning to teach?') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="persona.noOfStudents"
|
||||||
|
type="select"
|
||||||
|
:options="noOfStudentsOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-700 mb-2">
|
||||||
|
{{ __('3. What is your main use case for Frappe Learning?') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="persona.useCase"
|
||||||
|
type="select"
|
||||||
|
:options="useCaseOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-700 mb-2">
|
||||||
|
{{ __('4. Are you currently using any Frappe products?') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="persona.frappeProducts"
|
||||||
|
type="select"
|
||||||
|
:options="frappeProductsOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<Button variant="solid" class="mx-auto" @click="submitPersona()">
|
||||||
|
{{ __('Submit and Continue') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-center absolute bottom-0 right-0 left-0 mx-auto cursor-pointer text-sm pb-4"
|
||||||
|
@click="skipPersonaForm()"
|
||||||
|
>
|
||||||
|
{{ __('Skip') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||||
|
import { Button, call, FormControl, usePageMeta } from 'frappe-ui'
|
||||||
|
import { computed, inject, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { sessionStore } from '@/stores/session'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
const router = useRouter()
|
||||||
|
const { brand } = sessionStore()
|
||||||
|
console.log(user.data?.sitename)
|
||||||
|
|
||||||
|
const persona = reactive({
|
||||||
|
role: null,
|
||||||
|
noOfStudents: null,
|
||||||
|
useCase: null,
|
||||||
|
frappeProducts: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitPersona = () => {
|
||||||
|
let responses = {
|
||||||
|
site: user.data?.sitename,
|
||||||
|
role: persona.role,
|
||||||
|
no_of_students: persona.noOfStudents,
|
||||||
|
use_case: persona.useCase,
|
||||||
|
frappe_products: persona.frappeProducts,
|
||||||
|
}
|
||||||
|
call('lms.lms.api.capture_user_persona', {
|
||||||
|
responses: JSON.stringify(responses),
|
||||||
|
}).then(() => {
|
||||||
|
router.push({
|
||||||
|
name: 'Courses',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipPersonaForm = () => {
|
||||||
|
call('frappe.client.set_value', {
|
||||||
|
doctype: 'LMS Settings',
|
||||||
|
name: null,
|
||||||
|
fieldname: 'persona_captured',
|
||||||
|
value: 1,
|
||||||
|
}).then(() => {
|
||||||
|
router.push({
|
||||||
|
name: 'Courses',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
'Trainer / Instructor',
|
||||||
|
'Freelancer / Consultant',
|
||||||
|
'HR / L&D Professional',
|
||||||
|
'School / University Admin',
|
||||||
|
'Software Developer',
|
||||||
|
'Community Manager',
|
||||||
|
'Business Owner / Team Lead',
|
||||||
|
'Other',
|
||||||
|
]
|
||||||
|
|
||||||
|
return options.map((option) => ({
|
||||||
|
label: option,
|
||||||
|
value: option,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const noOfStudentsOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
'Less than 50',
|
||||||
|
'50-200',
|
||||||
|
'200-1000',
|
||||||
|
'1000+',
|
||||||
|
'Not sure yet',
|
||||||
|
]
|
||||||
|
|
||||||
|
return options.map((option) => ({
|
||||||
|
label: option,
|
||||||
|
value: option,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const useCaseOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
'Teaching students in a school/university',
|
||||||
|
'Training employees in my company',
|
||||||
|
'Onboarding and educating my users/community',
|
||||||
|
'Selling courses and earning income',
|
||||||
|
'Other',
|
||||||
|
]
|
||||||
|
|
||||||
|
return options.map((option) => ({
|
||||||
|
label: option,
|
||||||
|
value: option,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const frappeProductsOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
'Frappe Framework',
|
||||||
|
'ERPNext / Frappe HR',
|
||||||
|
'Frappe CRM / Helpdesk',
|
||||||
|
'Custom Frappe App',
|
||||||
|
'Other',
|
||||||
|
'Not using any Frappe product',
|
||||||
|
]
|
||||||
|
|
||||||
|
return options.map((option) => ({
|
||||||
|
label: option,
|
||||||
|
value: option,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
usePageMeta(() => {
|
||||||
|
return {
|
||||||
|
title: 'Persona',
|
||||||
|
icon: brand.favicon,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -210,6 +210,11 @@ const routes = [
|
|||||||
name: 'AssignmentSubmissionList',
|
name: 'AssignmentSubmissionList',
|
||||||
component: () => import('@/pages/AssignmentSubmissionList.vue'),
|
component: () => import('@/pages/AssignmentSubmissionList.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/persona',
|
||||||
|
name: 'PersonaForm',
|
||||||
|
component: () => import('@/pages/PersonaForm.vue'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let router = createRouter({
|
let router = createRouter({
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['fs', 'bs'],
|
allowedHosts: ['fs', 'persona'],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -184,9 +184,10 @@ def get_user_info():
|
|||||||
)
|
)
|
||||||
user.is_fc_site = is_fc_site()
|
user.is_fc_site = is_fc_site()
|
||||||
user.is_system_manager = "System Manager" in user.roles
|
user.is_system_manager = "System Manager" in user.roles
|
||||||
|
user.sitename = frappe.local.site
|
||||||
|
user.developer_mode = frappe.conf.developer_mode
|
||||||
if user.is_fc_site and user.is_system_manager:
|
if user.is_fc_site and user.is_system_manager:
|
||||||
user.site_info = current_site_info()
|
user.site_info = current_site_info()
|
||||||
user.sitename = frappe.local.site
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -1389,3 +1390,17 @@ def add_an_evaluator(email):
|
|||||||
evaluator.insert()
|
evaluator.insert()
|
||||||
|
|
||||||
return evaluator
|
return evaluator
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def capture_user_persona(responses):
|
||||||
|
frappe.only_for("System Manager")
|
||||||
|
data = frappe.parse_json(responses)
|
||||||
|
data = json.dumps(data)
|
||||||
|
response = frappe.integrations.utils.make_post_request(
|
||||||
|
"https://school.frappe.io/api/method/capture-persona",
|
||||||
|
data={"response": data},
|
||||||
|
)
|
||||||
|
if response.get("message").get("name"):
|
||||||
|
frappe.db.set_single_value("LMS Settings", "persona_captured", True)
|
||||||
|
return response
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"general_tab",
|
"general_tab",
|
||||||
"default_home",
|
"default_home",
|
||||||
"send_calendar_invite_for_evaluations",
|
"send_calendar_invite_for_evaluations",
|
||||||
|
"persona_captured",
|
||||||
"column_break_zdel",
|
"column_break_zdel",
|
||||||
"allow_guest_access",
|
"allow_guest_access",
|
||||||
"enable_learning_paths",
|
"enable_learning_paths",
|
||||||
@@ -108,7 +109,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "user_category",
|
"fieldname": "user_category",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Identify User Persona"
|
"label": "Identify User Category"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -380,6 +381,10 @@
|
|||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"label": "Meta Image"
|
"label": "Meta Image"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xijv",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Common keywords that will be used for all pages",
|
"description": "Common keywords that will be used for all pages",
|
||||||
"fieldname": "meta_keywords",
|
"fieldname": "meta_keywords",
|
||||||
@@ -387,15 +392,18 @@
|
|||||||
"label": "Meta Keywords"
|
"label": "Meta Keywords"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_xijv",
|
"default": "0",
|
||||||
"fieldtype": "Column Break"
|
"fieldname": "persona_captured",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Persona Captured",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-04-19 12:19:24.037931",
|
"modified": "2025-04-22 16:05:27.914422",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
|
|||||||
@@ -2167,3 +2167,7 @@ def get_palette(full_name):
|
|||||||
hash_name = hashlib.md5(encoded_name).hexdigest()
|
hash_name = hashlib.md5(encoded_name).hexdigest()
|
||||||
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
|
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
|
||||||
return palette[idx % 8]
|
return palette[idx % 8]
|
||||||
|
|
||||||
|
|
||||||
|
def persona_captured():
|
||||||
|
frappe.db.set_single_value("LMS Settings", "persona_captured", 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user