chore: resolved conflicts
This commit is contained in:
86
lms/command_palette.py
Normal file
86
lms/command_palette.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def search_sqlite(query: str):
|
||||
from lms.sqlite import LearningSearch, LearningSearchIndexMissingError
|
||||
|
||||
search = LearningSearch()
|
||||
|
||||
try:
|
||||
result = search.search(query)
|
||||
except LearningSearchIndexMissingError:
|
||||
return []
|
||||
|
||||
return prepare_search_results(result)
|
||||
|
||||
|
||||
def prepare_search_results(result):
|
||||
roles = frappe.get_roles()
|
||||
groups = {}
|
||||
|
||||
for r in result["results"]:
|
||||
doctype = r["doctype"]
|
||||
if doctype == "LMS Course" and can_access_course(r, roles):
|
||||
r["author_info"] = get_instructor_info(doctype, r)
|
||||
groups.setdefault("Courses", []).append(r)
|
||||
elif doctype == "LMS Batch" and can_access_batch(r, roles):
|
||||
r["author_info"] = get_instructor_info(doctype, r)
|
||||
groups.setdefault("Batches", []).append(r)
|
||||
elif doctype == "Job Opportunity" and can_access_job(r, roles):
|
||||
r["author_info"] = get_instructor_info(doctype, r)
|
||||
groups.setdefault("Job Opportunities", []).append(r)
|
||||
|
||||
out = []
|
||||
for key in groups:
|
||||
out.append({"title": key, "items": groups[key]})
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def can_access_course(course, roles):
|
||||
if can_create_course(roles):
|
||||
return True
|
||||
elif course.get("published"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_access_batch(batch, roles):
|
||||
if can_create_batch(roles):
|
||||
return True
|
||||
elif batch.get("published") and batch.get("start_date") >= nowdate():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_access_job(job, roles):
|
||||
if "Moderator" in roles:
|
||||
return True
|
||||
return job.get("status") == "Open"
|
||||
|
||||
|
||||
def can_create_course(roles):
|
||||
return "Course Creator" in roles or "Moderator" in roles
|
||||
|
||||
|
||||
def can_create_batch(roles):
|
||||
return "Batch Evaluator" in roles or "Moderator" in roles
|
||||
|
||||
|
||||
def get_instructor_info(doctype, record):
|
||||
instructors = frappe.get_all(
|
||||
"Course Instructor", filters={"parenttype": doctype, "parent": record.get("name")}, pluck="instructor"
|
||||
)
|
||||
|
||||
instructor = record.get("author")
|
||||
if len(instructors):
|
||||
instructor = instructors[0]
|
||||
|
||||
return frappe.db.get_value(
|
||||
"User",
|
||||
instructor,
|
||||
["full_name", "email", "user_image", "username"],
|
||||
as_dict=True,
|
||||
)
|
||||
19
lms/desktop_icon/frappe_lms.json
Normal file
19
lms/desktop_icon/frappe_lms.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"app": "lms",
|
||||
"creation": "2025-12-15 14:31:50.704854",
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon_type": "App",
|
||||
"idx": 0,
|
||||
"label": "Frappe LMS",
|
||||
"link": "/lms",
|
||||
"link_type": "External",
|
||||
"logo_url": "/assets/lms/frontend/learning.svg",
|
||||
"modified": "2025-12-15 14:31:50.704854",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Frappe LMS",
|
||||
"owner": "Administrator",
|
||||
"roles": [],
|
||||
"standard": 1
|
||||
}
|
||||
@@ -64,6 +64,9 @@ after_install = "lms.install.after_install"
|
||||
after_sync = "lms.install.after_sync"
|
||||
before_uninstall = "lms.install.before_uninstall"
|
||||
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
||||
after_migrate = [
|
||||
"lms.sqlite.build_index_in_background",
|
||||
]
|
||||
|
||||
# Desk Notifications
|
||||
# ------------------
|
||||
@@ -115,6 +118,9 @@ doc_events = {
|
||||
# Scheduled Tasks
|
||||
# ---------------
|
||||
scheduler_events = {
|
||||
"all": [
|
||||
"lms.sqlite.build_index_in_background",
|
||||
],
|
||||
"hourly": [
|
||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||
"lms.lms.api.update_course_statistics",
|
||||
@@ -191,7 +197,6 @@ update_website_context = [
|
||||
|
||||
jinja = {
|
||||
"methods": [
|
||||
"lms.lms.utils.get_signup_optin_checks",
|
||||
"lms.lms.utils.get_tags",
|
||||
"lms.lms.utils.get_lesson_count",
|
||||
"lms.lms.utils.get_instructors",
|
||||
@@ -254,3 +259,5 @@ add_to_apps_screen = [
|
||||
"has_permission": "lms.lms.api.check_app_permission",
|
||||
}
|
||||
]
|
||||
|
||||
sqlite_search = ["lms.sqlite.LearningSearch"]
|
||||
|
||||
443
lms/lms/api.py
443
lms/lms/api.py
@@ -6,6 +6,7 @@ import re
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from datetime import timedelta
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
import frappe
|
||||
@@ -23,47 +24,13 @@ from frappe.utils import (
|
||||
flt,
|
||||
format_date,
|
||||
get_datetime,
|
||||
getdate,
|
||||
now,
|
||||
)
|
||||
from frappe.utils.response import Response
|
||||
|
||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def autosave_section(section, code):
|
||||
"""Saves the code edited in one of the sections."""
|
||||
doc = frappe.get_doc(doctype="Code Revision", section=section, code=code, author=frappe.session.user)
|
||||
doc.insert()
|
||||
return {"name": doc.name}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit_solution(exercise, code):
|
||||
"""Submits a solution.
|
||||
|
||||
@exerecise: name of the exercise to submit
|
||||
@code: solution to the exercise
|
||||
"""
|
||||
ex = frappe.get_doc("LMS Exercise", exercise)
|
||||
if not ex:
|
||||
return
|
||||
doc = ex.submit(code)
|
||||
return {"name": doc.name, "creation": doc.creation}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_current_lesson(course_name, lesson_name):
|
||||
"""Saves the current lesson for a student/mentor."""
|
||||
name = frappe.get_value(
|
||||
doctype="LMS Enrollment",
|
||||
filters={"course": course_name, "member": frappe.session.user},
|
||||
fieldname="name",
|
||||
)
|
||||
if not name:
|
||||
return
|
||||
frappe.db.set_value("LMS Enrollment", name, "current_lesson", lesson_name)
|
||||
from lms.lms.utils import get_average_rating, get_batch_details, get_course_details, get_lesson_count
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@@ -102,9 +69,32 @@ def get_translations():
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_billing_access(billing_type, name):
|
||||
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
|
||||
access, message = verify_billing_access(doctype, name, billing_type)
|
||||
|
||||
address = frappe.db.get_value(
|
||||
"Address",
|
||||
{"email_id": frappe.session.user},
|
||||
[
|
||||
"name",
|
||||
"address_title as billing_name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"pincode",
|
||||
"phone",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return {"access": access, "message": message, "address": address}
|
||||
|
||||
|
||||
def verify_billing_access(doctype, name, billing_type):
|
||||
access = True
|
||||
message = ""
|
||||
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
access = False
|
||||
@@ -154,47 +144,7 @@ def validate_billing_access(billing_type, name):
|
||||
access = False
|
||||
message = _("You have already purchased the certificate for this course.")
|
||||
|
||||
address = frappe.db.get_value(
|
||||
"Address",
|
||||
{"email_id": frappe.session.user},
|
||||
[
|
||||
"name",
|
||||
"address_title as billing_name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"pincode",
|
||||
"phone",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return {"access": access, "message": message, "address": address}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_job_details(job):
|
||||
return frappe.db.get_value(
|
||||
"Job Opportunity",
|
||||
job,
|
||||
[
|
||||
"job_title",
|
||||
"location",
|
||||
"country",
|
||||
"type",
|
||||
"work_mode",
|
||||
"company_name",
|
||||
"company_logo",
|
||||
"company_website",
|
||||
"name",
|
||||
"creation",
|
||||
"description",
|
||||
"owner",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
return access, message
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@@ -1665,3 +1615,340 @@ def get_profile_details(username):
|
||||
|
||||
details.roles = frappe.get_roles(details.name)
|
||||
return details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_streak_info():
|
||||
if frappe.session.user == "Guest":
|
||||
return {}
|
||||
|
||||
all_dates = fetch_activity_dates(frappe.session.user)
|
||||
streak, longest_streak = calculate_streaks(all_dates)
|
||||
current_streak = calculate_current_streak(all_dates, streak)
|
||||
|
||||
return {
|
||||
"current_streak": current_streak,
|
||||
"longest_streak": longest_streak,
|
||||
}
|
||||
|
||||
|
||||
def fetch_activity_dates(user):
|
||||
doctypes = [
|
||||
"LMS Course Progress",
|
||||
"LMS Quiz Submission",
|
||||
"LMS Assignment Submission",
|
||||
"LMS Programming Exercise Submission",
|
||||
]
|
||||
|
||||
all_dates = []
|
||||
for dt in doctypes:
|
||||
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
|
||||
|
||||
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
|
||||
|
||||
|
||||
def calculate_streaks(all_dates):
|
||||
streak = 0
|
||||
longest_streak = 0
|
||||
prev_day = None
|
||||
|
||||
for d in all_dates:
|
||||
if d.weekday() in (5, 6):
|
||||
continue
|
||||
|
||||
if prev_day:
|
||||
expected = prev_day + timedelta(days=1)
|
||||
while expected.weekday() in (5, 6):
|
||||
expected += timedelta(days=1)
|
||||
|
||||
streak = streak + 1 if d == expected else 1
|
||||
else:
|
||||
streak = 1
|
||||
|
||||
longest_streak = max(longest_streak, streak)
|
||||
prev_day = d
|
||||
|
||||
return streak, longest_streak
|
||||
|
||||
|
||||
def calculate_current_streak(all_dates, streak):
|
||||
if not all_dates:
|
||||
return 0
|
||||
|
||||
last_date = all_dates[-1]
|
||||
today = getdate()
|
||||
|
||||
ref_day = today
|
||||
while ref_day.weekday() in (5, 6):
|
||||
ref_day -= timedelta(days=1)
|
||||
|
||||
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
|
||||
return streak
|
||||
return 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_my_live_classes():
|
||||
my_live_classes = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_live_classes
|
||||
|
||||
batches = frappe.get_all(
|
||||
"LMS Batch Enrollment",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
},
|
||||
order_by="creation desc",
|
||||
pluck="batch",
|
||||
)
|
||||
|
||||
live_class_details = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
filters={
|
||||
"date": [">=", getdate()],
|
||||
"batch_name": ["in", batches],
|
||||
},
|
||||
fields=[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"time",
|
||||
"date",
|
||||
"duration",
|
||||
"attendees",
|
||||
"start_url",
|
||||
"join_url",
|
||||
"owner",
|
||||
],
|
||||
limit=2,
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
if len(live_class_details):
|
||||
for live_class in live_class_details:
|
||||
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
|
||||
|
||||
my_live_classes.append(live_class)
|
||||
|
||||
return my_live_classes
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_created_courses():
|
||||
created_courses = []
|
||||
if frappe.session.user == "Guest":
|
||||
return created_courses
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Course = frappe.qb.DocType("LMS Course")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(Course)
|
||||
.on(CourseInstructor.parent == Course.name)
|
||||
.select(Course.name)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.orderby(Course.published_on, order=frappe.qb.desc)
|
||||
.limit(3)
|
||||
)
|
||||
|
||||
results = query.run(as_dict=True)
|
||||
courses = [row["name"] for row in results]
|
||||
|
||||
for course in courses:
|
||||
course_details = get_course_details(course)
|
||||
created_courses.append(course_details)
|
||||
|
||||
return created_courses
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_created_batches():
|
||||
created_batches = []
|
||||
if frappe.session.user == "Guest":
|
||||
return created_batches
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Batch = frappe.qb.DocType("LMS Batch")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(Batch)
|
||||
.on(CourseInstructor.parent == Batch.name)
|
||||
.select(Batch.name)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.where(Batch.start_date >= getdate())
|
||||
.orderby(Batch.start_date, order=frappe.qb.asc)
|
||||
.limit(4)
|
||||
)
|
||||
|
||||
results = query.run(as_dict=True)
|
||||
batches = [row["name"] for row in results]
|
||||
|
||||
for batch in batches:
|
||||
batch_details = get_batch_details(batch)
|
||||
created_batches.append(batch_details)
|
||||
|
||||
return created_batches
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_admin_live_classes():
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(LMSLiveClass)
|
||||
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
|
||||
.select(
|
||||
LMSLiveClass.name,
|
||||
LMSLiveClass.title,
|
||||
LMSLiveClass.description,
|
||||
LMSLiveClass.time,
|
||||
LMSLiveClass.date,
|
||||
LMSLiveClass.duration,
|
||||
LMSLiveClass.attendees,
|
||||
LMSLiveClass.start_url,
|
||||
LMSLiveClass.join_url,
|
||||
LMSLiveClass.owner,
|
||||
)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.where(LMSLiveClass.date >= getdate())
|
||||
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
|
||||
.limit(4)
|
||||
)
|
||||
results = query.run(as_dict=True)
|
||||
return results
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_admin_evals():
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
"evaluator": frappe.session.user,
|
||||
"date": [">=", getdate()],
|
||||
},
|
||||
[
|
||||
"name",
|
||||
"date",
|
||||
"start_time",
|
||||
"course",
|
||||
"evaluator",
|
||||
"google_meet_link",
|
||||
"member",
|
||||
"member_name",
|
||||
],
|
||||
limit=4,
|
||||
order_by="date asc",
|
||||
)
|
||||
|
||||
for evaluation in evals:
|
||||
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
|
||||
|
||||
return evals
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_my_courses():
|
||||
my_courses = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_courses
|
||||
|
||||
courses = get_my_latest_courses()
|
||||
|
||||
if not len(courses):
|
||||
courses = get_featured_home_courses()
|
||||
|
||||
if not len(courses):
|
||||
courses = get_popular_courses()
|
||||
|
||||
for course in courses:
|
||||
my_courses.append(get_course_details(course))
|
||||
|
||||
return my_courses
|
||||
|
||||
|
||||
def get_my_latest_courses():
|
||||
return frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
},
|
||||
order_by="modified desc",
|
||||
limit=3,
|
||||
pluck="course",
|
||||
)
|
||||
|
||||
|
||||
def get_featured_home_courses():
|
||||
return frappe.get_all(
|
||||
"LMS Course",
|
||||
{"published": 1, "featured": 1},
|
||||
order_by="published_on desc",
|
||||
limit=3,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
|
||||
def get_popular_courses():
|
||||
return frappe.get_all(
|
||||
"LMS Course",
|
||||
{
|
||||
"published": 1,
|
||||
},
|
||||
order_by="enrollments desc",
|
||||
limit=3,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_my_batches():
|
||||
my_batches = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_batches
|
||||
|
||||
batches = get_my_latest_batches()
|
||||
|
||||
if not len(batches):
|
||||
batches = get_upcoming_batches()
|
||||
|
||||
for batch in batches:
|
||||
batch_details = get_batch_details(batch)
|
||||
if batch_details:
|
||||
my_batches.append(batch_details)
|
||||
|
||||
return my_batches
|
||||
|
||||
|
||||
def get_my_latest_batches():
|
||||
return frappe.get_all(
|
||||
"LMS Batch Enrollment",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
},
|
||||
order_by="creation desc",
|
||||
limit=4,
|
||||
pluck="batch",
|
||||
)
|
||||
|
||||
|
||||
def get_upcoming_batches():
|
||||
return frappe.get_all(
|
||||
"LMS Batch",
|
||||
{
|
||||
"published": 1,
|
||||
"start_date": [">=", getdate()],
|
||||
},
|
||||
order_by="start_date asc",
|
||||
limit=4,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Exercise Latest Submission", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-12-08 17:56:26.049675",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"exercise",
|
||||
"status",
|
||||
"batch_old",
|
||||
"column_break_4",
|
||||
"exercise_title",
|
||||
"course",
|
||||
"lesson",
|
||||
"section_break_8",
|
||||
"solution",
|
||||
"image",
|
||||
"test_results",
|
||||
"comments",
|
||||
"latest_submission",
|
||||
"member",
|
||||
"member_email"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "exercise",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Exercise",
|
||||
"options": "LMS Exercise",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Correct\nIncorrect"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_old",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch Old",
|
||||
"options": "LMS Batch Old"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.title",
|
||||
"fieldname": "exercise_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Exercise Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.lesson",
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "latest_submission.solution",
|
||||
"fieldname": "solution",
|
||||
"fieldtype": "Code",
|
||||
"label": "Solution"
|
||||
},
|
||||
{
|
||||
"fetch_from": "latest_submission.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Code",
|
||||
"label": "Image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "latest_submission.test_results",
|
||||
"fieldname": "test_results",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Test Results"
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Comments"
|
||||
},
|
||||
{
|
||||
"fieldname": "latest_submission",
|
||||
"fieldtype": "Link",
|
||||
"label": "Latest Submission",
|
||||
"options": "Exercise Submission"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"label": "Member",
|
||||
"options": "LMS Enrollment"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.member",
|
||||
"fieldname": "member_email",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member Email",
|
||||
"options": "User",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-14 20:56:52.370697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Exercise Latest Submission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ExerciseLatestSubmission(Document):
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestExerciseLatestSubmission(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Exercise Submission", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-05-19 11:41:18.108316",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"exercise",
|
||||
"status",
|
||||
"batch_old",
|
||||
"column_break_4",
|
||||
"exercise_title",
|
||||
"course",
|
||||
"lesson",
|
||||
"section_break_8",
|
||||
"solution",
|
||||
"image",
|
||||
"test_results",
|
||||
"comments",
|
||||
"member"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "exercise",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Exercise",
|
||||
"options": "LMS Exercise"
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.title",
|
||||
"fieldname": "exercise_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Exercise Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_old",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch Old",
|
||||
"options": "LMS Batch Old"
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.lesson",
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson"
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Code",
|
||||
"label": "Image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Correct\nIncorrect"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_results",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Test Results"
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Comments"
|
||||
},
|
||||
{
|
||||
"fieldname": "solution",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Solution"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"label": "Member",
|
||||
"options": "LMS Enrollment"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-08 22:25:05.809377",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Exercise Submission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ExerciseSubmission(Document):
|
||||
def on_update(self):
|
||||
self.update_latest_submission()
|
||||
|
||||
def update_latest_submission(self):
|
||||
names = frappe.get_all(
|
||||
"Exercise Latest Submission", {"exercise": self.exercise, "member": self.member}
|
||||
)
|
||||
if names:
|
||||
doc = frappe.get_doc("Exercise Latest Submission", names[0])
|
||||
doc.latest_submission = self.name
|
||||
doc.save(ignore_permissions=True)
|
||||
else:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Exercise Latest Submission",
|
||||
"exercise": self.exercise,
|
||||
"member": self.member,
|
||||
"latest_submission": self.name,
|
||||
}
|
||||
)
|
||||
doc.insert(ignore_permissions=True)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestExerciseSubmission(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("LMS Batch Old", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format: BATCH-{#####}",
|
||||
"creation": "2021-03-18 19:37:34.614796",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"start_date",
|
||||
"start_time",
|
||||
"column_break_3",
|
||||
"title",
|
||||
"sessions_on",
|
||||
"end_time",
|
||||
"section_break_5",
|
||||
"description",
|
||||
"section_break_7",
|
||||
"visibility",
|
||||
"membership",
|
||||
"column_break_9",
|
||||
"status",
|
||||
"stage"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "Public",
|
||||
"fieldname": "visibility",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Visibility",
|
||||
"options": "Public\nUnlisted\nPrivate"
|
||||
},
|
||||
{
|
||||
"fieldname": "membership",
|
||||
"fieldtype": "Select",
|
||||
"label": "Membership",
|
||||
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
|
||||
},
|
||||
{
|
||||
"default": "Active",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Active\nInactive"
|
||||
},
|
||||
{
|
||||
"default": "Ready",
|
||||
"fieldname": "stage",
|
||||
"fieldtype": "Select",
|
||||
"label": "Stage",
|
||||
"options": "Ready\nIn Progress\nCompleted\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Batch Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Batch Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "sessions_on",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sessions On Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "End Time"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"group": "Members",
|
||||
"link_doctype": "LMS Enrollment",
|
||||
"link_fieldname": "batch_old"
|
||||
}
|
||||
],
|
||||
"modified": "2022-09-28 18:43:22.955907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Old",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.doctype.lms_enrollment.lms_enrollment import create_membership
|
||||
from lms.lms.utils import is_mentor
|
||||
|
||||
|
||||
class LMSBatchOld(Document):
|
||||
def validate(self):
|
||||
pass
|
||||
# self.validate_if_mentor()
|
||||
|
||||
def validate_if_mentor(self):
|
||||
if not is_mentor(self.course, frappe.session.user):
|
||||
course_title = frappe.db.get_value("LMS Course", self.course, "title")
|
||||
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
|
||||
|
||||
def after_insert(self):
|
||||
create_membership(batch=self.name, course=self.course, member_type="Mentor")
|
||||
|
||||
def is_member(self, email, member_type=None):
|
||||
"""Checks if a person is part of a batch.
|
||||
|
||||
If member_type is specified, checks if the person is a Student/Mentor.
|
||||
"""
|
||||
|
||||
filters = {"batch_old": self.name, "member": email}
|
||||
if member_type:
|
||||
filters["member_type"] = member_type
|
||||
return frappe.db.exists("LMS Enrollment", filters)
|
||||
|
||||
def get_membership(self, email):
|
||||
"""Returns the membership document of given user."""
|
||||
name = frappe.get_value(
|
||||
doctype="LMS Enrollment",
|
||||
filters={"batch_old": self.name, "member": email},
|
||||
fieldname="name",
|
||||
)
|
||||
return frappe.get_doc("LMS Enrollment", name)
|
||||
|
||||
def get_current_lesson(self, user):
|
||||
"""Returns the name of the current lesson for the given user."""
|
||||
membership = self.get_membership(user)
|
||||
return membership and membership.current_lesson
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_message(message, batch):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Message",
|
||||
"batch_old": batch,
|
||||
"author": frappe.session.user,
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def switch_batch(course_name, email, batch_name):
|
||||
"""Switches the user from the current batch of the course to a new batch."""
|
||||
membership = frappe.get_last_doc("LMS Enrollment", filters={"course": course_name, "member": email})
|
||||
|
||||
batch = frappe.get_doc("LMS Batch Old", batch_name)
|
||||
if not batch:
|
||||
raise ValueError(f"Invalid Batch: {batch_name}")
|
||||
|
||||
if batch.course != course_name:
|
||||
raise ValueError("Can not switch batches across courses")
|
||||
|
||||
if batch.is_member(email):
|
||||
print(f"{email} is already a member of {batch.title}")
|
||||
return
|
||||
|
||||
old_batch = frappe.get_doc("LMS Batch Old", membership.batch_old)
|
||||
|
||||
membership.batch_old = batch_name
|
||||
membership.save()
|
||||
|
||||
# update exercise submissions
|
||||
filters = {"owner": email, "batch_old": old_batch.name}
|
||||
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
|
||||
doc = frappe.get_doc("Exercise Submission", name)
|
||||
print("updating exercise submission", name)
|
||||
doc.batch_old = batch_name
|
||||
doc.save()
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestLMSBatchOld(unittest.TestCase):
|
||||
pass
|
||||
@@ -6,9 +6,7 @@ from frappe import _
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import add_years, nowdate
|
||||
|
||||
from lms.lms.utils import is_certified
|
||||
from frappe.utils import nowdate
|
||||
|
||||
|
||||
class LMSCertificate(Document):
|
||||
@@ -112,6 +110,13 @@ def has_website_permission(doc, ptype, user, verbose=False):
|
||||
return False
|
||||
|
||||
|
||||
def is_certified(course):
|
||||
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
|
||||
if len(certificate):
|
||||
return certificate[0].name
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate(course):
|
||||
if is_certified(course):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "lms.lms.doctype.lms_course.lms_course.reindex_exercises",
|
||||
"action_type": "Server Action",
|
||||
"group": "Reindex",
|
||||
"label": "Reindex Exercises"
|
||||
}
|
||||
],
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-02-22 15:28:26.091549",
|
||||
@@ -76,6 +69,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
@@ -152,8 +147,6 @@
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "In Progress\nUnder Review\nApproved",
|
||||
"read_only": 1
|
||||
@@ -313,7 +306,7 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-10-13 15:08:11.734204",
|
||||
"modified": "2025-12-15 15:15:42.226098",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
@@ -336,6 +329,7 @@
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
@@ -348,6 +342,7 @@
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
|
||||
@@ -9,8 +9,6 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, today
|
||||
|
||||
from lms.lms.utils import get_chapters
|
||||
|
||||
from ...utils import generate_slug, update_payment_record, validate_image
|
||||
|
||||
|
||||
@@ -133,77 +131,3 @@ 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."""
|
||||
if not email or email == "Guest":
|
||||
return False
|
||||
|
||||
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."""
|
||||
if not email:
|
||||
raise ValueError("Invalid email")
|
||||
if email == "Guest":
|
||||
raise ValueError("Guest user can not be added as a mentor")
|
||||
|
||||
# given user is already a mentor
|
||||
if self.has_mentor(email):
|
||||
return
|
||||
|
||||
doc = frappe.get_doc({"doctype": "LMS Course Mentor Mapping", "course": self.name, "mentor": email})
|
||||
doc.insert()
|
||||
|
||||
def get_student_batch(self, email):
|
||||
"""Returns the batch the given student is part of.
|
||||
|
||||
Returns None if the student is not part of any batch.
|
||||
"""
|
||||
if not email:
|
||||
return
|
||||
|
||||
batch_name = frappe.get_value(
|
||||
doctype="LMS Enrollment",
|
||||
filters={"course": self.name, "member_type": "Student", "member": email},
|
||||
fieldname="batch_old",
|
||||
)
|
||||
return batch_name and frappe.get_doc("LMS Batch Old", batch_name)
|
||||
|
||||
def get_batches(self, mentor=None):
|
||||
batches = frappe.get_all("LMS Batch Old", {"course": self.name})
|
||||
if mentor:
|
||||
# TODO: optimize this
|
||||
memberships = frappe.db.get_all("LMS Enrollment", {"member": mentor}, ["batch_old"])
|
||||
batch_names = {m.batch_old for m in memberships}
|
||||
return [b for b in batches if b.name in batch_names]
|
||||
|
||||
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):
|
||||
for exercise in lesson.get_exercises():
|
||||
exercise.index_ = i
|
||||
exercise.index_label = f"{index}.{i}"
|
||||
exercise.save()
|
||||
i += 1
|
||||
|
||||
def get_all_memberships(self, member):
|
||||
all_memberships = frappe.get_all(
|
||||
"LMS Enrollment", {"member": member, "course": self.name}, ["batch_old"]
|
||||
)
|
||||
for membership in all_memberships:
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch Old", membership.batch_old, "title")
|
||||
return all_memberships
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reindex_exercises(doc):
|
||||
course_data = json.loads(doc)
|
||||
course = frappe.get_doc("LMS Course", course_data["name"])
|
||||
course.reindex_exercises()
|
||||
frappe.msgprint("All exercises in this course have been re-indexed.")
|
||||
|
||||
@@ -13,31 +13,15 @@ class TestLMSCourse(unittest.TestCase):
|
||||
course = new_course("Test Course")
|
||||
assert course.title == "Test Course"
|
||||
|
||||
# disabled this test as it is failing
|
||||
def _test_add_mentors(self):
|
||||
course = new_course("Test Course")
|
||||
assert course.get_mentors() == []
|
||||
|
||||
new_user("Tester", "tester@example.com")
|
||||
course.add_mentor("tester@example.com")
|
||||
|
||||
mentors = course.get_mentors()
|
||||
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("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"})
|
||||
frappe.db.delete("LMS Batch Old", {"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`")
|
||||
@@ -80,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:
|
||||
|
||||
@@ -2,17 +2,24 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
class LMSCourseReview(Document):
|
||||
def validate(self):
|
||||
self.validate_enrollment()
|
||||
self.validate_if_already_reviewed()
|
||||
|
||||
def validate_enrollment(self):
|
||||
enrollment = frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.owner})
|
||||
if not enrollment:
|
||||
frappe.throw(_("You must be enrolled in the course to submit a review"))
|
||||
|
||||
def validate_if_already_reviewed(self):
|
||||
if frappe.db.exists("LMS Course Review", {"course": self.course, "owner": self.owner}):
|
||||
frappe.throw(frappe._("You have already reviewed this course"))
|
||||
frappe.throw(_("You have already reviewed this course"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -19,18 +19,11 @@
|
||||
"purchased_certificate",
|
||||
"certificate",
|
||||
"section_break_8",
|
||||
"batch_old",
|
||||
"column_break_12",
|
||||
"member_type",
|
||||
"role"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "batch_old",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch Old",
|
||||
"options": "LMS Batch Old"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
@@ -141,7 +134,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-14 20:59:37.166118",
|
||||
"modified": "2025-12-15 21:27:30.733483",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
|
||||
@@ -9,56 +9,43 @@ from frappe.utils import ceil
|
||||
|
||||
class LMSEnrollment(Document):
|
||||
def validate(self):
|
||||
self.validate_membership_in_same_batch()
|
||||
self.validate_membership_in_different_batch_same_course()
|
||||
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."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_membership_in_different_batch_same_course(self):
|
||||
"""Ensures that a studnet is only part of one batch."""
|
||||
# nothing to worry if the member is not a student
|
||||
if self.member_type != "Student":
|
||||
return
|
||||
if not course_details.published:
|
||||
frappe.throw(_("You cannot enroll in an unpublished course."))
|
||||
|
||||
course = frappe.db.get_value("LMS Batch Old", self.batch_old, "course")
|
||||
memberships = frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
filters={
|
||||
"member": self.member,
|
||||
"name": ["!=", self.name],
|
||||
"member_type": "Student",
|
||||
"course": self.course,
|
||||
},
|
||||
fields=["batch_old", "member_type", "name"],
|
||||
)
|
||||
|
||||
if memberships:
|
||||
membership = memberships[0]
|
||||
member_name = frappe.db.get_value("User", self.member, "full_name")
|
||||
frappe.throw(
|
||||
_("{0} is already a Student of {1} course through {2} batch").format(
|
||||
member_name, course, membership.batch_old
|
||||
)
|
||||
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"])
|
||||
@@ -77,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(
|
||||
{
|
||||
@@ -94,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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("LMS Exercise", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-05-19 17:43:39.923430",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"description",
|
||||
"code",
|
||||
"answer",
|
||||
"column_break_4",
|
||||
"course",
|
||||
"hints",
|
||||
"tests",
|
||||
"image",
|
||||
"lesson",
|
||||
"index_",
|
||||
"index_label"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"fieldname": "answer",
|
||||
"fieldtype": "Code",
|
||||
"label": "Answer"
|
||||
},
|
||||
{
|
||||
"fieldname": "tests",
|
||||
"fieldtype": "Code",
|
||||
"label": "Tests"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"fieldname": "hints",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Hints"
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"fieldname": "code",
|
||||
"fieldtype": "Code",
|
||||
"label": "Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Code",
|
||||
"label": "Image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson"
|
||||
},
|
||||
{
|
||||
"fieldname": "index_",
|
||||
"fieldtype": "Int",
|
||||
"label": "Index",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "index_label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Index Label",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-29 15:27:55.585874",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Exercise",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "title",
|
||||
"sort_field": "index_label",
|
||||
"sort_order": "ASC",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.utils import get_membership
|
||||
|
||||
|
||||
class LMSExercise(Document):
|
||||
def get_user_submission(self):
|
||||
"""Returns the latest submission for this user."""
|
||||
user = frappe.session.user
|
||||
if not user or user == "Guest":
|
||||
return
|
||||
|
||||
result = frappe.get_all(
|
||||
"Exercise Submission",
|
||||
fields="*",
|
||||
filters={"owner": user, "exercise": self.name},
|
||||
order_by="creation desc",
|
||||
page_length=1,
|
||||
)
|
||||
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
def submit(self, code):
|
||||
"""Submits the given code as solution to exercise."""
|
||||
user = frappe.session.user
|
||||
if not user or user == "Guest":
|
||||
return
|
||||
|
||||
old_submission = self.get_user_submission()
|
||||
if old_submission and old_submission.solution == code:
|
||||
return old_submission
|
||||
|
||||
member = get_membership(self.course, frappe.session.user)
|
||||
|
||||
doc = frappe.get_doc(
|
||||
doctype="Exercise Submission",
|
||||
exercise=self.name,
|
||||
exercise_title=self.title,
|
||||
course=self.course,
|
||||
lesson=self.lesson,
|
||||
batch=member.batch_old,
|
||||
solution=code,
|
||||
member=member.name,
|
||||
)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
@@ -1,10 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
# import frappe
|
||||
|
||||
|
||||
class TestLMSExercise(unittest.TestCase):
|
||||
pass
|
||||
@@ -2,16 +2,246 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from .utils import slugify
|
||||
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified
|
||||
|
||||
from .utils import (
|
||||
get_average_rating,
|
||||
get_chapters,
|
||||
get_instructors,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_lessons,
|
||||
get_membership,
|
||||
get_reviews,
|
||||
get_tags,
|
||||
has_course_instructor_role,
|
||||
has_evaluator_role,
|
||||
has_moderator_role,
|
||||
has_student_role,
|
||||
is_instructor,
|
||||
slugify,
|
||||
)
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
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", "Course Creator", "Batch Evaluator"]
|
||||
)
|
||||
|
||||
self.create_a_course()
|
||||
self.add_chapters()
|
||||
self.add_lessons()
|
||||
|
||||
self.add_enrollment(self.course.name, self.student1.email)
|
||||
self.add_enrollment(self.course.name, self.student2.email)
|
||||
|
||||
self.add_rating(self.course.name, self.student1.email, 0.8, "Good course")
|
||||
self.add_rating(self.course.name, self.student2.email, 1, "Excellent course")
|
||||
|
||||
self.create_certificate(self.course.name, self.student1.email)
|
||||
|
||||
def create_a_course(self):
|
||||
course = frappe.new_doc("LMS Course")
|
||||
course.title = "Utility Course"
|
||||
course.short_introduction = "A course to test utilities of Frappe Learning"
|
||||
course.description = "This is a detailed description of the Utility Course."
|
||||
course.tags = "Frappe,Learning,Utility"
|
||||
course.published = 1
|
||||
course.append("instructors", {"instructor": "frappe@example.com"})
|
||||
course.save()
|
||||
self.course = course
|
||||
|
||||
def add_chapters(self):
|
||||
chapters = []
|
||||
for i in range(1, 4):
|
||||
chapter = frappe.new_doc("Course Chapter")
|
||||
chapter.course = self.course.name
|
||||
chapter.title = f"Chapter {i}"
|
||||
chapter.save()
|
||||
chapters.append(chapter)
|
||||
|
||||
self.course.reload()
|
||||
for chapter in chapters:
|
||||
self.course.append("chapters", {"chapter": chapter.name})
|
||||
|
||||
self.course.save()
|
||||
|
||||
def add_lessons(self):
|
||||
for chapter in self.course.chapters:
|
||||
chapterDoc = frappe.get_doc("Course Chapter", chapter.chapter)
|
||||
lessons = []
|
||||
for j in range(1, 3):
|
||||
lesson = frappe.new_doc("Course Lesson")
|
||||
lesson.course = self.course.name
|
||||
lesson.chapter = chapter.chapter
|
||||
lesson.title = f"Lesson {j} of {chapter.chapter}"
|
||||
content = '{"time":1765194986690,"blocks":[{"id":"dkLzbW14ds","type":"markdown","data":{"text":"This is a simple content for the current lesson."}},{"id":"KBwuWPc8rV","type":"markdown","data":{"text":""}}],"version":"2.29.0"}'
|
||||
lesson.content = content
|
||||
lesson.save()
|
||||
lessons.append(lesson)
|
||||
|
||||
for lesson in lessons:
|
||||
chapterDoc.append("lessons", {"lesson": lesson.name})
|
||||
chapterDoc.save()
|
||||
|
||||
def create_user(self, email, first_name, last_name, roles):
|
||||
if not frappe.db.exists("User", email):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = email
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.user_type = "Website User"
|
||||
for role in roles:
|
||||
user.append("roles", {"role": role})
|
||||
user.save()
|
||||
return user
|
||||
else:
|
||||
return frappe.get_doc("User", email)
|
||||
|
||||
def create_certificate(self, course_name, member):
|
||||
certificate = frappe.new_doc("LMS Certificate")
|
||||
certificate.course = course_name
|
||||
certificate.member = member
|
||||
certificate.issue_date = frappe.utils.nowdate()
|
||||
certificate.template = get_default_certificate_template()
|
||||
certificate.save()
|
||||
return certificate
|
||||
|
||||
def test_simple_slugs(self):
|
||||
self.assertEqual(slugify("hello-world"), "hello-world")
|
||||
self.assertEqual(slugify("Hello World"), "hello-world")
|
||||
self.assertEqual(slugify("Hello, World!"), "hello-world")
|
||||
|
||||
def test_duplicates(self):
|
||||
def test_duplicates_slugs(self):
|
||||
self.assertEqual(slugify("Hello World", ["hello-world"]), "hello-world-2")
|
||||
|
||||
self.assertEqual(slugify("Hello World", ["hello-world", "hello-world-2"]), "hello-world-3")
|
||||
|
||||
def add_enrollment(self, course, member):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course
|
||||
enrollment.member = member
|
||||
enrollment.save()
|
||||
|
||||
def test_get_membership(self):
|
||||
membership = get_membership(self.course.name, self.student1.email)
|
||||
self.assertIsNotNone(membership)
|
||||
self.assertEqual(membership.course, self.course.name)
|
||||
self.assertEqual(membership.member, self.student1.email)
|
||||
|
||||
def test_get_chapters(self):
|
||||
chapters = get_chapters(self.course.name)
|
||||
self.assertEqual(len(chapters), len(self.course.chapters))
|
||||
|
||||
for i, chapter in enumerate(chapters, start=1):
|
||||
self.assertEqual(chapter.title, f"Chapter {i}")
|
||||
|
||||
def test_get_lessons(self):
|
||||
lessons = get_lessons(self.course.name)
|
||||
all_lessons = frappe.db.count("Course Lesson", {"course": self.course.name})
|
||||
self.assertEqual(len(lessons), all_lessons)
|
||||
|
||||
for chapter in self.course.chapters:
|
||||
chapter_lessons = [lesson for lesson in lessons if lesson.chapter == chapter.chapter]
|
||||
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}")
|
||||
|
||||
def test_get_tags(self):
|
||||
tags = get_tags(self.course.name)
|
||||
expected_tags = ["Frappe", "Learning", "Utility"]
|
||||
self.assertEqual(set(tags), set(expected_tags))
|
||||
|
||||
def test_get_instructors(self):
|
||||
instructors = get_instructors("LMS Course", self.course.name)
|
||||
self.assertEqual(len(instructors), len(self.course.instructors))
|
||||
self.assertEqual(instructors[0].name, "frappe@example.com")
|
||||
|
||||
def test_get_average_rating(self):
|
||||
average_rating = get_average_rating(self.course.name)
|
||||
self.assertEqual(average_rating, 4.5)
|
||||
|
||||
def add_rating(self, course_name, member, rating, review):
|
||||
frappe.session.user = member
|
||||
review = frappe.new_doc("LMS Course Review")
|
||||
review.course = course_name
|
||||
review.rating = rating
|
||||
review.review = review
|
||||
review.save()
|
||||
frappe.session.user = "Administrator"
|
||||
|
||||
def test_get_reviews(self):
|
||||
reviews = get_reviews(self.course.name)
|
||||
self.assertEqual(len(reviews), 2)
|
||||
|
||||
for review in reviews:
|
||||
if review.rating == 0.8:
|
||||
self.assertEqual(review.member, self.student1.email)
|
||||
self.assertEqual(review.review, "Good course")
|
||||
elif review.rating == 1:
|
||||
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 test_get_lesson_url(self):
|
||||
lessons = get_lessons(self.course.name)
|
||||
for lesson in lessons:
|
||||
expected_url = f"/lms/courses/{self.course.name}/learn/{lesson.number}"
|
||||
self.assertEqual(get_lesson_url(self.course.name, lesson.number), expected_url)
|
||||
|
||||
def test_is_instructor(self):
|
||||
frappe.session.user = "frappe@example.com"
|
||||
self.assertTrue(is_instructor(self.course.name))
|
||||
frappe.session.user = "Administrator"
|
||||
self.assertFalse(is_instructor(self.course.name))
|
||||
|
||||
def test_has_course_instructor_role(self):
|
||||
self.assertIsNotNone(has_course_instructor_role("frappe@example.com"))
|
||||
self.assertIsNone(has_course_instructor_role("student1@example.com"))
|
||||
|
||||
def test_has_moderator_role(self):
|
||||
self.assertIsNotNone(has_moderator_role("frappe@example.com"))
|
||||
self.assertIsNone(has_moderator_role("student2@example.com"))
|
||||
|
||||
def test_has_evaluator_role(self):
|
||||
self.assertIsNotNone(has_evaluator_role("frappe@example.com"))
|
||||
self.assertIsNone(has_evaluator_role("student2@example.com"))
|
||||
|
||||
def test_has_student_role(self):
|
||||
self.assertIsNotNone(has_student_role("student1@example.com"))
|
||||
self.assertIsNotNone(has_student_role("student2@example.com"))
|
||||
|
||||
def test_is_certified(self):
|
||||
frappe.session.user = self.student1.email
|
||||
self.assertIsNotNone(is_certified(self.course.name))
|
||||
frappe.session.user = self.student2.email
|
||||
self.assertIsNone(is_certified(self.course.name))
|
||||
frappe.session.user = "Administrator"
|
||||
|
||||
def test_rating_validation(self):
|
||||
student3 = self.create_user("student3@example.com", "Emily", "Cooper", ["LMS Student"])
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
self.add_rating(self.course.name, student3.email, -0.5, "Bad course")
|
||||
frappe.session.user = "Administrator"
|
||||
frappe.delete_doc("User", student3.email)
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.db.exists("LMS Course", self.course.name):
|
||||
frappe.db.delete("LMS Certificate", {"course": self.course.name})
|
||||
frappe.db.delete("LMS Enrollment", {"course": self.course.name})
|
||||
frappe.db.delete("LMS Course Review", {"course": self.course.name})
|
||||
frappe.db.delete("Course Lesson", {"course": self.course.name})
|
||||
frappe.db.delete("Course Chapter", {"course": self.course.name})
|
||||
frappe.db.delete("Course Instructor", {"parent": self.course.name})
|
||||
frappe.delete_doc("LMS Course", self.course.name)
|
||||
|
||||
frappe.delete_doc("User", "student1@example.com")
|
||||
frappe.delete_doc("User", "student2@example.com")
|
||||
frappe.delete_doc("User", "frappe@example.com")
|
||||
|
||||
637
lms/lms/utils.py
637
lms/lms/utils.py
@@ -1,8 +1,6 @@
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import string
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
@@ -14,7 +12,6 @@ from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
fmt_money,
|
||||
format_datetime,
|
||||
@@ -27,7 +24,7 @@ from frappe.utils import (
|
||||
rounded,
|
||||
)
|
||||
|
||||
from lms.lms.md import find_macros, markdown_to_html
|
||||
from lms.lms.md import find_macros
|
||||
|
||||
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
||||
|
||||
@@ -83,6 +80,7 @@ def get_membership(course, member=None):
|
||||
"current_lesson",
|
||||
"progress",
|
||||
"member",
|
||||
"course",
|
||||
"purchased_certificate",
|
||||
"certificate",
|
||||
],
|
||||
@@ -150,11 +148,12 @@ def get_lesson_details(chapter, progress=False):
|
||||
"file_type",
|
||||
"instructor_notes",
|
||||
"course",
|
||||
"chapter",
|
||||
"content",
|
||||
],
|
||||
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:
|
||||
@@ -228,16 +227,6 @@ def get_instructors(doctype, docname):
|
||||
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."""
|
||||
filters = {"course": course, "member_type": "Student"}
|
||||
|
||||
if batch:
|
||||
filters["batch_old"] = batch
|
||||
|
||||
return frappe.get_all("LMS Enrollment", filters, ["member"])
|
||||
|
||||
|
||||
def get_average_rating(course):
|
||||
ratings = [review.rating for review in get_reviews(course)]
|
||||
if not len(ratings):
|
||||
@@ -269,29 +258,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 is_certified(course):
|
||||
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
|
||||
if len(certificate):
|
||||
return certificate[0].name
|
||||
return
|
||||
|
||||
|
||||
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)
|
||||
@@ -311,14 +277,6 @@ def get_lesson_url(course, lesson_number):
|
||||
return f"/lms/courses/{course}/learn/{lesson_number}"
|
||||
|
||||
|
||||
def get_batch(course, batch_name):
|
||||
return frappe.get_all("LMS Batch Old", {"name": batch_name, "course": course})
|
||||
|
||||
|
||||
def get_slugified_chapter_title(chapter):
|
||||
return slugify(chapter)
|
||||
|
||||
|
||||
def get_progress(course, lesson, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
@@ -330,52 +288,6 @@ def get_progress(course, lesson, member=None):
|
||||
)
|
||||
|
||||
|
||||
def render_html(lesson):
|
||||
youtube = lesson.youtube
|
||||
quiz_id = lesson.quiz_id
|
||||
body = lesson.body
|
||||
|
||||
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
|
||||
|
||||
if lesson.question:
|
||||
assignment = "{{ Assignment('" + lesson.question + "-" + lesson.file_type + "') }}"
|
||||
text = text + assignment
|
||||
|
||||
return markdown_to_html(text)
|
||||
|
||||
|
||||
def is_mentor(course, email):
|
||||
"""Checks if given user is a mentor for this course."""
|
||||
if not email:
|
||||
return False
|
||||
return frappe.db.count("LMS Course Mentor Mapping", {"course": course, "mentor": email})
|
||||
|
||||
|
||||
def get_mentors(course):
|
||||
"""Returns the list of all mentors for this course."""
|
||||
course_mentors = []
|
||||
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": course}, ["mentor"])
|
||||
for mentor in mentors:
|
||||
member = frappe.db.get_value("User", mentor.mentor, ["name", "username", "full_name", "user_image"])
|
||||
member.batch_count = frappe.db.count(
|
||||
"LMS Enrollment", {"member": member.name, "member_type": "Mentor"}
|
||||
)
|
||||
course_mentors.append(member)
|
||||
return course_mentors
|
||||
|
||||
|
||||
def is_eligible_to_review(course):
|
||||
"""Checks if user is eligible to review the course"""
|
||||
if frappe.db.count("LMS Course Review", {"course": course, "owner": frappe.session.user}):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_course_progress(course, member=None):
|
||||
"""Returns the course progress of the session user"""
|
||||
lesson_count = get_lessons(course, get_details=False)
|
||||
@@ -389,20 +301,6 @@ def get_course_progress(course, member=None):
|
||||
return flt(((completed_lessons / lesson_count) * 100), precision)
|
||||
|
||||
|
||||
def get_initial_members(course):
|
||||
members = frappe.get_all("LMS Enrollment", {"course": course}, ["member"], limit=3)
|
||||
|
||||
member_details = []
|
||||
for member in members:
|
||||
member_details.append(
|
||||
frappe.db.get_value(
|
||||
"User", member.member, ["name", "username", "full_name", "user_image"], as_dict=True
|
||||
)
|
||||
)
|
||||
|
||||
return member_details
|
||||
|
||||
|
||||
def is_instructor(course):
|
||||
instructors = get_instructors("LMS Course", course)
|
||||
for instructor in instructors:
|
||||
@@ -411,57 +309,6 @@ def is_instructor(course):
|
||||
return False
|
||||
|
||||
|
||||
def convert_number_to_character(number):
|
||||
return string.ascii_uppercase[number]
|
||||
|
||||
|
||||
def get_signup_optin_checks():
|
||||
mapper = frappe._dict(
|
||||
{
|
||||
"terms_of_use": {"page_name": "terms_page", "title": _("Terms of Use")},
|
||||
"privacy_policy": {"page_name": "privacy_policy_page", "title": _("Privacy Policy")},
|
||||
"cookie_policy": {"page_name": "cookie_policy_page", "title": _("Cookie Policy")},
|
||||
}
|
||||
)
|
||||
checks = ["terms_of_use", "privacy_policy", "cookie_policy"]
|
||||
links = []
|
||||
|
||||
for check in checks:
|
||||
if frappe.db.get_single_value("LMS Settings", check):
|
||||
page = frappe.db.get_single_value("LMS Settings", mapper[check].get("page_name"))
|
||||
route = frappe.db.get_value("Web Page", page, "route")
|
||||
links.append("<a href='/" + route + "'>" + mapper[check].get("title") + "</a>")
|
||||
|
||||
return (", ").join(links)
|
||||
|
||||
|
||||
def format_amount(amount, currency):
|
||||
amount_reduced = amount / 1000
|
||||
if amount_reduced < 1:
|
||||
return fmt_money(amount, 0, currency)
|
||||
precision = 0 if amount % 1000 == 0 else 1
|
||||
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
|
||||
|
||||
|
||||
def format_number(number):
|
||||
number_reduced = number / 1000
|
||||
if number_reduced < 1:
|
||||
return number
|
||||
return f"{frappe.utils.flt(number_reduced, 1)}k"
|
||||
|
||||
|
||||
def first_lesson_exists(course):
|
||||
first_chapter = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": 1}, "name")
|
||||
if not first_chapter:
|
||||
return False
|
||||
|
||||
first_lesson = frappe.db.get_value("Lesson Reference", {"parent": first_chapter, "idx": 1}, "name")
|
||||
if not first_lesson:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def has_course_instructor_role(member=None):
|
||||
return frappe.db.get_value(
|
||||
"Has Role",
|
||||
@@ -470,33 +317,6 @@ def has_course_instructor_role(member=None):
|
||||
)
|
||||
|
||||
|
||||
def can_create_courses(course, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
instructors = frappe.get_all(
|
||||
"Course Instructor",
|
||||
{
|
||||
"parent": course,
|
||||
},
|
||||
pluck="instructor",
|
||||
)
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
return False
|
||||
|
||||
if has_moderator_role(member):
|
||||
return True
|
||||
|
||||
if has_course_instructor_role(member) and member in instructors:
|
||||
return True
|
||||
|
||||
if not course and has_course_instructor_role(member):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def can_create_batches(member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
@@ -702,44 +522,6 @@ def get_lesson_count(course):
|
||||
return lesson_count
|
||||
|
||||
|
||||
def get_all_memberships(member):
|
||||
return frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
{"member": member},
|
||||
["name", "course", "batch_old", "current_lesson", "member_type", "progress"],
|
||||
)
|
||||
|
||||
|
||||
def get_filtered_membership(course, memberships):
|
||||
current_membership = list(filter(lambda x: x.course == course, memberships))
|
||||
return current_membership[0] if len(current_membership) else None
|
||||
|
||||
|
||||
def show_start_learing_cta(course, membership):
|
||||
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):
|
||||
lesson_exists = False
|
||||
chapter_exists = frappe.db.get_value(
|
||||
"Chapter Reference", {"parent": course.name}, ["name", "chapter"], as_dict=True
|
||||
)
|
||||
|
||||
if chapter_exists:
|
||||
lesson_exists = frappe.db.exists("Lesson Reference", {"parent": chapter_exists.chapter})
|
||||
|
||||
return lesson_exists
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_chart_data(
|
||||
@@ -763,7 +545,6 @@ def get_chart_data(
|
||||
value_field = chart.value_based_on or "1"
|
||||
|
||||
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||
print(chart.filters_json)
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date])
|
||||
filters.append([doctype, datefield, "<=", to_date])
|
||||
@@ -801,43 +582,6 @@ def get_course_completion_data():
|
||||
]
|
||||
|
||||
|
||||
def get_telemetry_boot_info():
|
||||
POSTHOG_PROJECT_FIELD = "posthog_project_id"
|
||||
POSTHOG_HOST_FIELD = "posthog_host"
|
||||
|
||||
if not frappe.conf.get(POSTHOG_HOST_FIELD) or not frappe.conf.get(POSTHOG_PROJECT_FIELD):
|
||||
return {}
|
||||
|
||||
return {
|
||||
"posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD),
|
||||
"posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD),
|
||||
"enable_telemetry": 1,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_onboarding_complete():
|
||||
if not has_moderator_role():
|
||||
return {"is_onboarded": True}
|
||||
|
||||
course_created = frappe.db.a_row_exists("LMS Course")
|
||||
chapter_created = frappe.db.a_row_exists("Course Chapter")
|
||||
lesson_created = frappe.db.a_row_exists("Course Lesson")
|
||||
|
||||
if course_created and chapter_created and lesson_created:
|
||||
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
|
||||
|
||||
return {
|
||||
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
|
||||
"course_created": course_created,
|
||||
"chapter_created": chapter_created,
|
||||
"lesson_created": lesson_created,
|
||||
"first_course": frappe.get_all("LMS Course", limit=1, order_by=None, pluck="name")[0]
|
||||
if course_created
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
def get_evaluator(course, batch=None):
|
||||
evaluator = None
|
||||
if batch:
|
||||
@@ -958,13 +702,6 @@ def get_current_exchange_rate(source, target="USD"):
|
||||
return details["rates"][target]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def change_currency(amount, currency, country=None):
|
||||
amount = cint(amount)
|
||||
amount, currency = check_multicurrency(amount, currency, country)
|
||||
return fmt_money(amount, 0, currency)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_courses(filters=None, start=0):
|
||||
@@ -1085,6 +822,7 @@ def get_course_fields():
|
||||
"title",
|
||||
"tags",
|
||||
"image",
|
||||
"video_link",
|
||||
"card_gradient",
|
||||
"short_introduction",
|
||||
"published",
|
||||
@@ -1109,35 +847,11 @@ def get_course_fields():
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_course_details(course):
|
||||
fields = get_course_fields()
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"tags",
|
||||
"description",
|
||||
"image",
|
||||
"video_link",
|
||||
"short_introduction",
|
||||
"published",
|
||||
"upcoming",
|
||||
"featured",
|
||||
"disable_self_learning",
|
||||
"published_on",
|
||||
"category",
|
||||
"status",
|
||||
"paid_course",
|
||||
"paid_certificate",
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"enable_certification",
|
||||
"lessons",
|
||||
"enrollments",
|
||||
"rating",
|
||||
"card_gradient",
|
||||
],
|
||||
fields,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -2351,343 +2065,6 @@ def persona_captured():
|
||||
frappe.db.set_single_value("LMS Settings", "persona_captured", 1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_my_courses():
|
||||
my_courses = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_courses
|
||||
|
||||
courses = get_my_latest_courses()
|
||||
|
||||
if not len(courses):
|
||||
courses = get_featured_home_courses()
|
||||
|
||||
if not len(courses):
|
||||
courses = get_popular_courses()
|
||||
|
||||
for course in courses:
|
||||
my_courses.append(get_course_details(course))
|
||||
|
||||
return my_courses
|
||||
|
||||
|
||||
def get_my_latest_courses():
|
||||
return frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
},
|
||||
order_by="modified desc",
|
||||
limit=3,
|
||||
pluck="course",
|
||||
)
|
||||
|
||||
|
||||
def get_featured_home_courses():
|
||||
return frappe.get_all(
|
||||
"LMS Course",
|
||||
{"published": 1, "featured": 1},
|
||||
order_by="published_on desc",
|
||||
limit=3,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
|
||||
def get_popular_courses():
|
||||
return frappe.get_all(
|
||||
"LMS Course",
|
||||
{
|
||||
"published": 1,
|
||||
},
|
||||
order_by="enrollments desc",
|
||||
limit=3,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_my_batches():
|
||||
my_batches = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_batches
|
||||
|
||||
batches = get_my_latest_batches()
|
||||
|
||||
if not len(batches):
|
||||
batches = get_upcoming_batches()
|
||||
|
||||
for batch in batches:
|
||||
batch_details = get_batch_details(batch)
|
||||
if batch_details:
|
||||
my_batches.append(batch_details)
|
||||
|
||||
return my_batches
|
||||
|
||||
|
||||
def get_my_latest_batches():
|
||||
return frappe.get_all(
|
||||
"LMS Batch Enrollment",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
},
|
||||
order_by="creation desc",
|
||||
limit=4,
|
||||
pluck="batch",
|
||||
)
|
||||
|
||||
|
||||
def get_upcoming_batches():
|
||||
return frappe.get_all(
|
||||
"LMS Batch",
|
||||
{
|
||||
"published": 1,
|
||||
"start_date": [">=", getdate()],
|
||||
},
|
||||
order_by="start_date asc",
|
||||
limit=4,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_my_live_classes():
|
||||
my_live_classes = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_live_classes
|
||||
|
||||
batches = frappe.get_all(
|
||||
"LMS Batch Enrollment",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
},
|
||||
order_by="creation desc",
|
||||
pluck="batch",
|
||||
)
|
||||
|
||||
live_class_details = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
filters={
|
||||
"date": [">=", getdate()],
|
||||
"batch_name": ["in", batches],
|
||||
},
|
||||
fields=[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"time",
|
||||
"date",
|
||||
"duration",
|
||||
"attendees",
|
||||
"start_url",
|
||||
"join_url",
|
||||
"owner",
|
||||
],
|
||||
limit=2,
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
if len(live_class_details):
|
||||
for live_class in live_class_details:
|
||||
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
|
||||
|
||||
my_live_classes.append(live_class)
|
||||
|
||||
return my_live_classes
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_created_courses():
|
||||
created_courses = []
|
||||
if frappe.session.user == "Guest":
|
||||
return created_courses
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Course = frappe.qb.DocType("LMS Course")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(Course)
|
||||
.on(CourseInstructor.parent == Course.name)
|
||||
.select(Course.name)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.orderby(Course.published_on, order=frappe.qb.desc)
|
||||
.limit(3)
|
||||
)
|
||||
|
||||
results = query.run(as_dict=True)
|
||||
courses = [row["name"] for row in results]
|
||||
|
||||
for course in courses:
|
||||
course_details = get_course_details(course)
|
||||
created_courses.append(course_details)
|
||||
|
||||
return created_courses
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_created_batches():
|
||||
created_batches = []
|
||||
if frappe.session.user == "Guest":
|
||||
return created_batches
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Batch = frappe.qb.DocType("LMS Batch")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(Batch)
|
||||
.on(CourseInstructor.parent == Batch.name)
|
||||
.select(Batch.name)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.where(Batch.start_date >= getdate())
|
||||
.orderby(Batch.start_date, order=frappe.qb.asc)
|
||||
.limit(4)
|
||||
)
|
||||
|
||||
results = query.run(as_dict=True)
|
||||
batches = [row["name"] for row in results]
|
||||
|
||||
for batch in batches:
|
||||
batch_details = get_batch_details(batch)
|
||||
created_batches.append(batch_details)
|
||||
|
||||
return created_batches
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_admin_live_classes():
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(LMSLiveClass)
|
||||
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
|
||||
.select(
|
||||
LMSLiveClass.name,
|
||||
LMSLiveClass.title,
|
||||
LMSLiveClass.description,
|
||||
LMSLiveClass.time,
|
||||
LMSLiveClass.date,
|
||||
LMSLiveClass.duration,
|
||||
LMSLiveClass.attendees,
|
||||
LMSLiveClass.start_url,
|
||||
LMSLiveClass.join_url,
|
||||
LMSLiveClass.owner,
|
||||
)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.where(LMSLiveClass.date >= getdate())
|
||||
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
|
||||
.limit(4)
|
||||
)
|
||||
results = query.run(as_dict=True)
|
||||
return results
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_admin_evals():
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
"evaluator": frappe.session.user,
|
||||
"date": [">=", getdate()],
|
||||
},
|
||||
[
|
||||
"name",
|
||||
"date",
|
||||
"start_time",
|
||||
"course",
|
||||
"evaluator",
|
||||
"google_meet_link",
|
||||
"member",
|
||||
"member_name",
|
||||
],
|
||||
limit=4,
|
||||
order_by="date asc",
|
||||
)
|
||||
|
||||
for evaluation in evals:
|
||||
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
|
||||
|
||||
return evals
|
||||
|
||||
|
||||
def fetch_activity_dates(user):
|
||||
doctypes = [
|
||||
"LMS Course Progress",
|
||||
"LMS Quiz Submission",
|
||||
"LMS Assignment Submission",
|
||||
"LMS Programming Exercise Submission",
|
||||
]
|
||||
|
||||
all_dates = []
|
||||
for dt in doctypes:
|
||||
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
|
||||
|
||||
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
|
||||
|
||||
|
||||
def calculate_streaks(all_dates):
|
||||
streak = 0
|
||||
longest_streak = 0
|
||||
prev_day = None
|
||||
|
||||
for d in all_dates:
|
||||
if d.weekday() in (5, 6):
|
||||
continue
|
||||
|
||||
if prev_day:
|
||||
expected = prev_day + timedelta(days=1)
|
||||
while expected.weekday() in (5, 6):
|
||||
expected += timedelta(days=1)
|
||||
|
||||
streak = streak + 1 if d == expected else 1
|
||||
else:
|
||||
streak = 1
|
||||
|
||||
longest_streak = max(longest_streak, streak)
|
||||
prev_day = d
|
||||
|
||||
return streak, longest_streak
|
||||
|
||||
|
||||
def calculate_current_streak(all_dates, streak):
|
||||
if not all_dates:
|
||||
return 0
|
||||
|
||||
last_date = all_dates[-1]
|
||||
today = getdate()
|
||||
|
||||
ref_day = today
|
||||
while ref_day.weekday() in (5, 6):
|
||||
ref_day -= timedelta(days=1)
|
||||
|
||||
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
|
||||
return streak
|
||||
return 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_streak_info():
|
||||
if frappe.session.user == "Guest":
|
||||
return {}
|
||||
|
||||
all_dates = fetch_activity_dates(frappe.session.user)
|
||||
streak, longest_streak = calculate_streaks(all_dates)
|
||||
current_streak = calculate_current_streak(all_dates, streak)
|
||||
|
||||
return {
|
||||
"current_streak": current_streak,
|
||||
"longest_streak": longest_streak,
|
||||
}
|
||||
|
||||
|
||||
def validate_discussion_reply(doc, method):
|
||||
topic = frappe.db.get_value(
|
||||
"Discussion Topic", doc.topic, ["reference_doctype", "reference_docname"], as_dict=True
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-12-05 16:04+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 18:55\n"
|
||||
"PO-Revision-Date: 2025-12-13 20:35\n"
|
||||
"Last-Translator: jannat@frappe.io\n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -1514,7 +1514,7 @@ msgstr ""
|
||||
#. Label of the confirmation_email_template (Link) field in DocType 'LMS Batch'
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
msgid "Confirmation Email Template"
|
||||
msgstr ""
|
||||
msgstr "Megerősítő E-mail Sablon"
|
||||
|
||||
#: lms/lms/doctype/lms_certificate/lms_certificate.py:30
|
||||
msgid "Congratulations on getting certified!"
|
||||
@@ -4476,7 +4476,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/pages/BatchForm.vue:292 frontend/src/pages/CourseForm.vue:298
|
||||
msgid "Meta Tags"
|
||||
msgstr ""
|
||||
msgstr "Meta Címkék"
|
||||
|
||||
#: lms/lms/api.py:1510
|
||||
msgid "Meta tags should be a list."
|
||||
@@ -4994,7 +4994,7 @@ msgstr ""
|
||||
#. Label of the output (Data) field in DocType 'LMS Test Case Submission'
|
||||
#: lms/lms/doctype/lms_test_case_submission/lms_test_case_submission.json
|
||||
msgid "Output"
|
||||
msgstr ""
|
||||
msgstr "Kimenet"
|
||||
|
||||
#: frontend/src/components/Settings/BadgeForm.vue:216
|
||||
#: lms/lms/doctype/lms_badge/lms_badge.js:37
|
||||
@@ -5213,7 +5213,7 @@ msgstr ""
|
||||
#: lms/lms/doctype/lms_coupon/lms_coupon.json
|
||||
#: lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json
|
||||
msgid "Percentage"
|
||||
msgstr ""
|
||||
msgstr "Százalék"
|
||||
|
||||
#. Option for the 'Grade Type' (Select) field in DocType 'Education Detail'
|
||||
#: lms/lms/doctype/education_detail/education_detail.json
|
||||
@@ -5857,7 +5857,7 @@ msgstr ""
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
#: lms/lms/doctype/lms_lesson_note/lms_lesson_note.json
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
msgstr "Piros"
|
||||
|
||||
#. Label of the redemption_count (Int) field in DocType 'LMS Coupon'
|
||||
#: frontend/src/components/Settings/Coupons/CouponList.vue:189
|
||||
@@ -5873,7 +5873,7 @@ msgstr ""
|
||||
#. Timetable'
|
||||
#: lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json
|
||||
msgid "Reference DocName"
|
||||
msgstr ""
|
||||
msgstr "Hivatkozás DocNév"
|
||||
|
||||
#. Label of the reference_doctype (Link) field in DocType 'LMS Batch Timetable'
|
||||
#. Label of the reference_doctype (Select) field in DocType 'LMS Coupon Item'
|
||||
@@ -6532,7 +6532,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/pages/Billing.vue:118
|
||||
msgid "State/Province"
|
||||
msgstr ""
|
||||
msgstr "Állam / Megye"
|
||||
|
||||
#. Label of the tab_4_tab (Tab Break) field in DocType 'LMS Course'
|
||||
#. Label of the statistics (Check) field in DocType 'LMS Settings'
|
||||
@@ -6853,7 +6853,7 @@ msgstr ""
|
||||
#: lms/lms/doctype/lms_assignment/lms_assignment.json
|
||||
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
||||
msgid "Text"
|
||||
msgstr ""
|
||||
msgstr "Szöveg"
|
||||
|
||||
#: frontend/src/components/BatchFeedback.vue:6
|
||||
msgid "Thank you for providing your feedback."
|
||||
@@ -7396,12 +7396,12 @@ msgstr ""
|
||||
#. Label of the user_field (Select) field in DocType 'LMS Badge'
|
||||
#: lms/lms/doctype/lms_badge/lms_badge.json
|
||||
msgid "User Field"
|
||||
msgstr ""
|
||||
msgstr "Felhasználói Mező"
|
||||
|
||||
#. Label of the user_image (Attach Image) field in DocType 'Course Evaluator'
|
||||
#: lms/lms/doctype/course_evaluator/course_evaluator.json
|
||||
msgid "User Image"
|
||||
msgstr ""
|
||||
msgstr "Profilkép"
|
||||
|
||||
#. Option for the 'Type' (Select) field in DocType 'LMS Question'
|
||||
#. Option for the 'Type' (Select) field in DocType 'LMS Quiz Question'
|
||||
@@ -7459,7 +7459,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:39
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
msgstr "Nézet"
|
||||
|
||||
#: frontend/src/pages/JobDetail.vue:31
|
||||
msgid "View Applications"
|
||||
@@ -7958,7 +7958,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:79
|
||||
msgid "certificate"
|
||||
msgstr ""
|
||||
msgstr "tanúsítvány"
|
||||
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:78
|
||||
msgid "certificates"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
137
lms/sqlite.py
Normal file
137
lms/sqlite.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from contextlib import suppress
|
||||
|
||||
import frappe
|
||||
from frappe.search.sqlite_search import SQLiteSearch, SQLiteSearchIndexMissingError
|
||||
from frappe.utils import nowdate
|
||||
|
||||
|
||||
class LearningSearch(SQLiteSearch):
|
||||
INDEX_NAME = "learning.db"
|
||||
|
||||
INDEX_SCHEMA = {
|
||||
"metadata_fields": [
|
||||
"owner",
|
||||
"published",
|
||||
"published_on",
|
||||
"start_date",
|
||||
"status",
|
||||
"company_name",
|
||||
"creation",
|
||||
],
|
||||
"tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'",
|
||||
}
|
||||
|
||||
INDEXABLE_DOCTYPES = {
|
||||
"LMS Course": {
|
||||
"fields": [
|
||||
"name",
|
||||
"title",
|
||||
{"content": "description"},
|
||||
"short_introduction",
|
||||
"published",
|
||||
"category",
|
||||
"owner",
|
||||
{"modified": "published_on"},
|
||||
],
|
||||
},
|
||||
"LMS Batch": {
|
||||
"fields": [
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
{"content": "batch_details"},
|
||||
"published",
|
||||
"category",
|
||||
"owner",
|
||||
{"modified": "start_date"},
|
||||
],
|
||||
},
|
||||
"Job Opportunity": {
|
||||
"fields": [
|
||||
"name",
|
||||
{"title": "job_title"},
|
||||
{"content": "description"},
|
||||
"owner",
|
||||
"location",
|
||||
"country",
|
||||
"company_name",
|
||||
"status",
|
||||
"creation",
|
||||
{"modified": "creation"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
DOCTYPE_FIELDS = {
|
||||
"LMS Course": [
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"short_introduction",
|
||||
"category",
|
||||
"creation",
|
||||
"modified",
|
||||
"owner",
|
||||
],
|
||||
"LMS Batch": [
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"batch_details",
|
||||
"category",
|
||||
"creation",
|
||||
"modified",
|
||||
"owner",
|
||||
],
|
||||
"Job Opportunity": [
|
||||
"name",
|
||||
"job_title",
|
||||
"company_name",
|
||||
"description",
|
||||
"creation",
|
||||
"modified",
|
||||
"owner",
|
||||
],
|
||||
}
|
||||
|
||||
def build_index(self):
|
||||
try:
|
||||
super().build_index()
|
||||
except Exception as e:
|
||||
frappe.throw(e)
|
||||
|
||||
def get_search_filters(self):
|
||||
return {}
|
||||
|
||||
@SQLiteSearch.scoring_function
|
||||
def get_doctype_boost(self, row, query, query_words):
|
||||
doctype = row["doctype"]
|
||||
if doctype == "LMS Course":
|
||||
if row["published"]:
|
||||
return 1.3
|
||||
elif doctype == "LMS Batch":
|
||||
if row["published"] and row["start_date"] >= nowdate():
|
||||
return 1.3
|
||||
elif row["published"]:
|
||||
return 1.2
|
||||
return 1.0
|
||||
|
||||
|
||||
class LearningSearchIndexMissingError(SQLiteSearchIndexMissingError):
|
||||
pass
|
||||
|
||||
|
||||
def build_index():
|
||||
search = LearningSearch()
|
||||
search.build_index()
|
||||
|
||||
|
||||
def build_index_in_background():
|
||||
if not frappe.cache().get_value("learning_search_indexing_in_progress"):
|
||||
frappe.enqueue(build_index, queue="long")
|
||||
|
||||
|
||||
def build_index_if_not_exists():
|
||||
search = LearningSearch()
|
||||
if not search.index_exists():
|
||||
build_index()
|
||||
@@ -1,168 +0,0 @@
|
||||
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// comment_when is failing because of this
|
||||
if (!frappe.sys_defaults) {
|
||||
frappe.sys_defaults = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/assets/mon_school/js/livecode-files.js"></script>
|
||||
|
||||
<template id="livecode-template">
|
||||
<div class="livecode-editor livecode-editor-inline">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="controls">
|
||||
<button class="run">{{ _("Run") }}</button>
|
||||
|
||||
<div class="exercise-controls pull-right">
|
||||
<span style="padding-right: 10px;"><span class="last-submitted human-time" data-timestamp=""></span></span>
|
||||
<button class="submit btn-primary">{{ _("Submit") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="code-wrapper">
|
||||
<textarea class="code"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||
<div class="svg-image" width="300" height="300"></div>
|
||||
<pre class="output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
function getLiveCodeOptions() {
|
||||
return {
|
||||
base_url: "{{ livecode_url }}",
|
||||
runtime: "python",
|
||||
files: LIVECODE_FILES, // loaded from livecode-files.js
|
||||
command: ["python", "start.py"],
|
||||
codemirror: true,
|
||||
onMessage: {
|
||||
image: function(editor, msg) {
|
||||
const element = editor.parent.querySelector(".svg-image");
|
||||
element.innerHTML = msg.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(function() {
|
||||
var editorLookup = {};
|
||||
|
||||
$("pre.example, pre.exercise").each((i, e) => {
|
||||
var code = $(e).text();
|
||||
var template = document.querySelector('#livecode-template');
|
||||
var clone = template.content.cloneNode(true);
|
||||
|
||||
$(e)
|
||||
.wrap('<div></div>')
|
||||
.hide()
|
||||
.parent()
|
||||
.append(clone)
|
||||
.find("textarea.code")
|
||||
.val(code);
|
||||
|
||||
if ($(e).hasClass("exercise")) {
|
||||
var last_submitted = $(e).data("last-submitted");
|
||||
if (last_submitted) {
|
||||
$(e).parent().find(".last-submitted")
|
||||
.data("timestamp", last_submitted)
|
||||
.html(__("Submitted {0}", [comment_when(last_submitted)]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$(e).parent().find(".exercise-controls").remove();
|
||||
}
|
||||
|
||||
var editor = new LiveCodeEditor(e.parentElement, {
|
||||
...getLiveCodeOptions(),
|
||||
codemirror: true,
|
||||
onMessage: {
|
||||
image: function(editor, msg) {
|
||||
const canvasElement = editor.parent.querySelector("div.svg-image");
|
||||
canvasElement.innerHTML = msg.image;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(e).parent().find(".submit").on('click', function() {
|
||||
var name = $(e).data("name");
|
||||
let code = editor.codemirror.doc.getValue();
|
||||
|
||||
frappe.call("lms.lms.api.submit_solution", {
|
||||
"exercise": name,
|
||||
"code": code
|
||||
}).then(r => {
|
||||
if (r.message.name) {
|
||||
frappe.msgprint("Submitted successfully!");
|
||||
|
||||
let d = r.message.creation;
|
||||
$(e).parent().find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(".exercise-image").each((i, e) => {
|
||||
var svg = JSON.parse($(e).data("image"));
|
||||
$(e).html(svg);
|
||||
});
|
||||
|
||||
$("pre.exercise").each((i, e) => {
|
||||
var svg = JSON.parse($(e).data("image"));
|
||||
$(e).parent().find(".svg-image").html(svg);
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
.svg-image {
|
||||
border: 5px solid #ddd;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 310px;
|
||||
height: 310px;
|
||||
}
|
||||
.livecode-editor {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.livecode-editor-small .svg-image {
|
||||
border: 5px solid #ddd;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
}
|
||||
|
||||
/* work-in-progress styles for showing admonition */
|
||||
.admonition {
|
||||
border: 1px solid #aaa;
|
||||
border-left: .5rem solid #888;
|
||||
border-radius: .3em;
|
||||
font-size: 0.9em;
|
||||
margin: 1.5em 0;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
.admonition-title {
|
||||
padding: 0.5em 0px;
|
||||
font-weight: bold;
|
||||
padding-top:
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
|
||||
|
||||
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
|
||||
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
|
||||
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
|
||||
|
||||
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>
|
||||
@@ -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 %}
|
||||
@@ -1,9 +0,0 @@
|
||||
{% if membership and is_eligible_to_review(course.name) %}
|
||||
<span class="btn btn-secondary btn-sm review-link">
|
||||
{{ _("Write a review") }}
|
||||
</span>
|
||||
{% elif not is_instructor and frappe.session.user == "Guest" %}
|
||||
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
|
||||
{{ _("Write a review") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user