Merge pull request #1459 from pateljannat/user-persona

chore: identify user persona
This commit is contained in:
Jannat Patel
2025-04-22 17:47:20 +05:30
committed by GitHub
14 changed files with 294 additions and 30 deletions

View File

@@ -100,6 +100,7 @@ jobs:
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 set-password frappe@example.com admin
bench --site lms.test execute lms.lms.utils.persona_captured
- name: cypress pre-requisites
run: |

View File

@@ -13,6 +13,6 @@ module.exports = defineConfig({
openMode: 0,
},
e2e: {
baseUrl: "http://testui:8000",
baseUrl: "http://pertest:8000",
},
});

View File

@@ -19,13 +19,6 @@ describe("Course Creation", () => {
);
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")
.contains("Course Image")
.siblings("div")

View File

@@ -24,7 +24,7 @@ const router = useRouter()
const noSidebar = ref(false)
router.beforeEach((to, from, next) => {
if (to.query.fromLesson) {
if (to.query.fromLesson || to.path === '/persona') {
noSidebar.value = true
} else {
noSidebar.value = false

View File

@@ -76,7 +76,7 @@
/>
<div
class="flex items-center"
class="flex items-center mt-4"
:class="
sidebarStore.isSidebarCollapsed ? 'flex-col space-y-3' : 'flex-row'
"

View File

@@ -322,11 +322,11 @@ const tabsStructure = computed(() => {
icon: 'LogIn',
fields: [
{
label: 'Identify User Persona',
label: 'Identify User Category',
name: 'user_category',
type: 'checkbox',
description:
'Enable this option to identify the user persona during signup.',
'Enable this option to identify the user category during signup.',
},
{
label: 'Disable signup',

View File

@@ -96,6 +96,7 @@
import {
Breadcrumbs,
Button,
call,
createListResource,
FormControl,
Select,
@@ -107,6 +108,7 @@ import { BookOpen, Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import router from '../router'
const user = inject('$user')
const dayjs = inject('$dayjs')
@@ -121,6 +123,7 @@ const currentTab = ref('Live')
const { brand } = sessionStore()
onMounted(() => {
identifyUserPersona()
setFiltersFromQuery()
updateCourses()
categories.value = [
@@ -145,16 +148,52 @@ const courses = createListResource({
pageLength: pageLength.value,
start: start.value,
onSuccess(data) {
let allCategories = data.map((course) => course.category)
allCategories = allCategories.filter(
(category, index) => allCategories.indexOf(category) === index && category
)
if (categories.value.length <= allCategories.length) {
updateCategories(data)
}
setCategories(data)
},
})
const setCategories = (data) => {
let allCategories = data.map((course) => course.category)
allCategories = allCategories.filter(
(category, index) => allCategories.indexOf(category) === index && category
)
if (categories.value.length <= allCategories.length) {
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 = () => {
updateFilters()
courses.update({

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

View File

@@ -210,6 +210,11 @@ const routes = [
name: 'AssignmentSubmissionList',
component: () => import('@/pages/AssignmentSubmissionList.vue'),
},
{
path: '/persona',
name: 'PersonaForm',
component: () => import('@/pages/PersonaForm.vue'),
},
]
let router = createRouter({

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}),
],
server: {
allowedHosts: ['fs', 'bs'],
allowedHosts: ['fs', 'persona'],
},
resolve: {
alias: {

View File

@@ -184,9 +184,10 @@ def get_user_info():
)
user.is_fc_site = is_fc_site()
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:
user.site_info = current_site_info()
user.sitename = frappe.local.site
return user
@@ -678,13 +679,13 @@ def get_categories(doctype, filters):
@frappe.whitelist()
def get_members(start=0, search=""):
"""Get members for the given search term and start index.
Args: start (int): Start index for the query.
Args: start (int): Start index for the query.
<<<<<<< HEAD
search (str): Search term to filter the results.
search (str): Search term to filter the results.
=======
search (str): Search term to filter the results.
search (str): Search term to filter the results.
>>>>>>> 4869bba7bbb2fb38477d6fc29fb3b5838e075577
Returns: List of members.
Returns: List of members.
"""
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
@@ -1389,3 +1390,17 @@ def add_an_evaluator(email):
evaluator.insert()
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

View File

@@ -8,6 +8,7 @@
"general_tab",
"default_home",
"send_calendar_invite_for_evaluations",
"persona_captured",
"column_break_zdel",
"allow_guest_access",
"enable_learning_paths",
@@ -108,7 +109,7 @@
"default": "0",
"fieldname": "user_category",
"fieldtype": "Check",
"label": "Identify User Persona"
"label": "Identify User Category"
},
{
"default": "0",
@@ -380,6 +381,10 @@
"fieldtype": "Attach Image",
"label": "Meta Image"
},
{
"fieldname": "column_break_xijv",
"fieldtype": "Column Break"
},
{
"description": "Common keywords that will be used for all pages",
"fieldname": "meta_keywords",
@@ -387,15 +392,18 @@
"label": "Meta Keywords"
},
{
"fieldname": "column_break_xijv",
"fieldtype": "Column Break"
"default": "0",
"fieldname": "persona_captured",
"fieldtype": "Check",
"label": "Persona Captured",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-04-19 12:19:24.037931",
"modified": "2025-04-22 16:05:27.914422",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Settings",

View File

@@ -2167,3 +2167,7 @@ def get_palette(full_name):
hash_name = hashlib.md5(encoded_name).hexdigest()
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
return palette[idx % 8]
def persona_captured():
frappe.db.set_single_value("LMS Settings", "persona_captured", 1)