Compare commits

...

58 Commits

Author SHA1 Message Date
Youssef
597648db08 i18n: update fr.po and template and sync PO files 2025-08-15 21:00:57 -04:00
Youssef
16afb24276 i18n: add missing __() calls to complete French translation 2025-08-15 19:51:10 -04:00
stanig2106
cf5cc19fd5 i18n setup 2025-08-13 22:33:25 +02:00
Youssef
80f093958a i18n: progress on french translations 2025-08-13 11:02:58 -04:00
Youssef
57df376207 i18n: clean fr.csv and add __() translations in Statistics.vue 2025-08-10 16:46:00 -04:00
Youssef
a147aca24f i18m: auto import FR Translations from lms/translations/fr.csv 2025-08-10 12:28:50 -04:00
Youssef
1660f69930 i18n: populate fr.csv with French Translatations 2025-08-10 10:34:08 -04:00
Youssef
718657f493 i18n: wrap UI strings with __() (components/pages/modals) 2025-08-09 18:56:47 -04:00
Youssef
9a77b716a1 i18n : wrap core IU stringg with __() (nav, button, tabs, breadcrumbs ) 2025-08-09 17:43:45 -04:00
Youssef
6e3c624b91 i18n-> add initial fr.csv for French Translations 2025-08-09 15:57:25 -04:00
Jannat Patel
2271eb270e Merge pull request #1667 from harshpwctech/develop
refactor: Announcement mail being sent to students in BCC
2025-08-01 17:32:41 +05:30
CA Harsh Agrawal
7e5b2e4e79 refactor: Announcement mail being sent to students in BCC 2025-08-01 17:02:48 +05:30
Jannat Patel
36076068ec fix: text padding on card gradient 2025-07-30 12:30:12 +05:30
Frappe PR Bot
c868354b5b chore(release): Bumped to Version 2.33.0 2025-07-30 06:14:36 +00:00
Jannat Patel
db91f0b2a0 Merge pull request #1663 from pateljannat/issues-125
fix: show video statistics watch time in minutes
2025-07-30 11:43:01 +05:30
Jannat Patel
d7e83bb78e fix: show video statistics watch time in minutes 2025-07-30 11:30:17 +05:30
Jannat Patel
feb2a39e05 Merge pull request #1661 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-30 10:53:15 +05:30
Jannat Patel
a6cf910d05 chore: Esperanto translations 2025-07-30 04:23:56 +05:30
Jannat Patel
b891b44ac6 chore: Spanish translations 2025-07-30 04:23:54 +05:30
Jannat Patel
026a3ebb81 chore: Serbian (Latin) translations 2025-07-30 04:23:53 +05:30
Jannat Patel
71ba246011 chore: Bosnian translations 2025-07-30 04:23:51 +05:30
Jannat Patel
a391204fa6 chore: Croatian translations 2025-07-30 04:23:49 +05:30
Jannat Patel
9c773399a8 chore: Thai translations 2025-07-30 04:23:48 +05:30
Jannat Patel
528b85352a chore: Persian translations 2025-07-30 04:23:47 +05:30
Jannat Patel
249c369c14 chore: Indonesian translations 2025-07-30 04:23:45 +05:30
Jannat Patel
9803fc1031 chore: Portuguese, Brazilian translations 2025-07-30 04:23:44 +05:30
Jannat Patel
299fde1c98 chore: Vietnamese translations 2025-07-30 04:23:43 +05:30
Jannat Patel
7f55734fbb chore: Chinese Simplified translations 2025-07-30 04:23:41 +05:30
Jannat Patel
efe230865a chore: Turkish translations 2025-07-30 04:23:40 +05:30
Jannat Patel
6e52e684c8 chore: Swedish translations 2025-07-30 04:23:38 +05:30
Jannat Patel
99d880297a chore: Serbian (Cyrillic) translations 2025-07-30 04:23:36 +05:30
Jannat Patel
dec706ae72 chore: Russian translations 2025-07-30 04:23:35 +05:30
Jannat Patel
2e60f0a0c2 chore: Portuguese translations 2025-07-30 04:23:34 +05:30
Jannat Patel
ef612f86e5 chore: Polish translations 2025-07-30 04:23:32 +05:30
Jannat Patel
9c16e03ea7 chore: Dutch translations 2025-07-30 04:23:31 +05:30
Jannat Patel
7780c0310e chore: Italian translations 2025-07-30 04:23:29 +05:30
Jannat Patel
b0a23c0d1a chore: Hungarian translations 2025-07-30 04:23:27 +05:30
Jannat Patel
05c85cea08 chore: German translations 2025-07-30 04:23:26 +05:30
Jannat Patel
1ffae0a1de chore: Czech translations 2025-07-30 04:23:24 +05:30
Jannat Patel
15cbccd15f chore: Arabic translations 2025-07-30 04:23:23 +05:30
Jannat Patel
266b2f2ac8 chore: French translations 2025-07-30 04:23:21 +05:30
Jannat Patel
26f9fb4199 Merge pull request #1658 from frappe/pot_develop_2025-07-25
chore: update POT file
2025-07-29 12:05:37 +05:30
frappe-pr-bot
67887fb6ef chore: update POT file 2025-07-25 16:04:39 +00:00
Jannat Patel
3d102e39ff Merge pull request #1657 from pateljannat/course-card-gradient
feat: course card gradient
2025-07-25 18:56:50 +05:30
Jannat Patel
ddd9089130 fix: color swatch input style 2025-07-25 18:31:46 +05:30
Jannat Patel
d8ce88ab57 fix: color swatch input style 2025-07-25 18:30:58 +05:30
Jannat Patel
01794a47c6 feat: set a random color is no color or image is present 2025-07-25 17:46:50 +05:30
Jannat Patel
17626dbbdb feat: course card gradient 2025-07-25 17:29:48 +05:30
Jannat Patel
e5bd86658d Merge pull request #1655 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-24 10:41:13 +05:30
Jannat Patel
e911dc1353 chore: Thai translations 2025-07-24 02:45:56 +05:30
Jannat Patel
27e3e5aa6a chore: Indonesian translations 2025-07-24 02:45:53 +05:30
Jannat Patel
5b65525bf1 chore: Portuguese translations 2025-07-24 02:45:46 +05:30
Jannat Patel
277804f8b1 chore: Hungarian translations 2025-07-24 02:45:42 +05:30
Jannat Patel
4c77802e3c Merge pull request #1653 from pateljannat/issues-124
fix: progress timer in lessons
2025-07-23 11:32:51 +05:30
Jannat Patel
aacfea6ea5 fix: progress timer in lessons 2025-07-23 11:31:41 +05:30
Frappe PR Bot
6d55040e43 chore(release): Bumped to Version 2.32.2 2025-07-23 05:31:05 +00:00
Jannat Patel
290f785a47 Merge pull request #1651 from pateljannat/issues-123
fix: vimeo video embed with plyr
2025-07-23 11:00:03 +05:30
Jannat Patel
39ef187f6b fix: vimeo video embed with plyr 2025-07-23 10:44:53 +05:30
87 changed files with 19392 additions and 4142 deletions

View File

@@ -1,7 +1,7 @@
name: Create weekly release
on:
schedule:
- cron: '30 4 15 * *'
- cron: '30 3 * * 3'
workflow_dispatch:
jobs:

View 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
View 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
View 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
View 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
View 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">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;stanig2106&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/eduvia-app/lms&quot;,
&quot;accountId&quot;: &quot;07629746-0347-4709-8dfa-2d0f080158cf&quot;
}
}</component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: 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">{
&quot;associatedIndex&quot;: 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="&quot;&quot;" />
<option name="_new_target" value="&quot;&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<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>

View File

@@ -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");
});

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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',

View File

@@ -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' },
]
})

View File

@@ -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',
},

View File

@@ -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',

View File

@@ -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">

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

View File

@@ -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);
}

View File

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

View File

@@ -32,13 +32,13 @@
"
:options="[
{
label: 'Edit',
label: __('Edit'),
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
label: __('Delete'),
onClick() {
deleteReply(reply)
},

View File

@@ -102,7 +102,7 @@ const props = defineProps({
},
emptyStateText: {
type: String,
default: 'Start a discussion',
default: __('Start a discussion'),
},
singleThread: {
type: Boolean,

View File

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

View File

@@ -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()

View File

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

View File

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

View File

@@ -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,
}

View File

@@ -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',
},
]

View File

@@ -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(() => {

View File

@@ -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',
},

View File

@@ -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'
},

View File

@@ -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
}
}

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

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

View File

@@ -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'),
},
]
})

View File

@@ -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' },
},
])

View File

@@ -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: {

View File

@@ -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 } },

View File

@@ -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"

View File

@@ -132,6 +132,7 @@ const participants = createListResource({
doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certified_participants',
start: 0,
cache: ['certified_participants'],
pageLength: 100,
})

View File

@@ -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 } },

View File

@@ -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,

View File

@@ -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"

View File

@@ -81,7 +81,7 @@
</router-link>
</div>
</div>
<EmptyState v-else type="Job Openings" />
<EmptyState v-else :type="__('Job Openings').toLowerCase()" />
</div>
</div>
</template>

View File

@@ -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)

View File

@@ -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,
}
})

View File

@@ -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,

View File

@@ -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,
},
]
})

View File

@@ -82,7 +82,7 @@
</div>
</div>
</div>
<EmptyState v-else type="Programs" />
<EmptyState v-else :type="__('Programs').toLowerCase()" />
<Dialog
v-model="showDialog"

View File

@@ -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') }}

View File

@@ -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',
},

View File

@@ -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')}`
}

View File

@@ -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: {

View File

@@ -1 +1 @@
__version__ = "2.32.1"
__version__ = "2.33.0"

9
lms/lms.iml Normal file
View 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>

View File

@@ -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",

View File

@@ -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()

View File

@@ -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",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7434
lms/locale/id.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

372
lms/translations/fr.csv Normal file
View 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 nest 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 ladministrateur 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 denregistrement"
"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 ny 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 lexaminera 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étadescription"
"Meta Keywords","Motsclé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. Laccè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étabalises {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 lorsquil sera disponible"
"Continue Learning","Continuer lapprentissage"
"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 nest 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.","Nhé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 ny 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?","Voulezvous 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?","Voulezvous 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.","Aideznous à 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 linstant"
"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 ny a actuellement aucun {0}. Restez à laffût, de nouvelles expériences dapprentissage 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 dinscriptions"
"Courses Completed","Cours terminés"
"Lessons Completed","Leçons terminées"
1 Courses Cours
2 Batches Sessions
3 Certified Members Membres certifiés
4 Jobs Offres
5 Statistics Statistiques
6 Programs Programmes
7 Programming Exercises Exercices de programmation
8 Assignments Devoirs
9 Quizzes Quiz
10 More Plus
11 Expand Développer
12 Collapse Réduire
13 Help Aide
14 Powered by Learning Propulsé par Learning
15 Learning Learning
16 Details Détails
17 Assessments Évaluations
18 Discussions Discussions
19 People Personnes
20 Certification Certification
21 Course Outline Plan du cours
22 Average Rating Note moyenne
23 Enrolled Students Étudiants inscrits
24 Zen Mode Mode Zen
25 Video Statistics Statistiques vidéo
26 This lesson is locked Cette leçon est verrouillée
27 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.
28 Start Learning Commencer l'apprentissage
29 Contact the Administrator to enroll for this course. Contactez l’administrateur pour vous inscrire à ce cours.
30 Login Se connecter
31 completed terminé
32 Previous Précédent
33 Edit Modifier
34 Next Suivant
35 Back to Course Retour au cours
36 Instructor Notes Notes du formateur
37 Certification Certification
38 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.
39 Issued On Délivré le
40 Issued on Délivré le
41 {0} Open Jobs {0} offres ouvertes
42 New Job Nouvelle offre
43 Search Rechercher
44 Country Pays
45 Type Type
46 Full Time Temps plein
47 Part Time Temps partiel
48 Contract Contrat
49 Freelance Freelance
50 Jobs Offres
51 No results found Aucun résultat
52 Delete Supprimer
53 Add Row Ajouter une ligne
54 Add Ajouter
55 Save Enregistrer
56 Submit Envoyer
57 Create Créer
58 Post Publier
59 No Recording Pas d’enregistrement
60 Local Local
61 Cloud Cloud
62 PDF PDF
63 Image Image
64 Document Document
65 Text Texte
66 URL URL
67 Quiz Quiz
68 Assignment Devoir
69 Programming Exercise Exercice de programmation
70 Add a new member Ajouter un nouveau membre
71 Add Evaluator Ajouter un évaluateur
72 Evaluator added successfully Évaluateur ajouté avec succès
73 Evaluator deleted successfully Évaluateur supprimé avec succès
74 Email Adresse e-mail
75 First Name Prénom
76 Load More Charger plus
77 Student Étudiant
78 Moderator Modérateur
79 Evaluator Évaluateur
80 Mark all as read Tout marquer comme lu
81 Unread Non lus
82 Read Lus
83 View Voir
84 Nothing to see here. Rien à afficher.
85 Not Permitted Non autorisé
86 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.
87 Please login to access this page. Veuillez vous connecter pour accéder à cette page.
88 Upcoming Batches Sessions à venir
89 Assignment Devoir
90 Member Membre
91 Submitted Soumis
92 Status Statut
93 Pass Réussi
94 Fail Échoué
95 Not Graded Non noté
96 No submissions Aucune soumission
97 There are no submissions for this assignment. Il n’y a aucune soumission pour ce devoir.
98 You will have to complete the quiz to continue the video Vous devrez terminer le quiz pour continuer la vidéo
99 This quiz consists of {0} questions. Ce quiz comporte {0} questions.
100 Please ensure that you complete all the questions in {0} minutes. Veuillez vous assurer de terminer toutes les questions en {0} minutes.
101 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.
102 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.
103 You can attempt this quiz {0}. Vous pouvez tenter ce quiz {0}.
104 Time Temps
105 Start the Quiz Commencer le quiz
106 Start Démarrer
107 Resume Video Reprendre la vidéo
108 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.
109 Question {0} Question {0}
110 Mark Point
111 Marks Points
112 Question {0} of {1} Question {0} sur {1}
113 Check Vérifier
114 Submit Soumettre
115 Next Suivant
116 Quiz Summary Récapitulatif du quiz
117 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.
118 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}
119 Try Again Réessayer
120 No Quiz submissions found Aucune soumission de quiz trouvée
121 Choose all answers that apply Choisissez toutes les réponses correctes
122 Choose one answer Choisissez une réponse
123 Type your answer Saisissez votre réponse
124 Please select an option Veuillez sélectionner une option
125 Correct Correct
126 Incorrect Incorrect
127 Settings Paramètres
128 Programming Exercises Exercices de programmation
129 Certified Members Membres certifiés
130 Statistics Statistiques
131 Meta Description Méta‑description
132 Meta Keywords Mots‑clés
133 Meta Image Image méta
134 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.
135 Introduction Introduction
136 Setting up Mise en place
137 Creating a course Créer un cours
138 Create a course Créer un cours
139 Add a chapter Ajouter un chapitre
140 Add a lesson Ajouter une leçon
141 Creating a batch Créer une session
142 Create a batch Créer une session
143 Create a live class Créer un cours en direct
144 Assessments Évaluations
145 Quizzes Quiz
146 Assignments Devoirs
147 Certification Certification
148 Issue a Certificate Émettre un certificat
149 Custom Certificate Templates Modèles de certificat personnalisés
150 Monetization Monétisation
151 Setting up payment gateway Configuration de la passerelle de paiement
152 Roles Rôles
153 Create your first course Créez votre premier cours
154 Add your first chapter Ajoutez votre premier chapitre
155 Add your first lesson Ajoutez votre première leçon
156 Create your first quiz Créez votre premier quiz
157 Invite your team and students Invitez votre équipe et vos étudiants
158 Create your first batch Créez votre première session
159 Add students to your batch Ajoutez des étudiants à votre session
160 Add courses to your batch Ajoutez des cours à votre session
161 Only image file is allowed. Seuls les fichiers image sont autorisés.
162 Failed to update meta tags {0} Échec de la mise à jour des méta‑balises {0}
163 Courses deleted successfully Cours supprimés avec succès
164 No courses added Aucun cours ajouté
165 Course Cours
166 All Courses Tous les cours
167 Instructors Formateurs
168 Completed Terminé
169 Enrolled Inscrits
170 Get Certified Obtenir la certification
171 Reviews Avis
172 Write a review Rédiger un avis
173 Review the course Évaluer le cours
174 Help us improve our course material. Aidez-nous à améliorer le contenu du cours.
175 Add Chapter Ajouter un chapitre
176 Add Lesson Ajouter une leçon
177 Questions Questions
178 Notify me when available Me prévenir lorsqu’il sera disponible
179 Continue Learning Continuer l’apprentissage
180 Get Certificate Obtenir le certificat
181 Slots Créneaux
182 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.
183 Cancel Annuler
184 others autres
185 Course List Liste des cours
186 Headline Titre
187 Upcoming À venir
188 Live En direct
189 Created Créé
190 Category Catégorie
191 Search by Title Rechercher par titre
192 Generate Certificates Générer les certificats
193 Make an Announcement Faire une annonce
194 About this batch À propos de cette session
195 Create a Live Class Créer un cours en direct
196 Title Titre
197 Date Date
198 Duration of the live class in minutes Durée du cours en direct en minutes
199 Duration Durée
200 Time Heure
201 Timezone Fuseau horaire
202 Auto Recording Enregistrement automatique
203 Description Description
204 Please enter a title. Veuillez saisir un titre.
205 Please select a date. Veuillez sélectionner une date.
206 Please select a time. Veuillez sélectionner une heure.
207 Please select a timezone. Veuillez sélectionner un fuseau horaire.
208 Please enter a valid time in the format HH:mm. Veuillez saisir une heure valide au format HH:mm.
209 Please select a future date and time. Veuillez sélectionner une date et une heure futures.
210 Please select a duration. Veuillez sélectionner une durée.
211 Create an Assignment Créer un devoir
212 Edit Assignment Modifier le devoir
213 Submission Type Type de soumission
214 Question Question
215 Check Submissions Voir les soumissions
216 Assignment created successfully Devoir créé avec succès
217 Assignment updated successfully Devoir mis à jour avec succès
218 Add an assessment Ajouter une évaluation
219 Assessment Évaluation
220 Assessment added successfully Évaluation ajoutée avec succès
221 Discard Annuler
222 Submission by Soumission par
223 Submission Soumission
224 Not Saved Non enregistré
225 Feel free to make edits to your submission if needed. N’hésitez pas à modifier votre soumission si nécessaire.
226 Add your assignment as {0} Ajoutez votre devoir en tant que {0}
227 Uploading {0}% Téléversement {0}%
228 Upload File Téléverser un fichier
229 Enter a URL Saisir une URL
230 Write your answer here Écrivez votre réponse ici
231 Comments by Evaluator Commentaires de l’évaluateur
232 Grading Notation
233 Grade Note
234 Comments Commentaires
235 Assignment submitted successfully Devoir soumis avec succès
236 Students Étudiants
237 Certified Certifiés
238 Batch Summary Récapitulatif de la session
239 Progress of students in courses and assessments Progression des étudiants dans les cours et évaluations
240 Number of Students Nombre d’étudiants
241 There are no students in this batch. Il n’y a aucun étudiant dans cette session.
242 Students deleted successfully Étudiants supprimés avec succès
243 No Assessments Aucune évaluation
244 Status/Percentage Statut/Pourcentage
245 Create a Quiz Créer un quiz
246 No Quizzes Aucun quiz
247 Published Courses Cours publiés
248 Active Members Membres actifs
249 Course Enrollments Inscriptions aux cours
250 Course Completions Cours terminés
251 Add Chapter Ajouter un chapitre
252 Add Lesson Ajouter une leçon
253 Login to Frappe Cloud? Se connecter à Frappe Cloud ?
254 Are you sure you want to login to your Frappe Cloud dashboard? Voulez‑vous vraiment vous connecter à votre tableau de bord Frappe Cloud ?
255 Confirm Confirmer
256 New Nouveau
257 Edit Profile Modifier le profil
258 About À propos
259 Certificates Certificats
260 Roles Rôles
261 Assignment Submissions Soumissions de devoirs
262 Login to Frappe Cloud Se connecter à Frappe Cloud
263 Login to Frappe Cloud? Se connecter à Frappe Cloud ?
264 Are you sure you want to login to your Frappe Cloud dashboard? Voulez‑vous vraiment vous connecter à votre tableau de bord Frappe Cloud ?
265 Log out Se déconnecter
266 Log in Se connecter
267 Problem Statement Énoncé du problème
268 Run Exécuter
269 Compiler Message Message du compilateur
270 Test Cases Cas de test
271 Test {0} Test {0}
272 Input Entrée
273 Your Output Votre sortie
274 Expected Output Sortie attendue
275 Please run the code to execute the test cases. Veuillez exécuter le code pour lancer les cas de test.
276 Check All Submissions Voir toutes les soumissions
277 Check Submission Voir la soumission
278 Program Courses Cours du programme
279 Program Members Membres du programme
280 ID ID
281 Full Name Nom complet
282 Progress (%) Progression (%)
283 Name Nom
284 Subject Objet
285 Thank you for providing your feedback. Merci pour votre retour.
286 Click here Cliquez ici
287 to view your feedback. pour afficher votre retour.
288 Help us improve by providing your feedback. Aidez‑nous à nous améliorer en nous donnant votre avis.
289 Submit Feedback Envoyer le retour
290 Average Feedback Received Retour moyen reçu
291 View all feedback Voir tous les retours
292 No feedback received yet. Aucun retour reçu pour le moment.
293 Choose an existing question Choisir une question existante
294 Options Options
295 Possibilities Possibilités
296 Option Option
297 Explanation Explication
298 Correct Answer Réponse correcte
299 Select a question Sélectionner une question
300 Test Quiz Quiz de test
301 Questions Questions
302 New Question Nouvelle question
303 No questions added yet Aucune question ajoutée pour l’instant
304 Answer Réponse
305 My availability Ma disponibilité
306 Day Jour
307 Start Time Heure de début
308 End Time Heure de fin
309 Add Slot Ajouter un créneau
310 I am unavailable Je ne suis pas disponible
311 My calendar Mon calendrier
312 Your calendar is set. Votre calendrier est configuré.
313 Authorize Google Calendar Access Autoriser l'accès à Google Agenda
314 {0} Exercises {0} exercices
315 Unpublished Non publié
316 No {0} Aucun {0}
317 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 !
318 courses cours
319 Courses Cours
320 completed terminé
321 % completed % terminé
322 Skip Ignorer
323 Create your first course Créez votre premier cours
324 Add your first chapter Ajoutez votre premier chapitre
325 Add your first lesson Ajoutez votre première leçon
326 Create your first quiz Créez votre premier quiz
327 Invite your team and students Invitez votre équipe et vos étudiants
328 Create your first batch Créez votre première session
329 Add students to your batch Ajoutez des étudiants à votre session
330 Add courses to your batch Ajoutez des cours à votre session
331 More Plus
332 Certified Members Membres certifiés
333 Statistics Statistiques
334 Jobs Emplois
335 Batches Sessions
336 Programs Programmes
337 Programming Exercises Exercices de programmation
338 Quizzes Quiz
339 Assignments Affectations
340 Learning Apprentissage
341 Introduction Introduction
342 Setting up Configuration
343 Creating a course Créer un cours
344 Create a course Créer un cours
345 Add a chapter Ajouter un chapitre
346 Add a lesson Ajouter une leçon
347 Creating a batch Créer une session
348 Create a batch Créer une session
349 Create a live class Créer une classe en direct
350 Assessments Évaluations
351 Certification Certification
352 Issue a Certificate Délivrer un certificat
353 Custom Certificate Templates Modèles de certificats personnalisés
354 Monetization Monétisation
355 Setting up payment gateway Configuration de la passerelle de paiement
356 Settings Paramètres
357 Roles Rôles
358 My Profile Mon profil
359 Toggle Theme Changer de thème
360 Apps Applications
361 Log out Se déconnecter
362 Signups Inscriptions
363 Signups per month Inscriptions par mois
364 Enrollments Inscriptions
365 Enrollments per month Inscriptions par mois
366 Certifications Certifications
367 Certifications per month Certifications par mois
368 Completions Cours terminés
369 Total Signups Inscriptions totales
370 Enrollment Count Nombre d’inscriptions
371 Courses Completed Cours terminés
372 Lessons Completed Leçons terminées

View File

@@ -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"
}

View File

@@ -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",
]

1374
yarn.lock Normal file

File diff suppressed because it is too large Load Diff