Compare commits

..

26 Commits

Author SHA1 Message Date
frappe-pr-bot
01f41372c1 chore: update POT file 2024-08-30 16:04:05 +00:00
Jannat Patel
e07aae3fb0 Merge pull request #997 from pateljannat/issues-33
fix: slides rendering issue
2024-08-29 19:26:08 +05:30
Jannat Patel
65d628ffc0 fix: slides rendering issue 2024-08-29 11:10:43 +05:30
Jannat Patel
bf290bbf0a Merge pull request #994 from akhilnarang/fix-user-creation
fix(overrides): call parent's `after_insert()` as well
2024-08-27 14:56:11 +05:30
Akhil Narang
3c9059025b fix(overrides): call parent's after_insert() as well
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2024-08-27 14:08:35 +05:30
Jannat Patel
4b0413720b Merge pull request #993 from pateljannat/quiz-submission-issue
fix: quiz submission report issue
2024-08-27 11:58:14 +05:30
Jannat Patel
f8b4ff4bd3 fix: quiz submission report issue 2024-08-27 10:46:06 +05:30
Jannat Patel
3b8ff171f4 Merge pull request #989 from frappe/pot_develop_2024-08-23
chore: update POT file
2024-08-26 14:45:59 +05:30
frappe-pr-bot
dec270a10b chore: update POT file 2024-08-23 16:04:00 +00:00
Jannat Patel
152a339c4e Merge pull request #986 from pateljannat/app-switcher
feat: App switcher
2024-08-23 12:40:16 +05:30
Jannat Patel
395fe700e0 fix: removed switch to desk 2024-08-23 12:22:11 +05:30
Jannat Patel
ec25e895dc feat: app switcher 2024-08-23 12:21:22 +05:30
Frappe PR Bot
e02e4c7ab4 chore(release): Bumped to Version 2.3.0 2024-08-21 05:20:24 +00:00
Jannat Patel
e69cc9af1a Merge pull request #980 from pateljannat/member-addition
feat: Add users from the portal
2024-08-19 14:11:59 +05:30
Jannat Patel
98b8464e1a fix: ui test 2024-08-19 13:07:48 +05:30
Jannat Patel
0170fcc111 Merge pull request #968 from frappe/pot_develop_2024-08-16
chore: update POT file
2024-08-19 13:04:21 +05:30
Jannat Patel
0be5439e81 fix: tests 2024-08-19 12:31:46 +05:30
Jannat Patel
63f857b8fc fix: linters 2024-08-19 12:12:51 +05:30
Jannat Patel
a3b8ed8f91 fix: documented the api 2024-08-19 12:03:32 +05:30
Jannat Patel
cdd46667f3 feat: add new member 2024-08-19 11:47:17 +05:30
frappe-pr-bot
2f8acea988 chore: update POT file 2024-08-16 16:04:13 +00:00
Jannat Patel
75f0e5b9f1 feat: search member 2024-08-16 20:59:51 +05:30
Jannat Patel
ce51129e84 feat: member list 2024-08-16 11:26:11 +05:30
Jannat Patel
86aa8b0a2a Merge pull request #967 from pateljannat/issues-32
fix: settings ui
2024-08-14 12:47:31 +05:30
Jannat Patel
aeae62a45c chore: linters 2024-08-14 12:35:56 +05:30
Jannat Patel
6b12df44a0 fix: settings ui 2024-08-14 12:12:13 +05:30
16 changed files with 521 additions and 264 deletions

View File

@@ -99,6 +99,7 @@ jobs:
cd ~/frappe-bench/ cd ~/frappe-bench/
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
- name: cypress pre-requisites - name: cypress pre-requisites
run: | run: |

View File

@@ -0,0 +1,67 @@
<template>
<Popover placement="right-start" class="flex w-full">
<template #target="{ togglePopover }">
<button
:class="[
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-gray-800 hover:bg-gray-100',
]"
@click.prevent="togglePopover()"
>
<div class="flex gap-2">
<LayoutGrid class="size-4 stroke-1.5" />
<span class="whitespace-nowrap">
{{ __('Apps') }}
</span>
</div>
<ChevronRight class="h-4 w-4 stroke-1.5" />
</button>
</template>
<template #body>
<div
class="grid grid-cols-3 justify-between mx-3 p-2 rounded-lg border border-gray-100 bg-white shadow-xl"
>
<div v-for="app in apps.data" key="name">
<a
:href="app.route"
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-3 hover:bg-gray-100"
>
<img class="size-8" :src="app.logo" />
<div class="text-sm" @click="app.onClick">
{{ app.title }}
</div>
</a>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import { Popover, createResource } from 'frappe-ui'
import { LayoutGrid, ChevronRight } from 'lucide-vue-next'
const apps = createResource({
url: 'frappe.apps.get_apps',
cache: 'apps',
auto: true,
transform: (data) => {
let _apps = [
{
name: 'frappe',
logo: '/assets/lms/images/desk.png',
title: __('Desk'),
route: '/app',
},
]
data.map((app) => {
if (app.name === 'lms') return
_apps.push({
name: app.name,
logo: app.logo,
title: __(app.title),
route: app.route,
})
})
return _apps
},
})
</script>

View File

@@ -0,0 +1,183 @@
<template>
<div class="text-base p-4">
<div class="flex items-center justify-between">
<div>
<div class="font-semibold mb-1">
{{ __(label) }}
</div>
<div class="text-xs text-gray-600">
{{ __(description) }}
</div>
</div>
<div class="flex item-center space-x-2">
<FormControl
v-model="search"
:placeholder="__('Search')"
type="text"
:debounce="300"
/>
<Button @click="() => (showForm = true)">
<template #icon>
<Plus class="h-3 w-3 stroke-1.5" />
</template>
</Button>
</div>
</div>
<div class="my-4">
<!-- Form to add new member -->
<div v-if="showForm" class="flex items-center space-x-2 mb-4">
<FormControl
v-model="member.email"
:placeholder="__('Email')"
type="email"
class="w-full"
/>
<FormControl
v-model="member.first_name"
:placeholder="__('First Name')"
type="test"
class="w-full"
/>
<Button @click="addMember()" variant="subtle">
{{ __('Add') }}
</Button>
</div>
<!-- Member list -->
<div
v-for="member in memberList"
class="grid grid-cols-5 grid-flow-row py-2 cursor-pointer"
>
<div
@click="openProfile(member.username)"
class="flex items-center space-x-2 col-span-2"
>
<Avatar
:image="member.user_image"
:label="member.full_name"
size="sm"
/>
<div>
{{ member.full_name }}
</div>
</div>
<div class="text-sm text-gray-700 col-span-2">
{{ member.name }}
</div>
<div class="text-sm text-gray-700 justify-self-end">
{{ getRole(member.role) }}
</div>
</div>
</div>
<div v-if="hasNextPage" class="flex justify-center">
<Button variant="solid" @click="members.reload()">
<template #prefix>
<RefreshCw class="h-3 w-3 stroke-1.5" />
</template>
{{ __('Load More') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch, reactive } from 'vue'
import { RefreshCw, Plus } from 'lucide-vue-next'
const router = useRouter()
const show = defineModel('show')
const search = ref('')
const start = ref(0)
const memberList = ref([])
const hasNextPage = ref(false)
const showForm = ref(false)
const member = reactive({
email: '',
first_name: '',
})
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
show: {
type: Boolean,
},
})
const members = createResource({
url: 'lms.lms.api.get_members',
makeParams: () => {
return {
search: search.value,
start: start.value,
}
},
onSuccess(data) {
memberList.value = memberList.value.concat(data)
start.value = start.value + 20
hasNextPage.value = data.length === 20
},
auto: true,
})
const openProfile = (username) => {
show.value = false
router.push({
name: 'Profile',
params: {
username: username,
},
})
}
const newMember = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'User',
first_name: member.first_name,
email: member.email,
},
}
},
auto: false,
onSuccess(data) {
show.value = false
router.push({
name: 'Profile',
params: {
username: data.username,
},
})
},
})
const addMember = () => {
newMember.reload()
}
watch(search, () => {
memberList.value = []
start.value = 0
members.reload()
})
const getRole = (role) => {
const map = {
'LMS Student': 'Student',
'Course Creator': 'Instructor',
Moderator: 'Moderator',
'Batch Evaluator': 'Evaluator',
}
return map[role]
}
</script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<Dialog v-model="show" :options="{ size: '6xl' }"> <Dialog v-model="show" :options="{ size: '3xl' }">
<template #body> <template #body>
<div class="flex h-[calc(100vh_-_8rem)]"> <div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2"> <div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
@@ -28,10 +28,21 @@
</nav> </nav>
</div> </div>
</div> </div>
<div class="flex flex-1 flex-col overflow-y-auto"> <div
<SettingDetails
v-if="activeTab && data.doc" v-if="activeTab && data.doc"
class="flex flex-1 flex-col overflow-y-auto"
>
<Members
v-if="activeTab.label === 'Members'"
:label="activeTab.label"
:description="activeTab.description"
v-model:show="show"
/>
<SettingDetails
v-else
:fields="activeTab.fields" :fields="activeTab.fields"
:label="activeTab.label"
:description="activeTab.description"
:data="data" :data="data"
/> />
</div> </div>
@@ -44,6 +55,7 @@ import { Dialog, createDocumentResource } from 'frappe-ui'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import SettingDetails from '../SettingDetails.vue' import SettingDetails from '../SettingDetails.vue'
import SidebarLink from '@/components/SidebarLink.vue' import SidebarLink from '@/components/SidebarLink.vue'
import Members from '@/components/Members.vue'
const show = defineModel() const show = defineModel()
const doctype = ref('LMS Settings') const doctype = ref('LMS Settings')
@@ -63,9 +75,16 @@ const tabs = computed(() => {
label: 'Settings', label: 'Settings',
hideLabel: true, hideLabel: true,
items: [ items: [
{
label: 'Members',
description: 'Manage the members of your learning system',
icon: 'UserRoundPlus',
},
{ {
label: 'Payment Gateway', label: 'Payment Gateway',
icon: 'DollarSign', icon: 'DollarSign',
description:
'Configure the payment gateway and other payment related settings',
fields: [ fields: [
{ {
label: 'Razorpay Key', label: 'Razorpay Key',
@@ -97,7 +116,7 @@ const tabs = computed(() => {
type: 'checkbox', type: 'checkbox',
}, },
{ {
label: 'Apply rounding on equivalent amount', label: 'Apply rounding on equivalent',
name: 'apply_rounding', name: 'apply_rounding',
type: 'checkbox', type: 'checkbox',
}, },
@@ -105,62 +124,6 @@ const tabs = computed(() => {
}, },
], ],
}, },
{
label: 'Settings',
hideLabel: true,
items: [
{
label: 'Signup',
icon: 'LogIn',
fields: [
{
label: 'Show terms of use on signup page',
name: 'terms_of_use',
type: 'checkbox',
},
{
label: 'Terms of Use Page',
name: 'terms_page',
type: 'Link',
doctype: 'Web Page',
},
{
label: 'Ask user category during signup',
name: 'user_category',
type: 'checkbox',
},
{
type: 'Column Break',
},
{
label: 'Show privacy policy on signup page',
name: 'privacy_policy',
type: 'checkbox',
},
{
label: 'Privacy Policy Page',
name: 'privacy_policy_page',
type: 'Link',
doctype: 'Web Page',
},
{
type: 'Column Break',
},
{
label: 'Show cookie policy on signup page',
name: 'cookie_policy',
type: 'checkbox',
},
{
label: 'Cookie Policy Page',
name: 'cookie_policy_page',
type: 'Link',
doctype: 'Web Page',
},
],
},
],
},
{ {
label: 'Settings', label: 'Settings',
hideLabel: true, hideLabel: true,
@@ -168,6 +131,7 @@ const tabs = computed(() => {
{ {
label: 'Sidebar', label: 'Sidebar',
icon: 'PanelLeftIcon', icon: 'PanelLeftIcon',
description: 'Customize the sidebar as per your needs',
fields: [ fields: [
{ {
label: 'Courses', label: 'Courses',
@@ -213,6 +177,7 @@ const tabs = computed(() => {
{ {
label: 'Email Templates', label: 'Email Templates',
icon: 'MailPlus', icon: 'MailPlus',
description: 'Create email templates with the content you want',
fields: [ fields: [
{ {
label: 'Batch Confirmation Template', label: 'Batch Confirmation Template',
@@ -236,17 +201,61 @@ const tabs = computed(() => {
}, },
], ],
}, },
/* { {
label: 'Settings', label: 'Settings',
hideLabel: true, hideLabel: true,
items: [ items: [
{ {
label: 'Members', label: 'Signup',
icon: "UserRoundPlus", icon: 'LogIn',
component: markRaw(MemberSettings), description:
'Customize the signup page to inform users about your terms and policies',
fields: [
{
label: 'Show terms of use on signup',
name: 'terms_of_use',
type: 'checkbox',
},
{
label: 'Terms of Use Page',
name: 'terms_page',
type: 'Link',
doctype: 'Web Page',
},
{
label: 'Show privacy policy on signup',
name: 'privacy_policy',
type: 'checkbox',
},
{
label: 'Privacy Policy Page',
name: 'privacy_policy_page',
type: 'Link',
doctype: 'Web Page',
},
{
type: 'Column Break',
},
{
label: 'Show cookie policy on signup',
name: 'cookie_policy',
type: 'checkbox',
},
{
label: 'Cookie Policy Page',
name: 'cookie_policy_page',
type: 'Link',
doctype: 'Web Page',
},
{
label: 'Ask user category during signup',
name: 'user_category',
type: 'checkbox',
}, },
], ],
}, */ },
],
},
] ]
return _tabs.map((tab) => { return _tabs.map((tab) => {

View File

@@ -439,7 +439,7 @@ const checkAnswer = () => {
const addToLocalStorage = () => { const addToLocalStorage = () => {
let quizData = JSON.parse(localStorage.getItem(quiz.data.title)) let quizData = JSON.parse(localStorage.getItem(quiz.data.title))
let questionData = { let questionData = {
question_index: activeQuestion.value, question_name: currentQuestion.value,
answer: getAnswers().join(), answer: getAnswers().join(),
is_correct: showAnswers.filter((answer) => { is_correct: showAnswers.filter((answer) => {
return answer != undefined return answer != undefined

View File

@@ -1,9 +1,17 @@
<template> <template>
<div class="flex flex-col justify-between h-full p-8"> <div class="flex flex-col justify-between h-full p-4">
<div class="flex space-x-10"> <div>
<div class="font-semibold mb-1">
{{ __(label) }}
</div>
<div class="text-xs text-gray-600">
{{ __(description) }}
</div>
</div>
<div class="flex space-x-8 my-5">
<div v-for="(column, index) in columns" :key="index"> <div v-for="(column, index) in columns" :key="index">
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4 w-60">
<div v-for="field in column" :class="width"> <div v-for="field in column">
<Link <Link
v-if="field.type == 'Link'" v-if="field.type == 'Link'"
v-model="field.value" v-model="field.value"
@@ -31,11 +39,9 @@
<script setup> <script setup>
import { FormControl, Button } from 'frappe-ui' import { FormControl, Button } from 'frappe-ui'
import { computed, ref } from 'vue' import { computed } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
let width = ref('w-full')
const props = defineProps({ const props = defineProps({
fields: { fields: {
type: Array, type: Array,
@@ -45,6 +51,13 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
label: {
type: String,
required: true,
},
description: {
type: String,
},
}) })
const columns = computed(() => { const columns = computed(() => {
@@ -71,12 +84,6 @@ const columns = computed(() => {
cols.push(currentColumn) cols.push(currentColumn)
} }
if (cols.length == 3) {
width.value = 'w-64'
} else {
width.value = 'w-96'
}
return cols return cols
}) })

View File

@@ -66,6 +66,7 @@
import LMSLogo from '@/components/Icons/LMSLogo.vue' import LMSLogo from '@/components/Icons/LMSLogo.vue'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { Dropdown } from 'frappe-ui' import { Dropdown } from 'frappe-ui'
import Apps from '@/components/Apps.vue'
import { import {
ChevronDown, ChevronDown,
LogIn, LogIn,
@@ -77,7 +78,7 @@ import {
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { convertToTitleCase } from '../utils' import { convertToTitleCase } from '../utils'
import { usersStore } from '@/stores/user' import { usersStore } from '@/stores/user'
import { ref } from 'vue' import { ref, markRaw } from 'vue'
import SettingsModal from '@/components/Modals/Settings.vue' import SettingsModal from '@/components/Modals/Settings.vue'
const router = useRouter() const router = useRouter()
@@ -105,11 +106,7 @@ const userDropdownOptions = [
}, },
}, },
{ {
icon: ArrowRightLeft, component: markRaw(Apps),
label: 'Switch to Desk',
onClick: () => {
window.location.href = '/app'
},
condition: () => { condition: () => {
let cookies = new URLSearchParams(document.cookie.split('; ').join('&')) let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
let system_user = cookies.get('system_user') let system_user = cookies.get('system_user')

View File

@@ -233,9 +233,9 @@ export function getEditorTools() {
}, },
github: true, github: true,
slides: { slides: {
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/, regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/,
embedUrl: embedUrl:
'https://docs.google.com/presentation/d/e/<%= remote_id %>/embed', 'https://docs.google.com/presentation/d/<%= remote_id %>/embed',
html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>", html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>",
}, },
drive: { drive: {

View File

@@ -1 +1 @@
__version__ = "2.2.0" __version__ = "2.3.0"

View File

@@ -4,9 +4,11 @@ app_name = "frappe_lms"
app_title = "Frappe LMS" app_title = "Frappe LMS"
app_publisher = "Frappe" app_publisher = "Frappe"
app_description = "Frappe LMS App" app_description = "Frappe LMS App"
app_icon = "octicon octicon-file-directory" app_icon_url = "/assets/lms/images/lms-logo.png"
app_icon_title = "Learning"
app_icon_route = "/lms"
app_color = "grey" app_color = "grey"
app_email = "school@frappe.io" app_email = "jannat@frappe.io"
app_license = "AGPL" app_license = "AGPL"
# Includes in <head> # Includes in <head>
@@ -61,8 +63,6 @@ web_include_js = ["website.bundle.js"]
after_install = "lms.install.after_install" after_install = "lms.install.after_install"
after_sync = "lms.install.after_sync" after_sync = "lms.install.after_sync"
before_uninstall = "lms.install.before_uninstall" before_uninstall = "lms.install.before_uninstall"
setup_wizard_requires = "assets/lms/js/setup_wizard.js" setup_wizard_requires = "assets/lms/js/setup_wizard.js"
# Desk Notifications # Desk Notifications
@@ -231,3 +231,13 @@ profile_url_prefix = "/users/"
signup_form_template = "lms.plugins.show_custom_signup" signup_form_template = "lms.plugins.show_custom_signup"
on_session_creation = "lms.overrides.user.on_session_creation" on_session_creation = "lms.overrides.user.on_session_creation"
add_to_apps_screen = [
{
"name": "lms",
"logo": "/assets/lms/images/lms-logo.png",
"title": "Learning",
"route": "/lms",
"has_permission": "lms.lms.api.check_app_permission",
}
]

View File

@@ -562,3 +562,51 @@ def get_categories(doctype, filters):
categoryOptions.append({"label": category, "value": category}) categoryOptions.append({"label": category, "value": category})
return categoryOptions return categoryOptions
@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.
search (str): Search term to filter the results.
Returns: List of members.
"""
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
if search:
filters["full_name"] = ["like", f"%{search}%"]
members = frappe.get_all(
"User",
filters=filters,
fields=["name", "full_name", "user_image", "username"],
page_length=20,
start=start,
)
for member in members:
roles = frappe.get_roles(member.name)
if "Moderator" in roles:
member.role = "Moderator"
elif "Course Creator" in roles:
member.role = "Course Creator"
elif "Batch Evaluator" in roles:
member.role = "Batch Evaluator"
elif "LMS Student" in roles:
member.role = "LMS Student"
return members
def check_app_permission():
"""Check if the user has permission to access the app."""
if frappe.session.user == "Administrator":
return True
roles = frappe.get_roles()
lms_roles = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"]
if any(role in roles for role in lms_roles):
return True
return False

View File

@@ -11,74 +11,4 @@ from lms.lms.doctype.invite_request.invite_request import (
class TestInviteRequest(unittest.TestCase): class TestInviteRequest(unittest.TestCase):
@classmethod pass
def setUpClass(self):
create_invite_request("test_invite@example.com")
def test_create_invite_request(self):
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
invite = frappe.db.get_value(
"Invite Request",
filters={"invite_email": "test_invite@example.com"},
fieldname=["invite_email", "status", "signup_email"],
as_dict=True,
)
self.assertEqual(invite.status, "Approved")
self.assertEqual(invite.signup_email, None)
def test_create_invite_request_update(self):
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
data = {
"signup_email": "test_invite@example.com",
"username": "test_invite",
"full_name": "Test Invite",
"password": "Test@invite",
"invite_code": frappe.db.get_value(
"Invite Request", {"invite_email": "test_invite@example.com"}, "name"
),
}
update_invite(data)
invite = frappe.db.get_value(
"Invite Request",
filters={"invite_email": "test_invite@example.com"},
fieldname=[
"invite_email",
"status",
"signup_email",
"full_name",
"username",
"invite_code",
"name",
],
as_dict=True,
)
self.assertEqual(invite.signup_email, "test_invite@example.com")
self.assertEqual(invite.full_name, "Test Invite")
self.assertEqual(invite.username, "test_invite")
self.assertEqual(invite.invite_code, invite.name)
self.assertEqual(invite.status, "Registered")
user = frappe.db.get_value(
"User",
"test_invite@example.com",
fieldname=["first_name", "username", "send_welcome_email", "user_type"],
as_dict=True,
)
self.assertTrue(user)
self.assertEqual(user.first_name, invite.full_name.split(" ")[0])
self.assertEqual(user.username, invite.username)
self.assertEqual(user.send_welcome_email, 0)
self.assertEqual(user.user_type, "Website User")
@classmethod
def tearDownClass(self):
if frappe.db.exists("User", "test_invite@example.com"):
frappe.delete_doc("User", "test_invite@example.com")
invite_request = frappe.db.exists(
"Invite Request", {"invite_email": "test_invite@example.com"}
)
if invite_request:
frappe.delete_doc("Invite Request", invite_request)

View File

@@ -90,21 +90,19 @@ def quiz_summary(quiz, results):
question_details = frappe.db.get_value( question_details = frappe.db.get_value(
"LMS Quiz Question", "LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"]}, {"parent": quiz, "question": result["question_name"]},
["question", "marks"], ["question", "marks", "question_detail"],
as_dict=1, as_dict=1,
) )
result["question_name"] = question_details.question result["question_name"] = question_details.question
result["question"] = frappe.db.get_value( result["question"] = question_details.question_detail
"LMS Question", question_details.question, "question"
)
marks = question_details.marks if correct else 0 marks = question_details.marks if correct else 0
result["marks"] = marks result["marks"] = marks
score += marks score += marks
del result["question_index"] del result["question_name"]
quiz_details = frappe.db.get_value( quiz_details = frappe.db.get_value(
"LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1 "LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1
@@ -297,15 +295,6 @@ def check_choice_answers(question, answers):
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
""" if question_details.multiple:
correct_answers = [ question_details[f"option_{num}"] for num in range(1,5) if question_details[f"is_correct_{num}"]]
print(answers)
for ans in correct_answers:
if ans not in answers:
is_correct.append(0)
else:
is_correct.append(1)
else: """
for num in range(1, 5): for num in range(1, 5):
if question_details[f"option_{num}"] in answers: if question_details[f"option_{num}"] in answers:
is_correct.append(question_details[f"is_correct_{num}"]) is_correct.append(question_details[f"is_correct_{num}"])

View File

@@ -6,11 +6,11 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Frappe LMS VERSION\n" "Project-Id-Version: Frappe LMS VERSION\n"
"Report-Msgid-Bugs-To: school@frappe.io\n" "Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2024-08-09 16:04+0000\n" "POT-Creation-Date: 2024-08-30 16:04+0000\n"
"PO-Revision-Date: 2024-08-09 16:04+0000\n" "PO-Revision-Date: 2024-08-30 16:04+0000\n"
"Last-Translator: school@frappe.io\n" "Last-Translator: jannat@frappe.io\n"
"Language-Team: school@frappe.io\n" "Language-Team: jannat@frappe.io\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@@ -20,6 +20,46 @@ msgstr ""
msgid " Please evaluate and grade it." msgid " Please evaluate and grade it."
msgstr "" msgstr ""
#. Paragraph text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<a href=\"/app/lms-settings/LMS%20Settings\">LMS Settings</a>"
msgstr ""
#. Paragraph text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<a href=\"/app/web-page/new-web-page-1\">Setup a Home Page</a>"
msgstr ""
#. Paragraph text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<a href=\"/lms/courses\">Visit LMS Portal</a>"
msgstr ""
#. Paragraph text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<a href=\"/lms/courses/new/edit\">Create a Course</a>"
msgstr ""
#. Paragraph text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<a href=\"https://docs.frappe.io/learning\">Documentation</a>"
msgstr ""
#. Header text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<span class=\"h4\"><b>Get Started</b></span>"
msgstr ""
#. Header text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<span class=\"h4\"><b>Master</b></span>"
msgstr ""
#. Header text in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>"
msgstr ""
#. Label of the verify_terms (Check) field in DocType 'User' #. Label of the verify_terms (Check) field in DocType 'User'
#: fixtures/custom_field.json #: fixtures/custom_field.json
msgid "Acceptance for Terms and/or Policies" msgid "Acceptance for Terms and/or Policies"
@@ -48,7 +88,7 @@ msgstr ""
msgid "Add a Lesson" msgid "Add a Lesson"
msgstr "" msgstr ""
#: lms/doctype/lms_question/lms_question.py:59 #: lms/doctype/lms_question/lms_question.py:60
msgid "Add at least one possible answer for this question: {0}" msgid "Add at least one possible answer for this question: {0}"
msgstr "" msgstr ""
@@ -98,12 +138,7 @@ msgstr ""
msgid "Allow accessing future dates" msgid "Allow accessing future dates"
msgstr "" msgstr ""
#. Label of the allow_student_progress (Check) field in DocType 'LMS Settings' #: overrides/user.py:199
#: lms/doctype/lms_settings/lms_settings.json
msgid "Allow students to see each others progress in class"
msgstr ""
#: overrides/user.py:195
msgid "Already Registered" msgid "Already Registered"
msgstr "" msgstr ""
@@ -136,12 +171,6 @@ msgstr ""
msgid "Answer" msgid "Answer"
msgstr "" msgstr ""
#. Option for the 'Course Creation Access Through Website To' (Select) field in
#. DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Anyone"
msgstr ""
#. Label of the apply_gst (Check) field in DocType 'LMS Settings' #. Label of the apply_gst (Check) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json #: lms/doctype/lms_settings/lms_settings.json
msgid "Apply GST for India" msgid "Apply GST for India"
@@ -233,7 +262,7 @@ msgstr ""
msgid "Assignment will appear at the bottom of the lesson." msgid "Assignment will appear at the bottom of the lesson."
msgstr "" msgstr ""
#: lms/doctype/lms_question/lms_question.py:41 #: lms/doctype/lms_question/lms_question.py:42
msgid "At least one option must be correct for this question." msgid "At least one option must be correct for this question."
msgstr "" msgstr ""
@@ -850,11 +879,6 @@ msgstr ""
msgid "Course Content" msgid "Course Content"
msgstr "" msgstr ""
#. Label of the portal_course_creation (Select) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course Creation Access Through Website To"
msgstr ""
#. Name of a role #. Name of a role
#: lms/doctype/lms_course/lms_course.json #: lms/doctype/lms_course/lms_course.json
#: lms/doctype/lms_question/lms_question.json #: lms/doctype/lms_question/lms_question.json
@@ -862,12 +886,6 @@ msgstr ""
msgid "Course Creator" msgid "Course Creator"
msgstr "" msgstr ""
#. Option for the 'Course Creation Access Through Website To' (Select) field in
#. DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course Creator Role"
msgstr ""
#. Label of a Card Break in the LMS Workspace #. Label of a Card Break in the LMS Workspace
#: lms/workspace/lms/lms.json #: lms/workspace/lms/lms.json
msgid "Course Data" msgid "Course Data"
@@ -892,11 +910,6 @@ msgstr ""
msgid "Course List" msgid "Course List"
msgstr "" msgstr ""
#. Label of the search_placeholder (Data) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course List Search Bar Placeholder"
msgstr ""
#: lms/report/course_progress_summary/course_progress_summary.py:58 #: lms/report/course_progress_summary/course_progress_summary.py:58
msgid "Course Name" msgid "Course Name"
msgstr "" msgstr ""
@@ -912,10 +925,7 @@ msgid "Course Progress Summary"
msgstr "" msgstr ""
#. Label of the section_break_7 (Section Break) field in DocType 'LMS Course' #. Label of the section_break_7 (Section Break) field in DocType 'LMS Course'
#. Label of the course_settings_section (Section Break) field in DocType 'LMS
#. Settings'
#: lms/doctype/lms_course/lms_course.json #: lms/doctype/lms_course/lms_course.json
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course Settings" msgid "Course Settings"
msgstr "" msgstr ""
@@ -1116,7 +1126,7 @@ msgstr ""
msgid "Dream Companies" msgid "Dream Companies"
msgstr "" msgstr ""
#: lms/doctype/lms_question/lms_question.py:31 #: lms/doctype/lms_question/lms_question.py:32
msgid "Duplicate options found for this question." msgid "Duplicate options found for this question."
msgstr "" msgstr ""
@@ -1254,7 +1264,7 @@ msgstr ""
msgid "Enter the correct answer" msgid "Enter the correct answer"
msgstr "" msgstr ""
#: lms/utils.py:1088 #: lms/utils.py:1081
msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}" msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
msgstr "" msgstr ""
@@ -1539,6 +1549,7 @@ msgid "Here are a few courses we recommend for you to get started with {0}"
msgstr "" msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:6 #: lms/notification/certificate_request_creation/certificate_request_creation.html:6
#: templates/emails/certificate_request_notification.html:1
msgid "Hey {0}" msgid "Hey {0}"
msgstr "" msgstr ""
@@ -1738,7 +1749,7 @@ msgstr ""
msgid "Invalid Start or End Time." msgid "Invalid Start or End Time."
msgstr "" msgstr ""
#: lms/utils.py:932 #: lms/utils.py:925
msgid "Invalid document provided." msgid "Invalid document provided."
msgstr "" msgstr ""
@@ -2328,7 +2339,6 @@ msgstr ""
#. Label of the mentor_request_section (Section Break) field in DocType 'LMS #. Label of the mentor_request_section (Section Break) field in DocType 'LMS
#. Settings' #. Settings'
#. Label of the mentor_request_tab (Tab Break) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json #: lms/doctype/lms_settings/lms_settings.json
msgid "Mentor Request" msgid "Mentor Request"
msgstr "" msgstr ""
@@ -2385,11 +2395,11 @@ msgstr ""
msgid "Modified By" msgid "Modified By"
msgstr "" msgstr ""
#: lms/api.py:188 #: lms/api.py:189
msgid "Module Name is incorrect or does not exist." msgid "Module Name is incorrect or does not exist."
msgstr "" msgstr ""
#: lms/api.py:184 #: lms/api.py:185
msgid "Module is incorrect." msgid "Module is incorrect."
msgstr "" msgstr ""
@@ -2430,11 +2440,11 @@ msgstr ""
msgid "New Sign Up" msgid "New Sign Up"
msgstr "" msgstr ""
#: lms/utils.py:619 #: lms/utils.py:612
msgid "New comment in batch {0}" msgid "New comment in batch {0}"
msgstr "" msgstr ""
#: lms/utils.py:612 #: lms/utils.py:605
msgid "New reply on the topic {0} in course {1}" msgid "New reply on the topic {0} in course {1}"
msgstr "" msgstr ""
@@ -2471,11 +2481,6 @@ msgstr ""
msgid "No courses under review" msgid "No courses under review"
msgstr "" msgstr ""
#: templates/search_course/search_course.html:61
#: templates/search_course/search_course.js:47
msgid "No result found"
msgstr ""
#: templates/course_list.html:13 #: templates/course_list.html:13
msgid "No {0}" msgid "No {0}"
msgstr "" msgstr ""
@@ -2484,7 +2489,7 @@ msgstr ""
msgid "No." msgid "No."
msgstr "" msgstr ""
#: overrides/user.py:190 #: overrides/user.py:194
msgid "Not Allowed" msgid "Not Allowed"
msgstr "" msgstr ""
@@ -2719,7 +2724,7 @@ msgstr ""
msgid "Payment for Document Type" msgid "Payment for Document Type"
msgstr "" msgstr ""
#: lms/utils.py:949 #: lms/utils.py:942
msgid "Payment for {0} course" msgid "Payment for {0} course"
msgstr "" msgstr ""
@@ -2749,11 +2754,11 @@ msgstr ""
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations." msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
msgstr "" msgstr ""
#: overrides/user.py:236 #: overrides/user.py:240
msgid "Please ask your administrator to verify your sign-up" msgid "Please ask your administrator to verify your sign-up"
msgstr "" msgstr ""
#: overrides/user.py:234 #: overrides/user.py:238
msgid "Please check your email for verification" msgid "Please check your email for verification"
msgstr "" msgstr ""
@@ -2778,12 +2783,13 @@ msgstr ""
msgid "Please enter your answer" msgid "Please enter your answer"
msgstr "" msgstr ""
#: lms/api.py:180 #: lms/api.py:181
msgid "Please login to continue with payment." msgid "Please login to continue with payment."
msgstr "" msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:9 #: lms/notification/certificate_request_creation/certificate_request_creation.html:9
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:8 #: lms/notification/certificate_request_reminder/certificate_request_reminder.html:8
#: templates/emails/certificate_request_notification.html:4
msgid "Please prepare well and be on time for the evaluations." msgid "Please prepare well and be on time for the evaluations."
msgstr "" msgstr ""
@@ -2952,6 +2958,11 @@ msgstr ""
msgid "Question " msgid "Question "
msgstr "" msgstr ""
#. Label of the question_detail (Text) field in DocType 'LMS Quiz Question'
#: lms/doctype/lms_quiz_question/lms_quiz_question.json
msgid "Question Detail"
msgstr ""
#. Label of the question_name (Link) field in DocType 'LMS Quiz Result' #. Label of the question_name (Link) field in DocType 'LMS Quiz Result'
#: lms/doctype/lms_quiz_result/lms_quiz_result.json #: lms/doctype/lms_quiz_result/lms_quiz_result.json
msgid "Question Name" msgid "Question Name"
@@ -3048,7 +3059,7 @@ msgstr ""
msgid "Registered" msgid "Registered"
msgstr "" msgstr ""
#: overrides/user.py:197 #: overrides/user.py:201
msgid "Registered but disabled" msgid "Registered but disabled"
msgstr "" msgstr ""
@@ -3214,11 +3225,6 @@ msgstr ""
msgid "Set your Password" msgid "Set your Password"
msgstr "" msgstr ""
#. Label of the section_break_hsiv (Section Break) field in DocType 'LMS Quiz'
#: lms/doctype/lms_quiz/lms_quiz.json
msgid "Settings"
msgstr ""
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course' #. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
#: lms/doctype/lms_course/lms_course.json #: lms/doctype/lms_course/lms_course.json
msgid "Short Introduction" msgid "Short Introduction"
@@ -3289,7 +3295,7 @@ msgstr ""
msgid "Sidebar Items" msgid "Sidebar Items"
msgstr "" msgstr ""
#: overrides/user.py:190 #: overrides/user.py:194
msgid "Sign Up is disabled" msgid "Sign Up is disabled"
msgstr "" msgstr ""
@@ -3333,7 +3339,7 @@ msgstr ""
msgid "Skills" msgid "Skills"
msgstr "" msgstr ""
#: overrides/user.py:38 #: overrides/user.py:42
msgid "Skills must be unique" msgid "Skills must be unique"
msgstr "" msgstr ""
@@ -3625,7 +3631,7 @@ msgstr ""
msgid "Template" msgid "Template"
msgstr "" msgstr ""
#: overrides/user.py:201 #: overrides/user.py:205
msgid "Temporarily Disabled" msgid "Temporarily Disabled"
msgstr "" msgstr ""
@@ -3670,7 +3676,7 @@ msgstr ""
msgid "The course {0} is now available on {1}." msgid "The course {0} is now available on {1}."
msgstr "" msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:44 #: lms/doctype/lms_certificate_request/lms_certificate_request.py:47
msgid "The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}" msgid "The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
msgstr "" msgstr ""
@@ -3678,7 +3684,7 @@ msgstr ""
msgid "The quiz has a time limit. For each question you will be given {0} seconds." msgid "The quiz has a time limit. For each question you will be given {0} seconds."
msgstr "" msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:62 #: lms/doctype/lms_certificate_request/lms_certificate_request.py:65
msgid "The slot is already booked by another participant." msgid "The slot is already booked by another participant."
msgstr "" msgstr ""
@@ -3694,7 +3700,7 @@ msgstr ""
msgid "There are no {0} on this site." msgid "There are no {0} on this site."
msgstr "" msgstr ""
#: lms/utils.py:1070 #: lms/utils.py:1063
msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed." msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed."
msgstr "" msgstr ""
@@ -3709,7 +3715,7 @@ msgstr ""
msgid "This certificate does no expire" msgid "This certificate does no expire"
msgstr "" msgstr ""
#: lms/utils.py:1028 lms/utils.py:1769 #: lms/utils.py:1021 lms/utils.py:1762
msgid "This course is free." msgid "This course is free."
msgstr "" msgstr ""
@@ -3823,11 +3829,11 @@ msgstr ""
msgid "To Date is mandatory in Work Experience." msgid "To Date is mandatory in Work Experience."
msgstr "" msgstr ""
#: lms/utils.py:1037 lms/utils.py:1780 #: lms/utils.py:1030 lms/utils.py:1773
msgid "To join this batch, please contact the Administrator." msgid "To join this batch, please contact the Administrator."
msgstr "" msgstr ""
#: overrides/user.py:202 #: overrides/user.py:206
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour" msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
msgstr "" msgstr ""
@@ -4077,15 +4083,15 @@ msgstr ""
msgid "Write a review" msgid "Write a review"
msgstr "" msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:86 #: lms/doctype/lms_certificate_request/lms_certificate_request.py:89
msgid "You already have an evaluation on {0} at {1} for the course {2}." msgid "You already have an evaluation on {0} at {1} for the course {2}."
msgstr "" msgstr ""
#: lms/api.py:204 #: lms/api.py:205
msgid "You are already enrolled for this batch." msgid "You are already enrolled for this batch."
msgstr "" msgstr ""
#: lms/api.py:196 #: lms/api.py:197
msgid "You are already enrolled for this course." msgid "You are already enrolled for this course."
msgstr "" msgstr ""
@@ -4110,11 +4116,11 @@ msgstr ""
msgid "You can find their resume attached to this email." msgid "You can find their resume attached to this email."
msgstr "" msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:106 #: lms/doctype/lms_certificate_request/lms_certificate_request.py:109
msgid "You cannot schedule evaluations after {0}." msgid "You cannot schedule evaluations after {0}."
msgstr "" msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:95 #: lms/doctype/lms_certificate_request/lms_certificate_request.py:98
msgid "You cannot schedule evaluations for past slots." msgid "You cannot schedule evaluations for past slots."
msgstr "" msgstr ""
@@ -4122,7 +4128,7 @@ msgstr ""
msgid "You don't have any notifications." msgid "You don't have any notifications."
msgstr "" msgstr ""
#: templates/quiz/quiz.js:136 #: templates/quiz/quiz.js:137
msgid "You got" msgid "You got"
msgstr "" msgstr ""
@@ -4176,11 +4182,17 @@ msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:7 #: lms/notification/certificate_request_creation/certificate_request_creation.html:7
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:6 #: lms/notification/certificate_request_reminder/certificate_request_reminder.html:6
#: templates/emails/certificate_request_notification.html:2
msgid "Your evaluation for the course {0} has been scheduled on {1} at {2} {3}." msgid "Your evaluation for the course {0} has been scheduled on {1} at {2} {3}."
msgstr "" msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:119
msgid "Your evaluation slot has been booked"
msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:8 #: lms/notification/certificate_request_creation/certificate_request_creation.html:8
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7 #: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
#: templates/emails/certificate_request_notification.html:3
msgid "Your evaluator is {0}" msgid "Your evaluator is {0}"
msgstr "" msgstr ""
@@ -4188,7 +4200,7 @@ msgstr ""
msgid "Your request to join us as a mentor for the course" msgid "Your request to join us as a mentor for the course"
msgstr "" msgstr ""
#: templates/quiz/quiz.js:136 #: templates/quiz/quiz.js:140
msgid "Your score is" msgid "Your score is"
msgstr "" msgstr ""
@@ -4201,7 +4213,7 @@ msgstr ""
msgid "cancel your application" msgid "cancel your application"
msgstr "" msgstr ""
#: templates/quiz/quiz.js:136 #: templates/quiz/quiz.js:137
msgid "correct answers" msgid "correct answers"
msgstr "" msgstr ""
@@ -4217,7 +4229,7 @@ msgstr ""
msgid "of" msgid "of"
msgstr "" msgstr ""
#: templates/quiz/quiz.js:136 #: templates/quiz/quiz.js:141
msgid "out of" msgid "out of"
msgstr "" msgstr ""
@@ -4261,7 +4273,7 @@ msgstr ""
msgid "{0} is already certified for the course {1}" msgid "{0} is already certified for the course {1}"
msgstr "" msgstr ""
#: lms/utils.py:696 #: lms/utils.py:689
msgid "{0} mentioned you in a comment" msgid "{0} mentioned you in a comment"
msgstr "" msgstr ""
@@ -4269,7 +4281,7 @@ msgstr ""
msgid "{0} mentioned you in a comment in your batch." msgid "{0} mentioned you in a comment in your batch."
msgstr "" msgstr ""
#: lms/utils.py:649 lms/utils.py:655 #: lms/utils.py:642 lms/utils.py:648
msgid "{0} mentioned you in a comment in {1}" msgid "{0} mentioned you in a comment in {1}"
msgstr "" msgstr ""

View File

@@ -16,6 +16,10 @@ class CustomUser(User):
super().validate() super().validate()
self.validate_username_duplicates() self.validate_username_duplicates()
def after_insert(self):
super().after_insert()
self.add_roles("LMS Student")
def validate_username_duplicates(self): def validate_username_duplicates(self):
while not self.username or self.username_exists(): while not self.username or self.username_exists():
self.username = append_number_if_name_exists( self.username = append_number_if_name_exists(

BIN
lms/public/images/desk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB