Compare commits
58 Commits
v2.32.1
...
eduvia-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597648db08 | ||
|
|
16afb24276 | ||
|
|
cf5cc19fd5 | ||
|
|
80f093958a | ||
|
|
57df376207 | ||
|
|
a147aca24f | ||
|
|
1660f69930 | ||
|
|
718657f493 | ||
|
|
9a77b716a1 | ||
|
|
6e3c624b91 | ||
|
|
2271eb270e | ||
|
|
7e5b2e4e79 | ||
|
|
36076068ec | ||
|
|
c868354b5b | ||
|
|
db91f0b2a0 | ||
|
|
d7e83bb78e | ||
|
|
feb2a39e05 | ||
|
|
a6cf910d05 | ||
|
|
b891b44ac6 | ||
|
|
026a3ebb81 | ||
|
|
71ba246011 | ||
|
|
a391204fa6 | ||
|
|
9c773399a8 | ||
|
|
528b85352a | ||
|
|
249c369c14 | ||
|
|
9803fc1031 | ||
|
|
299fde1c98 | ||
|
|
7f55734fbb | ||
|
|
efe230865a | ||
|
|
6e52e684c8 | ||
|
|
99d880297a | ||
|
|
dec706ae72 | ||
|
|
2e60f0a0c2 | ||
|
|
ef612f86e5 | ||
|
|
9c16e03ea7 | ||
|
|
7780c0310e | ||
|
|
b0a23c0d1a | ||
|
|
05c85cea08 | ||
|
|
1ffae0a1de | ||
|
|
15cbccd15f | ||
|
|
266b2f2ac8 | ||
|
|
26f9fb4199 | ||
|
|
67887fb6ef | ||
|
|
3d102e39ff | ||
|
|
ddd9089130 | ||
|
|
d8ce88ab57 | ||
|
|
01794a47c6 | ||
|
|
17626dbbdb | ||
|
|
e5bd86658d | ||
|
|
e911dc1353 | ||
|
|
27e3e5aa6a | ||
|
|
5b65525bf1 | ||
|
|
277804f8b1 | ||
|
|
4c77802e3c | ||
|
|
aacfea6ea5 | ||
|
|
6d55040e43 | ||
|
|
290f785a47 | ||
|
|
39ef187f6b |
2
.github/workflows/make_release_pr.yml
vendored
2
.github/workflows/make_release_pr.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Create weekly release
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 4 15 * *'
|
||||
- cron: '30 3 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="HtmlUnknownBooleanAttribute" enabled="true" level="INFORMATION" enabled_by_default="true" editorAttributes="INFORMATION_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (env)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" project-jdk-name="Python 3.11 (env)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
9
.idea/modules.xml
generated
Normal file
9
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/frontend/frontend.iml" filepath="$PROJECT_DIR$/frontend/frontend.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/lms/lms.iml" filepath="$PROJECT_DIR$/lms/lms.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
265
.idea/workspace.xml
generated
Normal file
265
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,265 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8ceac89e-548b-4898-a7d0-cd9692210a67" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/frontend/vite.config.js" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/vite.config.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/ar.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/ar.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/bs.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/bs.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/cs.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/cs.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/de.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/de.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/eo.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/eo.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/es.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/es.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/fa.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/fa.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/fr.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/fr.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/hr.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/hr.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/hu.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/hu.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/id.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/id.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/it.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/it.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/main.pot" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/main.pot" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/nl.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/nl.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/pl.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/pl.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/pt.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/pt.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/pt_BR.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/pt_BR.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/ru.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/ru.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/sr.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/sr.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/sr_CS.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/sr_CS.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/sv.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/sv.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/th.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/th.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/tr.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/tr.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/vi.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/vi.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lms/locale/zh.po" beforeDir="false" afterPath="$PROJECT_DIR$/lms/locale/zh.po" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ComposerSettings">
|
||||
<execution />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="GitHubPullRequestSearchHistory">{
|
||||
"lastFilter": {
|
||||
"state": "OPEN",
|
||||
"assignee": "stanig2106"
|
||||
}
|
||||
}</component>
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/eduvia-app/lms",
|
||||
"accountId": "07629746-0347-4709-8dfa-2d0f080158cf"
|
||||
}
|
||||
}</component>
|
||||
<component name="KubernetesApiPersistence">{}</component>
|
||||
<component name="KubernetesApiProvider">{
|
||||
"isMigrated": true
|
||||
}</component>
|
||||
<component name="MacroExpansionManager">
|
||||
<option name="directoryName" value="zkk5YL6F" />
|
||||
</component>
|
||||
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="/opt/homebrew/Cellar/php/8.3.7/bin/php" />
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 3
|
||||
}</component>
|
||||
<component name="ProjectId" id="31EuJyp79zrs7MLT76Y4wgzYgGe" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="showLibraryContents" value="true" />
|
||||
<option name="showMembers" value="true" />
|
||||
<option name="showVisibilityIcons" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.go.formatter.settings.were.checked": "true",
|
||||
"RunOnceActivity.go.migrated.go.modules.settings": "true",
|
||||
"dart.analysis.tool.window.visible": "false",
|
||||
"git-widget-placeholder": "eduvia/prod",
|
||||
"go.import.settings.migrated": "true",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "yarn",
|
||||
"org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon": "",
|
||||
"org.rust.first.attach.projects": "true",
|
||||
"project.structure.last.edited": "Modules",
|
||||
"project.structure.proportion": "0.15",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "application.passwordSafe",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"ts.external.directory.path": "/Users/stani/Documents/campus-prive/frappe-bench/apps/lms/frontend/node_modules/typescript/lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RubyModuleManagerSettings">
|
||||
<option name="blackListedRootsPaths">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/lms" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration default="true" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="lms" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<go_parameters value="-i" />
|
||||
<kind />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="GoTestRunConfiguration" factoryName="Go Test">
|
||||
<module name="lms" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<go_parameters value="-i" />
|
||||
<kind />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<framework value="gotest" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="JetRunConfigurationType">
|
||||
<module name="lms" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
|
||||
<option name="filePath" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="lms" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="Python.FlaskServer">
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<module name="" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="launchJavascriptDebuger" value="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="Tox" factoryName="Tox">
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="tests" factoryName="Autodetect">
|
||||
<module name="lms" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="_new_additionalArguments" value="""" />
|
||||
<option name="_new_target" value="""" />
|
||||
<option name="_new_targetType" value=""PATH"" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="tests" factoryName="Doctests">
|
||||
<module name="lms" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="" />
|
||||
<option name="CLASS_NAME" value="" />
|
||||
<option name="METHOD_NAME" value="" />
|
||||
<option name="FOLDER_NAME" value="" />
|
||||
<option name="TEST_TYPE" value="TEST_SCRIPT" />
|
||||
<option name="PATTERN" value="" />
|
||||
<option name="USE_PATTERN" value="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="RustProjectSettings">
|
||||
<option name="toolchainHomeDirectory" value="$USER_HOME$/.cargo/bin" />
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-js-predefined-d6986cc7102b-b26f3e71634d-JavaScript-IU-251.26094.121" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="8ceac89e-548b-4898-a7d0-cd9692210a67" name="Changes" comment="" />
|
||||
<created>1755101486887</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1755101486887</updated>
|
||||
<workItem from="1755101488483" duration="947000" />
|
||||
<workItem from="1755102648895" duration="6458000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VgoProject">
|
||||
<settings-migrated>true</settings-migrated>
|
||||
</component>
|
||||
</project>
|
||||
@@ -107,7 +107,7 @@ describe("Course Creation", () => {
|
||||
cy.get("div").contains(
|
||||
"Test Course Short Introduction to test the UI"
|
||||
);
|
||||
cy.get(".course-image")
|
||||
cy.get(".bg-cover")
|
||||
.invoke("css", "background-image")
|
||||
.should("include", "/files/profile");
|
||||
});
|
||||
|
||||
@@ -18,12 +18,12 @@ services:
|
||||
|
||||
frappe:
|
||||
image: frappe/bench:latest
|
||||
command: bash /workspace/init.sh
|
||||
command: bash /workspace/docker/init.sh
|
||||
environment:
|
||||
- SHELL=/bin/bash
|
||||
working_dir: /home/frappe
|
||||
volumes:
|
||||
- .:/workspace
|
||||
- ..:/workspace
|
||||
ports:
|
||||
- 8000:8000
|
||||
- 9000:9000
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
if [ -d "/home/frappe/frappe-bench/apps/frappe" ]; then
|
||||
echo "Bench already exists, skipping init"
|
||||
@@ -37,4 +37,10 @@ bench --site lms.localhost set-config developer_mode 1
|
||||
bench --site lms.localhost clear-cache
|
||||
bench use lms.localhost
|
||||
|
||||
# Import French translations from workspace (persisted in repo)
|
||||
if [ -f "/workspace/lms/translations/fr.csv" ]; then
|
||||
bench --site lms.localhost import-translations fr /workspace/lms/translations/fr.csv || true
|
||||
bench --site lms.localhost clear-cache
|
||||
fi
|
||||
|
||||
bench start
|
||||
|
||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -40,6 +40,7 @@ declare module 'vue' {
|
||||
Code: typeof import('./src/components/Controls/Code.vue')['default']
|
||||
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
|
||||
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
|
||||
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
|
||||
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
|
||||
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
|
||||
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
|
||||
|
||||
9
frontend/frontend.iml
Normal file
9
frontend/frontend.iml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -157,7 +157,7 @@
|
||||
v-model="showHelpModal"
|
||||
v-model:articles="articles"
|
||||
appName="learning"
|
||||
title="Frappe Learning"
|
||||
:title="__('Frappe Learning')"
|
||||
:logo="LMSLogo"
|
||||
:afterSkip="(step) => capture('onboarding_step_skipped_' + step)"
|
||||
:afterSkipAll="() => capture('onboarding_steps_skipped')"
|
||||
@@ -303,7 +303,7 @@ const unreadNotifications = createResource({
|
||||
const addNotifications = () => {
|
||||
if (user) {
|
||||
sidebarLinks.value.push({
|
||||
label: 'Notifications',
|
||||
label: __('Notifications'),
|
||||
icon: 'Bell',
|
||||
to: 'Notifications',
|
||||
activeFor: ['Notifications'],
|
||||
@@ -315,7 +315,7 @@ const addNotifications = () => {
|
||||
const addQuizzes = () => {
|
||||
if (isInstructor.value || isModerator.value) {
|
||||
sidebarLinks.value.splice(4, 0, {
|
||||
label: 'Quizzes',
|
||||
label: __('Quizzes'),
|
||||
icon: 'CircleHelp',
|
||||
to: 'Quizzes',
|
||||
activeFor: [
|
||||
@@ -331,7 +331,7 @@ const addQuizzes = () => {
|
||||
const addAssignments = () => {
|
||||
if (isInstructor.value || isModerator.value) {
|
||||
sidebarLinks.value.splice(5, 0, {
|
||||
label: 'Assignments',
|
||||
label: __('Assignments'),
|
||||
icon: 'Pencil',
|
||||
to: 'Assignments',
|
||||
activeFor: [
|
||||
@@ -344,6 +344,22 @@ const addAssignments = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const addProgrammingExercises = () => {
|
||||
if (isInstructor.value || isModerator.value) {
|
||||
sidebarLinks.value.splice(3, 0, {
|
||||
label: __('Programming Exercises'),
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
activeFor: [
|
||||
'ProgrammingExercises',
|
||||
'ProgrammingExerciseForm',
|
||||
'ProgrammingExerciseSubmissions',
|
||||
'ProgrammingExerciseSubmission',
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const addPrograms = () => {
|
||||
let activeFor = ['Programs', 'ProgramForm']
|
||||
let index = 1
|
||||
@@ -367,7 +383,7 @@ const addPrograms = () => {
|
||||
|
||||
if (canAddProgram) {
|
||||
sidebarLinks.value.splice(index, 0, {
|
||||
label: 'Programs',
|
||||
label: __('Programs'),
|
||||
icon: 'Route',
|
||||
to: 'Programs',
|
||||
activeFor: activeFor,
|
||||
@@ -627,6 +643,7 @@ watch(userResource, () => {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
addPrograms()
|
||||
addProgrammingExercises()
|
||||
addQuizzes()
|
||||
addAssignments()
|
||||
setUpOnboarding()
|
||||
|
||||
@@ -208,12 +208,12 @@ const canAddAssessments = () => {
|
||||
const getAssessmentColumns = () => {
|
||||
let columns = [
|
||||
{
|
||||
label: 'Assessment',
|
||||
label: __('Assessment'),
|
||||
key: 'title',
|
||||
width: '25rem',
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
label: __('Type'),
|
||||
key: 'assessment_type',
|
||||
width: '15rem',
|
||||
},
|
||||
@@ -221,7 +221,7 @@ const getAssessmentColumns = () => {
|
||||
|
||||
if (!user.data?.is_moderator) {
|
||||
columns.push({
|
||||
label: 'Status/Percentage',
|
||||
label: __('Status/Percentage'),
|
||||
key: 'status',
|
||||
align: 'left',
|
||||
width: '10rem',
|
||||
|
||||
@@ -453,9 +453,9 @@ const canModifyAssignment = computed(() => {
|
||||
|
||||
const submissionStatusOptions = computed(() => {
|
||||
return [
|
||||
{ label: 'Not Graded', value: 'Not Graded' },
|
||||
{ label: 'Pass', value: 'Pass' },
|
||||
{ label: 'Fail', value: 'Fail' },
|
||||
{ label: __('Not Graded'), value: 'Not Graded' },
|
||||
{ label: __('Pass'), value: 'Pass' },
|
||||
{ label: __('Fail'), value: 'Fail' },
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -116,17 +116,17 @@ const openCourseModal = () => {
|
||||
const getCoursesColumns = () => {
|
||||
return [
|
||||
{
|
||||
label: 'Title',
|
||||
label: __('Title'),
|
||||
key: 'title',
|
||||
width: 2,
|
||||
},
|
||||
{
|
||||
label: 'Lessons',
|
||||
label: __('Lessons'),
|
||||
key: 'lessons',
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
label: 'Enrollments',
|
||||
label: __('Enrollments'),
|
||||
align: 'right',
|
||||
key: 'enrollments',
|
||||
},
|
||||
|
||||
@@ -231,19 +231,19 @@ const students = createResource({
|
||||
const getStudentColumns = () => {
|
||||
let columns = [
|
||||
{
|
||||
label: 'Full Name',
|
||||
label: __('Full Name'),
|
||||
key: 'full_name',
|
||||
width: '20rem',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
label: 'Progress',
|
||||
label: __('Progress'),
|
||||
key: 'progress',
|
||||
width: '15rem',
|
||||
icon: 'activity',
|
||||
},
|
||||
{
|
||||
label: 'Last Active',
|
||||
label: __('Last Active'),
|
||||
key: 'last_active',
|
||||
width: '10rem',
|
||||
align: 'center',
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"
|
||||
:value="query"
|
||||
autocomplete="off"
|
||||
placeholder="Search"
|
||||
:placeholder="__('Search')"
|
||||
/>
|
||||
<button
|
||||
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
|
||||
@@ -120,7 +120,7 @@
|
||||
v-if="groups.length == 0"
|
||||
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-ink-gray-5"
|
||||
>
|
||||
No results found
|
||||
{{ __('No results found') }}
|
||||
</li>
|
||||
</ComboboxOptions>
|
||||
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
|
||||
|
||||
108
frontend/src/components/Controls/ColorSwatches.vue
Normal file
108
frontend/src/components/Controls/ColorSwatches.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<Popover placement="bottom" class="!block">
|
||||
<template #target="{ togglePopover, isOpen }">
|
||||
<div class="space-y-2">
|
||||
<FormControl
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="w-full"
|
||||
:placeholder="__('Set Color')"
|
||||
@focus="togglePopover"
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="(val: string) => emit('update:modelValue', val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="size-4 rounded-full"
|
||||
:style="
|
||||
modelValue
|
||||
? {
|
||||
backgroundColor:
|
||||
theme.backgroundColor[modelValue.toLowerCase()][400],
|
||||
}
|
||||
: {}
|
||||
"
|
||||
>
|
||||
<Palette
|
||||
v-if="!modelValue"
|
||||
class="size-4 stroke-1.5 text-ink-gray-5"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<Button variant="ghost">
|
||||
<X
|
||||
class="size-3 text-ink-gray-5"
|
||||
@click="emit('update:modelValue', null)"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</FormControl>
|
||||
</div>
|
||||
</template>
|
||||
<template #body="{ close }">
|
||||
<div class="rounded-lg bg-surface-white p-3 border w-fit mt-2">
|
||||
<div class="text-xs text-ink-gray-5 mb-1.5">
|
||||
{{ __('Swatches') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
class="size-5 rounded-full cursor-pointer"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
theme.backgroundColor[color.toLowerCase()][400],
|
||||
}"
|
||||
@click="
|
||||
(e) => {
|
||||
emit('update:modelValue', color)
|
||||
close()
|
||||
emit('change', color)
|
||||
}
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<div class="text-sm text-ink-gray-5 mt-2">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Button, FormControl, Popover } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { Palette, X } from 'lucide-vue-next'
|
||||
import { theme } from '@/utils/theme'
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
label: string
|
||||
description?: string
|
||||
}>()
|
||||
|
||||
const colors = computed(() => {
|
||||
return [
|
||||
'Red',
|
||||
'Blue',
|
||||
'Green',
|
||||
'Amber',
|
||||
'Purple',
|
||||
'Cyan',
|
||||
'Orange',
|
||||
'Violet',
|
||||
'Pink',
|
||||
'Teal',
|
||||
'Gray',
|
||||
'Yellow',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -1,41 +1,51 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="course.title"
|
||||
class="flex flex-col h-full rounded-md border-2 overflow-auto"
|
||||
class="flex flex-col h-full rounded-md border-2 overflow-auto text-ink-gray-9"
|
||||
style="min-height: 350px"
|
||||
>
|
||||
<div
|
||||
class="course-image"
|
||||
:class="{ 'default-image': !course.image }"
|
||||
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
||||
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat"
|
||||
:style="
|
||||
course.image
|
||||
? { backgroundImage: `url('${encodeURI(course.image)}')` }
|
||||
: {
|
||||
backgroundImage: getGradientColor(),
|
||||
backgroundBlendMode: 'screen',
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
|
||||
<Badge
|
||||
<div
|
||||
v-if="course.featured"
|
||||
variant="subtle"
|
||||
theme="green"
|
||||
size="md"
|
||||
class="mb-1 mr-1"
|
||||
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white border border-outline-amber-1 px-2 py-0.5 rounded-md mr-1 mb-1"
|
||||
>
|
||||
{{ __('Featured') }}
|
||||
</Badge>
|
||||
<Star class="size-3 stroke-2" />
|
||||
<span>
|
||||
{{ __('Featured') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="course.tags"
|
||||
v-for="tag in course.tags?.split(', ')"
|
||||
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md mb-1 mr-1"
|
||||
class="text-xs border bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 mr-1"
|
||||
>
|
||||
{{ tag }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!course.image" class="image-placeholder">
|
||||
{{ course.title[0] }}
|
||||
<div
|
||||
v-if="!course.image"
|
||||
class="flex items-center justify-center text-white flex-1 font-extrabold text-2xl my-auto px-5 text-center leading-6"
|
||||
:class="course.tags ? 'h-[80%]' : 'h-full'"
|
||||
>
|
||||
{{ course.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div v-if="course.lessons">
|
||||
<Tooltip :text="__('Lessons')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<span class="flex items-center">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.lessons }}
|
||||
</span>
|
||||
@@ -44,8 +54,8 @@
|
||||
|
||||
<div v-if="course.enrollments">
|
||||
<Tooltip :text="__('Enrolled Students')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<Users class="h-4 w-4 stroke-1. mr-1" />
|
||||
<span class="flex items-center">
|
||||
<Users class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.enrollments }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -53,14 +63,14 @@
|
||||
|
||||
<div v-if="course.rating">
|
||||
<Tooltip :text="__('Average Rating')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<span class="flex items-center">
|
||||
<Star class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.rating }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="course.status != 'Approved'">
|
||||
<!-- <div v-if="course.status != 'Approved'">
|
||||
<Badge
|
||||
variant="subtle"
|
||||
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
|
||||
@@ -68,14 +78,14 @@
|
||||
>
|
||||
{{ course.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="text-xl font-semibold leading-6 text-ink-gray-9">
|
||||
<div v-if="course.image" class="text-xl font-semibold leading-6">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
<div class="short-introduction text-ink-gray-7 text-sm">
|
||||
<div class="short-introduction text-sm">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
|
||||
@@ -84,11 +94,8 @@
|
||||
:progress="course.membership.progress"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="user && course.membership"
|
||||
class="text-sm text-ink-gray-7 mt-2 mb-4"
|
||||
>
|
||||
{{ Math.ceil(course.membership.progress) }}% completed
|
||||
<div v-if="user && course.membership" class="text-sm mt-2 mb-4">
|
||||
{{ Math.ceil(course.membership.progress) }}% {{ __('completed') }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-auto">
|
||||
@@ -108,21 +115,23 @@
|
||||
<div v-if="course.paid_course" class="font-semibold">
|
||||
{{ course.price }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
<Tooltip
|
||||
v-if="course.paid_certificate || course.enable_certification"
|
||||
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
|
||||
:text="__('Get Certified')"
|
||||
>
|
||||
{{ __('Certification') }}
|
||||
</div>
|
||||
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import { BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Badge, Tooltip } from 'frappe-ui'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { theme } from '@/utils/theme'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
|
||||
@@ -134,16 +143,24 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const getGradientColor = () => {
|
||||
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
||||
let colorMap = theme.backgroundColor[color]
|
||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
|
||||
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
|
||||
/* return `radial-gradient(ellipse at 80% 20%, black 20%, ${colorMap[500]} 100%)` */
|
||||
/* return `radial-gradient(ellipse at 30% 70%, black 50%, ${colorMap[500]} 100%)` */
|
||||
/* return `radial-gradient(ellipse at 80% 20%, ${colorMap[100]} 0%, ${colorMap[300]} 50%, ${colorMap[500]} 100%)` */
|
||||
/* return `conic-gradient(from 180deg at 50% 50%, ${colorMap[100]} 0%, ${colorMap[200]} 50%, ${colorMap[400]} 100%)` */
|
||||
/* return `linear-gradient(135deg, ${colorMap[100]}, ${colorMap[300]}), linear-gradient(120deg, rgba(255,255,255,0.4) 0%, transparent 60%) ` */
|
||||
/* return `radial-gradient(circle at 20% 30%, ${colorMap[100]} 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 40%, ${colorMap[200]} 0%, transparent 50%),
|
||||
linear-gradient(135deg, ${colorMap[300]} 0%, ${colorMap[400]} 100%);` */
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.course-image {
|
||||
height: 168px;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.course-card-pills {
|
||||
background: #ffffff;
|
||||
margin-left: 0;
|
||||
@@ -157,14 +174,6 @@ const props = defineProps({
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.default-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: theme('colors.green.100');
|
||||
color: theme('colors.green.600');
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -173,14 +182,7 @@ const props = defineProps({
|
||||
.avatar-group .avatar {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
}
|
||||
.image-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
font-size: 5rem;
|
||||
color: theme('colors.gray.700');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.avatar-group.overlap .avatar + .avatar {
|
||||
margin-left: calc(-8px);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="text-ink-gray-7">
|
||||
<div class="">
|
||||
<span v-if="instructors?.length == 1">
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
{{ instructors[0].first_name }}
|
||||
</router-link>
|
||||
and
|
||||
{{ __('and') }}
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Profile',
|
||||
@@ -38,7 +38,7 @@
|
||||
>
|
||||
{{ instructors[0].first_name }}
|
||||
</router-link>
|
||||
and {{ instructors?.length - 1 }} others
|
||||
{{ __('and') }} {{ instructors?.length - 1 }} {{ __('others') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
"
|
||||
:options="[
|
||||
{
|
||||
label: 'Edit',
|
||||
label: __('Edit'),
|
||||
onClick() {
|
||||
reply.editable = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
label: __('Delete'),
|
||||
onClick() {
|
||||
deleteReply(reply)
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ const props = defineProps({
|
||||
},
|
||||
emptyStateText: {
|
||||
type: String,
|
||||
default: 'Start a discussion',
|
||||
default: __('Start a discussion'),
|
||||
},
|
||||
singleThread: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -2,23 +2,26 @@
|
||||
<div class="flex flex-col items-center justify-center mt-60">
|
||||
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
|
||||
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
|
||||
{{ __('No {0}').format(type?.toLowerCase()) }}
|
||||
|
||||
{{ __('No {0}').format(props.type) }}
|
||||
</div>
|
||||
<div
|
||||
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
|
||||
>
|
||||
{{
|
||||
__(
|
||||
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
|
||||
).format(type?.toLowerCase())
|
||||
__(
|
||||
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
|
||||
).format(props.type)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { BookOpen, GraduationCap } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
</script>
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Reply To') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<Input type="text" v-model="announcement.replyTo" />
|
||||
</div>
|
||||
@@ -70,8 +71,8 @@ const announcementResource = createResource({
|
||||
url: 'frappe.core.doctype.communication.email.make',
|
||||
makeParams(values) {
|
||||
return {
|
||||
recipients: props.students.join(', '),
|
||||
cc: announcement.replyTo,
|
||||
recipients: announcement.replyTo,
|
||||
bcc: props.students.join(', '),
|
||||
subject: announcement.subject,
|
||||
content: announcement.announcement,
|
||||
doctype: 'LMS Batch',
|
||||
@@ -95,6 +96,9 @@ const makeAnnouncement = (close) => {
|
||||
if (!announcement.announcement) {
|
||||
return __('Announcement is required')
|
||||
}
|
||||
if (!announcement.replyTo) {
|
||||
return __('Reply To is required')
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
close()
|
||||
|
||||
@@ -97,9 +97,9 @@ const addAssessment = (close) => {
|
||||
|
||||
const assessmentTypes = computed(() => {
|
||||
return [
|
||||
{ label: 'Quiz', value: 'LMS Quiz' },
|
||||
{ label: 'Assignment', value: 'LMS Assignment' },
|
||||
{ label: 'Programming Exercise', value: 'LMS Programming Exercise' },
|
||||
{ label: __('Quiz'), value: 'LMS Quiz' },
|
||||
{ label: __('Assignment'), value: 'LMS Assignment' },
|
||||
{ label: __('Programming Exercise'), value: 'LMS Programming Exercise' },
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -144,11 +144,11 @@ const saveAssignment = () => {
|
||||
|
||||
const assignmentOptions = computed(() => {
|
||||
return [
|
||||
{ label: 'PDF', value: 'PDF' },
|
||||
{ label: 'Image', value: 'Image' },
|
||||
{ label: 'Document', value: 'Document' },
|
||||
{ label: 'Text', value: 'Text' },
|
||||
{ label: 'URL', value: 'URL' },
|
||||
{ label: __('PDF'), value: 'PDF' },
|
||||
{ label: __('Image'), value: 'Image' },
|
||||
{ label: __('Document'), value: 'Document' },
|
||||
{ label: __('Text'), value: 'Text' },
|
||||
{ label: __('URL'), value: 'URL' },
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div> -->
|
||||
<FormControl
|
||||
v-model="searchFilter"
|
||||
:placeholder="__('Search by Member Name')"
|
||||
:placeholder="__('Search by Member')"
|
||||
type="text"
|
||||
class="w-full"
|
||||
/>
|
||||
@@ -149,6 +149,10 @@ import { theme } from '@/utils/theme'
|
||||
|
||||
const show = defineModel<boolean | undefined>()
|
||||
const searchFilter = ref<string | null>(null)
|
||||
type Filters = {
|
||||
course: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
courseName?: string
|
||||
@@ -184,10 +188,6 @@ const progressList = createListResource({
|
||||
|
||||
watch([searchFilter], () => {
|
||||
let filterApplied = false
|
||||
type Filters = {
|
||||
course: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
let filters: Filters = {
|
||||
course: props.courseName,
|
||||
}
|
||||
|
||||
@@ -139,15 +139,15 @@ const getTimezoneOptions = () => {
|
||||
const getRecordingOptions = () => {
|
||||
return [
|
||||
{
|
||||
label: 'No Recording',
|
||||
label: __('No Recording'),
|
||||
value: 'No Recording',
|
||||
},
|
||||
{
|
||||
label: 'Local',
|
||||
label: __('Local'),
|
||||
value: 'Local',
|
||||
},
|
||||
{
|
||||
label: 'Cloud',
|
||||
label: __('Cloud'),
|
||||
value: 'Cloud',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -8,13 +8,20 @@
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="text-base">
|
||||
<TabButtons
|
||||
v-if="tabs.length > 1"
|
||||
:buttons="tabs"
|
||||
v-model="currentTab"
|
||||
class="w-fit"
|
||||
/>
|
||||
<div v-if="currentTab" class="mt-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<TabButtons
|
||||
v-if="tabs.length > 1"
|
||||
:buttons="tabs"
|
||||
v-model="currentTab"
|
||||
class="w-fit"
|
||||
/>
|
||||
<!-- <FormControl
|
||||
v-model="searchText"
|
||||
:placeholder="__('Search by Member')"
|
||||
class="mt-2 mr-5 w-[25%]"
|
||||
/> -->
|
||||
</div>
|
||||
<div v-if="currentTab" class="mt-4">
|
||||
<div class="grid grid-cols-[55%,40%] gap-5">
|
||||
<div class="space-y-5 border rounded-md p-2 pt-4">
|
||||
<div class="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
|
||||
@@ -52,7 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-sm">
|
||||
{{ parseFloat(row.watch_time).toFixed(2) }}
|
||||
{{ convertToMinutes(row.watch_time) }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -62,7 +69,7 @@
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: __('Average Watch Time (seconds)'),
|
||||
title: __('Average Watch Time (minutes)'),
|
||||
value: averageWatchTime,
|
||||
}"
|
||||
/>
|
||||
@@ -73,6 +80,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-ink-gray-5">
|
||||
{{ __('No statistics available for this video.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -82,15 +92,21 @@ import {
|
||||
Avatar,
|
||||
createListResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
NumberChart,
|
||||
TabButtons,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { enablePlyr } from '@/utils'
|
||||
import { enablePlyr, convertToMinutes } from '@/utils'
|
||||
import VideoBlock from '@/components/VideoBlock.vue'
|
||||
|
||||
const show = defineModel<boolean | undefined>()
|
||||
const currentTab = ref<string>('')
|
||||
const searchText = ref<string>('')
|
||||
type Filters = {
|
||||
lesson: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
lessonName?: string
|
||||
@@ -127,6 +143,24 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(searchText, () => {
|
||||
let filterApplied = false
|
||||
let filters: Filters = {
|
||||
lesson: props.lessonName,
|
||||
}
|
||||
|
||||
if (searchText.value) {
|
||||
filters.member_name = ['like', `%${searchText.value}%`]
|
||||
filterApplied = true
|
||||
}
|
||||
|
||||
statistics.update({
|
||||
filters: filters,
|
||||
})
|
||||
|
||||
statistics.reload({})
|
||||
})
|
||||
|
||||
watch(show, () => {
|
||||
if (show.value) {
|
||||
enablePlyr()
|
||||
@@ -151,7 +185,7 @@ const averageWatchTime = computed(() => {
|
||||
totalWatchTime += parseFloat(item.watch_time)
|
||||
})
|
||||
|
||||
return totalWatchTime / currentTabData.value.length
|
||||
return convertToMinutes(totalWatchTime / currentTabData.value.length)
|
||||
})
|
||||
|
||||
const currentTabData = computed(() => {
|
||||
|
||||
@@ -665,25 +665,25 @@ const markLessonProgress = () => {
|
||||
const getSubmissionColumns = () => {
|
||||
return [
|
||||
{
|
||||
label: 'No.',
|
||||
label: __('No.'),
|
||||
key: 'idx',
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
label: __('Date'),
|
||||
key: 'creation',
|
||||
},
|
||||
{
|
||||
label: 'Score',
|
||||
label: __('Score'),
|
||||
key: 'score',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: 'Score out of',
|
||||
label: __('Score out of'),
|
||||
key: 'score_out_of',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: 'Percentage',
|
||||
label: __('Percentage'),
|
||||
key: 'percentage',
|
||||
align: 'center',
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
{{ branding.data?.app_name }}
|
||||
</span>
|
||||
<span v-else> Learning </span>
|
||||
<span v-else> {{ __('Learning') }} </span>
|
||||
</div>
|
||||
<div
|
||||
v-if="userResource.data"
|
||||
@@ -130,7 +130,7 @@ const userDropdownOptions = computed(() => {
|
||||
items: [
|
||||
{
|
||||
icon: User,
|
||||
label: 'My Profile',
|
||||
label: __('My Profile'),
|
||||
onClick: () => {
|
||||
router.push(`/user/${userResource.data?.username}`)
|
||||
},
|
||||
@@ -140,7 +140,7 @@ const userDropdownOptions = computed(() => {
|
||||
},
|
||||
{
|
||||
icon: theme.value === 'light' ? Moon : Sun,
|
||||
label: 'Toggle Theme',
|
||||
label: __('Toggle Theme'),
|
||||
onClick: () => {
|
||||
toggleTheme()
|
||||
},
|
||||
@@ -158,7 +158,7 @@ const userDropdownOptions = computed(() => {
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
label: 'Settings',
|
||||
label: __('Settings'),
|
||||
onClick: () => {
|
||||
settingsStore.isSettingsOpen = true
|
||||
},
|
||||
@@ -168,7 +168,7 @@ const userDropdownOptions = computed(() => {
|
||||
},
|
||||
{
|
||||
icon: FrappeCloudIcon,
|
||||
label: 'Login to Frappe Cloud',
|
||||
label: __('Login to Frappe Cloud'),
|
||||
onClick: () => {
|
||||
$dialog({
|
||||
title: __('Login to Frappe Cloud?'),
|
||||
@@ -196,7 +196,7 @@ const userDropdownOptions = computed(() => {
|
||||
},
|
||||
{
|
||||
icon: LogOut,
|
||||
label: 'Log out',
|
||||
label: __('Log out'),
|
||||
onClick: () => {
|
||||
logout.submit().then(() => {
|
||||
isLoggedIn = false
|
||||
@@ -208,7 +208,7 @@ const userDropdownOptions = computed(() => {
|
||||
},
|
||||
{
|
||||
icon: LogIn,
|
||||
label: 'Log in',
|
||||
label: __('Log in'),
|
||||
onClick: () => {
|
||||
window.location.href = '/login'
|
||||
},
|
||||
|
||||
5
frontend/src/global.d.ts
vendored
5
frontend/src/global.d.ts
vendored
@@ -1,11 +1,12 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
function __(text: string): string
|
||||
// May return string or an object with `.format(...)` when placeholders exist
|
||||
function __(text: string): any
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
__: (text: string) => string
|
||||
__: (text: string) => any
|
||||
}
|
||||
}
|
||||
70
frontend/src/overrides/Onboarding/GettingStartedBanner.vue
Normal file
70
frontend/src/overrides/Onboarding/GettingStartedBanner.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!isSidebarCollapsed"
|
||||
class="flex flex-col gap-3 shadow-sm rounded-lg py-2.5 px-3 bg-surface-modal text-base"
|
||||
>
|
||||
<div v-if="stepsCompleted != totalSteps" class="inline-flex text-ink-gray-9 gap-2">
|
||||
<StepsIcon class="h-4 my-0.5 shrink-0" />
|
||||
<div class="flex flex-col text-p-sm gap-0.5">
|
||||
<div class="font-medium">
|
||||
{{ __('Getting started') }}
|
||||
</div>
|
||||
<div class="text-ink-gray-7">
|
||||
{{ __('{0}/{1} steps').format(stepsCompleted, totalSteps) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<StepsIcon class="h-4 my-0.5" />
|
||||
<div class="text-ink-gray-9 font-medium">
|
||||
{{ __('You are all set') }}
|
||||
</div>
|
||||
</div>
|
||||
<FeatherIcon
|
||||
name="x"
|
||||
class="h-4 cursor-pointer"
|
||||
@click="() => { showHelpCenter = true; isOnboardingStepsCompleted = true }"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-p-sm text-ink-gray-7">
|
||||
{{ __('All steps are completed successfully') }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="stepsCompleted != totalSteps"
|
||||
:label="stepsCompleted == 0 ? __('Start now') : __('Continue')"
|
||||
theme="blue"
|
||||
@click="openOnboarding"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="chevrons-right" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<Button v-else-if="stepsCompleted != totalSteps" @click="openOnboarding">
|
||||
<StepsIcon class="h-4 my-0.5 shrink-0" />
|
||||
</Button>
|
||||
</template>
|
||||
<script setup>
|
||||
import StepsIcon from 'frappe-ui/frappe/Icons/StepsIcon.vue'
|
||||
import Button from 'frappe-ui/src/components/Button/Button.vue'
|
||||
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue'
|
||||
import { useOnboarding } from 'frappe-ui/frappe/Onboarding/onboarding'
|
||||
import { showHelpCenter } from 'frappe-ui/frappe/HelpCenter/helpCenter'
|
||||
import { showHelpModal, minimize } from 'frappe-ui/frappe/Help/help'
|
||||
|
||||
const props = defineProps({
|
||||
isSidebarCollapsed: { type: Boolean, default: false },
|
||||
appName: { type: String, default: 'frappecrm' },
|
||||
})
|
||||
|
||||
const { stepsCompleted, totalSteps, isOnboardingStepsCompleted } = useOnboarding(props.appName)
|
||||
|
||||
const openOnboarding = () => {
|
||||
minimize.value = false
|
||||
showHelpModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
112
frontend/src/overrides/Onboarding/OnboardingSteps.vue
Normal file
112
frontend/src/overrides/Onboarding/OnboardingSteps.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center gap-1 mt-4 mb-7">
|
||||
<component :is="logo" class="size-10 shrink-0 rounded mb-4" />
|
||||
<div class="text-base font-medium">
|
||||
{{ __('Welcome to {0}').format(title) }}
|
||||
</div>
|
||||
<div class="text-p-base font-normal">
|
||||
{{ __('{0}/{1} steps completed').format(stepsCompleted, totalSteps) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5 overflow-hidden">
|
||||
<div class="flex justify-between items-center py-0.5">
|
||||
<Badge
|
||||
:label="__('{0}% completed').format(completedPercentage)"
|
||||
:theme="completedPercentage == 100 ? 'green' : 'orange'"
|
||||
size="lg"
|
||||
/>
|
||||
<div class="flex">
|
||||
<Button
|
||||
v-if="completedPercentage != 0"
|
||||
variant="ghost"
|
||||
:label="__('Reset all')"
|
||||
@click="() => resetAll(afterResetAll)"
|
||||
/>
|
||||
<Button
|
||||
v-if="completedPercentage != 100"
|
||||
variant="ghost"
|
||||
:label="__('Skip all')"
|
||||
@click="() => skipAll(afterSkipAll)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5 overflow-y-auto">
|
||||
<div
|
||||
v-for="step in steps"
|
||||
:key="step.title"
|
||||
class="group w-full flex gap-2 justify-between items-center hover:bg-surface-gray-1 rounded px-2 py-1.5 cursor-pointer"
|
||||
@click.stop="() => !step.completed && !isDependent(step) && step.onClick()"
|
||||
>
|
||||
<component :is="isDependent(step) ? Tooltip : 'div'" :text="dependsOnTooltip(step)">
|
||||
<div
|
||||
class="flex gap-2 items-center"
|
||||
:class="[
|
||||
step.completed
|
||||
? 'text-ink-gray-5'
|
||||
: isDependent(step)
|
||||
? 'text-ink-gray-4'
|
||||
: 'text-ink-gray-8',
|
||||
]"
|
||||
>
|
||||
<component :is="step.icon" class="h-4" />
|
||||
<div class="text-base" :class="{ 'line-through': step.completed }">
|
||||
{{ step.title }}
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
<Button
|
||||
v-if="!step.completed && !isDependent(step)"
|
||||
:label="__('Skip')"
|
||||
class="!h-4 text-xs !text-ink-gray-6 hidden group-hover:flex"
|
||||
@click="() => skip(step.name, afterSkip)"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="!isDependent(step)"
|
||||
:label="__('Reset')"
|
||||
class="!h-4 text-xs !text-ink-gray-6 hidden group-hover:flex"
|
||||
@click.stop="() => reset(step.name, afterReset)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useOnboarding } from 'frappe-ui/frappe/Onboarding/onboarding'
|
||||
import Tooltip from 'frappe-ui/src/components/Tooltip/Tooltip.vue'
|
||||
import Button from 'frappe-ui/src/components/Button/Button.vue'
|
||||
import Badge from 'frappe-ui/src/components/Badge/Badge.vue'
|
||||
|
||||
const props = defineProps({
|
||||
appName: { type: String, default: 'frappecrm' },
|
||||
title: { type: String, default: 'Frappe CRM' },
|
||||
logo: { type: Object, required: true },
|
||||
afterSkip: { type: Function, default: () => {} },
|
||||
afterSkipAll: { type: Function, default: () => {} },
|
||||
afterReset: { type: Function, default: () => {} },
|
||||
afterResetAll: { type: Function, default: () => {} },
|
||||
})
|
||||
|
||||
function isDependent(step) {
|
||||
if (step.dependsOn && !step.completed) {
|
||||
const dependsOnStep = steps.find((s) => s.name === step.dependsOn)
|
||||
if (dependsOnStep && !dependsOnStep.completed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function dependsOnTooltip(step) {
|
||||
if (step.dependsOn && !step.completed) {
|
||||
const dependsOnStep = steps.find((s) => s.name === step.dependsOn)
|
||||
if (dependsOnStep && !dependsOnStep.completed) {
|
||||
return `You need to complete "${dependsOnStep.title}" first.`
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const { steps, stepsCompleted, totalSteps, completedPercentage, skip, skipAll, reset, resetAll } =
|
||||
useOnboarding(props.appName)
|
||||
</script>
|
||||
|
||||
@@ -166,23 +166,23 @@ const reloadSubmissions = () => {
|
||||
const submissionColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Member',
|
||||
label: __('Member'),
|
||||
key: 'member_name',
|
||||
width: 1,
|
||||
},
|
||||
{
|
||||
label: 'Assignment',
|
||||
label: __('Assignment'),
|
||||
key: 'assignment_title',
|
||||
width: 2,
|
||||
},
|
||||
{
|
||||
label: 'Submitted',
|
||||
label: __('Submitted'),
|
||||
key: 'creation',
|
||||
width: 1,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
label: __('Status'),
|
||||
key: 'status',
|
||||
width: 1,
|
||||
align: 'center',
|
||||
@@ -193,9 +193,9 @@ const submissionColumns = computed(() => {
|
||||
const statusOptions = computed(() => {
|
||||
return [
|
||||
{ label: '', value: '' },
|
||||
{ label: 'Pass', value: 'Pass' },
|
||||
{ label: 'Fail', value: 'Fail' },
|
||||
{ label: 'Not Graded', value: 'Not Graded' },
|
||||
{ label: __('Pass'), value: 'Pass' },
|
||||
{ label: __('Fail'), value: 'Fail' },
|
||||
{ label: __('Not Graded'), value: 'Not Graded' },
|
||||
]
|
||||
})
|
||||
|
||||
@@ -212,7 +212,7 @@ const getStatusTheme = (status) => {
|
||||
const breadcrumbs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Assignment Submissions',
|
||||
label: __('Assignment Submissions'),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}"
|
||||
>
|
||||
</ListView>
|
||||
<EmptyState v-else type="Assignments" />
|
||||
<EmptyState v-else :type="__('Assignments').toLowerCase()" />
|
||||
<div
|
||||
v-if="assignments.data && assignments.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
@@ -198,7 +198,7 @@ const assignmentTypes = computed(() => {
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{
|
||||
label: 'Assignments',
|
||||
label: __('Assignments'),
|
||||
route: { name: 'Assignments' },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -317,10 +317,10 @@ const batch = createResource({
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let crumbs = [{ label: 'Batches', route: { name: 'Batches' } }]
|
||||
let crumbs = [{ label: __('Batches'), route: { name: 'Batches' } }]
|
||||
if (!isStudent.value) {
|
||||
crumbs.push({
|
||||
label: 'Details',
|
||||
label: __('Details'),
|
||||
route: {
|
||||
name: 'BatchDetail',
|
||||
params: {
|
||||
|
||||
@@ -120,7 +120,7 @@ const courses = createResource({
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Batches', route: { name: 'Batches' } }]
|
||||
let items = [{ label: __('Batches'), route: { name: 'Batches' } }]
|
||||
items.push({
|
||||
label: batch?.data?.title,
|
||||
route: { name: 'BatchDetail', params: { batchName: batch?.data?.name } },
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<BatchCard :batch="batch" />
|
||||
</router-link>
|
||||
</div>
|
||||
<EmptyState v-else-if="!batches.list.loading" type="Batches" />
|
||||
<EmptyState v-else-if="!batches.list.loading" :type="__('Batches').toLowerCase()" />
|
||||
|
||||
<div
|
||||
v-if="!batches.list.loading && batches.hasNextPage"
|
||||
|
||||
@@ -132,6 +132,7 @@ const participants = createListResource({
|
||||
doctype: 'LMS Certificate',
|
||||
url: 'lms.lms.api.get_certified_participants',
|
||||
start: 0,
|
||||
cache: ['certified_participants'],
|
||||
pageLength: 100,
|
||||
})
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ watch(
|
||||
)
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
|
||||
let items = [{ label: __('Courses'), route: { name: 'Courses' } }]
|
||||
items.push({
|
||||
label: course?.data?.title,
|
||||
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
|
||||
|
||||
@@ -47,9 +47,16 @@
|
||||
:required="true"
|
||||
/>
|
||||
<div>
|
||||
<div class="mb-1.5 text-xs text-ink-gray-5">
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ __('Tags') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
:class="['w-full', 'flex-1', 'my-1']"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
<div>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<div
|
||||
@@ -64,37 +71,13 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
:class="[
|
||||
'w-full',
|
||||
'flex-1',
|
||||
{ 'mt-2': course.tags?.length },
|
||||
]"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
{{ __('Course Image') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
@@ -144,6 +127,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorSwatches
|
||||
v-model="course.card_gradient"
|
||||
:label="__('Color')"
|
||||
:description="__('Choose a color for the course card')"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +175,21 @@
|
||||
</div>
|
||||
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('About the Course') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Course Description') }}
|
||||
@@ -342,6 +347,7 @@ import {
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const newTag = ref('')
|
||||
@@ -365,6 +371,7 @@ const course = reactive({
|
||||
description: '',
|
||||
video_link: '',
|
||||
course_image: null,
|
||||
card_gradient: '',
|
||||
tags: '',
|
||||
category: '',
|
||||
published: false,
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="courses.data?.length"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-5"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8"
|
||||
>
|
||||
<router-link
|
||||
v-for="course in courses.data"
|
||||
@@ -66,7 +66,7 @@
|
||||
<CourseCard :course="course" />
|
||||
</router-link>
|
||||
</div>
|
||||
<EmptyState v-else-if="!courses.list.loading" type="Courses" />
|
||||
<EmptyState v-else-if="!courses.list.loading" :type="__('Courses').toLowerCase()" />
|
||||
<div
|
||||
v-if="!courses.list.loading && courses.hasNextPage"
|
||||
class="flex justify-center mt-5"
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Job Openings" />
|
||||
<EmptyState v-else :type="__('Job Openings').toLowerCase()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
<div class="mt-20" ref="discussionsContainer">
|
||||
<Discussions
|
||||
v-if="allowDiscussions"
|
||||
:title="'Questions'"
|
||||
:title="__('Questions')"
|
||||
:doctype="'Course Lesson'"
|
||||
:docname="lesson.data.name"
|
||||
:key="lesson.data.name"
|
||||
@@ -433,7 +433,7 @@ const progress = createResource({
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
|
||||
let items = [{ label: __('Courses'), route: { name: 'Courses' } }]
|
||||
items.push({
|
||||
label: lesson?.data?.course_title,
|
||||
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||
@@ -604,7 +604,7 @@ const updateVideoTime = (video) => {
|
||||
}
|
||||
|
||||
const startTimer = () => {
|
||||
timerInterval = setInterval(() => {
|
||||
let timerInterval = setInterval(() => {
|
||||
timer.value++
|
||||
if (timer.value == 30) {
|
||||
clearInterval(timerInterval)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</Button>
|
||||
<TabButtons
|
||||
class="inline-block"
|
||||
:buttons="[{ label: 'Unread', active: true }, { label: 'Read' }]"
|
||||
:buttons="[{ label: __('Unread'), active: true }, { label: __('Read') }]"
|
||||
v-model="activeTab"
|
||||
/>
|
||||
</div>
|
||||
@@ -144,7 +144,7 @@ onUnmounted(() => {
|
||||
const breadcrumbs = computed(() => {
|
||||
let crumbs = [
|
||||
{
|
||||
label: 'Notifications',
|
||||
label: __('Notifications'),
|
||||
route: {
|
||||
name: 'Notifications',
|
||||
},
|
||||
@@ -155,7 +155,7 @@ const breadcrumbs = computed(() => {
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Notifications',
|
||||
title: __('Notifications'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -195,14 +195,14 @@ const isSessionUser = () => {
|
||||
}
|
||||
|
||||
const getTabButtons = () => {
|
||||
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
|
||||
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
|
||||
let buttons = [{ label: __('About') }, { label: __('Certificates') }]
|
||||
if ($user.data?.is_moderator) buttons.push({ label: __('Roles') })
|
||||
if (
|
||||
isSessionUser() &&
|
||||
($user.data?.is_evaluator || $user.data?.is_moderator)
|
||||
) {
|
||||
buttons.push({ label: 'Slots' })
|
||||
buttons.push({ label: 'Schedule' })
|
||||
buttons.push({ label: __('Slots') })
|
||||
buttons.push({ label: __('Schedule') })
|
||||
}
|
||||
|
||||
return buttons
|
||||
@@ -211,7 +211,7 @@ const getTabButtons = () => {
|
||||
const breadcrumbs = computed(() => {
|
||||
let crumbs = [
|
||||
{
|
||||
label: 'People',
|
||||
label: __('People'),
|
||||
},
|
||||
{
|
||||
label: profile.data?.full_name,
|
||||
|
||||
@@ -323,12 +323,12 @@ const saveProgram = () => {
|
||||
const courseColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Title',
|
||||
label: __('Title'),
|
||||
key: 'course_title',
|
||||
width: 3,
|
||||
},
|
||||
{
|
||||
label: 'ID',
|
||||
label: __('ID'),
|
||||
key: 'course',
|
||||
width: 3,
|
||||
},
|
||||
@@ -338,19 +338,19 @@ const courseColumns = computed(() => {
|
||||
const memberColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Member',
|
||||
label: __('Member'),
|
||||
key: 'member',
|
||||
width: 3,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Full Name',
|
||||
label: __('Full Name'),
|
||||
key: 'full_name',
|
||||
width: 3,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Progress (%)',
|
||||
label: __('Progress (%)'),
|
||||
key: 'progress',
|
||||
width: 3,
|
||||
align: 'right',
|
||||
@@ -361,11 +361,11 @@ const memberColumns = computed(() => {
|
||||
const breadbrumbs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Programs',
|
||||
label: __('Programs'),
|
||||
route: { name: 'Programs' },
|
||||
},
|
||||
{
|
||||
label: props.programName === 'new' ? 'New Program' : props.programName,
|
||||
label: props.programName === 'new' ? __('New Program') : props.programName,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Programs" />
|
||||
<EmptyState v-else :type="__('Programs').toLowerCase()" />
|
||||
|
||||
<Dialog
|
||||
v-model="showDialog"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
: __('No Quizzes')
|
||||
}}
|
||||
</div>
|
||||
<FormControl v-model="search" type="text" placeholder="Search">
|
||||
<FormControl v-model="search" type="text" :placeholder="__('Search')">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="search" class="size-4 text-ink-gray-5" />
|
||||
</template>
|
||||
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
<EmptyState v-else type="Quizzes" />
|
||||
<EmptyState v-else :type="__('Quizzes').toLowerCase()" />
|
||||
<div v-if="quizzes.hasNextPage" class="flex justify-center my-5">
|
||||
<Button @click="quizzes.next()">
|
||||
{{ __('Load More') }}
|
||||
|
||||
@@ -7,23 +7,23 @@
|
||||
</header>
|
||||
<div v-if="chartDetails.data" class="p-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<Tooltip :text="__('Published Courses')">
|
||||
<Tooltip :text="__('Published Courses')">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{ title: 'Courses', value: chartDetails.data.courses }"
|
||||
:config="{ title: __('Courses'), value: chartDetails.data.courses }"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Active Members')">
|
||||
<Tooltip :text="__('Active Members')">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{ title: 'Signups', value: chartDetails.data.users }"
|
||||
:config="{ title: __('Signups'), value: chartDetails.data.users }"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Course Enrollments')">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: 'Enrollments',
|
||||
:config="{
|
||||
title: __('Enrollments'),
|
||||
value: chartDetails.data.enrollments,
|
||||
}"
|
||||
/>
|
||||
@@ -31,8 +31,8 @@
|
||||
<Tooltip :text="__('Course Completions')">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: 'Completions',
|
||||
:config="{
|
||||
title: __('Completions'),
|
||||
value: chartDetails.data.completions,
|
||||
}"
|
||||
/>
|
||||
@@ -40,8 +40,8 @@
|
||||
<Tooltip :text="__('Certified Members')">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: 'Certifications',
|
||||
:config="{
|
||||
title: __('Certifications'),
|
||||
value: chartDetails.data.certifications,
|
||||
}"
|
||||
/>
|
||||
@@ -53,16 +53,16 @@
|
||||
v-if="signupsChart.data"
|
||||
:config="{
|
||||
data: signupsChart.data,
|
||||
title: 'Signups',
|
||||
subtitle: 'Signups per month',
|
||||
title: __('Signups'),
|
||||
subtitle: __('Signups per month'),
|
||||
xAxis: {
|
||||
key: 'date',
|
||||
type: 'time',
|
||||
title: 'Date',
|
||||
title: __('Date'),
|
||||
timeGrain: 'day',
|
||||
},
|
||||
yAxis: {
|
||||
title: 'Signups',
|
||||
title: __('Signups'),
|
||||
},
|
||||
series: [{ name: 'signups', type: 'line', showDataPoints: true }],
|
||||
}"
|
||||
@@ -73,16 +73,16 @@
|
||||
v-if="enrollmentChart.data"
|
||||
:config="{
|
||||
data: enrollmentChart.data,
|
||||
title: 'Enrollments',
|
||||
subtitle: 'Enrollments per month',
|
||||
title: __('Enrollments'),
|
||||
subtitle: __('Enrollments per month'),
|
||||
xAxis: {
|
||||
key: 'date',
|
||||
type: 'time',
|
||||
title: 'Date',
|
||||
title: __('Date'),
|
||||
timeGrain: 'day',
|
||||
},
|
||||
yAxis: {
|
||||
title: 'Enrollments',
|
||||
title: __('Enrollments'),
|
||||
},
|
||||
series: [
|
||||
{ name: 'enrollments', type: 'line', showDataPoints: true },
|
||||
@@ -95,16 +95,16 @@
|
||||
v-if="certification.data"
|
||||
:config="{
|
||||
data: certification.data,
|
||||
title: 'Certifications',
|
||||
subtitle: 'Certifications per month',
|
||||
title: __('Certifications'),
|
||||
subtitle: __('Certifications per month'),
|
||||
xAxis: {
|
||||
key: 'date',
|
||||
type: 'time',
|
||||
title: 'Date',
|
||||
title: __('Date'),
|
||||
timeGrain: 'day',
|
||||
},
|
||||
yAxis: {
|
||||
title: 'Certifications',
|
||||
title: __('Certifications'),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -121,8 +121,8 @@
|
||||
v-if="courseCompletion.data"
|
||||
:config="{
|
||||
data: courseCompletion.data,
|
||||
title: 'Completions',
|
||||
subtitle: 'Course Completion',
|
||||
title: __('Completions'),
|
||||
subtitle: __('Course Completion'),
|
||||
categoryColumn: 'label',
|
||||
valueColumn: 'value',
|
||||
}"
|
||||
@@ -150,7 +150,7 @@ const { brand } = sessionStore()
|
||||
const breadcrumbs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Statistics',
|
||||
label: __('Statistics'),
|
||||
route: {
|
||||
name: 'Statistics',
|
||||
},
|
||||
|
||||
@@ -403,7 +403,7 @@ export function getUserTimezone() {
|
||||
export function getSidebarLinks() {
|
||||
return [
|
||||
{
|
||||
label: 'Courses',
|
||||
label: __('Courses'),
|
||||
icon: 'BookOpen',
|
||||
to: 'Courses',
|
||||
activeFor: [
|
||||
@@ -415,36 +415,25 @@ export function getSidebarLinks() {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Batches',
|
||||
label: __('Batches'),
|
||||
icon: 'Users',
|
||||
to: 'Batches',
|
||||
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
|
||||
},
|
||||
{
|
||||
label: 'Programming Exercises',
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
activeFor: [
|
||||
'ProgrammingExercises',
|
||||
'ProgrammingExerciseForm',
|
||||
'ProgrammingExerciseSubmissions',
|
||||
'ProgrammingExerciseSubmission',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Certified Members',
|
||||
label: __('Certified Members'),
|
||||
icon: 'GraduationCap',
|
||||
to: 'CertifiedParticipants',
|
||||
activeFor: ['CertifiedParticipants'],
|
||||
},
|
||||
{
|
||||
label: 'Jobs',
|
||||
label: __('Jobs'),
|
||||
icon: 'Briefcase',
|
||||
to: 'Jobs',
|
||||
activeFor: ['Jobs', 'JobDetail'],
|
||||
},
|
||||
{
|
||||
label: 'Statistics',
|
||||
label: __('Statistics'),
|
||||
icon: 'TrendingUp',
|
||||
to: 'Statistics',
|
||||
activeFor: ['Statistics'],
|
||||
@@ -551,11 +540,10 @@ export const enablePlyr = async () => {
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const setupPlyrForVideo = (video, players) => {
|
||||
const src = video.getAttribute('src') || video.getAttribute('data-src')
|
||||
const src = video.getAttribute('src')
|
||||
|
||||
if (src) {
|
||||
const videoID = extractYouTubeId(src)
|
||||
video.setAttribute('data-plyr-provider', 'youtube')
|
||||
video.setAttribute('data-plyr-embed-id', videoID)
|
||||
}
|
||||
|
||||
@@ -679,3 +667,9 @@ export const formatTimestamp = (seconds) => {
|
||||
const secs = String(date.getUTCSeconds()).padStart(2, '0')
|
||||
return `${minutes}:${secs}`
|
||||
}
|
||||
|
||||
export const convertToMinutes = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = Math.round(seconds % 60)
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import frappeui from 'frappe-ui/vite'
|
||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ['fs', 'per2'],
|
||||
allowedHosts: ['lms.localhost', 'fs', 'per2'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.32.1"
|
||||
__version__ = "2.33.0"
|
||||
|
||||
9
lms/lms.iml
Normal file
9
lms/lms.iml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -16,13 +16,14 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"video_link",
|
||||
"tags",
|
||||
"column_break_3",
|
||||
"instructors",
|
||||
"tags",
|
||||
"column_break_htgn",
|
||||
"image",
|
||||
"category",
|
||||
"status",
|
||||
"column_break_htgn",
|
||||
"image",
|
||||
"card_gradient",
|
||||
"section_break_7",
|
||||
"published",
|
||||
"published_on",
|
||||
@@ -98,8 +99,7 @@
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Preview Image",
|
||||
"reqd": 1
|
||||
"label": "Preview Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "tags",
|
||||
@@ -272,6 +272,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Evaluator",
|
||||
"options": "Course Evaluator"
|
||||
},
|
||||
{
|
||||
"fieldname": "card_gradient",
|
||||
"fieldtype": "Select",
|
||||
"label": "Color",
|
||||
"options": "Red\nBlue\nGreen\nAmber\nCyan\nOrange\nPink\nPurple\nTeal\nViolet\nYellow\nGray"
|
||||
}
|
||||
],
|
||||
"is_published_field": "published",
|
||||
@@ -290,8 +296,8 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-05-29 12:38:01.002898",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-07-25 17:50:44.983391",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -21,6 +21,7 @@ class LMSCourse(Document):
|
||||
self.validate_certification()
|
||||
self.validate_amount_and_currency()
|
||||
self.image = validate_image(self.image)
|
||||
self.validate_card_gradient()
|
||||
|
||||
def validate_published(self):
|
||||
if self.published and not self.published_on:
|
||||
@@ -73,6 +74,24 @@ class LMSCourse(Document):
|
||||
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
|
||||
frappe.throw(_("Amount and currency are required for paid certificates."))
|
||||
|
||||
def validate_card_gradient(self):
|
||||
if not self.image and not self.card_gradient:
|
||||
colors = [
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Yellow",
|
||||
"Orange",
|
||||
"Pink",
|
||||
"Amber",
|
||||
"Violet",
|
||||
"Cyan",
|
||||
"Teal",
|
||||
"Gray",
|
||||
"Purple",
|
||||
]
|
||||
self.card_gradient = random.choice(colors)
|
||||
|
||||
def on_update(self):
|
||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||
self.send_email_to_interested_users()
|
||||
|
||||
@@ -565,10 +565,13 @@ def get_courses_under_review():
|
||||
|
||||
def validate_image(path):
|
||||
if path and "/private" in path:
|
||||
file = frappe.get_doc("File", {"file_url": path})
|
||||
file.is_private = 0
|
||||
file.save()
|
||||
return file.file_url
|
||||
frappe.db.set_value(
|
||||
"File",
|
||||
{"file_url": path},
|
||||
"is_private",
|
||||
0,
|
||||
)
|
||||
return path.replace("/private", "")
|
||||
return path
|
||||
|
||||
|
||||
@@ -1097,6 +1100,7 @@ def get_course_fields():
|
||||
"title",
|
||||
"tags",
|
||||
"image",
|
||||
"card_gradient",
|
||||
"short_introduction",
|
||||
"published",
|
||||
"upcoming",
|
||||
|
||||
459
lms/locale/ar.po
459
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
463
lms/locale/bs.po
463
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/cs.po
459
lms/locale/cs.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/de.po
459
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
462
lms/locale/eo.po
462
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/es.po
459
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/fa.po
459
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
2448
lms/locale/fr.po
2448
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
463
lms/locale/hr.po
463
lms/locale/hr.po
File diff suppressed because it is too large
Load Diff
471
lms/locale/hu.po
471
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
7434
lms/locale/id.po
Normal file
7434
lms/locale/id.po
Normal file
File diff suppressed because it is too large
Load Diff
459
lms/locale/it.po
459
lms/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
459
lms/locale/nl.po
459
lms/locale/nl.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/pl.po
459
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
461
lms/locale/pt.po
461
lms/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
459
lms/locale/ru.po
459
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
462
lms/locale/sr.po
462
lms/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
463
lms/locale/sv.po
463
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
469
lms/locale/th.po
469
lms/locale/th.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/tr.po
459
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/vi.po
459
lms/locale/vi.po
File diff suppressed because it is too large
Load Diff
459
lms/locale/zh.po
459
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
372
lms/translations/fr.csv
Normal file
372
lms/translations/fr.csv
Normal file
@@ -0,0 +1,372 @@
|
||||
"Courses","Cours"
|
||||
"Batches","Sessions"
|
||||
"Certified Members","Membres certifiés"
|
||||
"Jobs","Offres"
|
||||
"Statistics","Statistiques"
|
||||
"Programs","Programmes"
|
||||
"Programming Exercises","Exercices de programmation"
|
||||
"Assignments","Devoirs"
|
||||
"Quizzes","Quiz"
|
||||
"More","Plus"
|
||||
"Expand","Développer"
|
||||
"Collapse","Réduire"
|
||||
"Help","Aide"
|
||||
"Powered by Learning","Propulsé par Learning"
|
||||
"Learning","Learning"
|
||||
"Details","Détails"
|
||||
"Assessments","Évaluations"
|
||||
"Discussions","Discussions"
|
||||
"People","Personnes"
|
||||
"Certification","Certification"
|
||||
"Course Outline","Plan du cours"
|
||||
"Average Rating","Note moyenne"
|
||||
"Enrolled Students","Étudiants inscrits"
|
||||
"Zen Mode","Mode Zen"
|
||||
"Video Statistics","Statistiques vidéo"
|
||||
"This lesson is locked","Cette leçon est verrouillée"
|
||||
"This lesson is not available for preview. Please enroll in the course to access it.","Cette leçon n’est pas disponible en aperçu. Veuillez vous inscrire au cours pour y accéder."
|
||||
"Start Learning","Commencer l'apprentissage"
|
||||
"Contact the Administrator to enroll for this course.","Contactez l’administrateur pour vous inscrire à ce cours."
|
||||
"Login","Se connecter"
|
||||
"completed","terminé"
|
||||
"Previous","Précédent"
|
||||
"Edit","Modifier"
|
||||
"Next","Suivant"
|
||||
"Back to Course","Retour au cours"
|
||||
"Instructor Notes","Notes du formateur"
|
||||
"Certification","Certification"
|
||||
"You are already certified for this course. Click on the card below to open your certificate.","Vous êtes déjà certifié pour ce cours. Cliquez sur la carte ci-dessous pour ouvrir votre certificat."
|
||||
"Issued On","Délivré le"
|
||||
"Issued on","Délivré le"
|
||||
"{0} Open Jobs","{0} offres ouvertes"
|
||||
"New Job","Nouvelle offre"
|
||||
"Search","Rechercher"
|
||||
"Country","Pays"
|
||||
"Type","Type"
|
||||
"Full Time","Temps plein"
|
||||
"Part Time","Temps partiel"
|
||||
"Contract","Contrat"
|
||||
"Freelance","Freelance"
|
||||
"Jobs","Offres"
|
||||
"No results found","Aucun résultat"
|
||||
"Delete","Supprimer"
|
||||
"Add Row","Ajouter une ligne"
|
||||
"Add","Ajouter"
|
||||
"Save","Enregistrer"
|
||||
"Submit","Envoyer"
|
||||
"Create","Créer"
|
||||
"Post","Publier"
|
||||
"No Recording","Pas d’enregistrement"
|
||||
"Local","Local"
|
||||
"Cloud","Cloud"
|
||||
"PDF","PDF"
|
||||
"Image","Image"
|
||||
"Document","Document"
|
||||
"Text","Texte"
|
||||
"URL","URL"
|
||||
"Quiz","Quiz"
|
||||
"Assignment","Devoir"
|
||||
"Programming Exercise","Exercice de programmation"
|
||||
"Add a new member","Ajouter un nouveau membre"
|
||||
"Add Evaluator","Ajouter un évaluateur"
|
||||
"Evaluator added successfully","Évaluateur ajouté avec succès"
|
||||
"Evaluator deleted successfully","Évaluateur supprimé avec succès"
|
||||
"Email","Adresse e-mail"
|
||||
"First Name","Prénom"
|
||||
"Load More","Charger plus"
|
||||
"Student","Étudiant"
|
||||
"Moderator","Modérateur"
|
||||
"Evaluator","Évaluateur"
|
||||
"Mark all as read","Tout marquer comme lu"
|
||||
"Unread","Non lus"
|
||||
"Read","Lus"
|
||||
"View","Voir"
|
||||
"Nothing to see here.","Rien à afficher."
|
||||
"Not Permitted","Non autorisé"
|
||||
"You are not a member of this batch. Please checkout our upcoming batches.","Vous n’êtes pas membre de cette session. Veuillez consulter nos sessions à venir."
|
||||
"Please login to access this page.","Veuillez vous connecter pour accéder à cette page."
|
||||
"Upcoming Batches","Sessions à venir"
|
||||
"Assignment","Devoir"
|
||||
"Member","Membre"
|
||||
"Submitted","Soumis"
|
||||
"Status","Statut"
|
||||
"Pass","Réussi"
|
||||
"Fail","Échoué"
|
||||
"Not Graded","Non noté"
|
||||
"No submissions","Aucune soumission"
|
||||
"There are no submissions for this assignment.","Il n’y a aucune soumission pour ce devoir."
|
||||
"You will have to complete the quiz to continue the video","Vous devrez terminer le quiz pour continuer la vidéo"
|
||||
"This quiz consists of {0} questions.","Ce quiz comporte {0} questions."
|
||||
"Please ensure that you complete all the questions in {0} minutes.","Veuillez vous assurer de terminer toutes les questions en {0} minutes."
|
||||
"If you fail to do so, the quiz will be automatically submitted when the timer ends.","À défaut, le quiz sera automatiquement soumis à la fin du minuteur."
|
||||
"You will have to get {0}% correct answers in order to pass the quiz.","Vous devrez obtenir {0}% de bonnes réponses pour réussir le quiz."
|
||||
"You can attempt this quiz {0}.","Vous pouvez tenter ce quiz {0}."
|
||||
"Time","Temps"
|
||||
"Start the Quiz","Commencer le quiz"
|
||||
"Start","Démarrer"
|
||||
"Resume Video","Reprendre la vidéo"
|
||||
"You have already exceeded the maximum number of attempts allowed for this quiz.","Vous avez déjà dépassé le nombre maximal de tentatives autorisées pour ce quiz."
|
||||
"Question {0}","Question {0}"
|
||||
"Mark","Point"
|
||||
"Marks","Points"
|
||||
"Question {0} of {1}","Question {0} sur {1}"
|
||||
"Check","Vérifier"
|
||||
"Submit","Soumettre"
|
||||
"Next","Suivant"
|
||||
"Quiz Summary","Récapitulatif du quiz"
|
||||
"Your submission has been successfully saved. The instructor will review and grade it shortly, and you'll be notified of your final result.","Votre soumission a bien été enregistrée. Le formateur l’examinera et la notera prochainement ; vous serez informé du résultat final."
|
||||
"You got {0}% correct answers with a score of {1} out of {2}","Vous avez {0}% de bonnes réponses avec un score de {1} sur {2}"
|
||||
"Try Again","Réessayer"
|
||||
"No Quiz submissions found","Aucune soumission de quiz trouvée"
|
||||
"Choose all answers that apply","Choisissez toutes les réponses correctes"
|
||||
"Choose one answer","Choisissez une réponse"
|
||||
"Type your answer","Saisissez votre réponse"
|
||||
"Please select an option","Veuillez sélectionner une option"
|
||||
"Correct","Correct"
|
||||
"Incorrect","Incorrect"
|
||||
"Settings","Paramètres"
|
||||
"Programming Exercises","Exercices de programmation"
|
||||
"Certified Members","Membres certifiés"
|
||||
"Statistics","Statistiques"
|
||||
"Meta Description","Méta‑description"
|
||||
"Meta Keywords","Mots‑clés"
|
||||
"Meta Image","Image méta"
|
||||
"This site is being updated. You will not be able to make any changes. Full access will be restored shortly.","Ce site est en cours de mise à jour. Vous ne pourrez pas effectuer de modifications. L’accès complet sera rétabli sous peu."
|
||||
"Introduction","Introduction"
|
||||
"Setting up","Mise en place"
|
||||
"Creating a course","Créer un cours"
|
||||
"Create a course","Créer un cours"
|
||||
"Add a chapter","Ajouter un chapitre"
|
||||
"Add a lesson","Ajouter une leçon"
|
||||
"Creating a batch","Créer une session"
|
||||
"Create a batch","Créer une session"
|
||||
"Create a live class","Créer un cours en direct"
|
||||
"Assessments","Évaluations"
|
||||
"Quizzes","Quiz"
|
||||
"Assignments","Devoirs"
|
||||
"Certification","Certification"
|
||||
"Issue a Certificate","Émettre un certificat"
|
||||
"Custom Certificate Templates","Modèles de certificat personnalisés"
|
||||
"Monetization","Monétisation"
|
||||
"Setting up payment gateway","Configuration de la passerelle de paiement"
|
||||
"Roles","Rôles"
|
||||
"Create your first course","Créez votre premier cours"
|
||||
"Add your first chapter","Ajoutez votre premier chapitre"
|
||||
"Add your first lesson","Ajoutez votre première leçon"
|
||||
"Create your first quiz","Créez votre premier quiz"
|
||||
"Invite your team and students","Invitez votre équipe et vos étudiants"
|
||||
"Create your first batch","Créez votre première session"
|
||||
"Add students to your batch","Ajoutez des étudiants à votre session"
|
||||
"Add courses to your batch","Ajoutez des cours à votre session"
|
||||
"Only image file is allowed.","Seuls les fichiers image sont autorisés."
|
||||
"Failed to update meta tags {0}","Échec de la mise à jour des méta‑balises {0}"
|
||||
"Courses deleted successfully","Cours supprimés avec succès"
|
||||
"No courses added","Aucun cours ajouté"
|
||||
"Course","Cours"
|
||||
"All Courses","Tous les cours"
|
||||
"Instructors","Formateurs"
|
||||
"Completed","Terminé"
|
||||
"Enrolled","Inscrits"
|
||||
"Get Certified","Obtenir la certification"
|
||||
"Reviews","Avis"
|
||||
"Write a review","Rédiger un avis"
|
||||
"Review the course","Évaluer le cours"
|
||||
"Help us improve our course material.","Aidez-nous à améliorer le contenu du cours."
|
||||
"Add Chapter","Ajouter un chapitre"
|
||||
"Add Lesson","Ajouter une leçon"
|
||||
"Questions","Questions"
|
||||
"Notify me when available","Me prévenir lorsqu’il sera disponible"
|
||||
"Continue Learning","Continuer l’apprentissage"
|
||||
"Get Certificate","Obtenir le certificat"
|
||||
"Slots","Créneaux"
|
||||
"This lesson is not available for preview. Please join the course to access it.","Cette leçon n’est pas disponible en aperçu. Veuillez rejoindre le cours pour y accéder."
|
||||
"Cancel","Annuler"
|
||||
"others","autres"
|
||||
"Course List","Liste des cours"
|
||||
"Headline","Titre"
|
||||
"Upcoming","À venir"
|
||||
"Live","En direct"
|
||||
"Created","Créé"
|
||||
"Category","Catégorie"
|
||||
"Search by Title","Rechercher par titre"
|
||||
"Generate Certificates","Générer les certificats"
|
||||
"Make an Announcement","Faire une annonce"
|
||||
"About this batch","À propos de cette session"
|
||||
"Create a Live Class","Créer un cours en direct"
|
||||
"Title","Titre"
|
||||
"Date","Date"
|
||||
"Duration of the live class in minutes","Durée du cours en direct en minutes"
|
||||
"Duration","Durée"
|
||||
"Time","Heure"
|
||||
"Timezone","Fuseau horaire"
|
||||
"Auto Recording","Enregistrement automatique"
|
||||
"Description","Description"
|
||||
"Please enter a title.","Veuillez saisir un titre."
|
||||
"Please select a date.","Veuillez sélectionner une date."
|
||||
"Please select a time.","Veuillez sélectionner une heure."
|
||||
"Please select a timezone.","Veuillez sélectionner un fuseau horaire."
|
||||
"Please enter a valid time in the format HH:mm.","Veuillez saisir une heure valide au format HH:mm."
|
||||
"Please select a future date and time.","Veuillez sélectionner une date et une heure futures."
|
||||
"Please select a duration.","Veuillez sélectionner une durée."
|
||||
"Create an Assignment","Créer un devoir"
|
||||
"Edit Assignment","Modifier le devoir"
|
||||
"Submission Type","Type de soumission"
|
||||
"Question","Question"
|
||||
"Check Submissions","Voir les soumissions"
|
||||
"Assignment created successfully","Devoir créé avec succès"
|
||||
"Assignment updated successfully","Devoir mis à jour avec succès"
|
||||
"Add an assessment","Ajouter une évaluation"
|
||||
"Assessment","Évaluation"
|
||||
"Assessment added successfully","Évaluation ajoutée avec succès"
|
||||
"Discard","Annuler"
|
||||
"Submission by","Soumission par"
|
||||
"Submission","Soumission"
|
||||
"Not Saved","Non enregistré"
|
||||
"Feel free to make edits to your submission if needed.","N’hésitez pas à modifier votre soumission si nécessaire."
|
||||
"Add your assignment as {0}","Ajoutez votre devoir en tant que {0}"
|
||||
"Uploading {0}%","Téléversement {0}%"
|
||||
"Upload File","Téléverser un fichier"
|
||||
"Enter a URL","Saisir une URL"
|
||||
"Write your answer here","Écrivez votre réponse ici"
|
||||
"Comments by Evaluator","Commentaires de l’évaluateur"
|
||||
"Grading","Notation"
|
||||
"Grade","Note"
|
||||
"Comments","Commentaires"
|
||||
"Assignment submitted successfully","Devoir soumis avec succès"
|
||||
"Students","Étudiants"
|
||||
"Certified","Certifiés"
|
||||
"Batch Summary","Récapitulatif de la session"
|
||||
"Progress of students in courses and assessments","Progression des étudiants dans les cours et évaluations"
|
||||
"Number of Students","Nombre d’étudiants"
|
||||
"There are no students in this batch.","Il n’y a aucun étudiant dans cette session."
|
||||
"Students deleted successfully","Étudiants supprimés avec succès"
|
||||
"No Assessments","Aucune évaluation"
|
||||
"Status/Percentage","Statut/Pourcentage"
|
||||
"Create a Quiz","Créer un quiz"
|
||||
"No Quizzes","Aucun quiz"
|
||||
"Published Courses","Cours publiés"
|
||||
"Active Members","Membres actifs"
|
||||
"Course Enrollments","Inscriptions aux cours"
|
||||
"Course Completions","Cours terminés"
|
||||
"Add Chapter","Ajouter un chapitre"
|
||||
"Add Lesson","Ajouter une leçon"
|
||||
"Login to Frappe Cloud?","Se connecter à Frappe Cloud ?"
|
||||
"Are you sure you want to login to your Frappe Cloud dashboard?","Voulez‑vous vraiment vous connecter à votre tableau de bord Frappe Cloud ?"
|
||||
"Confirm","Confirmer"
|
||||
"New","Nouveau"
|
||||
"Edit Profile","Modifier le profil"
|
||||
"About","À propos"
|
||||
"Certificates","Certificats"
|
||||
"Roles","Rôles"
|
||||
"Assignment Submissions","Soumissions de devoirs"
|
||||
"Login to Frappe Cloud","Se connecter à Frappe Cloud"
|
||||
"Login to Frappe Cloud?","Se connecter à Frappe Cloud ?"
|
||||
"Are you sure you want to login to your Frappe Cloud dashboard?","Voulez‑vous vraiment vous connecter à votre tableau de bord Frappe Cloud ?"
|
||||
"Log out","Se déconnecter"
|
||||
"Log in","Se connecter"
|
||||
"Problem Statement","Énoncé du problème"
|
||||
"Run","Exécuter"
|
||||
"Compiler Message","Message du compilateur"
|
||||
"Test Cases","Cas de test"
|
||||
"Test {0}","Test {0}"
|
||||
"Input","Entrée"
|
||||
"Your Output","Votre sortie"
|
||||
"Expected Output","Sortie attendue"
|
||||
"Please run the code to execute the test cases.","Veuillez exécuter le code pour lancer les cas de test."
|
||||
"Check All Submissions","Voir toutes les soumissions"
|
||||
"Check Submission","Voir la soumission"
|
||||
"Program Courses","Cours du programme"
|
||||
"Program Members","Membres du programme"
|
||||
"ID","ID"
|
||||
"Full Name","Nom complet"
|
||||
"Progress (%)","Progression (%)"
|
||||
"Name","Nom"
|
||||
"Subject","Objet"
|
||||
"Thank you for providing your feedback.","Merci pour votre retour."
|
||||
"Click here","Cliquez ici"
|
||||
"to view your feedback.","pour afficher votre retour."
|
||||
"Help us improve by providing your feedback.","Aidez‑nous à nous améliorer en nous donnant votre avis."
|
||||
"Submit Feedback","Envoyer le retour"
|
||||
"Average Feedback Received","Retour moyen reçu"
|
||||
"View all feedback","Voir tous les retours"
|
||||
"No feedback received yet.","Aucun retour reçu pour le moment."
|
||||
"Choose an existing question","Choisir une question existante"
|
||||
"Options","Options"
|
||||
"Possibilities","Possibilités"
|
||||
"Option","Option"
|
||||
"Explanation","Explication"
|
||||
"Correct Answer","Réponse correcte"
|
||||
"Select a question","Sélectionner une question"
|
||||
"Test Quiz","Quiz de test"
|
||||
"Questions","Questions"
|
||||
"New Question","Nouvelle question"
|
||||
"No questions added yet","Aucune question ajoutée pour l’instant"
|
||||
"Answer","Réponse"
|
||||
"My availability","Ma disponibilité"
|
||||
"Day","Jour"
|
||||
"Start Time","Heure de début"
|
||||
"End Time","Heure de fin"
|
||||
"Add Slot","Ajouter un créneau"
|
||||
"I am unavailable","Je ne suis pas disponible"
|
||||
"My calendar","Mon calendrier"
|
||||
"Your calendar is set.","Votre calendrier est configuré."
|
||||
"Authorize Google Calendar Access","Autoriser l'accès à Google Agenda"
|
||||
"{0} Exercises","{0} exercices"
|
||||
"Unpublished","Non publié"
|
||||
"No {0}","Aucun {0}"
|
||||
"There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!","Il n’y a actuellement aucun {0}. Restez à l’affût, de nouvelles expériences d’apprentissage arrivent bientôt !"
|
||||
"courses","cours"
|
||||
"Courses","Cours"
|
||||
"completed","terminé"
|
||||
"% completed","% terminé"
|
||||
"Skip","Ignorer"
|
||||
"Create your first course","Créez votre premier cours"
|
||||
"Add your first chapter","Ajoutez votre premier chapitre"
|
||||
"Add your first lesson","Ajoutez votre première leçon"
|
||||
"Create your first quiz","Créez votre premier quiz"
|
||||
"Invite your team and students","Invitez votre équipe et vos étudiants"
|
||||
"Create your first batch","Créez votre première session"
|
||||
"Add students to your batch","Ajoutez des étudiants à votre session"
|
||||
"Add courses to your batch","Ajoutez des cours à votre session"
|
||||
"More","Plus"
|
||||
"Certified Members","Membres certifiés"
|
||||
"Statistics","Statistiques"
|
||||
"Jobs","Emplois"
|
||||
"Batches","Sessions"
|
||||
"Programs","Programmes"
|
||||
"Programming Exercises","Exercices de programmation"
|
||||
"Quizzes","Quiz"
|
||||
"Assignments","Affectations"
|
||||
"Learning","Apprentissage"
|
||||
"Introduction","Introduction"
|
||||
"Setting up","Configuration"
|
||||
"Creating a course","Créer un cours"
|
||||
"Create a course","Créer un cours"
|
||||
"Add a chapter","Ajouter un chapitre"
|
||||
"Add a lesson","Ajouter une leçon"
|
||||
"Creating a batch","Créer une session"
|
||||
"Create a batch","Créer une session"
|
||||
"Create a live class","Créer une classe en direct"
|
||||
"Assessments","Évaluations"
|
||||
"Certification","Certification"
|
||||
"Issue a Certificate","Délivrer un certificat"
|
||||
"Custom Certificate Templates","Modèles de certificats personnalisés"
|
||||
"Monetization","Monétisation"
|
||||
"Setting up payment gateway","Configuration de la passerelle de paiement"
|
||||
"Settings","Paramètres"
|
||||
"Roles","Rôles"
|
||||
"My Profile","Mon profil"
|
||||
"Toggle Theme","Changer de thème"
|
||||
"Apps","Applications"
|
||||
"Log out","Se déconnecter"
|
||||
"Signups","Inscriptions"
|
||||
"Signups per month","Inscriptions par mois"
|
||||
"Enrollments","Inscriptions"
|
||||
"Enrollments per month","Inscriptions par mois"
|
||||
"Certifications","Certifications"
|
||||
"Certifications per month","Certifications par mois"
|
||||
"Completions","Cours terminés"
|
||||
"Total Signups","Inscriptions totales"
|
||||
"Enrollment Count","Nombre d’inscriptions"
|
||||
"Courses Completed","Cours terminés"
|
||||
"Lessons Completed","Leçons terminées"
|
||||
|
@@ -25,11 +25,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/frappe/lms#readme",
|
||||
"devDependencies": {
|
||||
"cypress": "^13.9.0",
|
||||
"cypress": "^14.5.2",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-real-events": "^1.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pre-commit": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ dependencies = [
|
||||
"markdown~=3.5.1",
|
||||
"beautifulsoup4~=4.12.2",
|
||||
"lxml~=4.9.3",
|
||||
"cairocffi~=1.6.1",
|
||||
"cairocffi==1.5.1",
|
||||
"razorpay~=1.4.1",
|
||||
"fuzzywuzzy~=0.18.0",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user