diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 87caa7a7..a76bdf8b 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -5,39 +5,50 @@ describe("Course Creation", () => { // Create a course cy.get("a.btn").contains("Create a Course").click(); cy.wait(1000); - cy.url().should("include", "/courses/new-course"); - cy.button("Add Tag").click(); - cy.get(".course-card-pills").type("Test"); + cy.url().should("include", "/courses/new-course/edit"); cy.get("#title").type("Test Course"); cy.get("#intro").type("Test Course Short Introduction"); - cy.get("#video-link").type("-LPmw2Znl2c"); - cy.get("#published").check(); cy.get("#description").type("Test Course Description"); + cy.get("#video-link").type("-LPmw2Znl2c"); + cy.get("#tags-input").type("Test"); + cy.get("#published").check(); cy.wait(1000); - cy.button("Save Course Details").click(); + cy.button("Save").click(); // Add Chapter - cy.wait(3000); - cy.button("New Chapter").click(); - cy.get(".new-chapter .chapter-title-main").type("Test Chapter"); - cy.get(".new-chapter .chapter-description").type( - "Test Chapter Description" - ); - cy.get(".new-chapter .btn-save-chapter").click(); + cy.wait(1000); + cy.link("Course Outline").click(); + + cy.wait(1000); + cy.get(".edit-header .btn-add-chapter").click(); + cy.get("#chapter-title").type("Test Chapter"); + cy.get("#chapter-description").type("Test Chapter Description"); + cy.button("Save").click(); // Add Lesson - cy.wait(3000); - cy.get(".chapter-parent .btn-lesson").click(); - - cy.wait(3000); - cy.get("#title").type("Test Lesson"); - cy.get("#youtube").type("GoDtyItReto"); - cy.get("#body").type("Test Lesson Content"); cy.wait(1000); - cy.get(".btn-lesson").click(); + cy.link("Add Lesson").click(); + cy.wait(1000); + cy.get("#lesson-title").type("Test Lesson"); + + // Content + cy.get(".ce-block").click().type("{enter}"); + cy.get(".ce-toolbar__plus").click(); + cy.get('[data-item-name="youtube"]').click(); + cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto"); + cy.button("Insert").click(); + cy.wait(1000); + + cy.get(".ce-block:last").click().type("{enter}"); + cy.get(".ce-block:last") + .click() + .type( + "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." + ); + cy.button("Save").click(); // View Course - cy.wait(3000); + cy.wait(1000); cy.visit("/courses"); cy.get(".course-card-title:first").contains("Test Course"); cy.get(".course-card:first").click(); @@ -59,7 +70,7 @@ describe("Course Creation", () => { cy.get(".lesson-info:first").click(); // View Lesson - cy.wait(3000); + cy.wait(1000); cy.url().should("include", "learn/1.1"); cy.get("#title").contains("Test Lesson"); cy.get(".lesson-video iframe").should( @@ -67,7 +78,9 @@ describe("Course Creation", () => { "src", "https://www.youtube.com/embed/GoDtyItReto" ); - cy.get(".lesson-content-card").contains("Test Lesson Content"); + cy.get(".lesson-content-card").contains( + "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." + ); // Add Discussion cy.get(".reply").click(); @@ -79,7 +92,7 @@ describe("Course Creation", () => { cy.get(".submit-discussion").click(); // View Discussion - cy.wait(3000); + cy.wait(1000); cy.get(".discussion-topic-title:first").contains("Question Title"); cy.get(".sidebar-parent:first").click(); cy.get(".reply-text").contains( diff --git a/lms/hooks.py b/lms/hooks.py index c9078ef3..2a6e7f15 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -138,12 +138,18 @@ fixtures = ["Custom Field", "Function", "Industry"] website_route_rules = [ {"from_route": "/sketches/", "to_route": "sketches/sketch"}, {"from_route": "/courses/", "to_route": "courses/course"}, + {"from_route": "/courses//edit", "to_route": "courses/create"}, + {"from_route": "/courses//outline", "to_route": "courses/outline"}, {"from_route": "/courses//", "to_route": "courses/certificate"}, {"from_route": "/courses//learn", "to_route": "batch/learn"}, { "from_route": "/courses//learn/.", "to_route": "batch/learn", }, + { + "from_route": "/courses//learn/./edit", + "to_route": "batch/edit", + }, {"from_route": "/quizzes", "to_route": "batch/quiz_list"}, {"from_route": "/quizzes/", "to_route": "batch/quiz"}, {"from_route": "/classes/", "to_route": "classes/class"}, diff --git a/lms/lms/doctype/course_chapter/course_chapter.py b/lms/lms/doctype/course_chapter/course_chapter.py index 63febc33..d980e26a 100644 --- a/lms/lms/doctype/course_chapter/course_chapter.py +++ b/lms/lms/doctype/course_chapter/course_chapter.py @@ -3,7 +3,9 @@ # import frappe from frappe.model.document import Document +from frappe.utils.telemetry import capture class CourseChapter(Document): - pass + def after_insert(self): + capture("chapter_created", "lms") diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index 1379088c..96e593d0 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -4,9 +4,8 @@ import frappe from frappe import _ from frappe.model.document import Document - -from lms.lms.utils import get_course_progress, get_lesson_url - +from frappe.utils.telemetry import capture +from lms.lms.utils import get_course_progress from ...md import find_macros @@ -24,6 +23,9 @@ class CourseLesson(Document): for section in dynamic_documents: self.update_lesson_name_in_document(section) + def after_insert(self): + capture("lesson_created", "lms") + def update_lesson_name_in_document(self, section): doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"} macros = find_macros(self.body) diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.py b/lms/lms/doctype/lms_certificate/lms_certificate.py index 5e4f8688..232770e6 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.py +++ b/lms/lms/doctype/lms_certificate/lms_certificate.py @@ -9,7 +9,10 @@ from lms.lms.utils import is_certified class LMSCertificate(Document): - def before_insert(self): + def validate(self): + self.validate_duplicate_certificate() + + def validate_duplicate_certificate(self): certificates = frappe.get_all( "LMS Certificate", {"member": self.member, "course": self.course} ) @@ -20,6 +23,18 @@ class LMSCertificate(Document): _("{0} is already certified for the course {1}").format(full_name, course_name) ) + def after_insert(self): + share = frappe.get_doc( + { + "doctype": "DocShare", + "read": 1, + "share_doctype": "LMS Certificate", + "share_name": self.name, + "user": self.member, + } + ) + share.save(ignore_permissions=True) + @frappe.whitelist() def create_certificate(course): diff --git a/lms/lms/doctype/lms_class/lms_class.json b/lms/lms/doctype/lms_class/lms_class.json index ffcf5c31..fd0edc2e 100644 --- a/lms/lms/doctype/lms_class/lms_class.json +++ b/lms/lms/doctype/lms_class/lms_class.json @@ -14,8 +14,10 @@ "paid_class", "column_break_4", "seat_count", - "description", + "start_time", + "end_time", "section_break_6", + "description", "students", "courses", "custom_component" @@ -84,11 +86,21 @@ "fieldname": "seat_count", "fieldtype": "Int", "label": "Seat Count" + }, + { + "fieldname": "start_time", + "fieldtype": "Time", + "label": "Start Time" + }, + { + "fieldname": "end_time", + "fieldtype": "Time", + "label": "End Time" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-03 17:52:28.226169", + "modified": "2023-05-03 23:07:06.725720", "modified_by": "Administrator", "module": "LMS", "name": "LMS Class", diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_class/lms_class.py index fd8b6a9f..203ab4a4 100644 --- a/lms/lms/doctype/lms_class/lms_class.py +++ b/lms/lms/doctype/lms_class/lms_class.py @@ -12,6 +12,8 @@ import json class LMSClass(Document): def validate(self): + if self.seat_count: + self.validate_seats_left() self.validate_duplicate_students() self.validate_membership() @@ -36,6 +38,10 @@ class LMSClass(Document): if not frappe.db.exists(filters): frappe.get_doc(filters).save() + def validate_seats_left(self): + if cint(self.seat_count) < len(self.students): + frappe.throw(_("There are no seats available in this class.")) + @frappe.whitelist() def add_student(email, class_name): @@ -147,7 +153,16 @@ def authenticate(): @frappe.whitelist() -def create_class(title, start_date, end_date, description=None, name=None): +def create_class( + title, + start_date, + end_date, + description=None, + seat_count=0, + start_time=None, + end_time=None, + name=None, +): if name: class_details = frappe.get_doc("LMS Class", name) else: @@ -159,6 +174,9 @@ def create_class(title, start_date, end_date, description=None, name=None): "start_date": start_date, "end_date": end_date, "description": description, + "seat_count": seat_count, + "start_time": start_time, + "end_time": end_time, } ) class_details.save() diff --git a/lms/lms/doctype/lms_course/lms_course.json b/lms/lms/doctype/lms_course/lms_course.json index beac3d4e..11e45195 100644 --- a/lms/lms/doctype/lms_course/lms_course.json +++ b/lms/lms/doctype/lms_course/lms_course.json @@ -57,7 +57,7 @@ }, { "fieldname": "description", - "fieldtype": "Markdown Editor", + "fieldtype": "Text Editor", "label": "Description", "reqd": 1 }, @@ -260,7 +260,7 @@ } ], "make_attachments_public": 1, - "modified": "2023-05-02 09:45:54.826328", + "modified": "2023-05-11 17:08:19.763405", "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 21bf0093..c71568a0 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -2,13 +2,12 @@ # For license information, please see license.txt import json - +import random import frappe from frappe.model.document import Document from frappe.utils import cint - +from frappe.utils.telemetry import capture from lms.lms.utils import get_chapters - from ...utils import generate_slug, validate_image @@ -43,6 +42,9 @@ class LMSCourse(Document): if not self.upcoming and self.has_value_changed("upcoming"): self.send_email_to_interested_users() + def after_insert(self): + capture("course_created", "lms") + def send_email_to_interested_users(self): interested_users = frappe.get_all( "LMS Course Interest", {"course": self.name}, ["name", "user"] @@ -72,7 +74,10 @@ class LMSCourse(Document): def autoname(self): if not self.name: - self.name = generate_slug(self.title, "LMS Course") + title = self.title + if self.title == "New Course": + title = self.title + str(random.randint(0, 99)) + self.name = generate_slug(title, "LMS Course") def __repr__(self): return f"" diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index f9b4326a..fa49b0a9 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -192,3 +192,10 @@ def check_input_answers(question, answer): return 1 return 0 + + +@frappe.whitelist() +def get_user_quizzes(): + return frappe.get_all( + "LMS Quiz", filters={"owner": frappe.session.user}, fields=["name", "title"] + ) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 3a22fbdf..e8c2554d 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -93,18 +93,24 @@ def get_chapters(course): return chapters -def get_lessons(course, chapter=None): +def get_lessons(course, chapter=None, get_details=True): """If chapter is passed, returns lessons of only that chapter. Else returns lessons of all chapters of the course""" lessons = [] + lesson_count = 0 if chapter: - return get_lesson_details(chapter) + if get_details: + return get_lesson_details(chapter) + else: + return frappe.db.count("Lesson Reference", {"parent": chapter.name}) for chapter in get_chapters(course): - lesson = get_lesson_details(chapter) - lessons += lesson + if get_details: + lessons += get_lesson_details(chapter) + else: + lesson_count += frappe.db.count("Lesson Reference", {"parent": chapter.name}) - return lessons + return lessons if get_details else lesson_count def get_lesson_details(chapter): @@ -135,8 +141,8 @@ def get_lesson_details(chapter): macros = find_macros(lesson_details.body) for macro in macros: - if macro[0] == "YouTubeVideo": - lesson_details.icon = "icon-video" + if macro[0] == "YouTubeVideo" or macro[0] == "Video": + lesson_details.icon = "icon-youtube" elif macro[0] == "Quiz": lesson_details.icon = "icon-quiz" lessons.append(lesson_details) @@ -495,12 +501,17 @@ def can_create_courses(member=None): if not member: member = frappe.session.user + if frappe.session.user == "Guest": + return False + + if has_course_instructor_role(member) or has_course_moderator_role(member): + return True + portal_course_creation = frappe.db.get_single_value( "LMS Settings", "portal_course_creation" ) - return frappe.session.user != "Guest" and ( - portal_course_creation == "Anyone" or has_course_instructor_role(member) - ) + + return portal_course_creation == "Anyone" def has_course_moderator_role(member=None): @@ -611,15 +622,17 @@ def get_filtered_membership(course, memberships): def show_start_learing_cta(course, membership): - return ( - not course.disable_self_learning - and not membership - and not course.upcoming - and not check_profile_restriction() - and not is_instructor(course.name) - and course.status == "Approved" - and has_lessons(course) - ) + + if course.disable_self_learning or course.upcoming: + return False + if is_instructor(course.name): + return False + if course.status != "Approved": + return False + if not has_lessons(course): + return False + if not membership: + return True def has_lessons(course): diff --git a/lms/lms/widgets/CourseOutline.html b/lms/lms/widgets/CourseOutline.html index 5a765218..1ef4059b 100644 --- a/lms/lms/widgets/CourseOutline.html +++ b/lms/lms/widgets/CourseOutline.html @@ -1,81 +1,66 @@ {% set chapters = get_chapters(course.name) %} +{% set is_instructor = is_instructor(course.name) %} -{% if course.edit_mode or chapters | length %} +{% if chapters | length %}
- - {% if course.edit_mode and course.name %} - - {% endif %} - - {% if course.name and (course.edit_mode or chapters | length) %} -
+ {% if not lesson_page %} +
{{ _("Course Content") }}
- {% endif %} - {% if course.edit_mode and course.name and not chapters | length %} -
-
-
- -
+ {% endif %} {% if chapters | length %} -
+
{% for chapter in chapters %} -
-