fix: conflicts
This commit is contained in:
71
README.md
71
README.md
@@ -1,45 +1,39 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/frappe/lms/commits/main">
|
||||
<img src="https://img.shields.io/github/last-commit/frappe/lms.svg?style=flat-square&logo=github&logoColor=white"
|
||||
alt="GitHub last commit">
|
||||
<img src="https://img.shields.io/github/last-commit/frappe/lms.svg?style=flat-square&logo=github&logoColor=white" alt="GitHub last commit">
|
||||
<a href="https://github.com/frappe/lms/issues">
|
||||
<img src="https://img.shields.io/github/issues-raw/frappe/lms.svg?style=flat-square&logo=github&logoColor=white"
|
||||
alt="GitHub issues">
|
||||
<img src="https://img.shields.io/github/issues-raw/frappe/lms.svg?style=flat-square&logo=github&logoColor=white" alt="GitHub issues">
|
||||
<a href="https://github.com/frappe/lms/pulls">
|
||||
<img src="https://img.shields.io/github/issues-pr-raw/frappe/lms.svg?style=flat-square&logo=github&logoColor=white"
|
||||
alt="GitHub pull requests">
|
||||
<img src="https://img.shields.io/github/issues-pr-raw/frappe/lms.svg?style=flat-square&logo=github&logoColor=white" alt="GitHub pull requests">
|
||||
<a href="https://github.com/frappe/lms/license">
|
||||
<img src="https://img.shields.io/github/license/frappe/lms.svg?style=flat-square&logo=github&logoColor=white"
|
||||
alt="GitHub pull requests">
|
||||
<img src="https://img.shields.io/github/license/frappe/lms.svg?style=flat-square&logo=github&logoColor=white" alt="GitHub pull requests">
|
||||
</p>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.frappelms.com/">
|
||||
<img src="https://www.frappelms.com/files/flms.svg" alt="Frappe LMS" width="80" height="80">
|
||||
</a>
|
||||
|
||||
<h3 align="center">Frappe LMS</h3>
|
||||
|
||||
<p align="center">
|
||||
Easy to Use, Open Source Learning Management System
|
||||
<br />
|
||||
<br/>
|
||||
<a href="https://www.frappelms.com"><strong>Visit the website »</strong></a>
|
||||
<br />
|
||||
<br />
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="https://www.frappelms.com/introduction">Explore the docs</a>.
|
||||
<a href="https://github.com/frappe/lms/issues">Report Bug</a>
|
||||
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ABOUT THE PROJECT -->
|
||||
|
||||
## About The Project
|
||||
|
||||

|
||||
|
||||
|
||||
Frappe LMS is an easy to use, open source learning management system. It has a clear UI that helps students focus only on whats important and assists in a distraction-free learning.
|
||||
Frappe LMS is an easy-to-use, open-source learning management system. It has a clear UI that helps students focus only on what's important and assists in distraction-free learning.
|
||||
|
||||
You can create courses and lessons through simple forms in the backend that you can analyze with the help of reports. Course Instructors and students can reach out to each other through the discussions section available for each lesson and get queries resolved.
|
||||
|
||||
@@ -48,21 +42,29 @@ Lessons can be in the form of text, videos, quizzes or a combination of all thes
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
|
||||
## Getting Started
|
||||
|
||||
Frappe LMS app is build using [Frappe Framework](https://frappeframework.com).
|
||||
Frappe LMS app is built using [Frappe Framework](https://frappeframework.com).
|
||||
|
||||
### Direct installation through bench
|
||||
|
||||
To setup the repository locally follow the steps mentioned below:
|
||||
To setup the repository locally, follow the steps mentioned below:
|
||||
|
||||
1. Install bench and set up a frappe-bench directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation).
|
||||
|
||||
1. Install bench and setup a frappe-bench directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation).
|
||||
2. Start the server by running ```bench start```.
|
||||
|
||||
3. In a separate terminal window, create a new site by running ```bench new-site lms.test```.
|
||||
|
||||
4. Fork the Frappe LMS app and clone it.
|
||||
5. Run ```bench get-app lms``` to get the app on your bench .
|
||||
|
||||
5. Run ```bench get-app lms``` to get the app on your bench.
|
||||
|
||||
6. Run ```bench --site lms.test install-app lms```.
|
||||
|
||||
7. Map your site to localhost with the command ```bench --site lms.test add-to-hosts```.
|
||||
|
||||
8. Now open the URL [http://lms.test:8000/](http://lms.test:8000/) in your browser, you should see the app running.
|
||||
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
@@ -72,6 +74,7 @@ To setup the repository locally follow the steps mentioned below:
|
||||
1. Clone the repo.
|
||||
|
||||
```
|
||||
|
||||
$ git clone https://github.com/frappe/lms.git
|
||||
|
||||
$ cd lms
|
||||
@@ -81,55 +84,63 @@ $ cd lms
|
||||
2. Run docker-compose
|
||||
|
||||
```
|
||||
|
||||
$ docker-compose up
|
||||
|
||||
```
|
||||
|
||||
3. Visit the website at [http://localhost:8000/](http://localhost:8000/)
|
||||
|
||||
You'll have to go through the setup wizard to setup the website for the first time you access it. Login using the following credentiasl to complete the setup wizard.
|
||||
You'll have to go through the setup wizard to set up the website the first time you access it. Log in using the following credentials to complete the setup wizard.
|
||||
|
||||
```
|
||||
|
||||
Username: Administrator
|
||||
|
||||
password: admin
|
||||
|
||||
```
|
||||
|
||||
## [](https://github.com/frappe/lms/blob/main/docker-installation.md#stopping-the-server)Stopping the server
|
||||
|
||||
Press `ctrl+c` in the terminal to stop the server. You can also run `docker-compose down` in another terminal to stop it.
|
||||
Press ctrl+c in the terminal to stop the server. You can also run docker-compose down in another terminal to stop it.
|
||||
|
||||
To completely reset the instance, do the following:
|
||||
|
||||
```
|
||||
|
||||
$ docker-compose down --volumes
|
||||
|
||||
$ docker-compose up
|
||||
|
||||
```
|
||||
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
|
||||
|
||||
Thank you for your interest in contributing to an open-source project! Our world works on people taking initiative to contribute to the "commons" and contributing to open source means you are contributing to make things better for not only yourself but everyone else too! So thank you for taking this initiative.
|
||||
Thank you for your interest in contributing to an open-source project! Our world works on people taking initiative to contribute to the "commons" and contributing to open source means you are contributing to making things better for not only yourself but everyone else too! So thank you for taking this initiative.
|
||||
|
||||
Great projects also work because of great quality. Open source or not, the user really cares that things should work as they are advertised, and consistently. New features should follow the same pattern and so that users don't have to learn things again and again.
|
||||
Great projects also work because of great quality. Open source or not, the user really cares that things should work as they are advertised, and consistently. New features should follow the same pattern so that users don't have to learn things again and again.
|
||||
|
||||
Developers who maintain open source also expect that you follow certain guidelines. These guidelines ensure that developers are able to quickly give feedback on your contribution and how to make it better. Most probably you might have to go back and change a few things, but it will be in the interest of making this process better for everyone. So be prepared for some back and forth.
|
||||
|
||||
Don't forget to give the project a star! Thanks again!
|
||||
|
||||
1. Go to the apps/lms directory of your installation and execute git pull --unshallow to ensure that you have the full git repository. Also, fork the frappe/lms repository on GitHub.
|
||||
|
||||
1. Go to the apps/lms directory of your installation and execute git pull --unshallow to ensure that you have the full git repository. Also fork the frappe/lms repository on GitHub.
|
||||
2. Check out a working branch in git (e.g. ```git checkout -b my-new-branch```).
|
||||
|
||||
3. Run your local version (e.g. bench start in your bench installation). Make sure that your changes work the way you want them to.
|
||||
|
||||
4. Commit your changes to your branch. Make sure to use a semantic commit message.
|
||||
|
||||
6. Push your branch to your fork on Github, and create a pull request.
|
||||
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
## License
|
||||
|
||||
|
||||
|
||||
Distributed under [GNU AFFERO GENERAL PUBLIC LICENSE](license.txt)
|
||||
|
||||
@@ -21,7 +21,7 @@ app_license = "AGPL"
|
||||
# include js, css files in header of web template
|
||||
web_include_css = "lms.bundle.css"
|
||||
# web_include_css = "/assets/lms/css/lms.css"
|
||||
web_include_js = "website.bundle.js"
|
||||
web_include_js = ["website.bundle.js", "controls.bundle.js"]
|
||||
|
||||
# include custom scss in every website theme (without file extension ".scss")
|
||||
# website_theme_scss = "lms/public/scss/website"
|
||||
@@ -192,7 +192,10 @@ jinja = {
|
||||
"lms.lms.utils.get_popular_courses",
|
||||
"lms.lms.utils.format_amount",
|
||||
"lms.lms.utils.first_lesson_exists",
|
||||
"lms.lms.utils.has_course_instructor_role"
|
||||
"lms.lms.utils.get_courses_under_review",
|
||||
"lms.lms.utils.has_course_instructor_role",
|
||||
"lms.lms.utils.has_course_moderator_role",
|
||||
"lms.lms.utils.get_certificates"
|
||||
],
|
||||
"filters": []
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Full Time",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
@@ -114,7 +115,8 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-28 13:41:29.224332",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-15 17:22:21.662675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Job",
|
||||
"name": "Job Opportunity",
|
||||
@@ -152,4 +154,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "job_title"
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,21 @@ from frappe.model.document import Document
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe import _
|
||||
from frappe.utils import get_link_to_form
|
||||
from lms.lms.utils import validate_image
|
||||
|
||||
class JobOpportunity(Document):
|
||||
|
||||
|
||||
def validate(self):
|
||||
self.validate_urls()
|
||||
self.company_logo = validate_image(self.company_logo)
|
||||
|
||||
|
||||
def validate_urls(self):
|
||||
frappe.utils.validate_url(self.company_website, True)
|
||||
frappe.utils.validate_url(self.application_link, True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def report(job, reason):
|
||||
system_managers = get_system_managers(only_name=True)
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"is_multi_step_form": 0,
|
||||
"is_standard": 1,
|
||||
"list_columns": [],
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-02-24 11:31:25.290524",
|
||||
"modified": "2022-09-15 17:22:43.957184",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Job",
|
||||
"name": "job-opportunity",
|
||||
@@ -28,11 +28,9 @@
|
||||
"payment_button_label": "Buy Now",
|
||||
"published": 1,
|
||||
"route": "job-opportunity",
|
||||
"route_to_success_link": 1,
|
||||
"show_attachments": 0,
|
||||
"show_in_grid": 0,
|
||||
"show_list": 1,
|
||||
"show_sidebar": 0,
|
||||
"sidebar_items": [],
|
||||
"success_message": "",
|
||||
"success_url": "/jobs",
|
||||
"title": "Job Opportunity",
|
||||
@@ -63,6 +61,7 @@
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"default": "Full Time",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
"include_in_preview",
|
||||
"index_label",
|
||||
"section_break_6",
|
||||
"youtube",
|
||||
"column_break_9",
|
||||
"quiz_id",
|
||||
"section_break_11",
|
||||
"body",
|
||||
"help_section",
|
||||
"help"
|
||||
@@ -81,11 +85,31 @@
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Quiz will appear at the bottom of the lesson.",
|
||||
"fieldname": "quiz_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Quiz ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "YouTube Video will appear at the top of the lesson.",
|
||||
"fieldname": "youtube",
|
||||
"fieldtype": "Data",
|
||||
"label": "YouTube Video URL"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-02 17:16:12.450460",
|
||||
"modified": "2022-09-02 11:30:15.450624",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Lesson",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from ...md import find_macros
|
||||
from lms.lms.utils import get_course_progress, get_lesson_url
|
||||
@@ -11,6 +12,11 @@ from lms.lms.utils import get_course_progress, get_lesson_url
|
||||
class CourseLesson(Document):
|
||||
def validate(self):
|
||||
self.check_and_create_folder()
|
||||
self.validate_quiz_id()
|
||||
|
||||
def validate_quiz_id(self):
|
||||
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
|
||||
frappe.throw(_("Invalid Quiz ID"))
|
||||
|
||||
def on_update(self):
|
||||
dynamic_documents = ["Exercise", "Quiz"]
|
||||
|
||||
@@ -6,10 +6,6 @@ import unittest
|
||||
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
||||
|
||||
class TestExercise(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql('delete from `tabLMS Batch Membership`')
|
||||
frappe.db.sql('delete from `tabExercise Submission`')
|
||||
frappe.db.sql('delete from `tabExercise`')
|
||||
|
||||
def new_exercise(self):
|
||||
course = new_course("Test Course")
|
||||
@@ -47,3 +43,8 @@ class TestExercise(unittest.TestCase):
|
||||
user_submission = e.get_user_submission()
|
||||
assert user_submission is not None
|
||||
assert user_submission.name == submission.name
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.sql('delete from `tabLMS Batch Membership`')
|
||||
frappe.db.sql('delete from `tabExercise Submission`')
|
||||
frappe.db.sql('delete from `tabExercise`')
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"member_type",
|
||||
"batch",
|
||||
"column_break_3",
|
||||
"member",
|
||||
"member_name",
|
||||
"member_username",
|
||||
"section_break_8",
|
||||
"cohort",
|
||||
"subgroup",
|
||||
"column_break_3",
|
||||
"batch",
|
||||
"current_lesson",
|
||||
"role",
|
||||
"member_section",
|
||||
"member",
|
||||
"member_type",
|
||||
"progress",
|
||||
"column_break_12",
|
||||
"member_name",
|
||||
"member_username"
|
||||
"current_lesson",
|
||||
"progress",
|
||||
"role"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -33,7 +33,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User"
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Student",
|
||||
@@ -70,7 +71,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
"options": "LMS Course",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "current_lesson",
|
||||
@@ -103,19 +105,18 @@
|
||||
"label": "Subgroup",
|
||||
"options": "Cohort Subgroup"
|
||||
},
|
||||
{
|
||||
"fieldname": "member_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Member"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-29 09:47:05.007133",
|
||||
"modified": "2022-09-01 17:11:08.065998",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Membership",
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
|
||||
frappe.ui.form.on('LMS Course', {
|
||||
|
||||
onload: function (frm) {
|
||||
onload: function (frm) {
|
||||
|
||||
frm.set_query("chapter", "chapters", function () {
|
||||
return {
|
||||
filters: {
|
||||
"course": frm.doc.name,
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query("chapter", "chapters", function () {
|
||||
return {
|
||||
filters: {
|
||||
"course": frm.doc.name,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("instructor", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
"ignore_user_type": 1,
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query("instructor", "instructors", function () {
|
||||
return {
|
||||
filters: {
|
||||
"ignore_user_type": 1,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("course", "related_courses", function () {
|
||||
return {
|
||||
filters: {
|
||||
"published": true,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
frm.set_query("course", "related_courses", function () {
|
||||
return {
|
||||
filters: {
|
||||
"published": true,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,7 +260,8 @@
|
||||
"link_fieldname": "course"
|
||||
}
|
||||
],
|
||||
"modified": "2022-05-19 16:59:21.933367",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-14 13:26:53.153822",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
|
||||
@@ -5,15 +5,17 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
import json
|
||||
from ...utils import generate_slug
|
||||
from frappe.utils import flt, cint
|
||||
from ...utils import generate_slug, validate_image
|
||||
from frappe.utils import cint
|
||||
from lms.lms.utils import get_chapters
|
||||
|
||||
|
||||
class LMSCourse(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_instructors()
|
||||
self.validate_status()
|
||||
self.image = validate_image(self.image)
|
||||
|
||||
def validate_instructors(self):
|
||||
if self.is_new() and not self.instructors:
|
||||
@@ -25,20 +27,22 @@ class LMSCourse(Document):
|
||||
"parenttype": "LMS Course"
|
||||
}).save(ignore_permissions=True)
|
||||
|
||||
|
||||
def validate_status(self):
|
||||
if self.published:
|
||||
self.status = "Approved"
|
||||
|
||||
|
||||
def on_update(self):
|
||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||
self.send_email_to_interested_users()
|
||||
|
||||
|
||||
def send_email_to_interested_users(self):
|
||||
interested_users = frappe.get_all("LMS Course Interest",
|
||||
{
|
||||
"course": self.name
|
||||
},
|
||||
["name", "user"])
|
||||
interested_users = frappe.get_all("LMS Course Interest", {
|
||||
"course": self.name
|
||||
},
|
||||
["name", "user"])
|
||||
subject = self.title + " is available!"
|
||||
args = {
|
||||
"title": self.title,
|
||||
@@ -68,6 +72,7 @@ class LMSCourse(Document):
|
||||
def __repr__(self):
|
||||
return f"<Course#{self.name}>"
|
||||
|
||||
|
||||
def has_mentor(self, email):
|
||||
"""Checks if this course has a mentor with given email.
|
||||
"""
|
||||
@@ -77,6 +82,7 @@ class LMSCourse(Document):
|
||||
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
|
||||
return mapping != []
|
||||
|
||||
|
||||
def add_mentor(self, email):
|
||||
"""Adds a new mentor to the course.
|
||||
"""
|
||||
@@ -97,7 +103,6 @@ class LMSCourse(Document):
|
||||
doc.insert()
|
||||
|
||||
|
||||
|
||||
def get_student_batch(self, email):
|
||||
"""Returns the batch the given student is part of.
|
||||
|
||||
@@ -116,6 +121,7 @@ class LMSCourse(Document):
|
||||
fieldname="batch")
|
||||
return batch_name and frappe.get_doc("LMS Batch", batch_name)
|
||||
|
||||
|
||||
def get_batches(self, mentor=None):
|
||||
batches = frappe.get_all("LMS Batch", {"course": self.name})
|
||||
if mentor:
|
||||
@@ -127,17 +133,21 @@ class LMSCourse(Document):
|
||||
batch_names = {m.batch for m in memberships}
|
||||
return [b for b in batches if b.name in batch_names]
|
||||
|
||||
|
||||
def get_cohorts(self):
|
||||
return frappe.get_all("Cohort", {"course": self.name}, order_by="creation")
|
||||
|
||||
|
||||
def get_cohort(self, cohort_slug):
|
||||
name = frappe.get_value("Cohort", {"course": self.name, "slug": cohort_slug})
|
||||
return name and frappe.get_doc("Cohort", name)
|
||||
|
||||
|
||||
def reindex_exercises(self):
|
||||
for i, c in enumerate(get_chapters(self.name), start=1):
|
||||
self._reindex_exercises_in_chapter(c, i)
|
||||
|
||||
|
||||
def _reindex_exercises_in_chapter(self, c, index):
|
||||
i = 1
|
||||
for lesson in self.get_lessons(c):
|
||||
@@ -147,12 +157,14 @@ class LMSCourse(Document):
|
||||
exercise.save()
|
||||
i += 1
|
||||
|
||||
|
||||
def get_all_memberships(self, member):
|
||||
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
|
||||
for membership in all_memberships:
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
return all_memberships
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reindex_exercises(doc):
|
||||
course_data = json.loads(doc)
|
||||
@@ -160,6 +172,7 @@ def reindex_exercises(doc):
|
||||
course.reindex_exercises()
|
||||
frappe.msgprint("All exercises in this course have been re-indexed.")
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def search_course(text):
|
||||
search_courses = []
|
||||
@@ -185,6 +198,7 @@ def search_course(text):
|
||||
|
||||
return courses
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit_for_review(course):
|
||||
chapters = frappe.get_all("Chapter Reference", {"parent": course})
|
||||
@@ -195,7 +209,7 @@ def submit_for_review(course):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_course(tags, title, short_introduction, video_link, description, course, image=None):
|
||||
def save_course(tags, title, short_introduction, video_link, description, course, published, upcoming, image=None):
|
||||
if course:
|
||||
doc = frappe.get_doc("LMS Course", course)
|
||||
else:
|
||||
@@ -209,7 +223,9 @@ def save_course(tags, title, short_introduction, video_link, description, course
|
||||
"video_link": video_link,
|
||||
"image": image,
|
||||
"description": description,
|
||||
"tags": tags
|
||||
"tags": tags,
|
||||
"published": cint(published),
|
||||
"upcoming": cint(upcoming)
|
||||
})
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.name
|
||||
@@ -249,7 +265,7 @@ def save_chapter(course, title, chapter_description, idx, chapter):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_lesson(title, body, chapter, preview, idx, lesson):
|
||||
def save_lesson(title, body, chapter, preview, idx, lesson, youtube=None, quiz_id=None):
|
||||
if lesson:
|
||||
doc = frappe.get_doc("Course Lesson", lesson)
|
||||
else:
|
||||
@@ -261,7 +277,9 @@ def save_lesson(title, body, chapter, preview, idx, lesson):
|
||||
"chapter": chapter,
|
||||
"title": title,
|
||||
"body": body,
|
||||
"include_in_preview": preview
|
||||
"include_in_preview": preview,
|
||||
"youtube": youtube,
|
||||
"quiz_id": quiz_id
|
||||
})
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@@ -7,13 +7,16 @@ import frappe
|
||||
from .lms_course import LMSCourse
|
||||
import unittest
|
||||
|
||||
|
||||
class TestLMSCourse(unittest.TestCase):
|
||||
|
||||
|
||||
def test_new_course(self):
|
||||
course = new_course("Test Course")
|
||||
assert course.title == "Test Course"
|
||||
assert course.name == "test-course"
|
||||
|
||||
|
||||
# disabled this test as it is failing
|
||||
def _test_add_mentors(self):
|
||||
course = new_course("Test Course")
|
||||
@@ -26,10 +29,23 @@ class TestLMSCourse(unittest.TestCase):
|
||||
mentors_data = [dict(email=mentor.email, batch_count=mentor.batch_count) for mentor in mentors]
|
||||
assert mentors_data == [{"email": "tester@example.com", "batch_count": 0}]
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.db.exists("User", "tester@example.com"):
|
||||
frappe.delete_doc("User", "tester@example.com")
|
||||
|
||||
if frappe.db.exists("LMS Course", "test-course"):
|
||||
frappe.db.delete("Exercise Submission", {"course": "test-course"})
|
||||
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
|
||||
frappe.db.delete("Exercise", {"course": "test-course"})
|
||||
frappe.db.delete("LMS Batch Membership", {"course": "test-course"})
|
||||
frappe.db.delete("LMS Batch", {"course": "test-course"})
|
||||
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})
|
||||
frappe.db.delete("Course Instructor", {"parent": "test-course"})
|
||||
frappe.db.sql('delete from `tabCourse Instructor`')
|
||||
frappe.delete_doc("LMS Course", "test-course")
|
||||
|
||||
|
||||
def new_user(name, email):
|
||||
user = frappe.db.exists("User", email)
|
||||
if user:
|
||||
@@ -46,6 +62,7 @@ def new_user(name, email):
|
||||
doc.insert()
|
||||
return doc
|
||||
|
||||
|
||||
def new_course(title, additional_filters=None):
|
||||
course = frappe.db.exists("LMS Course", { "title": title })
|
||||
if course:
|
||||
@@ -66,6 +83,7 @@ def new_course(title, additional_filters=None):
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
|
||||
def create_evaluator():
|
||||
if not frappe.db.exists("Course Evaluator", "evaluator@example.com"):
|
||||
new_user("Evaluator", "evaluator@example.com")
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
|
||||
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
||||
|
||||
|
||||
def slugify(title, used_slugs=[]):
|
||||
"""Converts title to a slug.
|
||||
|
||||
@@ -59,6 +60,7 @@ def get_membership(course, member, batch=None):
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
return membership
|
||||
|
||||
|
||||
def get_chapters(course):
|
||||
"""Returns all chapters of this course.
|
||||
"""
|
||||
@@ -85,6 +87,7 @@ def get_lessons(course, chapter=None):
|
||||
|
||||
return lessons
|
||||
|
||||
|
||||
def get_lesson_details(chapter):
|
||||
lessons = []
|
||||
lesson_list = frappe.get_all("Lesson Reference",
|
||||
@@ -94,10 +97,11 @@ def get_lesson_details(chapter):
|
||||
|
||||
for row in lesson_list:
|
||||
lesson_details = frappe.db.get_value("Course Lesson", row.lesson,
|
||||
["name", "title", "include_in_preview", "body", "creation"], as_dict=True)
|
||||
["name", "title", "include_in_preview", "body", "creation", "youtube", "quiz_id"], as_dict=True)
|
||||
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
|
||||
lesson_details.icon = "icon-list"
|
||||
macros = find_macros(lesson_details.body)
|
||||
|
||||
for macro in macros:
|
||||
if macro[0] == "YouTubeVideo":
|
||||
lesson_details.icon = "icon-video"
|
||||
@@ -106,10 +110,12 @@ def get_lesson_details(chapter):
|
||||
lessons.append(lesson_details)
|
||||
return lessons
|
||||
|
||||
|
||||
def get_tags(course):
|
||||
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||
return tags.split(",") if tags else []
|
||||
|
||||
|
||||
def get_instructors(course):
|
||||
instructor_details = []
|
||||
instructors = frappe.get_all("Course Instructor", {"parent": course},
|
||||
@@ -123,6 +129,7 @@ def get_instructors(course):
|
||||
as_dict=True))
|
||||
return instructor_details
|
||||
|
||||
|
||||
def get_students(course, batch=None):
|
||||
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
|
||||
"""
|
||||
@@ -137,12 +144,14 @@ def get_students(course, batch=None):
|
||||
filters,
|
||||
["member"])
|
||||
|
||||
|
||||
def get_average_rating(course):
|
||||
ratings = [review.rating for review in get_reviews(course)]
|
||||
if not len(ratings):
|
||||
return None
|
||||
return sum(ratings)/len(ratings)
|
||||
|
||||
|
||||
def get_reviews(course):
|
||||
reviews = frappe.get_all("LMS Course Review",
|
||||
{
|
||||
@@ -164,6 +173,7 @@ def get_reviews(course):
|
||||
|
||||
return reviews
|
||||
|
||||
|
||||
def get_sorted_reviews(course):
|
||||
rating_count = rating_percent = frappe._dict()
|
||||
keys = ["5.0", "4.0", "3.0", "2.0", "1.0"]
|
||||
@@ -180,6 +190,7 @@ def get_sorted_reviews(course):
|
||||
|
||||
return rating_percent
|
||||
|
||||
|
||||
def is_certified(course):
|
||||
certificate = frappe.get_all("LMS Certificate",
|
||||
{
|
||||
@@ -190,6 +201,7 @@ def is_certified(course):
|
||||
return certificate[0].name
|
||||
return
|
||||
|
||||
|
||||
def get_lesson_index(lesson_name):
|
||||
"""Returns the {chapter_index}.{lesson_index} for the lesson.
|
||||
"""
|
||||
@@ -205,6 +217,7 @@ def get_lesson_index(lesson_name):
|
||||
|
||||
return f"{chapter.idx}.{lesson.idx}"
|
||||
|
||||
|
||||
def get_lesson_url(course, lesson_number):
|
||||
if not lesson_number:
|
||||
return
|
||||
@@ -224,8 +237,16 @@ def get_progress(course, lesson):
|
||||
},
|
||||
["status"])
|
||||
|
||||
def render_html(body):
|
||||
return markdown_to_html(body)
|
||||
|
||||
def render_html(body, youtube, quiz_id):
|
||||
if youtube and "/" in youtube:
|
||||
youtube = youtube.split("/")[-1]
|
||||
|
||||
quiz_id = "{{ Quiz('" + quiz_id + "') }}" if quiz_id else ""
|
||||
youtube = "{{ YouTubeVideo('" + youtube + "') }}" if youtube else ""
|
||||
text = youtube + body + quiz_id
|
||||
return markdown_to_html(text)
|
||||
|
||||
|
||||
def is_mentor(course, email):
|
||||
"""Checks if given user is a mentor for this course.
|
||||
@@ -238,6 +259,7 @@ def is_mentor(course, email):
|
||||
"mentor": email
|
||||
})
|
||||
|
||||
|
||||
def is_cohort_staff(course, user_email):
|
||||
"""Returns True if the user is either a mentor or a staff for one or more active cohorts of this course.
|
||||
"""
|
||||
@@ -253,6 +275,7 @@ def is_cohort_staff(course, user_email):
|
||||
}
|
||||
return frappe.db.exists(staff) or frappe.db.exists(mentor)
|
||||
|
||||
|
||||
def get_mentors(course):
|
||||
"""Returns the list of all mentors for this course.
|
||||
"""
|
||||
@@ -269,6 +292,7 @@ def get_mentors(course):
|
||||
course_mentors.append(member)
|
||||
return course_mentors
|
||||
|
||||
|
||||
def is_eligible_to_review(course, membership):
|
||||
""" Checks if user is eligible to review the course """
|
||||
if not membership:
|
||||
@@ -281,6 +305,7 @@ def is_eligible_to_review(course, membership):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_course_progress(course, member=None):
|
||||
""" Returns the course progress of the session user """
|
||||
lesson_count = len(get_lessons(course))
|
||||
@@ -295,6 +320,7 @@ def get_course_progress(course, member=None):
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||
return flt(((completed_lessons/lesson_count) * 100), precision)
|
||||
|
||||
|
||||
def get_initial_members(course):
|
||||
members = frappe.get_all("LMS Batch Membership",
|
||||
{
|
||||
@@ -310,12 +336,15 @@ def get_initial_members(course):
|
||||
|
||||
return member_details
|
||||
|
||||
|
||||
def is_instructor(course):
|
||||
return len(list(filter(lambda x: x.name == frappe.session.user, get_instructors(course)))) > 0
|
||||
|
||||
|
||||
def convert_number_to_character(number):
|
||||
return string.ascii_uppercase[number]
|
||||
|
||||
|
||||
def get_signup_optin_checks():
|
||||
|
||||
mapper = frappe._dict({
|
||||
@@ -343,6 +372,7 @@ def get_signup_optin_checks():
|
||||
|
||||
return (", ").join(links)
|
||||
|
||||
|
||||
def get_popular_courses():
|
||||
courses = frappe.get_all("LMS Course", {"published": 1, "upcoming": 0})
|
||||
course_membership = []
|
||||
@@ -356,6 +386,7 @@ def get_popular_courses():
|
||||
course_membership = sorted(course_membership, key = lambda x: x.get("members"), reverse=True)
|
||||
return course_membership[:3]
|
||||
|
||||
|
||||
def get_evaluation_details(course, member=None):
|
||||
info = frappe.db.get_value("LMS Course", course, ["grant_certificate_after", "max_attempts", "duration"], as_dict=True)
|
||||
request = frappe.db.get_value("LMS Certificate Request", {
|
||||
@@ -378,6 +409,7 @@ def get_evaluation_details(course, member=None):
|
||||
"no_of_attempts": no_of_attempts
|
||||
})
|
||||
|
||||
|
||||
def format_amount(amount, currency):
|
||||
amount_reduced = amount / 1000
|
||||
if amount_reduced < 1:
|
||||
@@ -397,13 +429,43 @@ def first_lesson_exists(course):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def redirect_to_courses_list():
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
|
||||
|
||||
def has_course_instructor_role():
|
||||
def has_course_instructor_role(member=None):
|
||||
return frappe.db.get_value("Has Role", {
|
||||
"parent": frappe.session.user,
|
||||
"parent": member or frappe.session.user,
|
||||
"role": "Course Instructor"
|
||||
}, "name")
|
||||
|
||||
|
||||
def has_course_moderator_role(member=None):
|
||||
return frappe.db.get_value("Has Role", {
|
||||
"parent": member or frappe.session.user,
|
||||
"role": "Course Moderator"
|
||||
}, "name")
|
||||
|
||||
|
||||
def get_courses_under_review():
|
||||
return frappe.get_all("LMS Course", {
|
||||
"status": "Under Review"
|
||||
}, ["name", "upcoming", "title", "image", "enable_certification", "status", "published"]
|
||||
)
|
||||
|
||||
|
||||
def get_certificates(member=None):
|
||||
return frappe.get_all("LMS Certificate", {
|
||||
"member": member or frappe.session.user
|
||||
}, ["course", "member", "issue_date", "expiry_date", "name"])
|
||||
|
||||
|
||||
def validate_image(path):
|
||||
if path and "/private" in path:
|
||||
file = frappe.get_doc("File", {"file_url": path})
|
||||
file.is_private = 0
|
||||
file.save(ignore_permissions=True)
|
||||
return file.file_url
|
||||
return path
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"is_multi_step_form": 0,
|
||||
"is_standard": 1,
|
||||
"list_columns": [],
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-06-24 19:08:29.197279",
|
||||
"modified": "2022-09-05 13:08:40.071348",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "profile",
|
||||
@@ -30,11 +30,9 @@
|
||||
"payment_button_label": "Buy Now",
|
||||
"published": 1,
|
||||
"route": "edit-profile",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_in_grid": 0,
|
||||
"show_list": 0,
|
||||
"show_sidebar": 0,
|
||||
"sidebar_items": [],
|
||||
"success_url": "/profile",
|
||||
"title": "Profile",
|
||||
"web_form_fields": [
|
||||
@@ -50,18 +48,6 @@
|
||||
"reqd": 1,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "middle_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Middle Name (Optional)",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "last_name",
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
{% set enrolled = get_enrolled_courses().in_progress + get_enrolled_courses().completed %}
|
||||
|
||||
|
||||
{% if enrolled | length %}
|
||||
<div class="cards-parent">
|
||||
{% for course in enrolled %}
|
||||
{{ widgets.CourseCard(course=course) }}
|
||||
{% endfor %}
|
||||
{% for course in enrolled %}
|
||||
{{ widgets.CourseCard(course=course) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
{% set site_name = frappe.db.get_single_value("System Settings", "app_name") %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-text">
|
||||
<div class="text-center">
|
||||
<div class="empty-state-heading">{{ _("You haven't enrolled for any courses") }}</div>
|
||||
<div class="course-meta mb-6">{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}</div>
|
||||
<div class="empty-state p-5">
|
||||
<div style="text-align: left; flex: 1;">
|
||||
<div class="text-center">
|
||||
<div class="empty-state-heading">{{ _("You haven't enrolled for any courses") }}</div>
|
||||
<div class="course-meta mb-6">{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}</div>
|
||||
</div>
|
||||
{% set recommended_courses = get_popular_courses() %}
|
||||
<div class="cards-parent">
|
||||
{% for course in recommended_courses %}
|
||||
{% if course %}
|
||||
{% set course_details = frappe.get_doc("LMS Course", course.course) %}
|
||||
{{ widgets.CourseCard(course=course_details) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% set recommended_courses = get_popular_courses() %}
|
||||
<div class="cards-parent">
|
||||
{% for course in recommended_courses %}
|
||||
{% if course %}
|
||||
{% set course_details = frappe.get_doc("LMS Course", course.course) %}
|
||||
{{ widgets.CourseCard(course=course_details) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="common-card-style course-card" data-course="{{ course.name }}">
|
||||
|
||||
<div class="course-image {% if not course.image %}default-image{% endif %}" {% if course.image %}
|
||||
style="background-image: url( {{ course.image }} );" {% endif %}>
|
||||
style="background-image: url( {{ course.image | urlencode }} );" {% endif %}>
|
||||
<div class="course-tags">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
<div class="course-card-pills">{{ tag }}</div>
|
||||
@@ -90,10 +90,12 @@
|
||||
<a class="button-links" href="{{ get_profile_url(instructors[0].username) }}">
|
||||
<span class="course-instructor">
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{{ instructors[0].full_name }}
|
||||
{% elif ins_len == 2 %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ instructors[1].full_name.split(" ")[0] }}
|
||||
{% else %}
|
||||
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
<div class="course-home-outline">
|
||||
|
||||
{% set chapters = get_chapters(course.name) %}
|
||||
|
||||
{% if course.edit_mode and course.name %}
|
||||
<button class="btn btn-md btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
|
||||
{% endif %}
|
||||
|
||||
{% if course.name and (course.edit_mode or get_chapters(course.name) | length) %}
|
||||
{% if course.name and (course.edit_mode or chapters | length) %}
|
||||
<div class="course-home-headings" id="outline-heading">
|
||||
{{ _("Course Content") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if get_chapters(course.name) | length %}
|
||||
{% if course.edit_mode and course.name and not chapters | length %}
|
||||
<div class="chapter-parent chapter-edit new-chapter">
|
||||
<div contenteditable="true" data-placeholder="{{ _('Chapter Name') }}" class="chapter-title-main"></div>
|
||||
<div class="chapter-description small my-2" contenteditable="true" data-placeholder="{{ _('Short Description') }}"></div>
|
||||
<button class="btn btn-sm btn-secondary d-block btn-save-chapter" data-index="1"> {{ _('Save') }} </button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for chapter in get_chapters(course.name) %}
|
||||
{% if chapters | length %}
|
||||
|
||||
{% for chapter in chapters %}
|
||||
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %} ">
|
||||
<div class="chapter-title" {% if not course.edit_mode %} data-toggle="collapse" aria-expanded="false"
|
||||
data-target="#{{ get_slugified_chapter_title(chapter.title) }}" {% endif %} >
|
||||
@@ -55,7 +65,7 @@
|
||||
{% set active = membership.current_lesson == lesson.name %}
|
||||
<div class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
|
||||
|
||||
{% if membership or lesson.include_in_preview or is_instructor %}
|
||||
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
|
||||
<a class="lesson-links" data-course="{{ course.name }}"
|
||||
{% if is_instructor and not lesson.include_in_preview %}
|
||||
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
||||
@@ -185,3 +195,4 @@ const show_no_preview_dialog = (e) => {
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div> {{ member.headline }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% set course_count = get_authored_courses(member.name) | length %}
|
||||
{% set course_count = get_authored_courses(member.name, True) | length %}
|
||||
{% if show_course_count and course_count > 0 %}
|
||||
{% set suffix = "Courses" if course_count > 1 else "Course" %}
|
||||
<div class="">
|
||||
|
||||
@@ -1,166 +1,169 @@
|
||||
{% if not course.upcoming %}
|
||||
<div class="reviews-parent">
|
||||
{% set reviews = get_reviews(course.name) %}
|
||||
<div class="mb-5">
|
||||
<span class="course-home-headings"> {{ _("Reviews") }} </span>
|
||||
{% if is_eligible_to_review(course.name, membership) and reviews | length %}
|
||||
<span class="review-link button is-secondary pull-right">
|
||||
{{ _("Write a review") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% set avg_rating = get_average_rating(course.name) %}
|
||||
{% if avg_rating %}
|
||||
<div class="reviews-header">
|
||||
<div class="text-center">
|
||||
<div class="avg-rating"> {{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} </div>
|
||||
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
|
||||
<div class="avg-rating-stars">
|
||||
<div class="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-meta">
|
||||
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} {{ _("out of 5 ") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vertical-divider"></div>
|
||||
{% set sorted_reviews = get_sorted_reviews(course.name) %}
|
||||
<div>
|
||||
{% for review in sorted_reviews %}
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="course-meta mr-2"> {{ frappe.utils.cint(review) }} {{ _("stars") }} </div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ sorted_reviews[review] }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ sorted_reviews[review] }}%">
|
||||
<span class="sr-only"> {{ sorted_reviews[review] }} Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-meta ml-3"> {{ frappe.utils.cint(sorted_reviews[review]) }}% </div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reviews | length %}
|
||||
<div class="mt-12">
|
||||
{% for review in reviews %}
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
|
||||
<div class="mr-4">
|
||||
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<a class="button-links mr-4" href="{{get_profile_url(review.owner_details.username) }}">
|
||||
<span class="bold-heading">
|
||||
{{ review.owner_details.full_name }}
|
||||
</span>
|
||||
</a>
|
||||
<div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}"> frappe.utils.pretty_date(review.creation) </div>
|
||||
</div>
|
||||
|
||||
<div class="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="review-content"> {{ review.review }} </div>
|
||||
</div>
|
||||
{% if loop.index != reviews | length %}
|
||||
<div class="card-divider"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div>
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
</div>
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("Review the course") }}</div>
|
||||
<div class="course-meta">{{ _("Help us improve our course material.") }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if not is_instructor(course.name) %}
|
||||
{% set reviews = get_reviews(course.name) %}
|
||||
<div class="mb-5">
|
||||
<span class="course-home-headings"> {{ _("Reviews") }} </span>
|
||||
{% if is_eligible_to_review(course.name, membership) %}
|
||||
<span class="review-link button is-secondary">
|
||||
{{ _("Write a review") }}
|
||||
</span>
|
||||
{% elif frappe.session.user == "Guest" %}
|
||||
<a class="button is-primary" href="/login?redirect-to=/courses/{{ course.name }}"> {{ _("Login") }} </a>
|
||||
{% elif not membership %}
|
||||
<div class="button is-primary join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
|
||||
<span class="btn btn-secondary btn-sm review-link">
|
||||
{{ _("Write a review") }}
|
||||
</span>
|
||||
{% elif not is_instructor(course.name) and frappe.session.user == "Guest" %}
|
||||
<a class="btn btn-secondary btn-s pull-rightm" href="/login?redirect-to=/courses/{{ course.name }}"> {{ _("Login") }} </a>
|
||||
{% elif not is_instructor(course.name) and not membership and course.status == "Approved" %}
|
||||
<div class="btn btn-secondary btn-sm join-batch pull-right" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% set avg_rating = get_average_rating(course.name) %}
|
||||
{% if avg_rating %}
|
||||
<div class="reviews-header">
|
||||
<div class="text-center">
|
||||
<div class="avg-rating">
|
||||
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }}
|
||||
</div>
|
||||
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
|
||||
<div class="avg-rating-stars">
|
||||
<div class="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-meta">
|
||||
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} {{ _("out of 5 ") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="vertical-divider"></div>
|
||||
{% set sorted_reviews = get_sorted_reviews(course.name) %}
|
||||
<div>
|
||||
{% for review in sorted_reviews %}
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="course-meta mr-2">
|
||||
{{ frappe.utils.cint(review) }} {{ _("stars") }}
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ sorted_reviews[review] }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ sorted_reviews[review] }}%">
|
||||
<span class="sr-only"> {{ sorted_reviews[review] }} Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-meta ml-3"> {{ frappe.utils.cint(sorted_reviews[review]) }}% </div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if reviews | length %}
|
||||
<div class="mt-12">
|
||||
{% for review in reviews %}
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="mr-4">
|
||||
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<a class="button-links mr-4" href="{{get_profile_url(review.owner_details.username) }}">
|
||||
<span class="bold-heading">
|
||||
{{ review.owner_details.full_name }}
|
||||
</span>
|
||||
</a>
|
||||
<div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}">
|
||||
{{ frappe.utils.pretty_date(review.creation) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-content"> {{ review.review }} </div>
|
||||
</div>
|
||||
{% if loop.index != reviews | length %}
|
||||
<div class="card-divider"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("Review the course") }}</div>
|
||||
<div class="course-meta">{{ _("Help us improve our course material.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="font-weight-bold">{{ _("Write a review") }}</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="review-form" id="review-form">
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Rating") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<div class="rating rating-field" id="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Review") }}</label>
|
||||
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="font-weight-bold">{{ _("Write a review") }}</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
|
||||
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
|
||||
spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="review-form" id="review-form">
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Rating") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<div class="rating rating-field" id="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Review") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
|
||||
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
|
||||
spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="error-field muted-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="error-field muted-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
|
||||
{{ _("Submit") }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
|
||||
{{ _("Submit") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,17 +6,22 @@ import random
|
||||
import re
|
||||
from frappe import _
|
||||
from frappe.website.utils import is_signup_disabled
|
||||
from lms.lms.utils import validate_image
|
||||
import requests
|
||||
from frappe.geo.country_info import get_all
|
||||
from lms.widgets import Widgets
|
||||
|
||||
|
||||
class CustomUser(User):
|
||||
|
||||
|
||||
def validate(self):
|
||||
super(CustomUser, self).validate()
|
||||
self.validate_username_characters()
|
||||
self.validate_skills()
|
||||
self.validate_completion()
|
||||
self.user_image = validate_image(self.user_image)
|
||||
self.cover_image = validate_image(self.cover_image)
|
||||
|
||||
|
||||
def validate_username_characters(self):
|
||||
if len(self.username):
|
||||
@@ -127,7 +132,8 @@ class CustomUser(User):
|
||||
def get_enrolled_courses():
|
||||
in_progress = []
|
||||
completed = []
|
||||
memberships = get_course_membership(frappe.session.user, member_type="Student")
|
||||
memberships = get_course_membership(None, member_type="Student")
|
||||
|
||||
for membership in memberships:
|
||||
course = frappe.db.get_value("LMS Course", membership.course, ["name", "upcoming", "title", "image",
|
||||
"enable_certification", "paid_certificate", "price_certificate", "currency", "published"], as_dict=True)
|
||||
@@ -144,10 +150,11 @@ def get_enrolled_courses():
|
||||
"completed": completed
|
||||
}
|
||||
|
||||
def get_course_membership(member, member_type=None):
|
||||
""" Returns all memberships of the user """
|
||||
def get_course_membership(member=None, member_type=None):
|
||||
""" Returns all memberships of the user. """
|
||||
|
||||
filters = {
|
||||
"member": member
|
||||
"member": member or frappe.session.user
|
||||
}
|
||||
if member_type:
|
||||
filters["member_type"] = member_type
|
||||
@@ -155,24 +162,24 @@ def get_course_membership(member, member_type=None):
|
||||
return frappe.get_all("LMS Batch Membership", filters, ["name", "course", "progress"])
|
||||
|
||||
|
||||
def get_authored_courses(member, only_published=True):
|
||||
"""Returns the number of courses authored by this user.
|
||||
"""
|
||||
def get_authored_courses(member=None, only_published=True):
|
||||
""" Returns the number of courses authored by this user. """
|
||||
course_details = []
|
||||
|
||||
filters = {
|
||||
"instructor": member
|
||||
}
|
||||
if only_published:
|
||||
filters["published"] = True
|
||||
courses = frappe.get_all('LMS Course', filters)
|
||||
courses = frappe.get_all("Course Instructor", {
|
||||
"instructor": member or frappe.session.user
|
||||
}, ["parent"])
|
||||
|
||||
for course in courses:
|
||||
course_details.append(frappe.db.get_value("LMS Course", course,
|
||||
["name", "upcoming", "title", "image", "enable_certification", "status"], as_dict=True))
|
||||
detail = frappe.db.get_value("LMS Course", course.parent,
|
||||
["name", "upcoming", "title", "image", "enable_certification", "status", "published"], as_dict=True)
|
||||
|
||||
if only_published and detail and not detail.published:
|
||||
continue
|
||||
course_details.append(detail)
|
||||
|
||||
return course_details
|
||||
|
||||
|
||||
def get_palette(full_name):
|
||||
"""
|
||||
Returns a color unique to each member for Avatar """
|
||||
@@ -328,3 +335,22 @@ def get_users(or_filters, start, page_length, text):
|
||||
""".format(or_filters = or_filters, start=start, page_length=page_length), as_dict=1)
|
||||
|
||||
return users
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_role(user, role, value):
|
||||
if cint(value):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Has Role",
|
||||
"parent": user,
|
||||
"role": role,
|
||||
"parenttype": "User",
|
||||
"parentfield": "roles"
|
||||
})
|
||||
doc.save(ignore_permissions=True)
|
||||
else:
|
||||
frappe.db.delete("Has Role", {
|
||||
"parent": user,
|
||||
"role": role
|
||||
})
|
||||
return True
|
||||
|
||||
@@ -30,4 +30,5 @@ lms.patches.v0_0.move_certification_to_certificate
|
||||
lms.patches.v0_0.quiz_submission_member
|
||||
lms.patches.v0_0.delete_old_module_docs #08-07-2022
|
||||
lms.patches.v0_0.delete_course_web_forms #21-08-2022
|
||||
lms.patches.v0_0.create_course_instructor_role
|
||||
lms.patches.v0_0.create_course_instructor_role #29-08-2022
|
||||
lms.patches.v0_0.create_course_moderator_role
|
||||
|
||||
@@ -6,5 +6,6 @@ def execute():
|
||||
"doctype": "Role",
|
||||
"role_name": "Course Instructor",
|
||||
"home_page": "/dashboard",
|
||||
"desk_access": 0
|
||||
})
|
||||
role.save(ignore_permissions=True)
|
||||
|
||||
11
lms/patches/v0_0/create_course_moderator_role.py
Normal file
11
lms/patches/v0_0/create_course_moderator_role.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("Role", "Course Moderator"):
|
||||
role = frappe.get_doc({
|
||||
"doctype": "Role",
|
||||
"role_name": "Course Moderator",
|
||||
"home_page": "/dashboard",
|
||||
"desk_access": 0
|
||||
})
|
||||
role.save(ignore_permissions=True)
|
||||
@@ -44,37 +44,36 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.course-image .course-tags {
|
||||
width: 95%;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.course-card-pills {
|
||||
background: #ffffff;
|
||||
margin-left: 0;
|
||||
margin-right: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 3.5px 8px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.011em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
width: fit-content;
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: #ffffff;
|
||||
margin-left: 0;
|
||||
margin-right: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 3.5px 8px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.011em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
width: fit-content;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.dark-pills {
|
||||
background: rgba(25, 39, 52, 0.8);
|
||||
color: #ffffff;
|
||||
background: rgba(25, 39, 52, 0.8);
|
||||
color: #ffffff;
|
||||
}
|
||||
.dark-pills img {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.common-page-style {
|
||||
padding: 2rem 0 5rem;
|
||||
min-height: 60vh;
|
||||
padding-top: 3rem;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
@@ -126,9 +125,10 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.course-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
@@ -661,7 +661,7 @@ input[type=checkbox] {
|
||||
font-size: var(--text-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 2.5rem;
|
||||
padding-left: 200px;
|
||||
padding-right: 1rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
@@ -764,7 +764,7 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.education-details {
|
||||
margin-top: 3rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.bold-title {
|
||||
@@ -895,24 +895,22 @@ pre {
|
||||
}
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
flex-direction: column;
|
||||
padding: 1rem 1.25rem;
|
||||
.column-card {
|
||||
flex-direction: column;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: var(--gray-200);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 2rem;
|
||||
padding: 4rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
flex: 1;
|
||||
margin-left: 1.25rem;
|
||||
text-align: center;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.empty-state-heading {
|
||||
@@ -1400,19 +1398,25 @@ pre {
|
||||
overflow: inherit;
|
||||
}
|
||||
|
||||
.dashboard .nav-link {
|
||||
.lms-nav .nav-link {
|
||||
color: var(--text-muted);
|
||||
padding: 0 0 var(--padding-md);
|
||||
margin-right: var(--margin-xl);
|
||||
padding: var(--padding-md) 0;
|
||||
margin: 0 var(--margin-md);
|
||||
}
|
||||
|
||||
.dashboard .nav-link.active {
|
||||
.lms-nav .nav-link.active {
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--primary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.dashboard .nav-link:hover {
|
||||
@media (min-width: 500px) {
|
||||
.lms-nav .nav-item:first-child .nav-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lms-nav .nav-link:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -1541,7 +1545,7 @@ li {
|
||||
}
|
||||
}
|
||||
|
||||
[contenteditable] {
|
||||
[contenteditable="true"] {
|
||||
outline: none;
|
||||
background-color: var(--bg-light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
@@ -1550,7 +1554,7 @@ li {
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
[contenteditable]:empty:before {
|
||||
[contenteditable="true"]:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
@@ -1588,7 +1592,7 @@ li {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.course-card-pills[contenteditable] {
|
||||
.course-card-pills[contenteditable="true"] {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -1658,6 +1662,49 @@ li {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.help-article {
|
||||
.medium {
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.quiz-row {
|
||||
position: relative;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-creation-link {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.course-creation-link {
|
||||
float: inherit;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-pill::before {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.review-link {
|
||||
float: right
|
||||
}
|
||||
|
||||
.role {
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.role:last-child {
|
||||
margin-left: 5rem
|
||||
}
|
||||
}
|
||||
|
||||
.icon-xl {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ const join_course = (e) => {
|
||||
}, 3);
|
||||
setTimeout(function () {
|
||||
window.location.href = `/courses/${course}/learn/1.1`;
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -127,7 +127,7 @@ const scroll_to_chapter_container = () => {
|
||||
scrollTop: $(".new-chapter").offset().top
|
||||
}, 1000);
|
||||
$(".new-chapter").find(".chapter-title-main").focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const save_chapter = (e) => {
|
||||
@@ -150,7 +150,7 @@ const save_chapter = (e) => {
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
45
lms/subscription_utils.py
Normal file
45
lms/subscription_utils.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import frappe
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_add_on_details(plan: str) -> dict[str, int]:
|
||||
"""
|
||||
Returns the number of courses and course members to be billed under add-ons for SAAS subscription
|
||||
"""
|
||||
|
||||
return {
|
||||
"courses": get_add_on_courses(plan),
|
||||
"members": get_add_on_members(plan)
|
||||
}
|
||||
|
||||
|
||||
def get_published_courses() -> int:
|
||||
return frappe.db.count("LMS Course", {"published": 1})
|
||||
|
||||
|
||||
def get_add_on_courses(plan: str) -> int:
|
||||
COURSE_LIMITS = {"Lite": 5, "Pro": 20}
|
||||
add_on_courses = 0
|
||||
courses_included_in_plans = COURSE_LIMITS.get(plan)
|
||||
|
||||
if courses_included_in_plans:
|
||||
published_courses = get_published_courses()
|
||||
add_on_courses = published_courses - courses_included_in_plans if published_courses > courses_included_in_plans else 0
|
||||
|
||||
return add_on_courses
|
||||
|
||||
|
||||
def get_add_on_members(plan: str) -> int:
|
||||
MEMBER_LIMITS = {"Lite": 100, "Pro": 500}
|
||||
add_on_members = 0
|
||||
members_included_in_plans = MEMBER_LIMITS.get(plan)
|
||||
|
||||
if members_included_in_plans:
|
||||
active_members = get_members()
|
||||
add_on_members = active_members - members_included_in_plans if active_members > members_included_in_plans else 0
|
||||
|
||||
return add_on_members
|
||||
|
||||
|
||||
def get_members() -> int:
|
||||
return frappe.db.count("LMS Batch Membership")
|
||||
@@ -1,38 +1,44 @@
|
||||
|
||||
<div id="certificate-card" style="background: #FFFFFF; border-radius: 0.5rem; position: relative;
|
||||
box-shadow: 0px 1px 2px rgba(25, 39, 52, 0.05), 0px 0px 4px rgba(25, 39, 52, 0.1); padding: 1rem;">
|
||||
|
||||
<div style="border: 10px solid var(--primary-color); display: flex; flex-direction: column; align-items: center; padding: 4rem;
|
||||
justify-content: center; background-color: #FFFFFF;">
|
||||
|
||||
<img src="{{ logo }}" style="height: 1.5rem;">
|
||||
<div style="margin-top: 4rem;">
|
||||
{{ _("This certifies that") }}
|
||||
</div>
|
||||
<div style="font-size: 2rem; font-weight: 500; color: #192734;"> {{ member.full_name }} </div>
|
||||
<div style="margin-top: 0.5rem;"> {{ _("has successfully completed the course on") }}
|
||||
<b> {{ course.title }} </b> on {{ frappe.utils.format_date(certificate.issue_date, "medium") }}. </div>
|
||||
|
||||
<div style="font-size: 2rem; font-weight: 500; color: #192734;">
|
||||
{{ member.full_name }}
|
||||
</div>
|
||||
<div style="margin-top: 0.5rem;">
|
||||
{{ _("has successfully completed the course on") }}
|
||||
<b> {{ course.title }} </b>
|
||||
on {{ frappe.utils.format_date(certificate.issue_date, "medium") }}.
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin: 4rem auto 0; width: fit-content;">
|
||||
|
||||
{% if instructors %}
|
||||
<div>
|
||||
<div style="color: #192734; font-weight: bold; font-family: cursive; font-size: 1.25rem;">
|
||||
{{ instructors }}
|
||||
</div>
|
||||
<hr style="margin: 0.5rem 0;">
|
||||
<div> {{ _("Course Instructor") }} </div>
|
||||
<div class="text-center"> {{ _("Course Instructor") }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if certificate.expiry_date %}
|
||||
<div style="margin-left: 2rem;">
|
||||
<div style="color: #192734; font-weight: bold; font-family: cursive; font-size: 1.25rem;">
|
||||
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
|
||||
</div>
|
||||
<hr style="margin: 0.5rem 0;">
|
||||
<div> {{ _("Expiry date") }} </div>
|
||||
<div style="color: #192734; font-weight: bold; font-family: cursive; font-size: 1.25rem;">
|
||||
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
|
||||
</div>
|
||||
<hr style="margin: 0.5rem 0;">
|
||||
<div class="text-center"> {{ _("Expiry date") }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
30
lms/templates/certificates_section.html
Normal file
30
lms/templates/certificates_section.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% set certificates = get_certificates(user) %}
|
||||
|
||||
{% if certificates | length %}
|
||||
<div class="cards-parent">
|
||||
{% for certificate in certificates %}
|
||||
{% set course = frappe.db.get_value("LMS Course", certificate.course, ["title", "name", "image"], as_dict=True) %}
|
||||
|
||||
<div class="common-card-style column-card">
|
||||
<div class="font-weight-bold">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Issued on") }} : {{ frappe.utils.format_date(certificate.issue_date, "medium") }}
|
||||
</div>
|
||||
<a class="stretched-link" href="/courses/{{ course.name }}/{{ certificate.name }}"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% set course_list_link = "<a href='/courses'>course list</a>" %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No certificates") }}</div>
|
||||
<div class="course-meta">{{ _("Check out the {0} to know more about certification.").format(course_list_link) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% set courses = get_authored_courses(frappe.session.user, only_published=False) %}
|
||||
{% set courses = get_authored_courses(user or None, only_published or False) %}
|
||||
|
||||
{% if courses | length %}
|
||||
<div class="cards-parent">
|
||||
@@ -9,9 +9,7 @@
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div>
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
</div>
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No courses created") }}</div>
|
||||
<div class="course-meta">{{ _("Help others learn something new.") }}</div>
|
||||
|
||||
18
lms/templates/courses_under_review.html
Normal file
18
lms/templates/courses_under_review.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% set courses = get_courses_under_review() %}
|
||||
|
||||
{% if courses | length %}
|
||||
<div class="cards-parent">
|
||||
{% for course in courses %}
|
||||
{{ widgets.CourseCard(course=course) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No courses under review") }}</div>
|
||||
<div class="course-meta">{{ _("When a course gets submitted for review, it will be listed here.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -48,25 +48,25 @@
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$("#confirm").click((e) => {
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
|
||||
"args": {
|
||||
"batch": {{ batch.name }},
|
||||
"course": {{ batch.course }}
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
frappe.msgprint({
|
||||
message: __("You are now a member of this batch!"),
|
||||
clear: true
|
||||
});
|
||||
setTimeout(function () {
|
||||
window.location.href = "/courses/{{ batch.course }}/home";
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
|
||||
"args": {
|
||||
"batch": {{ batch.name }},
|
||||
"course": {{ batch.course }}
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
frappe.msgprint({
|
||||
message: __("You are now a member of this batch!"),
|
||||
clear: true
|
||||
});
|
||||
setTimeout(function () {
|
||||
window.location.href = "/courses/{{ batch.course }}/home";
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
</div>
|
||||
<div class="lesson-pagination-parent">
|
||||
{{ LessonContent(lesson) }}
|
||||
{% if not lesson.edit_mode %} {{ Discussions() }} {% endif %}
|
||||
{% if not lesson.edit_mode and course.status == "Approved" and not course.upcoming %}
|
||||
{{ Discussions() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +59,7 @@
|
||||
{% macro LessonContent(lesson) %}
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% set is_instructor = is_instructor(course.name) %}
|
||||
|
||||
<div class="common-card-style lesson-content">
|
||||
<div class="lesson-title">
|
||||
|
||||
@@ -67,10 +70,10 @@
|
||||
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
|
||||
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
|
||||
>{% if lesson.title %}{{ lesson.title }}{% endif %}</div>
|
||||
<span class="lesson-progress {{ hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
|
||||
<span class="indicator-pill green {{ hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
|
||||
|
||||
<!-- Edit Button -->
|
||||
{% if is_instructor and not lesson.edit_mode %}
|
||||
{% if (is_instructor or has_course_moderator_role()) and not lesson.edit_mode %}
|
||||
<button class="button is-default button-links ml-auto btn-edit"> {{ _("Edit") }} </button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -90,12 +93,14 @@
|
||||
{% endfor %}
|
||||
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
|
||||
<span class="course-meta">
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{% else %}
|
||||
{% set suffix = _("other") if ins_len - 1 == 1 else _("others") %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{% elif ins_len == 2 %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ instructors[1].full_name.split(" ")[0] }}
|
||||
{% else %}
|
||||
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
|
||||
@@ -103,7 +108,7 @@
|
||||
|
||||
<!-- Lesson Content -->
|
||||
<div class="markdown-source lesson-content-card {% if lesson.edit_mode %} mb-0 mt-2 {% endif %} ">
|
||||
{% if membership or lesson.include_in_preview or is_instructor %}
|
||||
{% if show_lesson %}
|
||||
|
||||
{% if is_instructor and not lesson.include_in_preview and not lesson.edit_mode %}
|
||||
<div class="small alert alert-secondary alert-dismissible mb-4">
|
||||
@@ -115,13 +120,17 @@
|
||||
{% if lesson.edit_mode %}
|
||||
{{ EditLesson(lesson) }}
|
||||
{% else %}
|
||||
{{ render_html(lesson.body) }}
|
||||
{{ render_html(lesson.body, lesson.youtube, lesson.quiz_id) }}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% set course_link = "<a class='join-batch' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %}
|
||||
<div class="">
|
||||
<div class="btn btn-primary pull-right join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
|
||||
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
|
||||
<div>
|
||||
{{ _("There is no preview available for this lesson.
|
||||
Please join the course to access it.
|
||||
Click {0} to enroll.").format(course_link) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -140,14 +149,14 @@
|
||||
<div>
|
||||
{% if prev_url %}
|
||||
<a class="btn btn-secondary dark-links prev" href="{{ prev_url }}">
|
||||
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
|
||||
{{ _("Prev") }}
|
||||
{{ _("Previous") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not is_mentor(course.name, frappe.session.user) and membership %}
|
||||
{% set progress = get_progress(course.name, lesson.name) %}
|
||||
|
||||
{% if not is_mentor(course.name, frappe.session.user) and membership %}
|
||||
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
|
||||
<label class="quiz-label">
|
||||
<input class="mark-progress" type="checkbox" checked>
|
||||
@@ -155,18 +164,19 @@
|
||||
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn btn-default mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
|
||||
data-progress="Incomplete">
|
||||
{{ _("Mark as Incomplete") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<a class="btn btn-primary next {% if not next_url and (membership.progress|int == 100 or is_instructor) %} hide {% endif %}"
|
||||
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
|
||||
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
|
||||
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
|
||||
{% if not is_mentor(course.name, frappe.session.user) and membership %}
|
||||
<div class="btn btn-default mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
|
||||
data-progress="Incomplete">
|
||||
{{ _("Mark as Incomplete") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-primary next ml-2 {% if not next_url and (membership.progress|int == 100 or is_instructor) %} hide {% endif %}"
|
||||
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
|
||||
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -176,20 +186,34 @@
|
||||
|
||||
<!-- Edit Lesson -->
|
||||
{% macro EditLesson(lesson) %}
|
||||
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
|
||||
|
||||
<label class="preview">
|
||||
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox"
|
||||
id="preview"> {{ _("Show preview of this lesson to Guest users.") }}
|
||||
</label>
|
||||
<div class="medium mt-2" contenteditable="true" data-placeholder="{{ _('YouTube Video ID') }}"
|
||||
id="youtube">{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}</div>
|
||||
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
|
||||
<div class="medium mb-4" contenteditable="true" data-placeholder="{{ _('Quiz ID') }}"
|
||||
id="quiz-id">{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
|
||||
{% if lesson.name %}
|
||||
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
|
||||
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
|
||||
{% endif %}
|
||||
<label class="preview" for="preview">
|
||||
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox" id="preview">
|
||||
{{ _("Show preview of this lesson to Guest users.") }}
|
||||
</label>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
|
||||
{% if lesson.name %}
|
||||
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
|
||||
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
|
||||
{% endif %}
|
||||
|
||||
{{ UploadAttachments() }}
|
||||
|
||||
</div>
|
||||
|
||||
{{ HelpArticle() }}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro UploadAttachments() %}
|
||||
<div class="attachments-parent">
|
||||
<div class="attachment-controls">
|
||||
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
|
||||
@@ -209,94 +233,111 @@
|
||||
</div>
|
||||
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ HelpArticle() }}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Help Article -->
|
||||
{% macro HelpArticle() %}
|
||||
<div class="help-article">
|
||||
<h3> {{ _("Help Article") }} </h3>
|
||||
<p>
|
||||
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions
|
||||
all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
|
||||
</p>
|
||||
<table class="table w-100">
|
||||
<tr>
|
||||
<th style="width: 20%;"> {{ _("Content Type") }} </th>
|
||||
<th style="width: 40%;"> {{ _("Syntax") }} </th>
|
||||
<th> {{ _("Description") }} </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ _("YouTube Video") }}
|
||||
</td>
|
||||
<td>
|
||||
{% raw %} {{ YouTubeVideo("embed_src") }} {% endraw %}
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{{ _("Copy and paste the syntax in the editor. Replace 'embed_src' with the embed source
|
||||
that YouTube provides. To get the source, follow the steps mentioned below.") }}
|
||||
</span>
|
||||
<ul class="p-4">
|
||||
<div class="medium">
|
||||
<h3> {{ _("Embed Components") }} </h3>
|
||||
<p>
|
||||
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions
|
||||
all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<b> {{ _("YouTube Video") }} </b>
|
||||
<p> To get the YouTube Video ID, follow the steps mentioned below. </p>
|
||||
<ul class="px-4">
|
||||
<li>
|
||||
{{ _("Upload the video on youtube.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("When you share a youtube video, it shows an option called Embed.") }}
|
||||
{{ _("When you share a youtube video, it shows a URL") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and
|
||||
paste it here.") }}
|
||||
{{ _("Copy the last parameter of the URL and paste it here.") }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ _("Quiz") }}
|
||||
</td>
|
||||
<td>
|
||||
{% raw %} {{ Quiz("lms_quiz_id") }} {% endraw %}
|
||||
</td>
|
||||
<td>
|
||||
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %}
|
||||
{{ _("Copy and paste the syntax in the editor. Replace 'lms_quiz_id' with the ID of the Quiz. You can get the ID of the quiz from the {0}.").format(quiz_link) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<!-- <table class="table w-100">
|
||||
<tr>
|
||||
<th style="width: 20%;"> {{ _("Content Type") }} </th>
|
||||
<th style="width: 40%;"> {{ _("Syntax") }} </th>
|
||||
<th> {{ _("Description") }} </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ _("YouTube Video") }}
|
||||
</td>
|
||||
<td>
|
||||
{% raw %} {{ YouTubeVideo('Video ID') }} {% endraw %}
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{{ _("Copy and paste the syntax in the editor. Replace 'Video ID' with the embed source
|
||||
that YouTube provides. To get the source, follow the steps mentioned below.") }}
|
||||
</span>
|
||||
<ul class="p-4">
|
||||
<li>
|
||||
{{ _("Upload the video on youtube.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("When you share a youtube video, it shows an option called Embed.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and
|
||||
paste it here.") }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ _("Quiz") }}
|
||||
</td>
|
||||
<td>
|
||||
{% raw %} {{ Quiz('Quiz ID') }} {% endraw %}
|
||||
</td>
|
||||
<td>
|
||||
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %}
|
||||
{{ _("Copy and paste the syntax in the editor. Replace 'Quiz ID' with the Id of the Quiz.
|
||||
You can get the Id of the quiz from the {0}.").format(quiz_link) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table> -->
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Discussions Component -->
|
||||
{% macro Discussions() %}
|
||||
{% set topics_count = frappe.db.count("Discussion Topic",
|
||||
{"reference_doctype": "Course Lesson", "reference_docname": lesson.name}) %}
|
||||
{% set is_instructor = frappe.session.user == course.instructor %}
|
||||
{% set condition = is_instructor if is_instructor else membership %}
|
||||
{% set doctype, docname = _("Course Lesson"), lesson.name %}
|
||||
{% set title = "Questions" if topics_count else "" %}
|
||||
{% set cta_title = "Ask a Question" %}
|
||||
{% set button_name = _("Start Learning") %}
|
||||
{% set redirect_to = "/courses/" + course.name %}
|
||||
{% set empty_state_title = _("Have a doubt?") %}
|
||||
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
{% set topics_count = frappe.db.count("Discussion Topic", {
|
||||
"reference_doctype": "Course Lesson",
|
||||
"reference_docname": lesson.name
|
||||
}) %}
|
||||
{% set is_instructor = frappe.session.user == course.instructor %}
|
||||
{% set condition = is_instructor if is_instructor else membership %}
|
||||
{% set doctype, docname = _("Course Lesson"), lesson.name %}
|
||||
{% set title = "Questions" if topics_count else "" %}
|
||||
{% set cta_title = "Ask a Question" %}
|
||||
{% set button_name = _("Start Learning") %}
|
||||
{% set redirect_to = "/courses/" + course.name %}
|
||||
{% set empty_state_title = _("Have a doubt?") %}
|
||||
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
</script>
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_footer() }}
|
||||
{% endfor %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
</script>
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_footer() }}
|
||||
{% endfor %}
|
||||
{%- endblock %}
|
||||
|
||||
@@ -66,7 +66,7 @@ frappe.ready(() => {
|
||||
|
||||
$(".btn-back").click((e) => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
})
|
||||
});
|
||||
|
||||
$(document).on("click", ".copy-link", (e) => {
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
|
||||
@@ -490,14 +490,22 @@ const save_lesson = (e) => {
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
|
||||
args: {
|
||||
"title": $("#title").text(),
|
||||
"body": this.code_field_group.fields_dict["code_md"].last_value,
|
||||
"body": this.code_field_group.fields_dict["code_md"].value,
|
||||
"youtube": $("#youtube").text(),
|
||||
"quiz_id": $("#quiz-id").text(),
|
||||
"chapter": $("#title").data("chapter"),
|
||||
"preview": $("#preview").prop("checked") ? 1 : 0,
|
||||
"idx": $("#title").data("index"),
|
||||
"lesson": lesson ? lesson : ""
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
callback: (data) => {;
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from lms.www.utils import get_common_context, redirect_to_lesson
|
||||
from lms.lms.utils import get_lesson_url, is_instructor, redirect_to_courses_list
|
||||
from lms.lms.utils import get_lesson_url, has_course_moderator_role, is_instructor, redirect_to_courses_list
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
def get_context(context):
|
||||
@@ -23,11 +23,14 @@ def get_context(context):
|
||||
redirect_to_lesson(context.course, index_)
|
||||
|
||||
context.lesson = get_current_lesson_details(lesson_number, context)
|
||||
instructor = is_instructor(context.course.name)
|
||||
context.show_lesson = context.membership or (context.lesson and context.lesson.include_in_preview) or instructor or has_course_moderator_role()
|
||||
|
||||
if not context.lesson:
|
||||
context.lesson = frappe._dict()
|
||||
|
||||
if frappe.form_dict.get("edit"):
|
||||
if not is_instructor(context.course.name):
|
||||
if not instructor and not has_course_moderator_role():
|
||||
redirect_to_courses_list()
|
||||
context.lesson.edit_mode = True
|
||||
else:
|
||||
|
||||
@@ -52,11 +52,13 @@
|
||||
<div class="d-flex justify-content-between option-{{ num }}">
|
||||
<div contenteditable="true" data-placeholder="{{ _('Option') }}"
|
||||
class="option-input">{% if option %}{{ option }}{% endif %}</div>
|
||||
<div contenteditable="true" data-placeholder="{{ _('Explanation') }}"
|
||||
<div contenteditable="true" data-placeholder="{{ _('Explain the option') }}"
|
||||
class="option-input">{% if explanation %}{{ explanation }}{% endif %}</div>
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
|
||||
<label class="mb-0"> {{ _("Is Correct") }} </label>
|
||||
<label class="mb-0">
|
||||
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
|
||||
{{ _("Is Correct") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,8 +70,17 @@
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-secondary btn-sm btn-question"> {{ _("New Question") }} </button>
|
||||
<button class="btn btn-primary btn-sm btn-save-question ml-2
|
||||
{% if not quiz.name %} hide {% endif %}"> {{ _("Save Quiz") }} </button>
|
||||
|
||||
{% if quiz.name %}
|
||||
<button class="btn btn-secondary btn-sm copy-quiz-id ml-2" data-name="'{{ quiz.name }}'">
|
||||
{{ _("Copy Quiz ID") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-primary btn-sm btn-save-question ml-2 {% if not quiz.name %} hide {% endif %}">
|
||||
{{ _("Save Quiz") }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
if(!$(".quiz-card").length) {
|
||||
add_question();
|
||||
}
|
||||
|
||||
$(".btn-question").click((e) => {
|
||||
add_question(e);
|
||||
add_question();
|
||||
});
|
||||
|
||||
$(".btn-save-question").click((e) => {
|
||||
save_question(e);
|
||||
});
|
||||
|
||||
get_questions()
|
||||
$(".copy-quiz-id").click((e) => {
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
|
||||
});
|
||||
|
||||
get_questions();
|
||||
|
||||
});
|
||||
|
||||
|
||||
const add_question = (e) => {
|
||||
const add_question = () => {
|
||||
if ($(".new-quiz-card").length) {
|
||||
scroll_to_question_container();
|
||||
return;
|
||||
}
|
||||
|
||||
let add_after = $(".quiz-card").length ? $(".quiz-card:last") : $("#quiz-title");
|
||||
let question_template = `<div class="quiz-card">
|
||||
let question_template = `<div class="quiz-card new-quiz-card">
|
||||
<div contenteditable="true" data-placeholder="${__("Question")}" class="question mb-4"></div>
|
||||
</div>`;
|
||||
$(question_template).insertAfter(add_after);
|
||||
@@ -54,6 +67,7 @@ const save_question = (e) => {
|
||||
if (!$("#quiz-title").text()) {
|
||||
frappe.throw(__("Quiz Title is mandatory."));
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||
args: {
|
||||
@@ -104,3 +118,11 @@ const get_questions = () => {
|
||||
|
||||
return questions;
|
||||
};
|
||||
|
||||
|
||||
const scroll_to_question_container = () => {
|
||||
$([document.documentElement, document.body]).animate({
|
||||
scrollTop: $(".new-quiz-card").offset().top
|
||||
}, 1000);
|
||||
$(".new-quiz-card").find(".question").focus();
|
||||
}
|
||||
|
||||
@@ -12,37 +12,55 @@
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
|
||||
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz">
|
||||
{{ _("Add Quiz") }}
|
||||
</a>
|
||||
<div class="course-home-headings">
|
||||
{{ _("Quiz List") }}
|
||||
</div>
|
||||
|
||||
{% if quiz_list | length %}
|
||||
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz"> {{ _("Add Quiz") }} </a>
|
||||
<div class="course-home-headings"> {{ _("Quiz List") }} </div>
|
||||
<div class="common-card-style">
|
||||
<table class="table">
|
||||
|
||||
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
|
||||
<td style="width: 10%;"> {{ _("No.") }} </td>
|
||||
<td style="width: 45%;"> {{ _("Title") }} </td>
|
||||
<td> {{ _("ID") }} </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
|
||||
{% for quiz in quiz_list %}
|
||||
<tr style="position: relative; color: var(--text-color);">
|
||||
<tr class="quiz-row" data-name="{{ quiz.name }}">
|
||||
<td> {{ loop.index }} </td>
|
||||
<td>
|
||||
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.title }}</a>
|
||||
{{ quiz.title }}
|
||||
</td>
|
||||
<td>
|
||||
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.name }}</a>
|
||||
{{ quiz.name }}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-secondary btn-sm copy-quiz-id" data-name="'{{ quiz.name }}'">
|
||||
{{ _("Copy Quiz ID") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("You have not created any quiz yet.") }}</div>
|
||||
<div class="course-meta mb-6">{{ _("Create a quiz and add it to your course to engage users.") }}</div>
|
||||
<a class="btn btn-secondary btn-sm"
|
||||
href="{% if frappe.session.user == 'Guest' %} /login?redirect-to=/quizzes {% else %} /quizzes/new-quiz {% endif %}">
|
||||
{{ _("Add Quiz") }} </a>
|
||||
<div class="empty-state-heading">
|
||||
{{ _("You have not created any quiz yet.") }}
|
||||
</div>
|
||||
<div class="course-meta ">
|
||||
{{ _("Create a quiz and add it to your course to engage your users.") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -50,3 +68,22 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
|
||||
$(".copy-quiz-id").click((e) => {
|
||||
e.preventDefault();
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
|
||||
});
|
||||
|
||||
$(".quiz-row").click((e) => {
|
||||
if (!$(e.target).hasClass("copy-quiz-id")) {
|
||||
window.location.href = `/quizzes/${$(e.currentTarget).data('name')}`;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
|
||||
<input class="search" id="search-user" placeholder="{{ _('Try a Name, Company, or Industry') }}">
|
||||
<input class="search pull-right" id="search-user" placeholder="{{ _('Search') }}">
|
||||
|
||||
<div class="course-home-headings">{{ _("Community") }} </div>
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||
</div>
|
||||
{% if custom_template %}
|
||||
{{ custom_template }}
|
||||
{{ custom_template }}
|
||||
{% else %}
|
||||
{% include "lms/templates/certificate.html" %}
|
||||
{% include "lms/templates/certificate.html" %}
|
||||
{% endif %}
|
||||
<script src="/assets/lms/js/html2canvas.js"></script>
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ def get_context(context):
|
||||
|
||||
context.course = frappe.db.get_value("LMS Course", course_name, ["title", "name", "image"], as_dict=True)
|
||||
context.instructors = (", ").join([x.full_name for x in get_instructors(course_name)])
|
||||
context.member = frappe.db.get_value("User", context.certificate.member,
|
||||
["full_name"], as_dict=True)
|
||||
context.member = frappe.db.get_value("User", context.certificate.member, ["full_name"], as_dict=True)
|
||||
|
||||
context.logo = frappe.db.get_single_value("Website Settings", "banner_image")
|
||||
template_name = frappe.db.get_single_value("LMS Settings", "custom_certificate_template")
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -18,10 +17,11 @@
|
||||
<div class="container">
|
||||
<div class="course-body-container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
{{ CourseSettings(course) }}
|
||||
{{ Description(course) }}
|
||||
{{ Save(course) }}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
|
||||
{% if not course.edit_mode %}
|
||||
{% if not course.edit_mode and course.status == "Approved" and not frappe.utils.cint(course.upcoming) %}
|
||||
{{ widgets.Reviews(course=course, membership=membership) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -114,7 +114,9 @@
|
||||
</div>
|
||||
|
||||
<div class="course-image-attachment {% if not course.image %} hide {% endif %} ">
|
||||
<a href="{{ course.image }}" id="image" target="_blank"> {{ course.image }} </a>
|
||||
<a {% if course.image %} href="{{ course.image }}" {% endif %} id="image" target="_blank">
|
||||
{{ course.image }}
|
||||
</a>
|
||||
<button class="btn btn-sm btn-default btn-clear ml-4"> {{ _("Clear") }} </button>
|
||||
</div>
|
||||
<a class="btn btn-default btn-sm btn-attach mt-1 {% if course.image %} hide {% endif %}"> {{ _("Attach Image") }} </a>
|
||||
@@ -202,9 +204,32 @@
|
||||
|
||||
<!-- Description -->
|
||||
{% macro Description(course) %}
|
||||
<div class="course-description-section" {% if course.edit_mode %} style="min-height: 100px" {% endif %}
|
||||
{% if course.edit_mode %} contenteditable="true" {% endif %} id="description"
|
||||
data-placeholder="Description">{% if course.description %}{{ frappe.utils.md_to_html(course.description) }}{% endif %}</div>
|
||||
{% if course.edit_mode %}
|
||||
<div id="description" {% if course.description %} data-description="{{ course.description }}" {% endif %}></div>
|
||||
{% else %}
|
||||
<div class="course-description-section">
|
||||
{{ frappe.utils.md_to_html(course.description) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Course Settings -->
|
||||
{% macro CourseSettings(course) %}
|
||||
|
||||
{% if course.edit_mode and has_course_moderator_role() %}
|
||||
<div class="mb-4">
|
||||
<label for="published" class="mb-0">
|
||||
<input type="checkbox" id="published" {% if course.published %} checked {% endif %}>
|
||||
{{ _("Published") }}
|
||||
</label>
|
||||
<label for="upcoming" class="mb-0 ml-20">
|
||||
<input type="checkbox" id="upcoming" {% if course.upcoming %} checked {% endif %}>
|
||||
{{ _("Upcoming") }}
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -225,23 +250,6 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CourseCreator(course) %}
|
||||
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
|
||||
<div class="common-card-style course-creators-card">
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% for instructor in instructors %}
|
||||
<div class="d-flex align-items-center">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
|
||||
<div class="ml-4">
|
||||
<div class="course-creator-name"> {{ instructor.full_name }} </div>
|
||||
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Related Courses Section -->
|
||||
{% macro RelatedCourses(course) %}
|
||||
{% if course.related_courses | length %}
|
||||
@@ -285,7 +293,7 @@
|
||||
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
|
||||
|
||||
{% if show_start_learing_cta %}
|
||||
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}">
|
||||
<div class="btn btn-primary wide-button join-batch mb-2" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Start Learning") }}
|
||||
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</div>
|
||||
@@ -301,7 +309,7 @@
|
||||
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
|
||||
{% elif course.upcoming and not is_user_interested %}
|
||||
{% elif course.upcoming and not is_user_interested and not is_instructor %}
|
||||
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
|
||||
{{ _("Notify me when available") }}
|
||||
</div>
|
||||
@@ -338,7 +346,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_instructor(course.name) %}
|
||||
{% if is_instructor(course.name) or has_course_moderator_role() %}
|
||||
<a class="btn btn-secondary wide-button" href="/courses/{{ course.name }}?edit=1"> {{ _("Edit Course") }} </a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -356,9 +364,9 @@
|
||||
frappe.utils.format_time(certificate_request.start_time, "short")) }} </p>
|
||||
{% endif %}
|
||||
|
||||
{% if course.status == "Under Review" %}
|
||||
{% if course.status == "Under Review" and is_instructor(course.name) %}
|
||||
<div class="mb-4">
|
||||
{{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
|
||||
{{ _("This course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ frappe.ready(() => {
|
||||
remove_tag(e);
|
||||
});
|
||||
|
||||
if ($("#description").length) {
|
||||
make_editor();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -206,7 +210,7 @@ const submit_for_review = (e) => {
|
||||
}, 3);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -329,6 +333,7 @@ const add_tag = (e) => {
|
||||
const save_course = (e) => {
|
||||
let tags = $('.course-card-pills').map((i, el) => $(el).text().trim()).get();
|
||||
tags = tags.filter(word => word.trim().length > 0);
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_course",
|
||||
args: {
|
||||
@@ -337,8 +342,10 @@ const save_course = (e) => {
|
||||
"short_introduction": $("#intro").text(),
|
||||
"video_link": $("#video-link").text(),
|
||||
"image": $("#image").attr("href"),
|
||||
"description": $("#description").text(),
|
||||
"course": $("#title").data("course") ? $("#title").data("course") : ""
|
||||
"description": this.code_field_group.fields_dict["code_md"].value,
|
||||
"course": $("#title").data("course") ? $("#title").data("course") : "",
|
||||
"published": $("#published").prop("checked") ? 1 : 0,
|
||||
"upcoming": $("#upcoming").prop("checked") ? 1 : 0
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
@@ -356,3 +363,26 @@ const save_course = (e) => {
|
||||
const remove_tag = (e) => {
|
||||
$(e.currentTarget).closest(".course-card-pills").remove();
|
||||
};
|
||||
|
||||
|
||||
const make_editor = () => {
|
||||
this.code_field_group = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "code_md",
|
||||
fieldtype: "Code",
|
||||
options: "Markdown",
|
||||
wrap: true,
|
||||
max_lines: Infinity,
|
||||
min_lines: 20,
|
||||
default: $("#description").data("description"),
|
||||
depends_on: 'eval:doc.type=="Markdown"',
|
||||
}
|
||||
],
|
||||
body: $("#description").get(0),
|
||||
});
|
||||
this.code_field_group.make();
|
||||
$("#description .form-section:last").removeClass("empty-section");
|
||||
$("#description .frappe-control").removeClass("hide-control");
|
||||
$("#description .form-column").addClass("p-0");
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction
|
||||
from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list
|
||||
from lms.lms.utils import get_membership, has_course_moderator_role, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
@@ -28,7 +28,7 @@ def set_course_context(context, course_name):
|
||||
as_dict=True)
|
||||
|
||||
if frappe.form_dict.get("edit"):
|
||||
if not is_instructor(course.name):
|
||||
if not is_instructor(course.name) and not has_course_moderator_role():
|
||||
redirect_to_courses_list()
|
||||
course.edit_mode = True
|
||||
|
||||
@@ -73,4 +73,4 @@ def get_user_interest(course):
|
||||
|
||||
|
||||
def show_start_learing_cta(course, membership, restriction):
|
||||
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name)
|
||||
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name) and course.status == "Approved"
|
||||
|
||||
@@ -6,29 +6,33 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
{% if restriction.restrict %}
|
||||
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
|
||||
<div class="empty-state">
|
||||
<div class="course-home-headings text-center mb-0" style="color: inherit;">{{ _("You haven't completed your profile.") }}</div>
|
||||
<p class="small text-center">{{ _("Complete your {0} to access the courses.").format(profile_link) }}</p>
|
||||
<div class="container">
|
||||
{% if restriction.restrict %}
|
||||
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
|
||||
<div class="empty-state">
|
||||
<div class="course-home-headings text-center mb-0" style="color: inherit;">
|
||||
{{ _("You haven't completed your profile.") }}
|
||||
</div>
|
||||
<p class="small text-center">
|
||||
{{ _("Complete your {0} to access the courses.").format(profile_link) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% include "lms/templates/search_course/search_course.html" %}
|
||||
|
||||
<div class="course-list">
|
||||
{% set courses = live_courses %}
|
||||
{% set title = _("Live Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "live-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
|
||||
{% set courses = upcoming_courses %}
|
||||
{% set title = _("Upcoming Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "upcoming-courses mt-10" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% include "lms/templates/search_course/search_course.html" %}
|
||||
|
||||
<div class="course-list">
|
||||
{% set courses = live_courses %}
|
||||
{% set title = _("Live Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "live-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
|
||||
{% set courses = upcoming_courses %}
|
||||
{% set title = _("Upcoming Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "upcoming-courses mt-10" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ _("Dashboard")}}
|
||||
{% block title %}
|
||||
{{ _("Dashboard")}}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
|
||||
{% set show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role() %}
|
||||
|
||||
<div class="common-page-style dashboard">
|
||||
<div class="container">
|
||||
|
||||
{% if show_creators_section %}
|
||||
<a class="btn btn-secondary btn-md pull-right" id="create-course-link" href="/courses/new-course">
|
||||
{{ _("Create a Course")}}
|
||||
<a class="btn btn-secondary btn-sm course-creation-link" id="create-course-link" href="/courses/new-course">
|
||||
{{ _("Create a Course") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav" id="courses-tab">
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
|
||||
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled">
|
||||
{{ _("Courses Enrolled") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if show_creators_section %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">
|
||||
{{ _("Courses Created") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_review_section %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-under-review">
|
||||
{{ _("Courses Under Review") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -38,16 +48,24 @@
|
||||
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
|
||||
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
|
||||
</div>
|
||||
|
||||
{% if show_creators_section %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% include "lms/templates/courses_created.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_review_section %}
|
||||
<div class="tab-pane fade" id="courses-under-review" role="tabpanel" aria-labelledby="courses-under-review">
|
||||
{% include "lms/templates/courses_under_review.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade" id="notifications" role="tabpanel" aria-labelledby="notifications">
|
||||
{% include "lms/templates/courses_created.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
9
lms/www/dashboard/index.py
Normal file
9
lms/www/dashboard/index.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation")
|
||||
context.show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role()
|
||||
context.show_review_section = has_course_moderator_role()
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="common-page-style">
|
||||
|
||||
<div class="container">
|
||||
{% if allow_posting and jobs | length %}
|
||||
{% if allow_posting %}
|
||||
<a class="button is-primary pull-right" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -48,18 +48,11 @@
|
||||
{% else %}
|
||||
|
||||
<div class="empty-state">
|
||||
<div>
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
</div>
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No open jobs") }}</div>
|
||||
<div class="course-meta">{{ _("There are no job openings at present.") }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if allow_posting %}
|
||||
<a class="button is-secondary dark-links m-auto" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -3,342 +3,395 @@
|
||||
<meta name="description" content="{{ member.full_name }}" />
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% set read_only = member.name != frappe.session.user %}
|
||||
{% set user = member.name %}
|
||||
{% set courses_created = get_authored_courses(member.name, True) %}
|
||||
{% set certificates = get_certificates(user) %}
|
||||
|
||||
<div class="common-page-style profile-page">
|
||||
{{ ProfileBanner(member) }}
|
||||
<div class="profile-page-body">
|
||||
<div class="container">
|
||||
{% set read_only = member.name != frappe.session.user %}
|
||||
{{ About(member) }}
|
||||
{{ EducationDetails(member) }}
|
||||
{{ WorkDetails(member) }}
|
||||
{{ Certification(member) }}
|
||||
{{ Contact(member) }}
|
||||
{{ Skills(member) }}
|
||||
{{ CareerPreference(member) }}
|
||||
{{ ProfileBanner(member) }}
|
||||
<div class="profile-page-body">
|
||||
<div class="container">
|
||||
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#profile">
|
||||
{{ _("Profile") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if courses_created | length %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">
|
||||
{{ _("Courses Created") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if certificates | length %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#certificates">
|
||||
{{ _("Certificates") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if has_course_moderator_role() %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#settings">
|
||||
{{ _("Settings") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="profile" role="tabpanel" aria-labelledby="profile">
|
||||
<div class="common-card-style column-card mt-5">
|
||||
{{ About(member) }}
|
||||
{{ EducationDetails(member) }}
|
||||
{{ WorkDetails(member) }}
|
||||
{{ ExternalCertification(member) }}
|
||||
{{ Contact(member) }}
|
||||
{{ Skills(member) }}
|
||||
{{ CareerPreference(member) }}
|
||||
{{ ProfileTabs(profile_tabs) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if courses_created | length %}
|
||||
{% set only_published = True %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% include "lms/templates/courses_created.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if certificates | length %}
|
||||
<div class="tab-pane fade" id="certificates" role="tabpanel" aria-labelledby="certificates">
|
||||
{% include "lms/templates/certificates_section.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade" id="settings" role="tabpanel" aria-labelledby="settings">
|
||||
{{ RoleSettings(member) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{{ CoursesCreated(member, read_only) }}
|
||||
{{ CoursesMentored(member, read_only) }}
|
||||
{{ ProfileTabs(profile_tabs) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<!-- Banner -->
|
||||
{% macro ProfileBanner(member) %}
|
||||
{% set cover_image = member.cover_image if member.cover_image else "/assets/lms/images/profile-banner.png" %}
|
||||
{% set enrollment = get_course_membership(frappe.session.user, member_type="Student") | length %}
|
||||
{% set enrollment = get_course_membership(member.name, member_type="Student") | length %}
|
||||
{% set enrollment_suffix = _("Courses") if enrollment > 1 else _("Course") %}
|
||||
|
||||
<div class="container">
|
||||
<div class="profile-banner" style="background-image: url({{ cover_image }})">
|
||||
<div class="profile-avatar">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-square") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<div class="profile-name-section">
|
||||
<div class="profile-name"> {{ member.full_name }} </div>
|
||||
|
||||
{% if get_authored_courses(member.name) | length %}
|
||||
<div class="creator-badge"> {{ _("Creator") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.looking_for_job %}
|
||||
<div class="creator-badge"> {{ _("Open Network") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user == member.email %}
|
||||
<a class="button is-secondary ml-auto mt-1" href="/edit-profile?name={{ member.email }}"> {{ _("Edit Profile") }} </a>
|
||||
{% endif %}
|
||||
<div class="profile-banner" style="background-image: url({{ cover_image | urlencode }})">
|
||||
<div class="profile-avatar">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-square") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-meta">
|
||||
{% if member.headline %}
|
||||
<div class="course-meta mr-3"> {{ member.headline }} </div>
|
||||
{% endif %}
|
||||
<div class="profile-info">
|
||||
<div class="profile-name-section">
|
||||
<div class="profile-name" data-name="{{ member.name }}"> {{ member.full_name }} </div>
|
||||
|
||||
{% if enrollment %}
|
||||
<div class="course-meta">
|
||||
<img src="/assets/lms/icons/book_plain.svg">
|
||||
{{ enrollment }} {{ enrollment_suffix }} {{ _("taken") }} </div>
|
||||
{% endif %}
|
||||
{% if courses_created | length %}
|
||||
<div class="creator-badge"> {{ _("Creator") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.looking_for_job %}
|
||||
<div class="creator-badge"> {{ _("Open Network") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user == member.email %}
|
||||
<div class="ml-auto mt-1">
|
||||
<a class="btn btn-secondary btn-sm" href="/dashboard"> {{ _("Visit Dashboard") }} </a>
|
||||
<a class="btn btn-secondary btn-sm ml-2" href="/edit-profile/{{ member.email }}/edit"> {{ _("Edit Profile") }} </a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="profile-meta">
|
||||
{% if member.headline %}
|
||||
<div class="course-meta mr-3"> {{ member.headline }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrollment %}
|
||||
<div class="course-meta">
|
||||
<img src="/assets/lms/icons/book_plain.svg">
|
||||
{{ enrollment }} {{ enrollment_suffix }} {{ _("taken") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesCreated(member, read_only) %}
|
||||
{% set authored_courses = get_authored_courses(member.name) %}
|
||||
{% if authored_courses | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Created") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in authored_courses %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Courses Mentored -->
|
||||
{% macro CoursesMentored(member, read_only) %}
|
||||
{% if member.get_mentored_courses() | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in member.get_mentored_courses() %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in member.get_mentored_courses() %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesEnrolled(member, read_only) %}
|
||||
{% set enrolled = get_enrolled_courses() %}
|
||||
|
||||
{% if enrolled.completed | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Completed") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in enrolled.completed %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrolled.in_progress | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses In Progress") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in enrolled.in_progress %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Profile Tabs Extension -->
|
||||
{% macro ProfileTabs(profile_tabs) %}
|
||||
<div>
|
||||
{% for tab in profile_tabs %}
|
||||
{% set slug = title.lower().replace(" ", "-") %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
|
||||
{{ tab.render() }}
|
||||
{% for tab in profile_tabs %}
|
||||
{% set slug = title.lower().replace(" ", "-") %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
|
||||
{{ tab.render() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro About(member) %}
|
||||
{% if member.bio %}
|
||||
<div class="">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("About") }} </div>
|
||||
<div class="description">{{ member.bio }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Role Settings -->
|
||||
{% macro RoleSettings(member) %}
|
||||
{% if has_course_moderator_role() %}
|
||||
<div class="">
|
||||
<div class="common-card-style column-card">
|
||||
<div class="course-home-headings"> {{ _("Role Settings") }} </div>
|
||||
<div>
|
||||
<label class="role">
|
||||
<input type="checkbox" id="instructor" data-role="Course Instructor"
|
||||
{% if has_course_instructor_role(member.name) %} checked {% endif %}>
|
||||
{{ _("Course Instructor") }}
|
||||
</label>
|
||||
<label class="role">
|
||||
<input type="checkbox" id="moderator" data-role="Course Moderator"
|
||||
{% if has_course_moderator_role(member.name) %} checked {% endif %}>
|
||||
{{ _("Course Moderator") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- About Section -->
|
||||
{% macro About(member) %}
|
||||
<div class="course-home-headings"> {{ _("About") }} </div>
|
||||
<div class="description">
|
||||
{% if member.bio %}
|
||||
{{ member.bio }}
|
||||
{% else %}
|
||||
{{ _("Hey, my name is ") }} {{ member.full_name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Work Preference -->
|
||||
{% macro WorkPreference(member) %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Work Preference") }} </div>
|
||||
<div class="course-home-headings mt-10"> {{ _("Work Preference") }} </div>
|
||||
<div> {{ member.attire }} </div>
|
||||
<div> {{ member.collaboration }} </div>
|
||||
<div> {{ member.role }} </div>
|
||||
<div> {{ member.location_preference }} </div>
|
||||
<div> {{ member.time }} </div>
|
||||
<div> {{ member.company_type }} </div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Career Preference -->
|
||||
{% macro CareerPreference(member) %}
|
||||
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Career Preference") }} </div>
|
||||
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Career Preference") }} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% if member.preferred_functions | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Functions:") }}</b>
|
||||
{% for function in member.preferred_functions %}
|
||||
<div class="description">{{ function.function }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_industries | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Industries:") }}</b>
|
||||
{% for industry in member.preferred_industries %}
|
||||
<div class="description">{{ industry.industry }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if member.preferred_functions | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Functions:") }}</b>
|
||||
{% for function in member.preferred_functions %}
|
||||
<div class="description">{{ function.function }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_location %}
|
||||
<div>
|
||||
<b> {{ _("Preferred Locations:") }} </b>
|
||||
<div class="description"> {{ member.preferred_location }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if member.preferred_industries | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Industries:") }}</b>
|
||||
{% for industry in member.preferred_industries %}
|
||||
<div class="description">{{ industry.industry }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_location %}
|
||||
<div>
|
||||
<b> {{ _("Preferred Locations:") }} </b>
|
||||
<div class="description"> {{ member.preferred_location }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.dream_companies %}
|
||||
<div>
|
||||
<b> {{ _("Dream Companies:") }} </b>
|
||||
<div class="description"> {{ member.dream_companies }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.dream_companies %}
|
||||
<div>
|
||||
<b> {{ _("Dream Companies:") }} </b>
|
||||
<div class="description"> {{ member.dream_companies }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Contact Section -->
|
||||
{% macro Contact(member) %}
|
||||
{% if member.linkedin or member.medium or member.github %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Contact") }} </div>
|
||||
{% if member.linkedin or member.medium or member.github %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Contact") }} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% if member.linkedin %}
|
||||
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
|
||||
<a class="button-links description" href="{{ member.linkedin }}">
|
||||
{% if member.linkedin %}
|
||||
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
|
||||
<a class="button-links description" href="{{ member.linkedin }}">
|
||||
<img src="/assets/lms/icons/linkedin.svg"> {{ linkedin.split("/")[-1] }}
|
||||
</a>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.medium %}
|
||||
<a class="button-links description" href="{{ member.medium}}">
|
||||
<img src="/assets/lms/icons/medium.svg"> {{ member.medium.split("/")[-1] }}
|
||||
<img src="/assets/lms/icons/medium.svg"> {{ member.medium.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.github %}
|
||||
<a class="button-links description" href="{{ member.github }}">
|
||||
<img src="/assets/lms/icons/github.svg"> {{ member.github.split("/")[-1] }}
|
||||
<img src="/assets/lms/icons/github.svg"> {{ member.github.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Skills -->
|
||||
{% macro Skills(member) %}
|
||||
{% if member.skill | length %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Skills")}} </div>
|
||||
{% if member.skill | length %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Skills")}} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% for skill in member.skill %}
|
||||
<div class="description"> {{ skill.skill_name }} </div>
|
||||
{% endfor %}
|
||||
{% for skill in member.skill %}
|
||||
<div class="description"> {{ skill.skill_name }} </div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Education Details -->
|
||||
{% macro EducationDetails(member) %}
|
||||
{% if member.education %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Education") }} </div>
|
||||
{% if member.education %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Education") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for edu in member.education %}
|
||||
<div class="profile-card-row">
|
||||
<div class="bold-title"> {{ edu.institution_name }} </div>
|
||||
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
|
||||
{% if not member.hide_private %}
|
||||
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
|
||||
{% if edu.grade %} <span></span> {{ edu.grade }} {% endif %}
|
||||
{% endif %}
|
||||
{% for edu in member.education %}
|
||||
<div class="column-card-row">
|
||||
<div class="bold-title"> {{ edu.institution_name }} </div>
|
||||
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
|
||||
{% if not member.hide_private %}
|
||||
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
|
||||
{% if edu.grade %} <span></span> {{ edu.grade }} {% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
{% if edu.start_date %}
|
||||
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} -
|
||||
{% endif %}
|
||||
{{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }}
|
||||
</div>
|
||||
<div class="description"> {{ edu.location }} </div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{% if edu.start_date %}
|
||||
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} -
|
||||
{% endif %}
|
||||
{{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }} </div>
|
||||
<div class="description"> {{ edu.location }} </div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Work Details -->
|
||||
{% macro WorkDetails(member) %}
|
||||
{% set work_details = member.work_experience + member.internship %}
|
||||
{% if work_details | length %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Work Experience") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for work in work_details %}
|
||||
<div class="">
|
||||
<div class="bold-title"> {{ work.title }} </div>
|
||||
<div class="profile-item"> {{ work.company }} </div>
|
||||
<div class="description"> {{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
|
||||
{% if work.to_date %} {{ frappe.utils.format_date(work.to_date, "MMM YYYY") }} {% else %} Present {% endif %} </div>
|
||||
<div class="description"> {{ work.location }} </div>
|
||||
{% if work.description %} <div class="profile-item"> {{ work.description }} </div> {% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% set work_details = member.work_experience + member.internship %}
|
||||
|
||||
{% macro Certification(member) %}
|
||||
{% if member.certification %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Certification") }} </div>
|
||||
{% if work_details | length %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Work Experience") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for cert in member.certification %}
|
||||
<div class="">
|
||||
<div class="bold-title"> {{ cert.certification_name }} </div>
|
||||
<div class="profile-item"> {{ cert.organization }} </div>
|
||||
<div class="description"> {{ frappe.utils.format_date(cert.issue_date, "MMM YYYY") }}
|
||||
{% if cert.expiration_date %} - {{ frappe.utils.format_date(cert.expiration_date, "MMM YYYY") }} {% endif %} </div>
|
||||
{% if cert.description %} <div class="profile-item"> {{ cert.description }} </div> {% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for work in work_details %}
|
||||
<div class="">
|
||||
<div class="bold-title"> {{ work.title }} </div>
|
||||
<div class="profile-item"> {{ work.company }} </div>
|
||||
<div class="description">
|
||||
{{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
|
||||
{% if work.to_date %} {{ frappe.utils.format_date(work.to_date, "MMM YYYY") }}
|
||||
{% else %} Present {% endif %}
|
||||
</div>
|
||||
|
||||
<div class="description"> {{ work.location }} </div>
|
||||
|
||||
{% if work.description %}
|
||||
<div class="profile-item">
|
||||
{{ work.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
if ("{{ member.name }}" == frappe.session.user) {
|
||||
setTimeout(() => {
|
||||
var link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile");
|
||||
link_array.length && $(link_array[0]).addClass("active");
|
||||
}, 0)
|
||||
}
|
||||
|
||||
if ($(".profile-column-one").children().length == 0) {
|
||||
$(".profile-column-one").hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
<!-- Certifications -->
|
||||
{% macro ExternalCertification(member) %}
|
||||
{% if member.certification %}
|
||||
<div class="course-home-headings mt-10"> {{ _("External Certification") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for cert in member.certification %}
|
||||
<div class="">
|
||||
|
||||
<div class="bold-title"> {{ cert.certification_name }} </div>
|
||||
<div class="profile-item"> {{ cert.organization }} </div>
|
||||
|
||||
<div class="description">
|
||||
{{ frappe.utils.format_date(cert.issue_date, "MMM YYYY") }}
|
||||
{% if cert.expiration_date %}
|
||||
- {{ frappe.utils.format_date(cert.expiration_date, "MMM YYYY") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if cert.description %}
|
||||
<div class="profile-item">
|
||||
{{ cert.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
42
lms/www/profiles/profile.js
Normal file
42
lms/www/profiles/profile.js
Normal file
@@ -0,0 +1,42 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
make_profile_active_in_navbar();
|
||||
|
||||
$(".role").change((e) => {
|
||||
save_role(e);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
const make_profile_active_in_navbar = () => {
|
||||
let member_name = $(".profile-name").data("name");
|
||||
if (member_name == frappe.session.user) {
|
||||
setTimeout(() => {
|
||||
let link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile");
|
||||
link_array.length && $(link_array[0]).addClass("active");
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const save_role = (e) => {
|
||||
let member_name = $(".profile-name").data("name");
|
||||
let role = $(e.currentTarget).children("input");
|
||||
frappe.call({
|
||||
method: "lms.overrides.user.save_role",
|
||||
args: {
|
||||
"user": member_name,
|
||||
"role": role.data("role"),
|
||||
"value": role.prop("checked") ? 1 : 0
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from lms.page_renderers import get_profile_url_prefix
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
@@ -12,13 +12,16 @@ def get_context(context):
|
||||
if username:
|
||||
frappe.local.flags.redirect_location = get_profile_url_prefix() + username
|
||||
raise frappe.Redirect
|
||||
|
||||
try:
|
||||
context.member = frappe.get_doc("User", {"username": username})
|
||||
except:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
context.profile_tabs = get_profile_tabs(context.member)
|
||||
|
||||
|
||||
def get_profile_tabs(user):
|
||||
"""Returns the enabled ProfileTab objects.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ def get_common_context(context):
|
||||
batch_name = None
|
||||
|
||||
course = frappe.db.get_value("LMS Course",
|
||||
frappe.form_dict["course"], ["name", "title", "video_link", "enable_certification"], as_dict=True)
|
||||
frappe.form_dict["course"], ["name", "title", "video_link", "enable_certification", "status"], as_dict=True)
|
||||
if not course:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user