test: utils
This commit is contained in:
@@ -3,35 +3,6 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint, nowdate
|
||||
|
||||
from lms.lms.doctype.lms_certificate.lms_certificate import create_certificate
|
||||
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
||||
|
||||
|
||||
class TestLMSCertificate(unittest.TestCase):
|
||||
def test_certificate_creation(self):
|
||||
course = new_course(
|
||||
"Test Certificate",
|
||||
{
|
||||
"enable_certification": 1,
|
||||
},
|
||||
)
|
||||
create_enrollment(course.name)
|
||||
certificate = create_certificate(course.name)
|
||||
|
||||
self.assertEqual(certificate.member, "Administrator")
|
||||
self.assertEqual(certificate.course, course.name)
|
||||
self.assertEqual(certificate.issue_date, nowdate())
|
||||
|
||||
frappe.db.delete("LMS Certificate", certificate.name)
|
||||
frappe.db.delete("LMS Course", course.name)
|
||||
|
||||
|
||||
def create_enrollment(course):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course
|
||||
enrollment.member = frappe.session.user
|
||||
enrollment.progress = cint(100)
|
||||
enrollment.save()
|
||||
pass
|
||||
|
||||
@@ -19,9 +19,6 @@ class TestLMSCourse(unittest.TestCase):
|
||||
|
||||
if frappe.db.exists("LMS Course", "test-course"):
|
||||
frappe.db.delete("Batch Course", {"course": "test-course"})
|
||||
frappe.db.delete("Exercise Submission", {"course": "test-course"})
|
||||
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
|
||||
frappe.db.delete("LMS Exercise", {"course": "test-course"})
|
||||
frappe.db.delete("LMS Enrollment", {"course": "test-course"})
|
||||
frappe.db.delete("Course Lesson", {"course": "test-course"})
|
||||
frappe.db.delete("Course Chapter", {"course": "test-course"})
|
||||
@@ -67,6 +64,7 @@ def new_course(title, additional_filters=None):
|
||||
"video_link": "https://youtu.be/pEbIhUySqbk",
|
||||
"image": "/assets/lms/images/course-home.png",
|
||||
"instructors": [{"instructor": user}],
|
||||
"published": 1,
|
||||
}
|
||||
|
||||
if additional_filters:
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"section_break_8",
|
||||
"cohort",
|
||||
"subgroup",
|
||||
"batch_old",
|
||||
"column_break_12",
|
||||
"member_type",
|
||||
"role"
|
||||
@@ -149,7 +148,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-08 21:27:30.733482",
|
||||
"modified": "2025-12-09 21:27:30.733482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
|
||||
@@ -9,28 +9,43 @@ from frappe.utils import ceil
|
||||
|
||||
class LMSEnrollment(Document):
|
||||
def validate(self):
|
||||
self.validate_membership_in_same_batch()
|
||||
self.validate_course_enrollment_eligibility()
|
||||
|
||||
def on_update(self):
|
||||
update_program_progress(self.member)
|
||||
|
||||
def validate_membership_in_same_batch(self):
|
||||
filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]}
|
||||
if self.batch_old:
|
||||
filters["batch_old"] = self.batch_old
|
||||
previous_membership = frappe.db.get_value(
|
||||
"LMS Enrollment", filters, fieldname=["member_type", "member"], as_dict=1
|
||||
def validate_course_enrollment_eligibility(self):
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
self.course,
|
||||
["published", "disable_self_learning", "paid_course", "paid_certificate"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if previous_membership:
|
||||
member_name = frappe.db.get_value("User", self.member, "full_name")
|
||||
course_title = frappe.db.get_value("LMS Course", self.course, "title")
|
||||
if course_details.disable_self_learning:
|
||||
frappe.throw(
|
||||
_("{0} is already a {1} of the course {2}").format(
|
||||
member_name, previous_membership.member_type, course_title
|
||||
_(
|
||||
"You cannot enroll in this course as self-learning is disabled. Please contact the Administrator."
|
||||
)
|
||||
)
|
||||
|
||||
if not course_details.published:
|
||||
frappe.throw(_("You cannot enroll in an unpublished course."))
|
||||
|
||||
if course_details.paid_course:
|
||||
payment = frappe.db.exists(
|
||||
"LMS Payment",
|
||||
{
|
||||
"reference_doctype": "LMS Course",
|
||||
"reference_docname": course,
|
||||
"member": member,
|
||||
"payment_receipt": True,
|
||||
},
|
||||
)
|
||||
|
||||
if not payment:
|
||||
frappe.throw(_("You need to complete the payment for this course before enrolling."))
|
||||
|
||||
|
||||
def update_program_progress(member):
|
||||
programs = frappe.get_all("LMS Program Member", {"member": member}, ["parent", "name"])
|
||||
@@ -49,8 +64,6 @@ def update_program_progress(member):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
||||
validate_course_enrollment_eligibility(course, member)
|
||||
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.update(
|
||||
{
|
||||
@@ -66,42 +79,6 @@ def create_membership(course, batch=None, member=None, member_type="Student", ro
|
||||
return enrollment
|
||||
|
||||
|
||||
def validate_course_enrollment_eligibility(course, member):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course,
|
||||
["published", "disable_self_learning", "paid_course", "paid_certificate"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if course_details.disable_self_learning:
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot enroll in this course as self-learning is disabled. Please contact the Administrator."
|
||||
)
|
||||
)
|
||||
|
||||
if not course_details.published:
|
||||
frappe.throw(_("You cannot enroll in an unpublished course."))
|
||||
|
||||
if course_details.paid_course:
|
||||
payment = frappe.db.exists(
|
||||
"LMS Payment",
|
||||
{
|
||||
"reference_doctype": "LMS Course",
|
||||
"reference_docname": course,
|
||||
"member": member,
|
||||
"payment_receipt": True,
|
||||
},
|
||||
)
|
||||
|
||||
if not payment:
|
||||
frappe.throw(_("You need to complete the payment for this course before enrolling."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_current_membership(batch, course, member):
|
||||
all_memberships = frappe.get_all("LMS Enrollment", {"member": member, "course": course})
|
||||
|
||||
@@ -5,19 +5,6 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
|
||||
|
||||
|
||||
class TestLMSEnrollment(unittest.TestCase):
|
||||
def test_membership(self):
|
||||
course = new_course("Test Enrollment")
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course.name
|
||||
enrollment.member = frappe.session.user
|
||||
|
||||
enrollment.save()
|
||||
|
||||
self.assertEqual(enrollment.course, course.name)
|
||||
self.assertEqual(enrollment.member, "Administrator")
|
||||
frappe.db.delete("LMS Enrollment", enrollment.name)
|
||||
frappe.db.delete("LMS Course", course.name)
|
||||
pass
|
||||
|
||||
@@ -6,6 +6,7 @@ from .utils import (
|
||||
get_average_rating,
|
||||
get_chapters,
|
||||
get_instructors,
|
||||
get_lesson_index,
|
||||
get_lessons,
|
||||
get_membership,
|
||||
get_reviews,
|
||||
@@ -16,13 +17,14 @@ from .utils import (
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.student1 = self.create_user("student1@example.com", "Ashley", "Smith", "LMS Student")
|
||||
self.student2 = self.create_user("student2@example.com", "John", "Doe", "LMS Student")
|
||||
self.admin = self.create_user("frappe@example.com", "Frappe", "Admin", "Moderator")
|
||||
|
||||
self.create_a_course()
|
||||
self.add_chapters()
|
||||
self.add_lessons()
|
||||
|
||||
self.student1 = self.create_student("student1@example.com", "Ashley", "Smith")
|
||||
self.student2 = self.create_student("student2@example.com", "John", "Doe")
|
||||
|
||||
self.add_enrollment(self.course.name, self.student1.email)
|
||||
self.add_enrollment(self.course.name, self.student2.email)
|
||||
|
||||
@@ -73,15 +75,18 @@ class TestUtils(unittest.TestCase):
|
||||
chapterDoc.append("lessons", {"lesson": lesson.name})
|
||||
chapterDoc.save()
|
||||
|
||||
def create_student(self, email, first_name, last_name):
|
||||
student = frappe.new_doc("User")
|
||||
student.email = email
|
||||
student.first_name = first_name
|
||||
student.last_name = last_name
|
||||
student.user_type = "Website User"
|
||||
student.append("roles", {"role": "LMS Student"})
|
||||
student.save()
|
||||
return student
|
||||
def create_user(self, email, first_name, last_name, role):
|
||||
if not frappe.db.exists("User", email):
|
||||
student = frappe.new_doc("User")
|
||||
student.email = email
|
||||
student.first_name = first_name
|
||||
student.last_name = last_name
|
||||
student.user_type = "Website User"
|
||||
student.append("roles", {"role": role})
|
||||
student.save()
|
||||
return student
|
||||
else:
|
||||
return frappe.get_doc("User", email)
|
||||
|
||||
def test_simple_slugs(self):
|
||||
self.assertEqual(slugify("hello-world"), "hello-world")
|
||||
@@ -121,7 +126,7 @@ class TestUtils(unittest.TestCase):
|
||||
self.assertEqual(len(chapter_lessons), 2)
|
||||
for j, lesson in enumerate(chapter_lessons, start=1):
|
||||
self.assertEqual(lesson.title, f"Lesson {j} of {chapter.chapter}")
|
||||
self.assertEqual(lesson.number, f"{chapter.idx}.{j}")
|
||||
self.assertEqual(lesson.number, f"{chapter.idx}-{j}")
|
||||
|
||||
def test_get_tags(self):
|
||||
tags = get_tags(self.course.name)
|
||||
@@ -158,6 +163,11 @@ class TestUtils(unittest.TestCase):
|
||||
self.assertEqual(review.member, self.student2.email)
|
||||
self.assertEqual(review.review, "Excellent course")
|
||||
|
||||
def test_get_lesson_index(self):
|
||||
lessons = get_lessons(self.course.name)
|
||||
for lesson in lessons:
|
||||
self.assertEqual(get_lesson_index(lesson.name), lesson.number)
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.db.exists("LMS Course", self.course.name):
|
||||
frappe.db.delete("LMS Enrollment", {"course": self.course.name})
|
||||
@@ -168,3 +178,4 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
frappe.delete_doc("User", "student1@example.com")
|
||||
frappe.delete_doc("User", "student2@example.com")
|
||||
frappe.delete_doc("User", "frappe@example.com")
|
||||
|
||||
@@ -154,7 +154,7 @@ def get_lesson_details(chapter, progress=False):
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
lesson_details.number = f"{chapter.idx}.{row.idx}"
|
||||
lesson_details.number = f"{chapter.idx}-{row.idx}"
|
||||
lesson_details.icon = get_lesson_icon(lesson_details.body, lesson_details.content)
|
||||
|
||||
if progress:
|
||||
@@ -259,22 +259,6 @@ 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"]
|
||||
for key in keys:
|
||||
rating_count[key] = 0
|
||||
|
||||
reviews = get_reviews(course)
|
||||
for review in reviews:
|
||||
rating_count[cstr(review.rating)] += 1
|
||||
|
||||
for key in keys:
|
||||
rating_percent[key] = rating_count[key] / len(reviews) * 100
|
||||
|
||||
return rating_percent
|
||||
|
||||
|
||||
def get_lesson_index(lesson_name):
|
||||
"""Returns the {chapter_index}.{lesson_index} for the lesson."""
|
||||
lesson = frappe.db.get_value("Lesson Reference", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
{% if not course.upcoming %}
|
||||
<div class="reviews-parent">
|
||||
{% set reviews = get_reviews(course.name) %}
|
||||
<div class="page-title mb-5"> {{ _("Reviews") }} </div>
|
||||
|
||||
|
||||
{% 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="avg-rating-stars">
|
||||
<div class="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-lg {% 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"> {{ reviews | length }} {{ _("ratings") }} </div>
|
||||
<!--
|
||||
|
||||
-->
|
||||
|
||||
<div class="mt-5">
|
||||
{% include "lms/templates/reviews_cta.html" %}
|
||||
</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="/lms/users/{{ 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 }}">
|
||||
{{ 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 class="mt-2">
|
||||
{% include "lms/templates/reviews_cta.html" %}
|
||||
</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="modal-title">{{ _("Write a Review") }}</div>
|
||||
</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" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="error-field muted-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm mr-2" data-dismiss="modal" aria-label="Close">
|
||||
{{ _("Discard") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-sm" data-course="{{ course.name | urlencode}}" id="submit-review">
|
||||
{{ _("Submit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user