diff --git a/README.md b/README.md index b46849e7..60b632a6 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,39 @@

- GitHub last commit + GitHub last commit - GitHub issues + GitHub issues - GitHub pull requests + GitHub pull requests - GitHub pull requests + GitHub pull requests

-
Frappe LMS -

Frappe LMS

-

Easy to Use, Open Source Learning Management System -
+
Visit the website ยป -
-
+
+
Explore the docs. Report Bug +

+
- + ## About The Project ![Frappe LMS](/lms/public/images/course-home.png) - -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

(back to top)

+ ## 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.

(back to top)

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

(back to top)

## License - - Distributed under [GNU AFFERO GENERAL PUBLIC LICENSE](license.txt) diff --git a/lms/hooks.py b/lms/hooks.py index 3e1e92d2..fc70e0e6 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -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": [] } diff --git a/lms/job/doctype/job_opportunity/job_opportunity.json b/lms/job/doctype/job_opportunity/job_opportunity.json index b0637b5f..7ac25e65 100644 --- a/lms/job/doctype/job_opportunity/job_opportunity.json +++ b/lms/job/doctype/job_opportunity/job_opportunity.json @@ -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" -} +} \ No newline at end of file diff --git a/lms/job/doctype/job_opportunity/job_opportunity.py b/lms/job/doctype/job_opportunity/job_opportunity.py index fc4ebc75..643638ba 100644 --- a/lms/job/doctype/job_opportunity/job_opportunity.py +++ b/lms/job/doctype/job_opportunity/job_opportunity.py @@ -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) diff --git a/lms/job/web_form/job_opportunity/job_opportunity.json b/lms/job/web_form/job_opportunity/job_opportunity.json index a8f1e611..f54dc557 100644 --- a/lms/job/web_form/job_opportunity/job_opportunity.json +++ b/lms/job/web_form/job_opportunity/job_opportunity.json @@ -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, diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index 6e61622b..d74a463d 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -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", diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index ac639e24..ab74884a 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -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"] diff --git a/lms/lms/doctype/exercise/test_exercise.py b/lms/lms/doctype/exercise/test_exercise.py index 139b6f7a..9deaa690 100644 --- a/lms/lms/doctype/exercise/test_exercise.py +++ b/lms/lms/doctype/exercise/test_exercise.py @@ -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`') diff --git a/lms/lms/doctype/lms_batch_membership/lms_batch_membership.json b/lms/lms/doctype/lms_batch_membership/lms_batch_membership.json index 7a8123f0..6574cf21 100644 --- a/lms/lms/doctype/lms_batch_membership/lms_batch_membership.json +++ b/lms/lms/doctype/lms_batch_membership/lms_batch_membership.json @@ -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", diff --git a/lms/lms/doctype/lms_course/lms_course.js b/lms/lms/doctype/lms_course/lms_course.js index 2fc82620..c08e1683 100644 --- a/lms/lms/doctype/lms_course/lms_course.js +++ b/lms/lms/doctype/lms_course/lms_course.js @@ -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, + } + }; + }); + } }); diff --git a/lms/lms/doctype/lms_course/lms_course.json b/lms/lms/doctype/lms_course/lms_course.json index 1a12c77a..f2e4ce4a 100644 --- a/lms/lms/doctype/lms_course/lms_course.json +++ b/lms/lms/doctype/lms_course/lms_course.json @@ -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", diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 76b8e097..4de53314 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -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"" + 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) diff --git a/lms/lms/doctype/lms_course/test_lms_course.py b/lms/lms/doctype/lms_course/test_lms_course.py index 542db568..ef133bda 100644 --- a/lms/lms/doctype/lms_course/test_lms_course.py +++ b/lms/lms/doctype/lms_course/test_lms_course.py @@ -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") diff --git a/lms/lms/utils.py b/lms/lms/utils.py index c8c8e28a..a36f8438 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -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 diff --git a/lms/lms/web_form/profile/profile.json b/lms/lms/web_form/profile/profile.json index 705b1e0d..f0e5ab81 100644 --- a/lms/lms/web_form/profile/profile.json +++ b/lms/lms/web_form/profile/profile.json @@ -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", diff --git a/lms/lms/web_template/courses_enrolled/courses_enrolled.html b/lms/lms/web_template/courses_enrolled/courses_enrolled.html index f254a3ef..298a2363 100644 --- a/lms/lms/web_template/courses_enrolled/courses_enrolled.html +++ b/lms/lms/web_template/courses_enrolled/courses_enrolled.html @@ -1,27 +1,31 @@ {% set enrolled = get_enrolled_courses().in_progress + get_enrolled_courses().completed %} + + {% if enrolled | length %}
- {% for course in enrolled %} - {{ widgets.CourseCard(course=course) }} - {% endfor %} + {% for course in enrolled %} + {{ widgets.CourseCard(course=course) }} + {% endfor %}
+ + {% else %} {% set site_name = frappe.db.get_single_value("System Settings", "app_name") %} -
-
-
-
{{ _("You haven't enrolled for any courses") }}
-
{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}
+
+
+
+
{{ _("You haven't enrolled for any courses") }}
+
{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}
+
+ {% set recommended_courses = get_popular_courses() %} +
+ {% for course in recommended_courses %} + {% if course %} + {% set course_details = frappe.get_doc("LMS Course", course.course) %} + {{ widgets.CourseCard(course=course_details) }} + {% endif %} + {% endfor %} +
- {% set recommended_courses = get_popular_courses() %} -
- {% for course in recommended_courses %} - {% if course %} - {% set course_details = frappe.get_doc("LMS Course", course.course) %} - {{ widgets.CourseCard(course=course_details) }} - {% endif %} - {% endfor %} -
-
{% endif %} diff --git a/lms/lms/widgets/CourseCard.html b/lms/lms/widgets/CourseCard.html index e601811a..c84ab45d 100644 --- a/lms/lms/widgets/CourseCard.html +++ b/lms/lms/widgets/CourseCard.html @@ -3,7 +3,7 @@
+ style="background-image: url( {{ course.image | urlencode }} );" {% endif %}>
{% for tag in get_tags(course.name) %}
{{ tag }}
@@ -90,10 +90,12 @@ {% 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 %} diff --git a/lms/lms/widgets/CourseOutline.html b/lms/lms/widgets/CourseOutline.html index b146c3fd..41c22908 100644 --- a/lms/lms/widgets/CourseOutline.html +++ b/lms/lms/widgets/CourseOutline.html @@ -1,18 +1,28 @@
+ {% set chapters = get_chapters(course.name) %} + {% if course.edit_mode and course.name %} {% endif %} - {% if course.name and (course.edit_mode or get_chapters(course.name) | length) %} + {% if course.name and (course.edit_mode or chapters | length) %}
{{ _("Course Content") }}
{% endif %} - {% if get_chapters(course.name) | length %} + {% if course.edit_mode and course.name and not chapters | length %} +
+
+
+ +
+ {% endif %} - {% for chapter in get_chapters(course.name) %} + {% if chapters | length %} + + {% for chapter in chapters %}