Compare commits

..

1 Commits

Author SHA1 Message Date
Hussain Nagaria
a35638d289 feat: add reply_to in email students 2023-10-17 22:01:04 +05:30
117 changed files with 1018 additions and 2627 deletions

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.frappelms.com/"> <a href="https://www.frappelms.com/">
<img src="https://frappe.io/files/lms.png" alt="Frappe LMS" width="50px" height="50px"> <img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px">
</a> </a>
<p align="center">Easy to use, open source, learning management system.</p> <p align="center">Easy to use, open source, learning management system.</p>
</p> </p>

View File

@@ -1,5 +0,0 @@
# Security Policy
The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process.

View File

@@ -13,6 +13,6 @@ module.exports = defineConfig({
openMode: 0, openMode: 0,
}, },
e2e: { e2e: {
baseUrl: "http://pyp:8000", baseUrl: "http://dd1:8000",
}, },
}); });

View File

@@ -4,8 +4,6 @@
$ git clone https://github.com/frappe/lms.git $ git clone https://github.com/frappe/lms.git
$ cd lms $ cd lms
$ cd docker
``` ```
**Step 2:** Run docker-compose **Step 2:** Run docker-compose

Submodule frappe-ui deleted from 2898a0bdd1

View File

@@ -138,12 +138,12 @@
"label": "User Category", "label": "User Category",
"length": 0, "length": 0,
"mandatory_depends_on": null, "mandatory_depends_on": null,
"modified": "2022-04-19 13:02:18.219510", "modified": "2022-04-19 13:02:18.219508",
"module": "LMS", "module": "LMS",
"name": "User-user_category", "name": "User-user_category",
"no_copy": 0, "no_copy": 0,
"non_negative": 0, "non_negative": 0,
"options": "\nBusiness Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers", "options": "Business Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,

View File

@@ -97,7 +97,8 @@ override_doctype_class = {
# Hook on document methods and events # Hook on document methods and events
doc_events = { doc_events = {
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"}, "Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"},
"Course Lesson": {"on_update": "lms.lms.doctype.lms_quiz.lms_quiz.update_lesson_info"},
} }
# Scheduled Tasks # Scheduled Tasks
@@ -118,9 +119,9 @@ fixtures = ["Custom Field", "Function", "Industry"]
# Overriding Methods # Overriding Methods
# ------------------------------ # ------------------------------
# #
override_whitelisted_methods = { # override_whitelisted_methods = {
# "frappe.desk.search.get_names_for_mentions": "lms.lms.utils.get_names_for_mentions", # "frappe.desk.doctype.event.event.get_events": "lms.event.get_events"
} # }
# #
# each overriding function accepts a `data` argument; # each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard, # generated from the base implementation of the doctype dashboard,
@@ -173,8 +174,7 @@ website_route_rules = [
"to_route": "cohorts/join", "to_route": "cohorts/join",
}, },
{"from_route": "/users", "to_route": "profiles/profile"}, {"from_route": "/users", "to_route": "profiles/profile"},
{"from_route": "/job-openings", "to_route": "jobs_openings/index"}, {"from_route": "/jobs/<job>", "to_route": "jobs/job"},
{"from_route": "/job-openings/<job>", "to_route": "jobs_openings/job"},
{ {
"from_route": "/batches/<batchname>/students/<username>", "from_route": "/batches/<batchname>/students/<username>",
"to_route": "/batches/progress", "to_route": "/batches/progress",

View File

@@ -4,11 +4,11 @@ from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
def after_install(): def after_install():
add_pages_to_nav() add_pages_to_nav()
create_batch_source()
def after_sync(): def after_sync():
create_lms_roles() create_lms_roles()
set_default_home()
set_default_certificate_print_format() set_default_certificate_print_format()
add_all_roles_to("Administrator") add_all_roles_to("Administrator")
@@ -19,7 +19,7 @@ def add_pages_to_nav():
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2}, {"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3}, {"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4}, {"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
{"label": "Jobs", "url": "/job-openings", "parent": "Explore", "idx": 5}, {"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5},
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6}, {"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
] ]
@@ -64,6 +64,10 @@ def delete_lms_roles():
frappe.db.delete("Role", role) frappe.db.delete("Role", role)
def set_default_home():
frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses")
def create_course_creator_role(): def create_course_creator_role():
if not frappe.db.exists("Role", "Course Creator"): if not frappe.db.exists("Role", "Course Creator"):
role = frappe.get_doc( role = frappe.get_doc(
@@ -178,20 +182,3 @@ def delete_custom_fields():
for field in fields: for field in fields:
frappe.db.delete("Custom Field", {"fieldname": field}) frappe.db.delete("Custom Field", {"fieldname": field})
def create_batch_source():
sources = [
"Newsletter",
"LinkedIn",
"Twitter",
"Website",
"Friend/Colleague/Connection",
"Google Search",
]
for source in sources:
if not frappe.db.exists("LMS Source", source):
doc = frappe.new_doc("LMS Source")
doc.source = source
doc.save()

View File

@@ -4,6 +4,6 @@
frappe.ui.form.on("Job Opportunity", { frappe.ui.form.on("Job Opportunity", {
refresh: (frm) => { refresh: (frm) => {
if (frm.doc.name) if (frm.doc.name)
frm.add_web_link(`/job-openings/${frm.doc.name}`, "See on Website"); frm.add_web_link(`/jobs/${frm.doc.name}`, "See on Website");
}, },
}); });

View File

@@ -1,7 +1,7 @@
frappe.ready(function () { frappe.ready(function () {
frappe.web_form.after_save = () => { frappe.web_form.after_save = () => {
setTimeout(() => { setTimeout(() => {
window.location.href = `/job-openings`; window.location.href = `/jobs`;
}); });
}; };
}); });

View File

@@ -20,7 +20,7 @@
"list_columns": [], "list_columns": [],
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2022-09-15 17:22:43.957185", "modified": "2022-09-15 17:22:43.957184",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Job", "module": "Job",
"name": "job-opportunity", "name": "job-opportunity",
@@ -32,7 +32,7 @@
"show_list": 1, "show_list": 1,
"show_sidebar": 0, "show_sidebar": 0,
"success_message": "", "success_message": "",
"success_url": "/job-openings", "success_url": "/jobs",
"title": "Job Opportunity", "title": "Job Opportunity",
"web_form_fields": [ "web_form_fields": [
{ {

View File

@@ -9,12 +9,11 @@
"field_order": [ "field_order": [
"student_details_section", "student_details_section",
"student", "student",
"student_name",
"username",
"column_break_oduu",
"payment", "payment",
"source", "confirmation_email_sent",
"confirmation_email_sent" "column_break_oduu",
"student_name",
"username"
], ],
"fields": [ "fields": [
{ {
@@ -60,18 +59,12 @@
"fieldname": "confirmation_email_sent", "fieldname": "confirmation_email_sent",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Confirmation Email Sent" "label": "Confirmation Email Sent"
},
{
"fieldname": "source",
"fieldtype": "Link",
"label": "Source",
"options": "LMS Source"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-26 16:52:04.266693", "modified": "2023-10-09 17:09:50.481794",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Batch Student", "name": "Batch Student",

View File

@@ -1,23 +1,9 @@
# Copyright (c) 2022, Frappe and contributors # Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt # For license information, please see license.txt
import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class BatchStudent(Document): class BatchStudent(Document):
pass pass
@frappe.whitelist()
def enroll_batch(batch_name):
if frappe.db.exists(
"Batch Student", {"student": frappe.session.user, "parent": batch_name}
):
frappe.throw("You are already enrolled in this batch")
enrollment = frappe.new_doc("Batch Student")
enrollment.student = frappe.session.user
enrollment.parent = batch_name
enrollment.parentfield = "students"
enrollment.parenttype = "LMS Batch"
enrollment.save(ignore_permissions=True)

View File

@@ -99,14 +99,8 @@ def save_progress(lesson, course, status):
quizzes = [value for name, value in macros if name == "Quiz"] quizzes = [value for name, value in macros if name == "Quiz"]
for quiz in quizzes: for quiz in quizzes:
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
if not frappe.db.exists( if not frappe.db.exists(
"LMS Quiz Submission", "LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user}
{
"quiz": quiz,
"owner": frappe.session.user,
"percentage": [">=", passing_percentage],
},
): ):
return 0 return 0

View File

@@ -11,11 +11,7 @@ from frappe.utils.password import get_decrypted_password
class InviteRequest(Document): class InviteRequest(Document):
def on_update(self): def on_update(self):
if ( if self.has_value_changed("status") and self.status == "Approved":
self.has_value_changed("status")
and self.status == "Approved"
and not frappe.flags.in_test
):
self.send_email() self.send_email()
def create_user(self, password): def create_user(self, password):

View File

@@ -2,13 +2,6 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("LMS Assignment Submission", { frappe.ui.form.on("LMS Assignment Submission", {
onload: function (frm) { // refresh: function(frm) {
frm.set_query("member", function (doc) { // }
return {
filters: {
ignore_user_type: 1,
},
};
});
},
}); });

View File

@@ -4,18 +4,13 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import validate_url, validate_email_address from frappe.utils import validate_url
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSAssignmentSubmission(Document): class LMSAssignmentSubmission(Document):
def validate(self): def validate(self):
self.validate_duplicates() self.validate_duplicates()
def after_insert(self):
if not frappe.flags.in_test:
self.send_mail()
def validate_duplicates(self): def validate_duplicates(self):
if frappe.db.exists( if frappe.db.exists(
"LMS Assignment Submission", "LMS Assignment Submission",
@@ -28,38 +23,6 @@ class LMSAssignmentSubmission(Document):
) )
) )
def send_mail(self):
subject = _("New Assignment Submission")
template = "assignment_submission"
custom_template = frappe.db.get_single_value(
"LMS Settings", "assignment_submission_template"
)
args = {
"member_name": self.member_name,
"assignment_name": self.assignment,
"assignment_title": self.assignment_title,
"submission_name": self.name,
}
moderators = frappe.get_all("Has Role", {"role": "Moderator"}, pluck="parent")
for moderator in moderators:
if not validate_email_address(moderator):
moderators.remove(moderator)
if custom_template:
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=moderators,
subject=subject,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
)
@frappe.whitelist() @frappe.whitelist()
def upload_assignment( def upload_assignment(

View File

@@ -28,19 +28,11 @@ frappe.ui.form.on("LMS Batch", {
}, },
}; };
}); });
if (frm.doc.timetable.length && !frm.doc.timetable_legends.length) {
set_default_legends(frm);
}
}, },
timetable_template: function (frm) { timetable_template: function (frm) {
set_timetable(frm); set_timetable(frm);
}, },
refresh: (frm) => {
frm.add_web_link(`/batches/details/${frm.doc.name}`, "See on website");
},
}); });
const set_timetable = (frm) => { const set_timetable = (frm) => {
@@ -60,7 +52,6 @@ const set_timetable = (frm) => {
"start_time", "start_time",
"end_time", "end_time",
"duration", "duration",
"milestone",
], ],
filters: { filters: {
parent: frm.doc.timetable_template, parent: frm.doc.timetable_template,
@@ -91,7 +82,6 @@ const add_timetable_rows = (frm, timetable) => {
.format("HH:mm") .format("HH:mm")
: null; : null;
child.duration = row.duration; child.duration = row.duration;
child.milestone = row.milestone;
}); });
frm.refresh_field("timetable"); frm.refresh_field("timetable");
@@ -131,37 +121,3 @@ const add_legend_rows = (frm, legends) => {
frm.refresh_field("timetable_legends"); frm.refresh_field("timetable_legends");
frm.save(); frm.save();
}; };
const set_default_legends = (frm) => {
const data = [
{
reference_doctype: "Course Lesson",
label: "Lesson",
color: "#449CF0",
},
{
reference_doctype: "LMS Quiz",
label: "LMS Quiz",
color: "#39E4A5",
},
{
reference_doctype: "LMS Assignment",
label: "LMS Assignment",
color: "#ECAD4B",
},
{
reference_doctype: "LMS Live Class",
label: "LMS Live Class",
color: "#bb8be8",
},
];
data.forEach((detail) => {
let child = frm.add_child("timetable_legends");
child.reference_doctype = detail.reference_doctype;
child.label = detail.label;
child.color = detail.color;
});
frm.refresh_field("timetable_legends");
frm.save();
};

View File

@@ -15,13 +15,11 @@
"start_time", "start_time",
"end_time", "end_time",
"published", "published",
"allow_self_enrollment",
"section_break_rgfj", "section_break_rgfj",
"medium", "medium",
"category", "category",
"column_break_flwy", "column_break_flwy",
"seat_count", "seat_count",
"evaluation_end_date",
"section_break_6", "section_break_6",
"description", "description",
"batch_details_raw", "batch_details_raw",
@@ -47,7 +45,6 @@
"column_break_iens", "column_break_iens",
"amount", "amount",
"currency", "currency",
"amount_usd",
"customisations_tab", "customisations_tab",
"section_break_ubxi", "section_break_ubxi",
"custom_component", "custom_component",
@@ -123,14 +120,12 @@
{ {
"fieldname": "start_time", "fieldname": "start_time",
"fieldtype": "Time", "fieldtype": "Time",
"label": "Start Time", "label": "Start Time"
"reqd": 1
}, },
{ {
"fieldname": "end_time", "fieldname": "end_time",
"fieldtype": "Time", "fieldtype": "Time",
"label": "End Time", "label": "End Time"
"reqd": 1
}, },
{ {
"fieldname": "assessment_tab", "fieldname": "assessment_tab",
@@ -282,29 +277,11 @@
"fieldname": "allow_future", "fieldname": "allow_future",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow accessing future dates" "label": "Allow accessing future dates"
},
{
"fieldname": "evaluation_end_date",
"fieldtype": "Date",
"label": "Evaluation End Date"
},
{
"depends_on": "paid_batch",
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
"fieldname": "amount_usd",
"fieldtype": "Currency",
"label": "Amount (USD)"
},
{
"default": "0",
"fieldname": "allow_self_enrollment",
"fieldtype": "Check",
"label": "Allow Self Enrollment"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-01-22 10:42:42.872995", "modified": "2023-10-12 12:53:37.351989",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -12,7 +12,9 @@ from frappe.utils import (
cint, cint,
format_date, format_date,
format_datetime, format_datetime,
get_time, add_to_date,
getdate,
get_datetime,
) )
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
from lms.www.utils import get_quiz_details, get_assignment_details from lms.www.utils import get_quiz_details, get_assignment_details
@@ -29,7 +31,6 @@ class LMSBatch(Document):
self.validate_membership() self.validate_membership()
self.validate_timetable() self.validate_timetable()
self.send_confirmation_mail() self.send_confirmation_mail()
self.validate_evaluation_end_date()
def validate_duplicate_students(self): def validate_duplicate_students(self):
students = [row.student for row in self.students] students = [row.student for row in self.students]
@@ -65,14 +66,11 @@ class LMSBatch(Document):
def send_confirmation_mail(self): def send_confirmation_mail(self):
for student in self.students: for student in self.students:
if not student.confirmation_email_sent: if not student.confirmation_email_sent:
self.send_mail(student) self.send_mail(student)
student.confirmation_email_sent = 1 student.confirmation_email_sent = 1
def validate_evaluation_end_date(self):
if self.evaluation_end_date and self.evaluation_end_date < self.end_date:
frappe.throw(_("Evaluation end date cannot be less than the batch end date."))
def send_mail(self, student): def send_mail(self, student):
subject = _("Enrollment Confirmation for the Next Training Batch") subject = _("Enrollment Confirmation for the Next Training Batch")
template = "batch_confirmation" template = "batch_confirmation"
@@ -121,27 +119,23 @@ class LMSBatch(Document):
def validate_timetable(self): def validate_timetable(self):
for schedule in self.timetable: for schedule in self.timetable:
if schedule.start_time and schedule.end_time: if schedule.start_time and schedule.end_time:
if get_time(schedule.start_time) > get_time(schedule.end_time) or get_time( if (
schedule.start_time schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
) == get_time(schedule.end_time): ):
frappe.throw( frappe.throw(
_("Row #{0} Start time cannot be greater than or equal to end time.").format( _("Row #{0} Start time cannot be greater than or equal to end time.").format(
schedule.idx schedule.idx
) )
) )
if get_time(schedule.start_time) < get_time(self.start_time) or get_time( if schedule.start_time < self.start_time or schedule.start_time > self.end_time:
schedule.start_time
) > get_time(self.end_time):
frappe.throw( frappe.throw(
_("Row #{0} Start time cannot be outside the batch duration.").format( _("Row #{0} Start time cannot be outside the batch duration.").format(
schedule.idx schedule.idx
) )
) )
if get_time(schedule.end_time) < get_time(self.start_time) or get_time( if schedule.end_time < self.start_time or schedule.end_time > self.end_time:
schedule.end_time
) > get_time(self.end_time):
frappe.throw( frappe.throw(
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx) _("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
) )
@@ -256,10 +250,8 @@ def create_batch(
paid_batch=0, paid_batch=0,
amount=0, amount=0,
currency=None, currency=None,
amount_usd=0,
name=None, name=None,
published=0, published=0,
evaluation_end_date=None,
): ):
frappe.only_for("Moderator") frappe.only_for("Moderator")
if name: if name:
@@ -275,7 +267,7 @@ def create_batch(
"description": description, "description": description,
"batch_details": batch_details, "batch_details": batch_details,
"batch_details_raw": batch_details_raw, "batch_details_raw": batch_details_raw,
"meta_image": meta_image, "image": meta_image,
"seat_count": seat_count, "seat_count": seat_count,
"start_time": start_time, "start_time": start_time,
"end_time": end_time, "end_time": end_time,
@@ -284,9 +276,7 @@ def create_batch(
"paid_batch": paid_batch, "paid_batch": paid_batch,
"amount": amount, "amount": amount,
"currency": currency, "currency": currency,
"amount_usd": amount_usd,
"published": published, "published": published,
"evaluation_end_date": evaluation_end_date,
} }
) )
doc.save() doc.save()
@@ -335,17 +325,7 @@ def get_batch_timetable(batch):
timetable = frappe.get_all( timetable = frappe.get_all(
"LMS Batch Timetable", "LMS Batch Timetable",
filters={"parent": batch}, filters={"parent": batch},
fields=[ fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"],
"reference_doctype",
"reference_docname",
"date",
"start_time",
"end_time",
"milestone",
"name",
"idx",
"parent",
],
order_by="date", order_by="date",
) )
@@ -382,26 +362,20 @@ def get_timetable_details(timetable):
assessment = frappe._dict({"assessment_name": entry.reference_docname}) assessment = frappe._dict({"assessment_name": entry.reference_docname})
if entry.reference_doctype == "Course Lesson": if entry.reference_doctype == "Course Lesson":
entry.icon = "icon-list"
course = frappe.db.get_value( course = frappe.db.get_value(
entry.reference_doctype, entry.reference_docname, "course" entry.reference_doctype, entry.reference_docname, "course"
) )
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname)) entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
entry.completed = (
True
if frappe.db.exists(
"LMS Course Progress",
{"lesson": entry.reference_docname, "member": frappe.session.user},
)
else False
)
elif entry.reference_doctype == "LMS Quiz": elif entry.reference_doctype == "LMS Quiz":
entry.icon = "icon-quiz"
entry.url = "/quizzes" entry.url = "/quizzes"
details = get_quiz_details(assessment, frappe.session.user) details = get_quiz_details(assessment, frappe.session.user)
entry.update(details) entry.update(details)
elif entry.reference_doctype == "LMS Assignment": elif entry.reference_doctype == "LMS Assignment":
entry.icon = "icon-quiz"
details = get_assignment_details(assessment, frappe.session.user) details = get_assignment_details(assessment, frappe.session.user)
entry.update(details) entry.update(details)
@@ -410,37 +384,12 @@ def get_timetable_details(timetable):
@frappe.whitelist() @frappe.whitelist()
def is_milestone_complete(idx, batch): def send_email_to_students(batch, subject, reply_to, message):
previous_rows = frappe.get_all( frappe.only_for("Moderator")
"LMS Batch Timetable", students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
filters={"parent": batch, "idx": ["<", cint(idx)]}, frappe.sendmail(
fields=["reference_doctype", "reference_docname", "idx"], recipients=students,
order_by="idx", subject=subject,
reply_to=reply_to,
message=message
) )
for row in previous_rows:
if row.reference_doctype == "Course Lesson":
if not frappe.db.exists(
"LMS Course Progress",
{"member": frappe.session.user, "lesson": row.reference_docname},
):
return False
if row.reference_doctype == "LMS Quiz":
passing_percentage = frappe.db.get_value(
row.reference_doctype, row.reference_docname, "passing_percentage"
)
if not frappe.db.exists(
"LMS Quiz Submission",
{"quiz": row.reference_docname, "member": frappe.session.user},
):
return False
if row.reference_doctype == "LMS Assignment":
if not frappe.db.exists(
"LMS Assignment Submission",
{"assignment": row.reference_docname, "member": frappe.session.user},
):
return False
return True

View File

@@ -16,8 +16,7 @@
"column_break_merq", "column_break_merq",
"start_time", "start_time",
"end_time", "end_time",
"duration", "duration"
"milestone"
], ],
"fields": [ "fields": [
{ {
@@ -70,17 +69,12 @@
"fieldname": "day", "fieldname": "day",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Day" "label": "Day"
},
{
"fieldname": "milestone",
"fieldtype": "Check",
"label": "Milestone"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-20 11:58:01.782921", "modified": "2023-10-03 17:40:31.530181",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch Timetable", "name": "LMS Batch Timetable",

View File

@@ -10,14 +10,6 @@ frappe.ui.form.on("LMS Certificate", {
}, },
}; };
}); });
frm.set_query("template", function (doc) {
return {
filters: {
doc_type: "LMS Certificate",
},
};
});
}, },
refresh: (frm) => { refresh: (frm) => {
if (frm.doc.name) if (frm.doc.name)

View File

@@ -8,12 +8,11 @@
"course", "course",
"member", "member",
"member_name", "member_name",
"template", "published",
"column_break_3", "column_break_3",
"issue_date", "issue_date",
"expiry_date", "expiry_date",
"batch_name", "batch_name"
"published"
], ],
"fields": [ "fields": [
{ {
@@ -68,18 +67,11 @@
"fieldname": "published", "fieldname": "published",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Publish on Participant Page" "label": "Publish on Participant Page"
},
{
"fieldname": "template",
"fieldtype": "Link",
"label": "Template",
"options": "Print Format",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-10-25 12:20:56.091979", "modified": "2023-09-13 11:03:23.479255",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate", "name": "LMS Certificate",

View File

@@ -6,42 +6,12 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_years, nowdate from frappe.utils import add_years, nowdate
from lms.lms.utils import is_certified from lms.lms.utils import is_certified
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSCertificate(Document): class LMSCertificate(Document):
def validate(self): def validate(self):
self.validate_duplicate_certificate() self.validate_duplicate_certificate()
def after_insert(self):
if not frappe.flags.in_test:
self.send_mail()
def send_mail(self):
subject = _("Congratulations on getting certified!")
template = "certification"
custom_template = frappe.db.get_single_value("LMS Settings", "certification_template")
args = {
"student_name": self.member_name,
"course_name": self.course,
"course_title": frappe.db.get_value("LMS Course", self.course, "title"),
"certificate_name": self.name,
}
if custom_template:
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=self.member,
subject=subject,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
)
def validate_duplicate_certificate(self): def validate_duplicate_certificate(self):
certificates = frappe.get_all( certificates = frappe.get_all(
"LMS Certificate", "LMS Certificate",
@@ -78,15 +48,6 @@ def create_certificate(course):
if expires_after_yrs: if expires_after_yrs:
expiry_date = add_years(nowdate(), expires_after_yrs) expiry_date = add_years(nowdate(), expires_after_yrs)
default_certificate_template = frappe.db.get_value(
"Property Setter",
{
"doc_type": "LMS Certificate",
"property": "default_print_format",
},
"value",
)
certificate = frappe.get_doc( certificate = frappe.get_doc(
{ {
"doctype": "LMS Certificate", "doctype": "LMS Certificate",
@@ -94,7 +55,6 @@ def create_certificate(course):
"course": course, "course": course,
"issue_date": nowdate(), "issue_date": nowdate(),
"expiry_date": expiry_date, "expiry_date": expiry_date,
"template": default_certificate_template,
} }
) )
certificate.save(ignore_permissions=True) certificate.save(ignore_permissions=True)

View File

@@ -47,13 +47,12 @@
"fieldtype": "Rating", "fieldtype": "Rating",
"in_list_view": 1, "in_list_view": 1,
"label": "Rating", "label": "Rating",
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'" "reqd": 1
}, },
{ {
"fieldname": "summary", "fieldname": "summary",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Summary", "label": "Summary"
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
}, },
{ {
"fieldname": "date", "fieldname": "date",
@@ -107,7 +106,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-12-18 20:03:27.040073", "modified": "2023-09-26 19:44:43.594892",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate Evaluation", "name": "LMS Certificate Evaluation",

View File

@@ -2,19 +2,13 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from lms.lms.utils import has_course_moderator_role from lms.lms.utils import has_course_moderator_role
class LMSCertificateEvaluation(Document): class LMSCertificateEvaluation(Document):
def validate(self): pass
self.validate_rating()
def validate_rating(self):
if self.status not in ["Pending", "In Progress"] and self.rating == 0:
frappe.throw(_("Rating cannot be 0"))
def has_website_permission(doc, ptype, user, verbose=False): def has_website_permission(doc, ptype, user, verbose=False):

View File

@@ -103,13 +103,13 @@
"fieldname": "batch_name", "fieldname": "batch_name",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Batch", "label": "Batch Name",
"options": "LMS Batch" "options": "LMS Batch"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-11-29 15:00:30.617298", "modified": "2023-08-23 14:50:37.618352",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate Request", "name": "LMS Certificate Request",

View File

@@ -11,20 +11,7 @@ from lms.lms.utils import get_evaluator
class LMSCertificateRequest(Document): class LMSCertificateRequest(Document):
def validate(self): def validate(self):
self.validate_slot()
self.validate_if_existing_requests() self.validate_if_existing_requests()
self.validate_evaluation_end_date()
def validate_slot(self):
if frappe.db.exists(
"LMS Certificate Request",
{
"evaluator": self.evaluator,
"date": self.date,
"start_time": self.start_time,
},
):
frappe.throw(_("The slot is already booked by another participant."))
def validate_if_existing_requests(self): def validate_if_existing_requests(self):
existing_requests = frappe.get_all( existing_requests = frappe.get_all(
@@ -45,20 +32,6 @@ class LMSCertificateRequest(Document):
) )
) )
def validate_evaluation_end_date(self):
if self.batch_name:
evaluation_end_date = frappe.db.get_value(
"LMS Batch", self.batch_name, "evaluation_end_date"
)
if evaluation_end_date:
if getdate(self.date) > getdate(evaluation_end_date):
frappe.throw(
_("You cannot schedule evaluations after {0}.").format(
format_date(evaluation_end_date, "medium")
)
)
def schedule_evals(): def schedule_evals():
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"): if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
@@ -131,9 +104,7 @@ def update_meeting_details(eval, event, calendar):
@frappe.whitelist() @frappe.whitelist()
def create_certificate_request( def create_certificate_request(course, date, day, start_time, end_time, batch=None):
course, date, day, start_time, end_time, batch_name=None
):
is_member = frappe.db.exists( is_member = frappe.db.exists(
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user} {"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
) )
@@ -144,13 +115,13 @@ def create_certificate_request(
eval.update( eval.update(
{ {
"course": course, "course": course,
"evaluator": get_evaluator(course, batch_name), "evaluator": get_evaluator(course, batch),
"member": frappe.session.user, "member": frappe.session.user,
"date": date, "date": date,
"day": day, "day": day,
"start_time": start_time, "start_time": start_time,
"end_time": end_time, "end_time": end_time,
"batch_name": batch_name, "batch": batch,
} }
) )
eval.save(ignore_permissions=True) eval.save(ignore_permissions=True)

View File

@@ -34,10 +34,8 @@
"related_courses", "related_courses",
"pricing_section", "pricing_section",
"paid_course", "paid_course",
"column_break_acoj",
"course_price",
"currency", "currency",
"amount_usd", "course_price",
"certification_section", "certification_section",
"enable_certification", "enable_certification",
"expiry", "expiry",
@@ -224,22 +222,12 @@
"fieldname": "course_price", "fieldname": "course_price",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Course Price", "label": "Course Price",
"option": "currency",
"mandatory_depends_on": "paid_course" "mandatory_depends_on": "paid_course"
}, },
{ {
"fieldname": "column_break_rxww", "fieldname": "column_break_rxww",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "column_break_acoj",
"fieldtype": "Column Break"
},
{
"depends_on": "paid_course",
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
"fieldname": "amount_usd",
"fieldtype": "Currency",
"label": "Amount (USD)"
} }
], ],
"is_published_field": "published", "is_published_field": "published",
@@ -266,7 +254,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-12-21 12:27:32.559901", "modified": "2023-08-28 11:09:11.945066",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -1,14 +1,8 @@
// Copyright (c) 2023, Frappe and contributors // Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("LMS Payment", { // frappe.ui.form.on("LMS Payment", {
onload(frm) { // refresh(frm) {
frm.set_query("member", function (doc) {
return { // },
filters: { // });
ignore_user_type: 1,
},
};
});
},
});

View File

@@ -8,11 +8,8 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"payment_for_document_type",
"member", "member",
"source",
"column_break_rqkd", "column_break_rqkd",
"payment_for_document",
"billing_name", "billing_name",
"payment_received", "payment_received",
"payment_details_section", "payment_details_section",
@@ -118,29 +115,11 @@
"fieldname": "amount_with_gst", "fieldname": "amount_with_gst",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount with GST" "label": "Amount with GST"
},
{
"fieldname": "payment_for_document_type",
"fieldtype": "Select",
"label": "Payment for Document Type",
"options": "\nLMS Course\nLMS Batch"
},
{
"fieldname": "payment_for_document",
"fieldtype": "Dynamic Link",
"label": "Payment for Document",
"options": "payment_for_document_type"
},
{
"fieldname": "source",
"fieldtype": "Link",
"label": "Source",
"options": "LMS Source"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-10-26 16:54:12.408274", "modified": "2023-09-12 10:40:22.721371",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Payment", "name": "LMS Payment",

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Question", {
// refresh(frm) {
// },
// });

View File

@@ -1,245 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:QTS-{YYYY}-{#####}",
"creation": "2023-10-10 10:24:14.035772",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"question",
"type",
"multiple",
"section_break_ytxi",
"option_1",
"is_correct_1",
"column_break_fpvl",
"explanation_1",
"section_break_eiaa",
"option_2",
"is_correct_2",
"column_break_akwy",
"explanation_2",
"section_break_cwqv",
"option_3",
"is_correct_3",
"column_break_atpl",
"explanation_3",
"section_break_yqel",
"option_4",
"is_correct_4",
"column_break_lknb",
"explanation_4",
"section_break_hkfe",
"possibility_1",
"possibility_3",
"column_break_wpjr",
"possibility_2",
"possibility_4"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Question"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Choices\nUser Input"
},
{
"depends_on": "eval:doc.type == \"Choices\";",
"fieldname": "section_break_ytxi",
"fieldtype": "Section Break"
},
{
"fieldname": "option_1",
"fieldtype": "Small Text",
"label": "Option 1",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"default": "0",
"fieldname": "is_correct_1",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_fpvl",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_1",
"fieldtype": "Small Text",
"label": "Explanation"
},
{
"depends_on": "eval:doc.type == \"Choices\";",
"fieldname": "section_break_eiaa",
"fieldtype": "Section Break"
},
{
"fieldname": "option_2",
"fieldtype": "Small Text",
"label": "Option 2",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"default": "0",
"fieldname": "is_correct_2",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_akwy",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_2",
"fieldtype": "Small Text",
"label": "Explanation "
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_cwqv",
"fieldtype": "Section Break"
},
{
"fieldname": "option_3",
"fieldtype": "Small Text",
"label": "Option 3"
},
{
"default": "0",
"fieldname": "is_correct_3",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_atpl",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_3",
"fieldtype": "Small Text",
"label": "Explanation"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_yqel",
"fieldtype": "Section Break"
},
{
"fieldname": "option_4",
"fieldtype": "Small Text",
"label": "Option 4"
},
{
"default": "0",
"fieldname": "is_correct_4",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_lknb",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_4",
"fieldtype": "Small Text",
"label": "Explanation"
},
{
"default": "0",
"fieldname": "multiple",
"fieldtype": "Check",
"hidden": 1,
"label": "Multiple Correct Answers"
},
{
"depends_on": "eval: doc.type == 'User Input'",
"fieldname": "section_break_hkfe",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_wpjr",
"fieldtype": "Column Break"
},
{
"fieldname": "possibility_1",
"fieldtype": "Small Text",
"label": "Possible Answer 1",
"mandatory_depends_on": "eval: doc.type == 'User Input'"
},
{
"fieldname": "possibility_3",
"fieldtype": "Small Text",
"label": "Possible Answer 3"
},
{
"fieldname": "possibility_2",
"fieldtype": "Small Text",
"label": "Possible Answer 2"
},
{
"fieldname": "possibility_4",
"fieldtype": "Small Text",
"label": "Possible Answer 4"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-18 21:58:42.653317",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Question",
"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
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "question"
}

View File

@@ -1,92 +0,0 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
class LMSQuestion(Document):
def validate(self):
validate_correct_answers(self)
def validate_correct_answers(question):
if question.type == "Choices":
validate_duplicate_options(question)
validate_correct_options(question)
else:
validate_possible_answer(question)
def validate_duplicate_options(question):
options = []
for num in range(1, 5):
if question.get(f"option_{num}"):
options.append(question.get(f"option_{num}"))
if len(set(options)) != len(options):
frappe.throw(_("Duplicate options found for this question."))
def validate_correct_options(question):
correct_options = get_correct_options(question)
if len(correct_options) > 1:
question.multiple = 1
if not len(correct_options):
frappe.throw(_("At least one option must be correct for this question."))
def validate_possible_answer(question):
possible_answers = []
possible_answers_fields = [
"possibility_1",
"possibility_2",
"possibility_3",
"possibility_4",
]
for field in possible_answers_fields:
if question.get(field):
possible_answers.append(field)
if not len(possible_answers):
frappe.throw(
_("Add at least one possible answer for this question: {0}").format(
frappe.bold(question.question)
)
)
def get_correct_options(question):
correct_options = []
correct_option_fields = [
"is_correct_1",
"is_correct_2",
"is_correct_3",
"is_correct_4",
]
for field in correct_option_fields:
if question.get(field) == 1:
correct_options.append(field)
return correct_options
@frappe.whitelist()
def get_question_details(question):
if not has_course_instructor_role() or not has_course_moderator_role():
return
fields = ["question", "type", "name"]
for i in range(1, 5):
fields.append(f"option_{i}")
fields.append(f"is_correct_{i}")
fields.append(f"explanation_{i}")
fields.append(f"possibility_{i}")
return frappe.db.get_value("LMS Question", question, fields, as_dict=1)

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSQuestion(FrappeTestCase):
pass

View File

@@ -5,13 +5,3 @@ frappe.ui.form.on("LMS Quiz", {
// refresh: function(frm) { // refresh: function(frm) {
// } // }
}); });
frappe.ui.form.on("LMS Quiz Question", {
marks: function (frm) {
total_marks = 0;
frm.doc.questions.forEach((question) => {
total_marks += question.marks;
});
frm.doc.total_marks = total_marks;
},
});

View File

@@ -12,10 +12,6 @@
"column_break_gaac", "column_break_gaac",
"max_attempts", "max_attempts",
"show_submission_history", "show_submission_history",
"section_break_hsiv",
"passing_percentage",
"column_break_rocd",
"total_marks",
"section_break_sbjx", "section_break_sbjx",
"questions", "questions",
"section_break_3", "section_break_3",
@@ -47,7 +43,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "0", "default": "1",
"fieldname": "max_attempts", "fieldname": "max_attempts",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Max Attempts" "label": "Max Attempts"
@@ -94,35 +90,11 @@
"fieldname": "show_submission_history", "fieldname": "show_submission_history",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Submission History" "label": "Show Submission History"
},
{
"fieldname": "section_break_hsiv",
"fieldtype": "Section Break"
},
{
"fieldname": "passing_percentage",
"fieldtype": "Int",
"label": "Passing Percentage",
"non_negative": 1,
"reqd": 1
},
{
"fieldname": "column_break_rocd",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "total_marks",
"fieldtype": "Int",
"label": "Total Marks",
"non_negative": 1,
"read_only": 1,
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-11-07 10:11:49.126789", "modified": "2023-07-04 15:26:24.457745",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",
@@ -151,18 +123,6 @@
"role": "Moderator", "role": "Moderator",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
} }
], ],
"show_title_field_in_link": 1, "show_title_field_in_link": 1,

View File

@@ -5,8 +5,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, comma_and from frappe.utils import cstr
from lms.lms.doctype.lms_question.lms_question import validate_correct_answers
from lms.lms.utils import ( from lms.lms.utils import (
generate_slug, generate_slug,
has_course_moderator_role, has_course_moderator_role,
@@ -15,22 +14,13 @@ from lms.lms.utils import (
class LMSQuiz(Document): class LMSQuiz(Document):
def validate(self):
self.validate_duplicate_questions()
self.total_marks = set_total_marks(self.name, self.questions)
def validate_duplicate_questions(self):
questions = [row.question for row in self.questions]
rows = [i + 1 for i, x in enumerate(questions) if questions.count(x) > 1]
if len(rows):
frappe.throw(
_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows)))
)
def autoname(self): def autoname(self):
if not self.name: if not self.name:
self.name = generate_slug(self.title, "LMS Quiz") self.name = generate_slug(self.title, "LMS Quiz")
def validate(self):
validate_correct_answers(self.questions)
def get_last_submission_details(self): def get_last_submission_details(self):
"""Returns the latest submission for this user.""" """Returns the latest submission for this user."""
user = frappe.session.user user = frappe.session.user
@@ -49,11 +39,76 @@ class LMSQuiz(Document):
return result[0] return result[0]
def set_total_marks(quiz, questions): def get_correct_options(question):
marks = 0 correct_option_fields = [
"is_correct_1",
"is_correct_2",
"is_correct_3",
"is_correct_4",
]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def validate_correct_answers(questions):
for question in questions: for question in questions:
marks += question.get("marks") if question.type == "Choices":
return marks validate_duplicate_options(question)
validate_correct_options(question)
else:
validate_possible_answer(question)
def validate_duplicate_options(question):
options = []
for num in range(1, 5):
if question.get(f"option_{num}"):
options.append(question.get(f"option_{num}"))
if len(set(options)) != len(options):
frappe.throw(
_("Duplicate options found for this question: {0}").format(
frappe.bold(question.question)
)
)
def validate_correct_options(question):
correct_options = get_correct_options(question)
if len(correct_options) > 1:
question.multiple = 1
if not len(correct_options):
frappe.throw(
_("At least one option must be correct for this question: {0}").format(
frappe.bold(question.question)
)
)
def validate_possible_answer(question):
possible_answers_fields = [
"possibility_1",
"possibility_2",
"possibility_3",
"possibility_4",
]
possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields))
if not len(possible_answers):
frappe.throw(
_("Add at least one possible answer for this question: {0}").format(
frappe.bold(question.question)
)
)
def update_lesson_info(doc, method):
if doc.quiz_id:
frappe.db.set_value(
"LMS Quiz", doc.quiz_id, {"lesson": doc.name, "course": doc.course}
)
@frappe.whitelist() @frappe.whitelist()
@@ -63,73 +118,45 @@ def quiz_summary(quiz, results):
for result in results: for result in results:
correct = result["is_correct"][0] correct = result["is_correct"][0]
result["question"] = frappe.db.get_value(
"LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"] + 1},
["question"],
)
for point in result["is_correct"]: for point in result["is_correct"]:
correct = correct and point correct = correct and point
result["is_correct"] = correct result["is_correct"] = correct
score += correct
question_details = frappe.db.get_value(
"LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"]},
["question", "marks"],
as_dict=1,
)
result["question_name"] = question_details.question
result["question"] = frappe.db.get_value(
"LMS Question", question_details.question, "question"
)
marks = question_details.marks if correct else 0
result["marks"] = marks
score += marks
del result["question_index"] del result["question_index"]
quiz_details = frappe.db.get_value(
"LMS Quiz", quiz, ["total_marks", "passing_percentage"], as_dict=1
)
score_out_of = quiz_details.total_marks
percentage = (score / score_out_of) * 100
submission = frappe.get_doc( submission = frappe.get_doc(
{ {
"doctype": "LMS Quiz Submission", "doctype": "LMS Quiz Submission",
"quiz": quiz, "quiz": quiz,
"result": results, "result": results,
"score": score, "score": score,
"score_out_of": score_out_of,
"member": frappe.session.user, "member": frappe.session.user,
"percentage": percentage,
"passing_percentage": quiz_details.passing_percentage,
} }
) )
submission.save(ignore_permissions=True) submission.save(ignore_permissions=True)
return { return {
"score": score, "score": score,
"score_out_of": score_out_of,
"submission": submission.name, "submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
} }
@frappe.whitelist() @frappe.whitelist()
def save_quiz( def save_quiz(
quiz_title, quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0
passing_percentage,
questions,
max_attempts=0,
quiz=None,
show_answers=1,
show_submission_history=0,
): ):
if not has_course_moderator_role() or not has_course_instructor_role(): if not has_course_moderator_role() or not has_course_instructor_role():
return return
values = { values = {
"title": quiz_title, "title": quiz_title,
"passing_percentage": passing_percentage,
"max_attempts": max_attempts, "max_attempts": max_attempts,
"show_answers": show_answers, "show_answers": show_answers,
"show_submission_history": show_submission_history, "show_submission_history": show_submission_history,
@@ -137,77 +164,41 @@ def save_quiz(
if quiz: if quiz:
frappe.db.set_value("LMS Quiz", quiz, values) frappe.db.set_value("LMS Quiz", quiz, values)
update_questions(quiz, questions)
return quiz return quiz
else: else:
doc = frappe.new_doc("LMS Quiz") doc = frappe.new_doc("LMS Quiz")
doc.update(values) doc.update(values)
doc.save() doc.save(ignore_permissions=True)
update_questions(doc.name, questions)
return doc.name return doc.name
def update_questions(quiz, questions):
questions = json.loads(questions)
delete_questions(quiz, questions)
add_questions(quiz, questions)
frappe.db.set_value("LMS Quiz", quiz, "total_marks", set_total_marks(quiz, questions))
def delete_questions(quiz, questions):
existing_questions = frappe.get_all(
"LMS Quiz Question",
{
"parent": quiz,
},
pluck="name",
)
current_questions = [question.get("question_name") for question in questions]
for question in existing_questions:
if question not in current_questions:
frappe.db.delete("LMS Quiz Question", question)
def add_questions(quiz, questions):
for index, question in enumerate(questions):
question = frappe._dict(question)
if question.question_name:
doc = frappe.get_doc("LMS Quiz Question", question.question_name)
else:
doc = frappe.new_doc("LMS Quiz Question")
doc.update(
{
"parent": quiz,
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index + 1,
}
)
doc.update({"question": question.question, "marks": question.marks})
doc.save()
@frappe.whitelist() @frappe.whitelist()
def save_question(quiz, values, index): def save_question(quiz, values, index):
values = frappe._dict(json.loads(values)) values = frappe._dict(json.loads(values))
validate_correct_answers([values])
if values.get("name"): if values.get("name"):
doc = frappe.get_doc("LMS Question", values.get("name")) doc = frappe.get_doc("LMS Quiz Question", values.get("name"))
else: else:
doc = frappe.new_doc("LMS Question") doc = frappe.new_doc("LMS Quiz Question")
doc.update( doc.update(
{ {
"question": values.question, "question": values["question"],
"type": values["type"], "type": values["type"],
} }
) )
if not values.get("name"):
doc.update(
{
"parent": quiz,
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index,
}
)
for num in range(1, 5): for num in range(1, 5):
if values.get(f"option_{num}"): if values.get(f"option_{num}"):
doc.update( doc.update(
@@ -231,8 +222,9 @@ def save_question(quiz, values, index):
} }
) )
doc.save() doc.save(ignore_permissions=True)
return doc.name
return quiz
@frappe.whitelist() @frappe.whitelist()
@@ -265,13 +257,13 @@ def check_choice_answers(question, answers):
fields.append(f"option_{cstr(num)}") fields.append(f"option_{cstr(num)}")
fields.append(f"is_correct_{cstr(num)}") fields.append(f"is_correct_{cstr(num)}")
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) question_details = frappe.db.get_value(
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5): for num in range(1, 5):
if question_details[f"option_{num}"] in answers: if question_details[f"option_{num}"] in answers:
is_correct.append(question_details[f"is_correct_{num}"]) is_correct.append(question_details[f"is_correct_{num}"])
elif question_details[f"is_correct_{num}"]:
is_correct.append(2)
else: else:
is_correct.append(0) is_correct.append(0)
@@ -283,7 +275,9 @@ def check_input_answers(question, answer):
for num in range(1, 5): for num in range(1, 5):
fields.append(f"possibility_{cstr(num)}") fields.append(f"possibility_{cstr(num)}")
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) question_details = frappe.db.get_value(
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5): for num in range(1, 5):
current_possibility = question_details[f"possibility_{num}"] current_possibility = question_details[f"possibility_{num}"]
if current_possibility and current_possibility.lower() == answer.lower(): if current_possibility and current_possibility.lower() == answer.lower():

View File

@@ -10,36 +10,51 @@ import frappe
class TestLMSQuiz(unittest.TestCase): class TestLMSQuiz(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls) -> None: def setUpClass(cls) -> None:
frappe.get_doc( frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save(
{"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90} ignore_permissions=True
).save(ignore_permissions=True) )
def test_with_multiple_options(self): def test_with_multiple_options(self):
question = frappe.new_doc("LMS Question") quiz = frappe.get_doc("LMS Quiz", "test-quiz")
question.question = "Question Multiple" quiz.append(
question.type = "Choices" "questions",
question.option_1 = "Option 1" {
question.is_correct_1 = 1 "question": "Question Multiple",
question.option_2 = "Option 2" "type": "Choices",
question.is_correct_2 = 1 "option_1": "Option 1",
question.save() "is_correct_1": 1,
self.assertTrue(question.multiple) "option_2": "Option 2",
"is_correct_2": 1,
},
)
quiz.save()
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self): def test_with_no_correct_option(self):
question = frappe.new_doc("LMS Question") quiz = frappe.get_doc("LMS Quiz", "test-quiz")
question.question = "Question Multiple" quiz.append(
question.type = "Choices" "questions",
question.option_1 = "Option 1" {
question.option_2 = "Option 2" "question": "Question no correct option",
self.assertRaises(frappe.ValidationError, question.save) "type": "Choices",
"option_1": "Option 1",
"option_2": "Option 2",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
def test_with_no_possible_answers(self): def test_with_no_possible_answers(self):
question = frappe.new_doc("LMS Question") quiz = frappe.get_doc("LMS Quiz", "test-quiz")
question.question = "Question Multiple" quiz.append(
question.type = "User Input" "questions",
self.assertRaises(frappe.ValidationError, question.save) {
"question": "Question Possible Answers",
"type": "User Input",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod @classmethod
def tearDownClass(cls) -> None: def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "test-quiz") frappe.db.delete("LMS Quiz", "test-quiz")
frappe.db.delete("LMS Question") frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"})

View File

@@ -6,31 +6,208 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"marks" "type",
"options_section",
"option_1",
"is_correct_1",
"column_break_5",
"explanation_1",
"section_break_5",
"option_2",
"is_correct_2",
"column_break_10",
"explanation_2",
"column_break_4",
"option_3",
"is_correct_3",
"column_break_15",
"explanation_3",
"section_break_11",
"option_4",
"is_correct_4",
"column_break_20",
"explanation_4",
"section_break_mnhr",
"possibility_1",
"possibility_3",
"column_break_vnaj",
"possibility_2",
"possibility_4",
"section_break_c1lf",
"multiple"
], ],
"fields": [ "fields": [
{ {
"fieldname": "question", "fieldname": "question",
"fieldtype": "Link", "fieldtype": "Text Editor",
"in_list_view": 1, "in_list_view": 1,
"label": "Question", "label": "Question",
"options": "LMS Question",
"reqd": 1 "reqd": 1
}, },
{ {
"default": "1", "fieldname": "option_1",
"fieldname": "marks", "fieldtype": "Small Text",
"fieldtype": "Int", "label": "Option 1",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"fieldname": "option_2",
"fieldtype": "Small Text",
"label": "Option 2",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"fieldname": "option_3",
"fieldtype": "Small Text",
"label": "Option 3"
},
{
"fieldname": "option_4",
"fieldtype": "Small Text",
"label": "Option 4"
},
{
"default": "0",
"depends_on": "option_1",
"fieldname": "is_correct_1",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_2",
"fieldname": "is_correct_2",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_3",
"fieldname": "is_correct_3",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_4",
"fieldname": "is_correct_4",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"fieldname": "multiple",
"fieldtype": "Check",
"hidden": 1,
"label": "Multiple Correct Answers",
"read_only": 1
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "options_section",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "column_break_4",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"depends_on": "option_1",
"fieldname": "explanation_1",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_2",
"fieldname": "explanation_2",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_3",
"fieldname": "explanation_3",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_4",
"fieldname": "explanation_4",
"fieldtype": "Data",
"label": "Explanation"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Marks", "label": "Type",
"non_negative": 1, "options": "Choices\nUser Input"
"reqd": 1 },
{
"depends_on": "eval: doc.type == 'User Input'",
"fieldname": "section_break_mnhr",
"fieldtype": "Section Break"
},
{
"fieldname": "possibility_1",
"fieldtype": "Small Text",
"label": "Possible Answer 1",
"mandatory_depends_on": "eval: doc.type == 'User Input'"
},
{
"fieldname": "possibility_2",
"fieldtype": "Small Text",
"label": "Possible Answer 2"
},
{
"fieldname": "possibility_3",
"fieldtype": "Small Text",
"label": "Possible Answer 3"
},
{
"fieldname": "possibility_4",
"fieldtype": "Small Text",
"label": "Possible Answer 4"
},
{
"fieldname": "section_break_c1lf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vnaj",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-16 19:51:03.893144", "modified": "2023-07-04 16:43:49.837134",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Question", "name": "LMS Quiz Question",

View File

@@ -6,11 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"section_break_fztv",
"question_name",
"answer", "answer",
"column_break_flus",
"marks",
"is_correct" "is_correct"
], ],
"fields": [ "fields": [
@@ -35,33 +31,12 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Is Correct", "label": "Is Correct",
"read_only": 1 "read_only": 1
},
{
"fieldname": "section_break_fztv",
"fieldtype": "Section Break"
},
{
"fieldname": "question_name",
"fieldtype": "Link",
"label": "Question Name",
"options": "LMS Question"
},
{
"fieldname": "column_break_flus",
"fieldtype": "Column Break"
},
{
"fieldname": "marks",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Marks",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-17 11:55:25.641214", "modified": "2022-11-24 11:15:45.931119",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Result", "name": "LMS Quiz Result",

View File

@@ -6,16 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"quiz", "quiz",
"score",
"course", "course",
"column_break_3", "column_break_3",
"member", "member",
"member_name", "member_name",
"section_break_dkpn",
"score",
"score_out_of",
"column_break_gkip",
"percentage",
"passing_percentage",
"section_break_6", "section_break_6",
"result" "result"
], ],
@@ -36,11 +31,9 @@
}, },
{ {
"fieldname": "score", "fieldname": "score",
"fieldtype": "Int", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Score", "label": "Score"
"read_only": 1,
"reqd": 1
}, },
{ {
"fieldname": "member", "fieldname": "member",
@@ -72,45 +65,12 @@
"label": "Course", "label": "Course",
"options": "LMS Course", "options": "LMS Course",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "quiz.total_marks",
"fieldname": "score_out_of",
"fieldtype": "Int",
"label": "Score Out Of",
"non_negative": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_dkpn",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_gkip",
"fieldtype": "Column Break"
},
{
"fieldname": "percentage",
"fieldtype": "Int",
"label": "Percentage",
"non_negative": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "passing_percentage",
"fieldtype": "Int",
"label": "Passing Percentage",
"non_negative": 1,
"read_only": 1,
"reqd": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-10-17 13:07:27.979975", "modified": "2022-11-15 15:27:07.770945",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Submission", "name": "LMS Quiz Submission",

View File

@@ -6,10 +6,4 @@ from frappe.model.document import Document
class LMSQuizSubmission(Document): class LMSQuizSubmission(Document):
def before_insert(self): pass
if not self.percentage:
self.set_percentage()
def set_percentage(self):
if self.score and self.score_out_of:
self.percentage = (self.score / self.score_out_of) * 100

View File

@@ -16,16 +16,18 @@
"portal_course_creation", "portal_course_creation",
"section_break_szgq", "section_break_szgq",
"send_calendar_invite_for_evaluations", "send_calendar_invite_for_evaluations",
"show_day_view", "batch_confirmation_template",
"allow_student_progress",
"column_break_2", "column_break_2",
"show_dashboard", "allow_student_progress",
"show_courses", "payment_section",
"show_students", "razorpay_key",
"show_assessments", "razorpay_secret",
"show_live_class", "apply_gst",
"show_discussions", "column_break_cfcv",
"show_emails", "default_currency",
"show_usd_equivalent",
"apply_rounding",
"exception_country",
"signup_settings_tab", "signup_settings_tab",
"signup_settings_section", "signup_settings_section",
"terms_of_use", "terms_of_use",
@@ -40,22 +42,7 @@
"mentor_request_tab", "mentor_request_tab",
"mentor_request_section", "mentor_request_section",
"mentor_request_creation", "mentor_request_creation",
"mentor_request_status_update", "mentor_request_status_update"
"payment_settings_tab",
"payment_section",
"razorpay_key",
"razorpay_secret",
"apply_gst",
"column_break_cfcv",
"default_currency",
"show_usd_equivalent",
"apply_rounding",
"exception_country",
"email_templates_tab",
"certification_template",
"batch_confirmation_template",
"column_break_uwsp",
"assignment_submission_template"
], ],
"fields": [ "fields": [
{ {
@@ -84,8 +71,7 @@
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"label": "Show Tab in Batch"
}, },
{ {
"fieldname": "search_placeholder", "fieldname": "search_placeholder",
@@ -213,7 +199,8 @@
}, },
{ {
"fieldname": "payment_section", "fieldname": "payment_section",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Payment"
}, },
{ {
"fieldname": "default_currency", "fieldname": "default_currency",
@@ -274,86 +261,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch Confirmation Template", "label": "Batch Confirmation Template",
"options": "Email Template" "options": "Email Template"
},
{
"default": "1",
"fieldname": "show_courses",
"fieldtype": "Check",
"label": "Courses"
},
{
"default": "1",
"fieldname": "show_students",
"fieldtype": "Check",
"label": "Students"
},
{
"default": "1",
"fieldname": "show_assessments",
"fieldtype": "Check",
"label": "Assessments"
},
{
"default": "1",
"fieldname": "show_live_class",
"fieldtype": "Check",
"label": "Live Class"
},
{
"default": "1",
"fieldname": "show_discussions",
"fieldtype": "Check",
"label": "Discussions"
},
{
"default": "1",
"fieldname": "show_emails",
"fieldtype": "Check",
"label": "Emails"
},
{
"fieldname": "payment_settings_tab",
"fieldtype": "Tab Break",
"label": "Payment Settings"
},
{
"default": "1",
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Dashboard"
},
{
"fieldname": "certification_template",
"fieldtype": "Link",
"label": "Certificate Email Template",
"options": "Email Template"
},
{
"fieldname": "email_templates_tab",
"fieldtype": "Tab Break",
"label": "Email Templates"
},
{
"fieldname": "assignment_submission_template",
"fieldtype": "Link",
"label": "Assignment Submission Template",
"options": "Email Template"
},
{
"fieldname": "column_break_uwsp",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "show_day_view",
"fieldtype": "Check",
"label": "Show Day View in Timetable"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-12-12 10:32:13.638368", "modified": "2023-10-09 17:27:28.615355",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Source", {
// refresh(frm) {
// },
// });

View File

@@ -1,69 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:source",
"creation": "2023-10-26 16:28:53.932278",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source"
],
"fields": [
{
"fieldname": "source",
"fieldtype": "Data",
"label": "Source",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-26 17:25:09.144367",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Source",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"select": 1,
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "source"
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSSource(Document):
pass

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSSource(FrappeTestCase):
pass

View File

@@ -0,0 +1,27 @@
{
"attach_print": 0,
"channel": "Email",
"creation": "2023-03-27 16:34:03.505647",
"days_in_advance": 0,
"docstatus": 0,
"doctype": "Notification",
"document_type": "LMS Assignment Submission",
"enabled": 1,
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "<h3> {{ _(\"Assignment Submission\") }}\n\n{% set title = frappe.db.get_value(\"Course Lesson\", doc.lesson, \"title\") %}\n\n<p> {{ _(\"{0} has submitted their assignment for the lesson {1}\").format(doc.member_name, title) }} </p>\n\n <p> {{ _(\" Please evaluate and grade the assignment. \") }} </p>",
"modified": "2023-03-27 16:46:44.564007",
"modified_by": "Administrator",
"module": "LMS",
"name": "Assignment Submission Notification",
"owner": "Administrator",
"recipients": [
{
"receiver_by_document_field": "evaluator"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "Assignment Submission"
}

View File

@@ -0,0 +1,11 @@
<div style="background-color: #f4f5f6; padding: 1rem;">
<div style="background-color: #ffffff; width: 75%; margin: 0 auto; padding: 1rem;">
<h3> {{ _("Assignment Submission") }} </h3>
{% set title = frappe.db.get_value("Course Lesson", doc.lesson, "title") %}
<br>
<p> {{ _("{0} has submitted their assignment for the lesson {1}").format(frappe.bold(doc.member_name), frappe.bold(title)) }}
</p>
<p> {{ _(" Please evaluate and grade the assignment.") }} </p>
</div>
</div>

View File

@@ -0,0 +1,6 @@
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -11,8 +11,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n", "message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message_type": "HTML", "modified": "2023-02-28 19:53:47.716135",
"modified": "2023-11-29 17:34:54.514031",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Certificate Request Creation", "name": "Certificate Request Creation",
@@ -28,4 +27,4 @@
"send_system_notification": 0, "send_system_notification": 0,
"send_to_all_assignees": 0, "send_to_all_assignees": 0,
"subject": "Your evaluation slot has been booked" "subject": "Your evaluation slot has been booked"
} }

View File

@@ -0,0 +1 @@
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -1,5 +1,5 @@
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %} {% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p> <p> {{ _("Hey {0}").format(doc.member_name) }} </p>
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p> <p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -11,9 +11,8 @@
"event": "Days Before", "event": "Days Before",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n", "message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message_type": "HTML", "modified": "2022-06-03 11:51:02.681803",
"modified": "2023-11-29 17:26:53.355501",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Certificate Request Reminder", "name": "Certificate Request Reminder",
@@ -21,12 +20,9 @@
"recipients": [ "recipients": [
{ {
"receiver_by_document_field": "member" "receiver_by_document_field": "member"
},
{
"receiver_by_document_field": "evaluator"
} }
], ],
"send_system_notification": 0, "send_system_notification": 0,
"send_to_all_assignees": 0, "send_to_all_assignees": 0,
"subject": "Reminder for Certificate Evaluation" "subject": "Reminder for Certificate Evaluation"
} }

View File

@@ -0,0 +1,3 @@
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -2,7 +2,7 @@
"absolute_value": 0, "absolute_value": 0,
"align_labels_right": 0, "align_labels_right": 0,
"creation": "2023-08-09 17:02:21.430320", "creation": "2023-08-09 17:02:21.430320",
"css": ".outer-border {\n font-family: \"Inter\" sans-serif;\n font-size: 16px;\n border-radius: 0.5rem;\n border: 1px solid #E2E6E9;\n padding: 1rem;\n}\n\n.inner-border {\n border: 8px solid #0089FF;\n border-radius: 8px;\n text-align: center;\n padding: 6rem 4rem;\n background-color: #FFFFFF;\n}\n\n.certificate-logo {\n height: 1.5rem;\n margin-bottom: 4rem;\n}\n\n.certificate-name {\n font-size: 2rem;\n font-weight: 500;\n color: #192734;\n margin-bottom: 0.5rem;\n}\n\n.certificate-footer {\n margin: 4rem auto 0;\n width: 70%;\n text-align: center;\n}\n\n.certificate-footer-item {\n color: #192734;\n}\n\n.cursive-font {\n font-family: cursive;\n font-weight: 600;\n}\n\n.certificate-divider {\n margin: 0.5rem 0;\n}\n\n.certificate-expiry {\n margin-left: 2rem;\n}", "css": ".outer-border {\n font-family: \"Inter\" sans-serif;\n font-size: 16px;\n border-radius: 0.5rem;\n border: 1px solid #E2E6E9;\n padding: 1rem;\n}\n\n.inner-border {\n border: 10px solid #0089FF;\n border-radius: 8px;\n text-align: center;\n padding: 6rem 4rem;\n background-color: #FFFFFF;\n}\n\n.certificate-logo {\n height: 1.5rem;\n margin-bottom: 4rem;\n}\n\n.certificate-name {\n font-size: 2rem;\n font-weight: 500;\n color: #192734;\n margin-bottom: 0.5rem;\n}\n\n.certificate-footer {\n margin: 4rem auto 0;\n width: 70%;\n text-align: center;\n}\n\n.certificate-footer-item {\n color: #192734;\n}\n\n.cursive-font {\n font-family: cursive;\n font-weight: 600;\n}\n\n.certificate-divider {\n margin: 0.5rem 0;\n}\n\n.certificate-expiry {\n margin-left: 2rem;\n}",
"custom_format": 1, "custom_format": 1,
"disabled": 0, "disabled": 0,
"doc_type": "LMS Certificate", "doc_type": "LMS Certificate",
@@ -10,20 +10,19 @@
"doctype": "Print Format", "doctype": "Print Format",
"font_size": 14, "font_size": 14,
"format_data": "{\"header\":\"<div class=\\\"document-header\\\">\\n\\t<h3>LMS Certificate</h3>\\n\\t<p>{{ doc.name }}</p>\\n</div>\",\"sections\":[{\"label\":\"\",\"columns\":[{\"label\":\"\",\"fields\":[{\"label\":\"Course\",\"fieldname\":\"course\",\"fieldtype\":\"Link\",\"options\":\"LMS Course\"},{\"label\":\"Member\",\"fieldname\":\"member\",\"fieldtype\":\"Link\",\"options\":\"User\"},{\"label\":\"Member Name\",\"fieldname\":\"member_name\",\"fieldtype\":\"Data\"},{\"label\":\"Evaluator\",\"fieldname\":\"evaluator\",\"fieldtype\":\"Data\",\"options\":\"\"}]},{\"label\":\"\",\"fields\":[{\"label\":\"Issue Date\",\"fieldname\":\"issue_date\",\"fieldtype\":\"Date\"},{\"label\":\"Expiry Date\",\"fieldname\":\"expiry_date\",\"fieldtype\":\"Date\"},{\"label\":\"Version\",\"fieldname\":\"version\",\"fieldtype\":\"Select\",\"options\":\"V13\\nV14\"},{\"label\":\"Module Names for Certificate\",\"fieldname\":\"module_names_for_certificate\",\"fieldtype\":\"Data\"}]}],\"has_fields\":true}]}", "format_data": "{\"header\":\"<div class=\\\"document-header\\\">\\n\\t<h3>LMS Certificate</h3>\\n\\t<p>{{ doc.name }}</p>\\n</div>\",\"sections\":[{\"label\":\"\",\"columns\":[{\"label\":\"\",\"fields\":[{\"label\":\"Course\",\"fieldname\":\"course\",\"fieldtype\":\"Link\",\"options\":\"LMS Course\"},{\"label\":\"Member\",\"fieldname\":\"member\",\"fieldtype\":\"Link\",\"options\":\"User\"},{\"label\":\"Member Name\",\"fieldname\":\"member_name\",\"fieldtype\":\"Data\"},{\"label\":\"Evaluator\",\"fieldname\":\"evaluator\",\"fieldtype\":\"Data\",\"options\":\"\"}]},{\"label\":\"\",\"fields\":[{\"label\":\"Issue Date\",\"fieldname\":\"issue_date\",\"fieldtype\":\"Date\"},{\"label\":\"Expiry Date\",\"fieldname\":\"expiry_date\",\"fieldtype\":\"Date\"},{\"label\":\"Version\",\"fieldname\":\"version\",\"fieldtype\":\"Select\",\"options\":\"V13\\nV14\"},{\"label\":\"Module Names for Certificate\",\"fieldname\":\"module_names_for_certificate\",\"fieldtype\":\"Data\"}]}],\"has_fields\":true}]}",
"html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div>\n <div class=\"inner-border\">\n \n {% if logo %}\n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n {% endif %}\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>", "html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div class=\"outer-border\">\n <div class=\"inner-border\">\n \n {% if logo %}\n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n {% endif %}\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>",
"idx": 0, "idx": 0,
"line_breaks": 0, "line_breaks": 0,
"margin_bottom": 0.0, "margin_bottom": 0.0,
"margin_left": 0.0, "margin_left": 0.0,
"margin_right": 0.0, "margin_right": 0.0,
"margin_top": 0.0, "margin_top": 0.0,
"modified": "2023-11-01 18:22:56.715846", "modified": "2023-08-09 17:02:21.430320",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Certificate", "name": "Certificate",
"owner": "Administrator", "owner": "Administrator",
"page_number": "Hide", "page_number": "Hide",
"print_designer": 0,
"print_format_builder": 0, "print_format_builder": 0,
"print_format_builder_beta": 1, "print_format_builder_beta": 1,
"print_format_type": "Jinja", "print_format_type": "Jinja",

View File

@@ -4,16 +4,10 @@ import frappe
import json import json
import razorpay import razorpay
import requests import requests
import base64
from frappe import _ from frappe import _
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
from frappe.desk.doctype.notification_log.notification_log import ( from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
make_notification_logs,
enqueue_create_notification,
get_title,
)
from frappe.utils import get_fullname
from frappe.desk.search import get_user_groups
from frappe.desk.notifications import extract_mentions
from frappe.utils import ( from frappe.utils import (
add_months, add_months,
cint, cint,
@@ -156,7 +150,7 @@ def get_lesson_details(chapter):
], ],
as_dict=True, as_dict=True,
) )
lesson_details.number = f"{chapter.idx}.{row.idx}" lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
lesson_details.icon = get_lesson_icon(lesson_details.body) lesson_details.icon = get_lesson_icon(lesson_details.body)
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons
@@ -555,9 +549,6 @@ def can_create_courses(course, member=None):
if portal_course_creation == "Anyone" and member in instructors: if portal_course_creation == "Anyone" and member in instructors:
return True return True
if not course and has_course_instructor_role(member):
return True
return False return False
@@ -613,20 +604,17 @@ def validate_image(path):
return path return path
def handle_notifications(doc, method): def create_notification_log(doc, method):
topic = frappe.db.get_value( topic = frappe.db.get_value(
"Discussion Topic", "Discussion Topic",
doc.topic, doc.topic,
["reference_doctype", "reference_docname", "owner", "title"], ["reference_doctype", "reference_docname", "owner", "title"],
as_dict=1, as_dict=1,
) )
if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]:
if topic.reference_doctype != "Course Lesson":
return return
create_notification_log(doc, topic)
notify_mentions(doc, topic)
def create_notification_log(doc, topic):
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course") course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
instructors = frappe.db.get_all( instructors = frappe.db.get_all(
"Course Instructor", {"parent": course}, pluck="instructor" "Course Instructor", {"parent": course}, pluck="instructor"
@@ -653,47 +641,6 @@ def create_notification_log(doc, topic):
make_notification_logs(notification, users) make_notification_logs(notification, users)
def notify_mentions(doc, topic):
mentions = extract_mentions(doc.reply)
if not mentions:
return
sender_fullname = get_fullname(doc.owner)
recipients = [
frappe.db.get_value(
"User",
{"enabled": 1, "name": name},
"email",
)
for name in mentions
]
subject = _("{0} mentioned you in a comment").format(sender_fullname)
template = "mention_template"
if topic.reference_doctype == "LMS Batch":
link = f"/batches/{topic.reference_docname}#discussions"
if topic.reference_doctype == "Course Lesson":
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
lesson_index = get_lesson_index(topic.reference_docname)
link = get_lesson_url(course, lesson_index)
args = {
"sender": sender_fullname,
"content": doc.reply,
"link": link,
}
for recipient in recipients:
frappe.sendmail(
recipients=recipient,
subject=subject,
template=template,
args=args,
header=[subject, "green"],
retry=3,
)
def get_lesson_count(course): def get_lesson_count(course):
lesson_count = 0 lesson_count = 0
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"]) chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
@@ -827,17 +774,6 @@ def get_telemetry_boot_info():
def is_onboarding_complete(): def is_onboarding_complete():
onboarding_status = frappe.db.get_single_value(
"LMS Settings", "is_onboarding_complete"
)
if onboarding_status:
return {
"is_onboarded": onboarding_status,
"course_created": True,
"chapter_created": True,
"lesson_created": True,
"first_course": None,
}
course_created = frappe.db.a_row_exists("LMS Course") course_created = frappe.db.a_row_exists("LMS Course")
chapter_created = frappe.db.a_row_exists("Course Chapter") chapter_created = frappe.db.a_row_exists("Course Chapter")
lesson_created = frappe.db.a_row_exists("Course Lesson") lesson_created = frappe.db.a_row_exists("Course Lesson")
@@ -846,7 +782,7 @@ def is_onboarding_complete():
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1) frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
return { return {
"is_onboarded": onboarding_status, "is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
"course_created": course_created, "course_created": course_created,
"chapter_created": chapter_created, "chapter_created": chapter_created,
"lesson_created": lesson_created, "lesson_created": lesson_created,
@@ -917,9 +853,8 @@ def get_payment_options(doctype, docname, phone, country):
validate_phone_number(phone, True) validate_phone_number(phone, True)
details = get_details(doctype, docname) details = get_details(doctype, docname)
details.amount, details.currency = check_multicurrency( details.amount, details.currency = check_multicurrency(
details.amount, details.currency, country, details.amount_usd details.amount, details.currency, country
) )
if details.currency == "INR": if details.currency == "INR":
details.amount, details.gst_applied = apply_gst(details.amount, country) details.amount, details.gst_applied = apply_gst(details.amount, country)
@@ -932,7 +867,7 @@ def get_payment_options(doctype, docname, phone, country):
"name": frappe.db.get_single_value("Website Settings", "app_name"), "name": frappe.db.get_single_value("Website Settings", "app_name"),
"description": _("Payment for {0} course").format(details["title"]), "description": _("Payment for {0} course").format(details["title"]),
"order_id": order["id"], "order_id": order["id"],
"amount": cint(order["amount"]) * 100, "amount": order["amount"] * 100,
"currency": order["currency"], "currency": order["currency"],
"prefill": { "prefill": {
"name": frappe.db.get_value("User", frappe.session.user, "full_name"), "name": frappe.db.get_value("User", frappe.session.user, "full_name"),
@@ -943,21 +878,16 @@ def get_payment_options(doctype, docname, phone, country):
return options return options
def check_multicurrency(amount, currency, country=None, amount_usd=None): def check_multicurrency(amount, currency, country=None):
show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent") show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent")
exception_country = frappe.get_all( exception_country = frappe.get_all(
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country" "Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
) )
country = ( apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
country country = country or frappe.db.get_value(
or frappe.db.get_value("Address", {"email_id": frappe.session.user}, "country") "Address", {"email_id": frappe.session.user}, "country"
or frappe.db.get_value("User", frappe.session.user, "country")
or get_country_code()
) )
if amount_usd and country and country not in exception_country:
return amount_usd, "USD"
if not show_usd_equivalent or currency == "USD": if not show_usd_equivalent or currency == "USD":
return amount, currency return amount, currency
@@ -968,9 +898,8 @@ def check_multicurrency(amount, currency, country=None, amount_usd=None):
amount = amount * exchange_rate amount = amount * exchange_rate
currency = "USD" currency = "USD"
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
if apply_rounding and amount % 100 != 0: if apply_rounding and amount % 100 != 0:
amount = amount + 100 - amount % 100 amount = ceil(amount + 100 - amount % 100)
return amount, currency return amount, currency
@@ -994,7 +923,7 @@ def get_details(doctype, docname):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Course", "LMS Course",
docname, docname,
["name", "title", "paid_course", "currency", "course_price as amount", "amount_usd"], ["name", "title", "paid_course", "currency", "course_price as amount"],
as_dict=True, as_dict=True,
) )
if not details.paid_course: if not details.paid_course:
@@ -1003,7 +932,7 @@ def get_details(doctype, docname):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Batch", "LMS Batch",
docname, docname,
["name", "title", "paid_batch", "currency", "amount", "amount_usd"], ["name", "title", "paid_batch", "currency", "amount"],
as_dict=True, as_dict=True,
) )
if not details.paid_batch: if not details.paid_batch:
@@ -1052,15 +981,13 @@ def create_order(client, amount, currency):
try: try:
return client.order.create( return client.order.create(
{ {
"amount": cint(amount) * 100, "amount": amount * 100,
"currency": currency, "currency": currency,
} }
) )
except Exception as e: except Exception as e:
frappe.throw( frappe.throw(
_( _("Error during payment: {0}. Please contact the Administrator.").format(e)
"Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
).format(e, amount, currency, cint(amount))
) )
@@ -1102,23 +1029,19 @@ def record_payment(address, response, client, doctype, docname):
"amount_with_gst": payment_details["amount_with_gst"], "amount_with_gst": payment_details["amount_with_gst"],
"gstin": address.gstin, "gstin": address.gstin,
"pan": address.pan, "pan": address.pan,
"source": address.source,
"payment_for_document_type": doctype,
"payment_for_document": docname,
} }
) )
payment_doc.save(ignore_permissions=True) payment_doc.save(ignore_permissions=True)
return payment_doc return payment_doc.name
def get_payment_details(doctype, docname, address): def get_payment_details(doctype, docname, address):
amount_field = "course_price" if doctype == "LMS Course" else "amount" amount_field = "course_price" if doctype == "LMS Course" else "amount"
amount = frappe.db.get_value(doctype, docname, amount_field) amount = frappe.db.get_value(doctype, docname, amount_field)
currency = frappe.db.get_value(doctype, docname, "currency") currency = frappe.db.get_value(doctype, docname, "currency")
amount_usd = frappe.db.get_value(doctype, docname, "amount_usd")
amount_with_gst = 0 amount_with_gst = 0
amount, currency = check_multicurrency(amount, currency, None, amount_usd) amount, currency = check_multicurrency(amount, currency)
if currency == "INR" and address.country == "India": if currency == "INR" and address.country == "India":
amount_with_gst, gst_applied = apply_gst(amount, address.country) amount_with_gst, gst_applied = apply_gst(amount, address.country)
@@ -1132,7 +1055,7 @@ def get_payment_details(doctype, docname, address):
def create_membership(course, payment): def create_membership(course, payment):
membership = frappe.new_doc("LMS Enrollment") membership = frappe.new_doc("LMS Enrollment")
membership.update( membership.update(
{"member": frappe.session.user, "course": course, "payment": payment.name} {"member": frappe.session.user, "course": course, "payment": payment}
) )
membership.save(ignore_permissions=True) membership.save(ignore_permissions=True)
return f"/courses/{course}/learn/1.1" return f"/courses/{course}/learn/1.1"
@@ -1143,8 +1066,7 @@ def add_student_to_batch(batchname, payment):
student.update( student.update(
{ {
"student": frappe.session.user, "student": frappe.session.user,
"payment": payment.name, "payment": payment,
"source": payment.source,
"parent": batchname, "parent": batchname,
"parenttype": "LMS Batch", "parenttype": "LMS Batch",
"parentfield": "students", "parentfield": "students",
@@ -1167,16 +1089,3 @@ def change_currency(amount, currency, country=None):
amount = cint(amount) amount = cint(amount)
amount, currency = check_multicurrency(amount, currency, country) amount, currency = check_multicurrency(amount, currency, country)
return fmt_money(amount, 0, currency) return fmt_money(amount, 0, currency)
def get_country_code():
ip = frappe.local.request_ip
res = requests.get(f"http://ip-api.com/json/{ip}")
try:
data = res.json()
if data.get("status") != "fail":
return frappe.db.get_value("Country", {"code": data.get("countryCode")}, "name")
except Exception:
pass
return

View File

@@ -29,6 +29,10 @@
<button class="btn btn-primary btn-sm notify-me pull-right" data-course="{{course.name | urlencode}}"> <button class="btn btn-primary btn-sm notify-me pull-right" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }} {{ _("Notify me when available") }}
</button> </button>
{% elif show_start_learing_cta(course, membership) %}
<button class="btn btn-primary btn-sm enroll-in-course pull-right" data-course="{{ course.name | urlencode}}">
{{ _("Start Learning") }}
</button>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -5,7 +5,7 @@ from frappe import _
from frappe.core.doctype.user.user import User from frappe.core.doctype.user.user import User
from frappe.utils import cint, escape_html, random_string from frappe.utils import cint, escape_html, random_string
from frappe.website.utils import is_signup_disabled from frappe.website.utils import is_signup_disabled
from lms.lms.utils import get_average_rating, get_country_code from lms.lms.utils import get_average_rating
from frappe.website.utils import cleanup_page_name from frappe.website.utils import cleanup_page_name
from frappe.model.naming import append_number_if_name_exists from frappe.model.naming import append_number_if_name_exists
from lms.widgets import Widgets from lms.widgets import Widgets
@@ -260,6 +260,19 @@ def set_country_from_ip(login_manager=None, user=None):
return return
def get_country_code():
ip = frappe.local.request_ip
res = requests.get(f"http://ip-api.com/json/{ip}")
try:
data = res.json()
if data.get("status") != "fail":
return frappe.db.get_value("Country", {"code": data.get("countryCode")}, "name")
except Exception:
pass
return
def on_session_creation(login_manager): def on_session_creation(login_manager):
if frappe.db.get_single_value( if frappe.db.get_single_value(
"System Settings", "setup_complete" "System Settings", "setup_complete"

View File

@@ -1,4 +1,3 @@
[pre_model_sync]
community.patches.set_email_preferences community.patches.set_email_preferences
community.patches.change_name_for_community_members community.patches.change_name_for_community_members
community.patches.save_abbr_for_community_members community.patches.save_abbr_for_community_members
@@ -72,14 +71,4 @@ lms.patches.v1_0.publish_batches
lms.patches.v1_0.publish_certificates lms.patches.v1_0.publish_certificates
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
execute:frappe.permissions.reset_perms("LMS Enrollment") execute:frappe.permissions.reset_perms("LMS Enrollment")
lms.patches.v1_0.create_student_role lms.patches.v1_0.create_student_role
lms.patches.v1_0.mark_confirmation_for_batch_students
lms.patches.v1_0.create_quiz_questions
lms.patches.v1_0.add_default_marks #16-10-2023
lms.patches.v1_0.add_certificate_template #26-10-2023
lms.patches.v1_0.create_batch_source
[post_model_sync]
lms.patches.v1_0.batch_tabs_settings
execute:frappe.delete_doc("Notification", "Assignment Submission Notification")
lms.patches.v1_0.change_jobs_url #17-01-2024

View File

@@ -1,20 +0,0 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_certificate")
default_certificate_template = frappe.db.get_value(
"Property Setter",
{
"doc_type": "LMS Certificate",
"property": "default_print_format",
},
"value",
)
if frappe.db.exists("Print Format", default_certificate_template):
certificates = frappe.get_all("LMS Certificate", pluck="name")
for certificate in certificates:
frappe.db.set_value(
"LMS Certificate", certificate, "template", default_certificate_template
)

View File

@@ -1,18 +0,0 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_quiz_question")
frappe.reload_doc("lms", "doctype", "lms_quiz")
questions = frappe.get_all("LMS Quiz Question", pluck="name")
for question in questions:
frappe.db.set_value("LMS Quiz Question", question, "marks", 1)
quizzes = frappe.get_all("LMS Quiz", pluck="name")
for quiz in quizzes:
questions_count = frappe.db.count("LMS Quiz Question", {"parent": quiz})
frappe.db.set_value(
"LMS Quiz", quiz, {"total_marks": questions_count, "passing_percentage": 100}
)

View File

@@ -1,16 +0,0 @@
import frappe
def execute():
fields = [
"show_dashboard",
"show_courses",
"show_students",
"show_emails",
"show_assessments",
"show_discussions",
"show_live_class",
]
for field in fields:
frappe.db.set_single_value("LMS Settings", field, 1)

View File

@@ -1,15 +0,0 @@
import frappe
def execute():
jobs_link = frappe.db.exists(
"Top Bar Item",
{
"label": "Jobs",
"url": "/jobs",
"parent_label": "Explore",
},
)
if jobs_link:
frappe.db.set_value("Top Bar Item", jobs_link, "url", "/job-openings")

View File

@@ -1,7 +0,0 @@
import frappe
from lms.install import create_batch_source
def execute():
frappe.reload_doc("lms", "doctype", "lms_source")
create_batch_source()

View File

@@ -1,43 +0,0 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_question")
fields = ["name", "question", "type", "multiple"]
for num in range(1, 5):
fields.append(f"option_{num}")
fields.append(f"is_correct_{num}")
fields.append(f"explanation_{num}")
fields.append(f"possibility_{num}")
questions = frappe.get_all(
"LMS Quiz Question",
fields=fields,
)
for question in questions:
print(question.name)
doc = frappe.new_doc("LMS Question")
doc.update(
{
"question": question.question,
"type": question.type,
"multiple": question.multiple,
}
)
for num in range(1, 5):
if question.get(f"option_{num}"):
doc.update(
{
f"option_{num}": question[f"option_{num}"],
f"is_correct_{num}": question[f"is_correct_{num}"],
f"explanation_{num}": question[f"explanation_{num}"],
f"possibility_{num}": question[f"possibility_{num}"],
}
)
doc.save()
print(doc.name)
frappe.db.set_value("LMS Quiz Question", question.name, "question", doc.name)

View File

@@ -1,9 +0,0 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "batch_student")
students = frappe.get_all("Batch Student", pluck="name")
for student in students:
frappe.db.set_value("Batch Student", student, "confirmation_email_sent", 1)

View File

@@ -109,39 +109,7 @@ def quiz_renderer(quiz_name):
) )
+"</div>" +"</div>"
quiz = frappe.db.get_value( quiz = frappe.get_doc("LMS Quiz", quiz_name)
"LMS Quiz",
quiz_name,
[
"name",
"title",
"max_attempts",
"show_answers",
"show_submission_history",
"passing_percentage",
],
as_dict=True,
)
quiz.questions = []
fields = ["name", "question", "type", "multiple"]
for num in range(1, 5):
fields.append(f"option_{num}")
fields.append(f"is_correct_{num}")
fields.append(f"explanation_{num}")
fields.append(f"possibility_{num}")
questions = frappe.get_all(
"LMS Quiz Question",
filters={"parent": quiz.name},
fields=["question", "marks"],
order_by="idx",
)
for question in questions:
details = frappe.db.get_value("LMS Question", question.question, fields, as_dict=1)
details["marks"] = question.marks
quiz.questions.append(details)
no_of_attempts = frappe.db.count( no_of_attempts = frappe.db.count(
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name} "LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
) )

View File

@@ -785,13 +785,12 @@ input[type=checkbox] {
} }
.breadcrumb { .breadcrumb {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: var(--text-base); font-size: var(--text-base);
line-height: 20px; line-height: 20px;
color: var(--gray-900); color: var(--gray-900);
padding: 0; padding: 0;
border-radius: 0;
} }
.course-details-outline { .course-details-outline {
@@ -2387,7 +2386,6 @@ select {
border: 1px solid var(--gray-200) !important; border: 1px solid var(--gray-200) !important;
border-radius: var(--border-radius-md) !important; border-radius: var(--border-radius-md) !important;
background-color: var(--gray-100) !important; background-color: var(--gray-100) !important;
overflow: auto;
} }
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week { .toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
@@ -2443,23 +2441,13 @@ select {
} }
.calendar-legends { .calendar-legends {
display: grid; display: flex;
grid-template-columns: repeat(4, 1fr); align-items: center;
width: 75%; justify-content: space-between;
width: 50%;
margin: 0 auto 1rem; margin: 0 auto 1rem;
} }
@media (max-width: 767px) {
.calendar-legends {
grid-template-columns: repeat(2, 1fr);
width: 100%;
}
.legend-item {
margin-bottom: 0.5rem;
}
}
.batch-details { .batch-details {
width: 50%; width: 50%;
margin: 2rem 0; margin: 2rem 0;
@@ -2485,16 +2473,4 @@ select {
.modal-body .ql-container { .modal-body .ql-container {
max-height: unset !important; max-height: unset !important;
}
.questions-table .row-index {
display: none;
}
.text-color {
color: var(--text-color);
}
.toastui-calendar-weekday-event-block {
box-shadow: none !important;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -267,6 +267,15 @@ const open_batch_dialog = () => {
fieldname: "published", fieldname: "published",
default: batch_info && batch_info.published, default: batch_info && batch_info.published,
}, },
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
}, },
@@ -284,6 +293,13 @@ const open_batch_dialog = () => {
reqd: 1, reqd: 1,
default: batch_info && batch_info.end_date, default: batch_info && batch_info.end_date,
}, },
{
fieldtype: "Select",
label: __("Medium"),
fieldname: "medium",
options: ["Online", "Offline"],
default: (batch_info && batch_info.medium) || "Online",
},
{ {
fieldtype: "Column Break", fieldtype: "Column Break",
}, },
@@ -292,24 +308,12 @@ const open_batch_dialog = () => {
label: __("Start Time"), label: __("Start Time"),
fieldname: "start_time", fieldname: "start_time",
default: batch_info && batch_info.start_time, default: batch_info && batch_info.start_time,
reqd: 1,
}, },
{ {
fieldtype: "Time", fieldtype: "Time",
label: __("End Time"), label: __("End Time"),
fieldname: "end_time", fieldname: "end_time",
default: batch_info && batch_info.end_time, default: batch_info && batch_info.end_time,
reqd: 1,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Select",
label: __("Medium"),
fieldname: "medium",
options: ["Online", "Offline"],
default: (batch_info && batch_info.medium) || "Online",
}, },
{ {
fieldtype: "Link", fieldtype: "Link",
@@ -319,21 +323,6 @@ const open_batch_dialog = () => {
only_select: 1, only_select: 1,
default: batch_info && batch_info.category, default: batch_info && batch_info.category,
}, },
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{
fieldtype: "Date",
label: __("Evaluation End Date"),
fieldname: "evaluation_end_date",
default: batch_info && batch_info.evaluation_end_date,
},
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
}, },
@@ -392,15 +381,6 @@ const open_batch_dialog = () => {
depends_on: "paid_batch", depends_on: "paid_batch",
only_select: 1, only_select: 1,
}, },
{
fieldtype: "Currency",
label: __("Amount (USD)"),
fieldname: "amount_usd",
depends_on: "paid_batch",
description: __(
"If you set an amount here, then the USD equivalent setting will not get applied."
),
},
], ],
primary_action_label: __("Save"), primary_action_label: __("Save"),
primary_action: (values) => { primary_action: (values) => {

View File

@@ -1,3 +1,5 @@
{% set certificates = get_certificates(user) %}
{% if certificates | length %} {% if certificates | length %}
<div class="cards-parent"> <div class="cards-parent">
{% for certificate in certificates %} {% for certificate in certificates %}

View File

@@ -1,10 +0,0 @@
<p>
{{ _("{0} has submitted the assignment {1}").format(frappe.bold(member_name), frappe.bold(assignment_title)) }}
</p>
<br>
<p> {{ _(" Please evaluate and grade it.") }} </p>
<br>`
<a href="/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
{{ _("Open Assignment") }}
</a>

View File

@@ -3,7 +3,7 @@
</p> </p>
<br> <br>
<p> <p>
{{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }} {{ _("I am pleased to inform you that your enrollment for the upcoming training batch has been successfully processed. Congratulations!") }}
</p> </p>
<br> <br>
<p> <p>

View File

@@ -1,21 +0,0 @@
<p>
{{ _("Dear ") }} {{ student_name }},
</p>
<br>
<p>
{{ _("I am delighted to inform you that you have successfully earned your certification for the {0} course. Congratulations!").format(frappe.bold(course_title)) }}
</p>
<br>
<p>
{{ _("With this certification, you can now showcase your updated skills and share your achievement with your colleagues and on LinkedIn. To access your certificate, please click on the link provided below.") }}
</p>
<br>
<a href="/courses/{{ course_name }}/{{certificate_name}}">{{ _("Certificate Link") }}</a>
<br>
<p>
{{ _("Once again, congratulations on this significant accomplishment.")}}
</p>
<br>
<p>
{{ _("Best Regards") }}
</p>

View File

@@ -1,11 +0,0 @@
<p>
{{ _("{0} mentioned you in a comment in your batch.").format(sender) }}
</p>
<p>
<blockquote>
{{ content | markdown }}
</blockquote>
</p>
<div class="more-info">
<a href="{{ link }}">{{ _("Check Discussion") }}</a>
</div>

View File

@@ -6,12 +6,6 @@
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
</li> </li>
{% if quiz.passing_percentage %}
<li>
{{ _("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.passing_percentage) }}
</li>
{% endif %}
{% if quiz.max_attempts %} {% if quiz.max_attempts %}
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %} {% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
<li> <li>
@@ -24,7 +18,8 @@
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }} {{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
<div id="start-banner" class="common-card-style column-card align-items-center"> <div id="start-banner" class="common-card-style column-card align-items-center">
@@ -55,12 +50,8 @@
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}" <div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}"> data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
<div> <div>
<div class="pull-right font-weight-bold">
{{ question.marks }} {{ _("Marks") }}
</div>
<div class="question-number"> <div class="question-number">
{{ _("Question ") }}{{ loop.index }}: {{ instruction }} {{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
</div>
<div class="question-text"> <div class="question-text">
{{ question.question }} {{ question.question }}
</div> </div>

View File

@@ -4,7 +4,6 @@ frappe.ready(() => {
this.answer = []; this.answer = [];
this.is_correct = []; this.is_correct = [];
this.show_answers = $("#quiz-title").data("show-answers"); this.show_answers = $("#quiz-title").data("show-answers");
this.current_index = 0;
localStorage.removeItem($("#quiz-title").data("name")); localStorage.removeItem($("#quiz-title").data("name"));
$(".btn-start-quiz").click((e) => { $(".btn-start-quiz").click((e) => {
@@ -38,6 +37,7 @@ frappe.ready(() => {
$("#next").click((e) => { $("#next").click((e) => {
e.preventDefault(); e.preventDefault();
if (!this.show_answers) check_answer(); if (!this.show_answers) check_answer();
mark_active_question(e); mark_active_question(e);
}); });
@@ -48,7 +48,7 @@ frappe.ready(() => {
const mark_active_question = (e = undefined) => { const mark_active_question = (e = undefined) => {
let total_questions = $(".question").length; let total_questions = $(".question").length;
let current_index = this.current_index; let current_index = $(".active-question").attr("data-qt-index") || 0;
let next_index = parseInt(current_index) + 1; let next_index = parseInt(current_index) + 1;
if (this.show_answers) { if (this.show_answers) {
@@ -120,6 +120,7 @@ const enable_check = (e) => {
const quiz_summary = (e = undefined) => { const quiz_summary = (e = undefined) => {
e && e.preventDefault(); e && e.preventDefault();
let quiz_name = $("#quiz-title").data("name"); let quiz_name = $("#quiz-title").data("name");
let total_questions = $(".question").length;
let self = this; let self = this;
frappe.call({ frappe.call({
@@ -134,20 +135,14 @@ const quiz_summary = (e = undefined) => {
$(".quiz-footer span").addClass("hide"); $(".quiz-footer span").addClass("hide");
$("#quiz-form").prepend( $("#quiz-form").prepend(
`<div class="summary bold-heading text-center"> `<div class="summary bold-heading text-center">
${__("You got")} ${Math.ceil(data.message.percentage)}% ${__("correct answers")}
</div>
<div class="summary bold-heading text-center mt-2">
${__("Your score is")} ${data.message.score} ${__("Your score is")} ${data.message.score}
${__("out of")} ${data.message.score_out_of} ${__("out of")} ${total_questions}
</div>` </div>`
); );
$("#try-again").attr("data-submission", data.message.submission); $("#try-again").attr("data-submission", data.message.submission);
$("#try-again").removeClass("hide"); $("#try-again").removeClass("hide");
self.quiz_submitted = true; self.quiz_submitted = true;
if ( if (this.hasOwnProperty("marked_as_complete")) {
this.hasOwnProperty("marked_as_complete") &&
data.message.pass
) {
mark_progress(); mark_progress();
} }
}, },
@@ -170,7 +165,7 @@ const check_answer = (e = undefined) => {
e && e.preventDefault(); e && e.preventDefault();
let answer = $(".active-question textarea"); let answer = $(".active-question textarea");
let total_questions = $(".question").length; let total_questions = $(".question").length;
let current_index = this.current_index; let current_index = $(".active-question").attr("data-qt-index");
if (answer.length && !answer.val().trim()) { if (answer.length && !answer.val().trim()) {
frappe.throw(__("Please enter your answer")); frappe.throw(__("Please enter your answer"));
@@ -182,13 +177,12 @@ const check_answer = (e = undefined) => {
$(".explanation").removeClass("hide"); $(".explanation").removeClass("hide");
$("#check").addClass("hide"); $("#check").addClass("hide");
if (current_index == total_questions - 1) { if (current_index == total_questions) {
$("#summary").removeClass("hide"); $("#summary").removeClass("hide");
} else if (this.show_answers) { } else if (this.show_answers) {
$("#next").removeClass("hide"); $("#next").removeClass("hide");
} }
parse_options(); parse_options();
this.current_index += 1;
}; };
const parse_options = () => { const parse_options = () => {
@@ -239,9 +233,7 @@ const parse_choices = (element, is_correct) => {
? add_icon(elem, "check") ? add_icon(elem, "check")
: add_icon(elem, "wrong"); : add_icon(elem, "wrong");
} else { } else {
if (this.show_answers && is_correct[i] == 2) add_icon(elem, "minus-circle");
add_icon(elem, "minus-circle-green");
else add_icon(elem, "minus-circle");
} }
}); });
}; };
@@ -280,10 +272,12 @@ const add_icon = (element, icon) => {
}; };
const add_to_local_storage = () => { const add_to_local_storage = () => {
let current_index = $(".active-question").attr("data-qt-index");
let quiz_name = $("#quiz-title").data("name"); let quiz_name = $("#quiz-title").data("name");
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name)); let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
let quiz_obj = { let quiz_obj = {
question_index: this.current_index, question_index: current_index - 1,
answer: self.answer.join(), answer: self.answer.join(),
is_correct: self.is_correct, is_correct: self.is_correct,
}; };

View File

@@ -6,4 +6,8 @@
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}"> <a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
{{ _("Write a review") }} {{ _("Write a review") }}
</a> </a>
{% elif show_start_learing_cta(course, membership) %}
<div class="btn btn-secondary btn-sm enroll-in-course" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
</div>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@ def get_context(context):
context.no_cache = 1 context.no_cache = 1
if frappe.session.user == "Guest": if frappe.session.user == "Guest":
raise frappe.PermissionError(_("Please login to submit the assignment.")) raise frappe.PermissionError(_("You don't have permission to access this page."))
context.is_moderator = has_course_moderator_role() context.is_moderator = has_course_moderator_role()
submission = frappe.form_dict["submission"] submission = frappe.form_dict["submission"]

View File

@@ -70,7 +70,7 @@
{{ _("Title") }} {{ _("Title") }}
</div> </div>
<div class=""> <div class="">
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter | urlencode }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}> <input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
</div> </div>
</div> </div>
@@ -127,7 +127,7 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.10.0"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
{% endblock %} {% endblock %}

View File

@@ -49,9 +49,9 @@ const get_tools = () => {
vimeo: true, vimeo: true,
codepen: true, codepen: true,
slides: { slides: {
regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/, regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
embedUrl: embedUrl:
"https://docs.google.com/presentation/d/<%= remote_id %>/embed", "https://docs.google.com/presentation/d/e/<%= remote_id %>/embed",
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>", html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
}, },
}, },
@@ -234,7 +234,7 @@ const save = () => {
args: { args: {
title: $("#lesson-title").val(), title: $("#lesson-title").val(),
body: this.lesson_content_data, body: this.lesson_content_data,
chapter: decodeURIComponent($("#lesson-title").data("chapter")), chapter: $("#lesson-title").data("chapter"),
preview: $("#preview").prop("checked") ? 1 : 0, preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"), idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "", lesson: lesson ? lesson : "",
@@ -429,9 +429,9 @@ class Quiz {
} }
render_quiz(quiz) { render_quiz(quiz) {
return `<a class="common-card-style p-20 my-2 justify-center bold-heading" target="_blank" href=/quizzes/${quiz}> return `<div class="common-card-style p-2 my-2 bold-heading">
Quiz: ${quiz} Quiz: ${quiz}
</a>`; </div>`;
} }
validate(savedData) { validate(savedData) {

View File

@@ -107,13 +107,10 @@ def get_page_extensions(context):
def get_neighbours(current, lessons): def get_neighbours(current, lessons):
numbers = [lesson.number for lesson in lessons] current = flt(current)
tuples_list = [tuple(int(x) for x in s.split(".")) for s in numbers] numbers = sorted(lesson.number for lesson in lessons)
sorted_tuples = sorted(tuples_list) index = numbers.index(current)
sorted_numbers = [".".join(str(num) for num in t) for t in sorted_tuples]
index = sorted_numbers.index(current)
return { return {
"prev": sorted_numbers[index - 1] if index - 1 >= 0 else None, "prev": numbers[index - 1] if index - 1 >= 0 else None,
"next": sorted_numbers[index + 1] if index + 1 < len(sorted_numbers) else None, "next": numbers[index + 1] if index + 1 < len(numbers) else None,
} }

View File

@@ -16,9 +16,25 @@
{% macro QuizForm(quiz) %} {% macro QuizForm(quiz) %}
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}> <div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
{{ QuizDetails(quiz) }} {{ QuizDetails(quiz) }}
<div class="field-group"> {% if quiz.questions %}
<div class="questions-table"></div> <div class="field-group">
</div> <div class="field-label mb-1">
{{ _("Questions") }}
</div>
<div class="common-card-style column-card px-3 py-0">
{% for question in quiz.questions %}
{{ Question(question, loop.index) }}
{% endfor %}
</div>
<button class="btn btn-secondary btn-sm btn-add-question mt-4">
{{ _("Add Question") }}
</button>
</div>
{% endif %}
{% if quiz.name and not quiz.questions | length %}
{{ EmptyState() }}
{% endif %}
</div> </div>
{% endmacro %} {% endmacro %}
@@ -43,6 +59,11 @@
</div> </div>
<div class="align-self-center"> <div class="align-self-center">
{% if quiz.name %}
<button class="btn btn-secondary btn-sm btn-add-question mr-2">
{{ _("Add Question") }}
</button>
{% endif %}
<button class="btn btn-primary btn-sm btn-save-quiz"> <button class="btn btn-primary btn-sm btn-save-quiz">
{{ _("Save") }} {{ _("Save") }}
</button> </button>
@@ -77,30 +98,18 @@
{{ _("Enter the maximum number of times a user can attempt this quiz") }} {{ _("Enter the maximum number of times a user can attempt this quiz") }}
</div> </div>
<div> <div>
{% set max_attempts = quiz.max_attempts if quiz.name else 0 %} {% set max_attempts = quiz.max_attempts if quiz.name else 1 %}
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}"> <input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
</div> </div>
</div> </div>
<div class="field-group">
<div class="field-label reqd">
{{ _("Passing Percentage") }}
</div>
<div class="field-description">
{{ _("Minimum percentage required to pass this quiz.") }}
</div>
<div>
<input type="number" class="field-input" id="passing-percentage" value="{{ quiz.passing_percentage }}">
</div>
</div>
<div class="field-group vertically-center"> <div class="field-group vertically-center">
{% set show_answers = quiz.show_answers or not quiz.name %} {% set show_answers = quiz.show_answers or not quiz.name %}
<label for="show-answers" class="vertically-center mb-0"> <label for="show-answers" class="vertically-center mb-0">
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}> <input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
{{ _("Show Answers") }} {{ _("Show Answers") }}
</label> </label>
<label for="show-submission-history" class="vertically-center mb-0 ml-20"> <label for="upcoming" class="vertically-center mb-0 ml-20">
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}> <input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
{{ _("Show Submission History") }} {{ _("Show Submission History") }}
</label> </label>
@@ -142,9 +151,5 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
{% if has_course_instructor_role() or has_course_moderator_role() %} {{ include_script('controls.bundle.js') }}
<script>
const quiz_questions = {{ quiz.questions or [] }}
</script>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,21 +1,17 @@
frappe.ready(() => { frappe.ready(() => {
if ($(".questions-table").length) {
frappe.require("controls.bundle.js", () => {
create_questions_table();
});
}
$(".btn-save-quiz").click((e) => { $(".btn-save-quiz").click((e) => {
save_quiz(); save_quiz({
quiz_title: $("#quiz-title").val(),
max_attempts: $("#max-attempts").val(),
});
}); });
$(".question-row").click((e) => { $(".question-row").click((e) => {
edit_question(e); edit_question(e);
}); });
$(document).on("click", ".questions-table .link-btn", (e) => { $(".btn-add-question").click((e) => {
e.preventDefault(); show_question_modal();
fetch_question_data(e);
}); });
}); });
@@ -35,8 +31,6 @@ const show_question_modal = (values = {}) => {
}; };
const get_question_fields = (values = {}) => { const get_question_fields = (values = {}) => {
if (!values.question) values = {};
let dialog_fields = [ let dialog_fields = [
{ {
fieldtype: "Text Editor", fieldtype: "Text Editor",
@@ -72,7 +66,6 @@ const get_question_fields = (values = {}) => {
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'"; if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
dialog_fields.push(option); dialog_fields.push(option);
console.log(dialog_fields);
dialog_fields.push({ dialog_fields.push({
fieldtype: "Data", fieldtype: "Data",
@@ -127,16 +120,12 @@ const edit_question = (e) => {
const save_quiz = (values) => { const save_quiz = (values) => {
validate_mandatory(); validate_mandatory();
validate_questions();
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz", method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: { args: {
quiz_title: $("#quiz-title").val(), quiz_title: values.quiz_title,
max_attempts: $("#max-attempts").val(), max_attempts: values.max_attempts,
passing_percentage: $("#passing-percentage").val(),
quiz: $("#quiz-form").data("name") || "", quiz: $("#quiz-form").data("name") || "",
questions: this.table.get_value("questions"),
show_answers: $("#show-answers").is(":checked") ? 1 : 0, show_answers: $("#show-answers").is(":checked") ? 1 : 0,
show_submission_history: $("#show-submission-history").is( show_submission_history: $("#show-submission-history").is(
":checked" ":checked"
@@ -157,45 +146,13 @@ const save_quiz = (values) => {
}; };
const validate_mandatory = () => { const validate_mandatory = () => {
let fields = ["#quiz-title", "#passing-percentage"]; if (!$("#quiz-title").val()) {
fields.forEach((field, idx) => { let error = $("p")
if (!$(field).val()) { .addClass("error-message")
let error = $("p") .text(__("Please enter a Quiz Title"));
.addClass("error-message") $(error).insertAfter("#quiz-title");
.text(__("Please enter a value")); $("#quiz-title").focus();
$(error).insertAfter(field); throw "Title is mandatory";
scroll_to_element($(field));
throw "This field is mandatory";
}
});
};
const validate_questions = () => {
let questions = this.table.get_value("questions");
if (!questions.length) {
frappe.throw(__("Please add a question."));
}
questions.forEach((question, index) => {
if (!question.question) {
frappe.throw(__("Please add question in row") + " " + (index + 1));
}
if (!question.marks) {
frappe.throw(__("Please add marks in row") + " " + (index + 1));
}
});
};
const scroll_to_element = (element) => {
if ($(element).length) {
$([document.documentElement, document.body]).animate(
{
scrollTop: $(element).offset().top - 100,
},
1000
);
} }
}; };
@@ -210,98 +167,13 @@ const save_question = (values) => {
callback: (data) => { callback: (data) => {
if (data.message) this.question_dialog.hide(); if (data.message) this.question_dialog.hide();
if (values.name) { frappe.show_alert({
frappe.show_alert({ message: __("Saved"),
message: __("Saved"), indicator: "green",
indicator: "green", });
}); setTimeout(() => {
setTimeout(() => { window.location.reload();
window.location.reload(); }, 1000);
}, 1000);
} else {
let details = {
question: data.message,
};
index = this.table.get_value("questions").length;
add_question_row(details, index);
}
},
});
};
const create_questions_table = () => {
this.table = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "questions",
fieldtype: "Table",
in_place_edit: 1,
label: __("Questions"),
fields: [
{
fieldname: "question",
fieldtype: "Link",
label: __("Question"),
options: "LMS Question",
in_list_view: 1,
only_select: 1,
reqd: 1,
},
{
fieldname: "marks",
fieldtype: "Int",
label: __("Marks"),
in_list_view: 1,
reqd: 1,
},
{
fieldname: "question_name",
fieldname: "Link",
options: "LMS Quiz Question",
label: __("Question Name"),
},
],
},
],
body: $(".questions-table").get(0),
});
this.table.make();
$(".questions-table .form-section:last").removeClass("empty-section");
$(".questions-table .frappe-control").removeClass("hide-control");
$(".questions-table .form-column").addClass("p-0");
quiz_questions.forEach((question, idx) => {
add_question_row(question, idx);
});
this.table.fields_dict["questions"].grid.add_custom_button(
"New Question",
show_question_modal,
"bottom"
);
};
const add_question_row = (question, idx) => {
this.table.fields_dict["questions"].grid.add_new_row();
this.table.get_value("questions")[idx] = {
question: question.question,
marks: question.marks,
};
this.table.refresh();
};
const fetch_question_data = (e) => {
let question_name = $(e.currentTarget)
.find(".btn-open")
.attr("href")
.split("/")[3];
frappe.call({
method: "lms.lms.doctype.lms_question.lms_question.get_question_details",
args: {
question: question_name,
},
callback: (data) => {
show_question_modal(data.message);
}, },
}); });
}; };

View File

@@ -18,22 +18,14 @@ def get_context(context):
if quizname == "new-quiz": if quizname == "new-quiz":
context.quiz = frappe._dict() context.quiz = frappe._dict()
else: else:
fields_arr = ["name", "question", "type"]
context.quiz = frappe.db.get_value( context.quiz = frappe.db.get_value(
"LMS Quiz", "LMS Quiz",
quizname, quizname,
[ ["title", "name", "max_attempts", "show_answers", "show_submission_history"],
"title",
"name",
"max_attempts",
"passing_percentage",
"show_answers",
"show_submission_history",
],
as_dict=1, as_dict=1,
) )
fields_arr = ["name", "question", "marks"]
context.quiz.questions = frappe.get_all( context.quiz.questions = frappe.get_all(
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx" "LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
) )

View File

@@ -49,14 +49,11 @@
<use href="#icon-calendar"></use> <use href="#icon-calendar"></use>
</svg> </svg>
<span> <span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }} {{ frappe.utils.format_date(batch_info.start_date, "long") }} -
</span> </span>
{% if batch_info.start_date != batch_info.end_date %}
<span> <span>
- {{ frappe.utils.format_date(batch_info.end_date, "long") }} {{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span> </span>
{% endif %}
</div> </div>
<span class="seperator"></span> <span class="seperator"></span>
@@ -78,6 +75,14 @@
</div> </div>
</div> </div>
{% if is_moderator %}
<div class="mt-4">
<button class="btn btn-secondary btn-sm btn-email">
{{ _("Email to Students") }}
</button>
</div>
{% endif %}
{% if batch_info.custom_component %} {% if batch_info.custom_component %}
<div class="mt-4"> <div class="mt-4">
{{ batch_info.custom_component }} {{ batch_info.custom_component }}
@@ -91,7 +96,8 @@
<div class="mt-4"> <div class="mt-4">
<ul class="nav lms-nav" id="batches-tab"> <ul class="nav lms-nav" id="batches-tab">
{% if settings.show_dashboard and is_student %}
{% if is_student %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard"> <a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
{{ _("Dashboard") }} {{ _("Dashboard") }}
@@ -99,7 +105,6 @@
</li> </li>
{% endif %} {% endif %}
{% if settings.show_courses %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses"> <a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
{{ _("Courses") }} {{ _("Courses") }}
@@ -108,7 +113,6 @@
</span> </span>
</a> </a>
</li> </li>
{% endif %}
{% if show_timetable %} {% if show_timetable %}
<li class="nav-item"> <li class="nav-item">
@@ -119,59 +123,40 @@
{% endif %} {% endif %}
{% if is_moderator %} {% if is_moderator %}
{% if settings.show_students %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" data-toggle="tab" href="#students">
<a class="nav-link" data-toggle="tab" href="#students"> {{ _("Students") }}
{{ _("Students") }} <span class="course-list-count">
<span class="course-list-count"> {{ batch_students | length }}
{{ batch_students | length }} </span>
</span> </a>
</a> </li>
</li>
{% endif %}
{% if settings.show_assessments %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" data-toggle="tab" href="#assessments">
<a class="nav-link" data-toggle="tab" href="#assessments"> {{ _("Assessments") }}
{{ _("Assessments") }} <span class="course-list-count">
<span class="course-list-count"> {{ assessments | length }}
{{ assessments | length }} </span>
</span> </a>
</a> </li>
</li>
{% endif %}
{% if settings.show_emails %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#emails">
{{ _("Emails") }}
<span class="course-list-count">
{{ batch_emails | length }}
</span>
</a>
</li>
{% endif %}
{% endif %} {% endif %}
{% if batch_students | length and (is_moderator or is_student) %} {% if batch_students | length and (is_moderator or is_student) %}
{% if settings.show_discussions %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" data-toggle="tab" href="#discussions">
<a class="nav-link" data-toggle="tab" href="#discussions"> {{ _("Discussions") }}
{{ _("Discussions") }} </a>
</a> </li>
</li>
{% endif %}
{% if settings.show_live_class %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" data-toggle="tab" href="#live-class">
<a class="nav-link" data-toggle="tab" href="#live-class"> {{ _("Live Class") }}
{{ _("Live Class") }} <span class="course-list-count">
<span class="course-list-count"> {{ live_classes | length }}
{{ live_classes | length }} </span>
</span> </a>
</a> </li>
</li>
{% endif %}
{% endif %} {% endif %}
{% if custom_tabs_header %} {% if custom_tabs_header %}
@@ -183,17 +168,15 @@
<div class="tab-content"> <div class="tab-content">
{% if settings.show_dashboard and is_student %} {% if is_student %}
<div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard"> <div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard">
{{ Dashboard(batch_info, batch_courses, current_student) }} {{ Dashboard(batch_info, batch_courses, current_student) }}
</div> </div>
{% endif %} {% endif %}
{% if settings.show_courses %}
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses"> <div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
{{ CoursesSection(batch_info, batch_courses) }} {{ CoursesSection(batch_info, batch_courses) }}
</div> </div>
{% endif %}
{% if show_timetable %} {% if show_timetable %}
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable"> <div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
@@ -202,37 +185,23 @@
{% endif %} {% endif %}
{% if is_moderator %} {% if is_moderator %}
{% if settings.show_students %} <div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students"> {{ StudentsSection(batch_info, batch_students) }}
{{ StudentsSection(batch_info, batch_students) }} </div>
</div>
{% endif %}
{% if settings.show_assessments %} <div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments"> {{ AssessmentsSection(batch_info) }}
{{ AssessmentsSection(batch_info) }} </div>
</div>
{% endif %}
{% if settings.show_emails %}
<div class="tab-pane" id="emails" role="tabpanel" aria-labelledby="emails">
{{ EmailsSection() }}
</div>
{% endif %}
{% endif %} {% endif %}
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %} {% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
{% if settings.show_discussions %} <div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions"> {{ Discussions(batch_info) }}
{{ Discussions(batch_info) }} </div>
</div>
{% endif %}
{% if settings.show_live_class %} <div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class"> {{ LiveClassSection(batch_info, live_classes) }}
{{ LiveClassSection(batch_info, live_classes) }} </div>
</div>
{% endif %}
{% endif %} {% endif %}
{% if custom_tabs_content %} {% if custom_tabs_content %}
@@ -407,41 +376,6 @@
</article> </article>
{% endmacro %} {% endmacro %}
{% macro EmailsSection() %}
<div class="my-4">
<button class="btn btn-secondary btn-sm btn-email">
{{ _("Email to Students") }}
</button>
</div>
<div>
{% for email in batch_emails %}
<div class="frappe-card mb-5">
<div class="flex justify-between m-1">
<span class="text-color flex">
<span class="margin-right">
{% set member = frappe.db.get_value("User", email.sender, ["full_name", "username", "name", "user_image"], as_dict=1) %}
{{ widgets.Avatar(member=member, avatar_class="avatar-small") }}
</span>
<span>
{{ member.full_name }}
<div class="text-muted">
<span class="frappe-timestamp" data-timestamp="{{ email.communication_date }}" title="{{ communication_date }}">
{{ frappe.utils.pretty_date(email.communication_date) }}
</span>
</div>
</span>
</span>
</div>
<div class="ml-10">
{{ email.content }}
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro AssessmentList(assessments) %} {% macro AssessmentList(assessments) %}
{% if assessments | length %} {% if assessments | length %}
<div class="form-grid"> <div class="form-grid">
@@ -641,12 +575,9 @@
frappe.boot.single_types = [] frappe.boot.single_types = []
let courses = {{ course_list | json }}; let courses = {{ course_list | json }};
const legends = {{ legends | json }}; const legends = {{ legends | json }};
const allow_future = {{ batch_info.allow_future }}; const allow_future = {{ batch_info.allow_future }}
const is_student = "{{ is_student or '' }}";
const evaluation_end_date = "{{ batch_info.evaluation_end_date if batch_info.evaluation_end_date else '' }}"
const show_day_view = {{ settings.show_day_view }};
</script> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" /> <link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script> <script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
{% endblock %} {% endblock %}

View File

@@ -517,10 +517,6 @@ const open_evaluation_form = (e) => {
}, },
filter_description: " ", filter_description: " ",
only_select: 1, only_select: 1,
change: () => {
this.eval_form.set_value("date", "");
$("[data-fieldname='slots']").html("");
},
}, },
{ {
fieldtype: "Date", fieldtype: "Date",
@@ -530,11 +526,8 @@ const open_evaluation_form = (e) => {
min_date: new Date( min_date: new Date(
frappe.datetime.add_days(frappe.datetime.get_today(), 1) frappe.datetime.add_days(frappe.datetime.get_today(), 1)
), ),
max_date: evaluation_end_date
? new Date(evaluation_end_date)
: "",
change: () => { change: () => {
if (this.eval_form.get_value("date")) get_slots(); get_slots();
}, },
}, },
{ {
@@ -559,7 +552,7 @@ const get_slots = () => {
args: { args: {
course: this.eval_form.get_value("course"), course: this.eval_form.get_value("course"),
date: this.eval_form.get_value("date"), date: this.eval_form.get_value("date"),
batch: $(".class-details").data("batch"), batch_name: $(".class-details").data("batch"),
}, },
callback: (r) => { callback: (r) => {
if (r.message) { if (r.message) {
@@ -660,8 +653,7 @@ const setup_calendar = (events) => {
const options = get_calendar_options(element, calendar_id); const options = get_calendar_options(element, calendar_id);
const calendar = new Calendar(container, options); const calendar = new Calendar(container, options);
this.calendar_ = calendar; this.calendar_ = calendar;
create_events(calendar, events);
create_events(calendar, events, calendar_id);
add_links_to_events(calendar, events); add_links_to_events(calendar, events);
scroll_to_date(calendar, events); scroll_to_date(calendar, events);
set_calendar_range(calendar, events); set_calendar_range(calendar, events);
@@ -672,7 +664,7 @@ const get_calendar_options = (element, calendar_id) => {
const end_time = element.data("end"); const end_time = element.data("end");
return { return {
defaultView: $(window).width() < 768 || show_day_view ? "day" : "week", defaultView: "week",
usageStatistics: false, usageStatistics: false,
week: { week: {
narrowWeekend: true, narrowWeekend: true,
@@ -692,33 +684,11 @@ const get_calendar_options = (element, calendar_id) => {
}, },
], ],
template: { template: {
allday: function (event) {
let hide = event.raw.completed ? "" : "hide";
return `<div class="calendar-event-time" title="${
event.title
} - ${frappe.datetime.get_time(
event.start.d.d
)} - ${frappe.datetime.get_time(event.end.d.d)}">
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
<div class="calendar-event-title"> ${event.title} </div>
</div>`;
},
time: function (event) { time: function (event) {
let hide = event.raw.completed ? "" : "hide"; return `<div class="calendar-event-time">
return `<div class="calendar-event-time" title="${ <div> ${frappe.datetime.get_time(event.start.d.d)} -
event.title ${frappe.datetime.get_time(event.end.d.d)} </div>
} - ${frappe.datetime.get_time( <div class="calendar-event-title"> ${event.title} </div>
event.start.d.d
)} - ${frappe.datetime.get_time(event.end.d.d)}">
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
<div>
<span class="calendar-event-title"> ${event.title} </span>
<span>
${frappe.datetime.get_time(event.start.d.d)} - ${frappe.datetime.get_time(
event.end.d.d
)}
</span>
</div>
</div>`; </div>`;
}, },
}, },
@@ -733,10 +703,9 @@ const create_events = (calendar, events, calendar_id) => {
id: `event${idx}`, id: `event${idx}`,
calendarId: calendar_id, calendarId: calendar_id,
title: event.title, title: event.title,
start: `${event.date}T${format_time(event.start_time)}`, start: `${event.date}T${event.start_time}`,
end: `${event.date}T${format_time(event.end_time)}`, end: `${event.date}T${event.end_time}`,
isAllday: event.start_time ? false : true, isAllday: event.start_time ? false : true,
category: event.start_time ? "time" : "allday",
borderColor: clr, borderColor: clr,
backgroundColor: "var(--fg-color)", backgroundColor: "var(--fg-color)",
customStyle: { customStyle: {
@@ -747,11 +716,6 @@ const create_events = (calendar, events, calendar_id) => {
}, },
raw: { raw: {
url: event.url, url: event.url,
milestone: event.milestone,
name: event.name,
idx: event.idx,
parent: event.parent,
completed: event.completed,
}, },
}); });
}); });
@@ -759,92 +723,47 @@ const create_events = (calendar, events, calendar_id) => {
calendar.createEvents(calendar_events); calendar.createEvents(calendar_events);
}; };
const format_time = (time) => {
if (!time) return "00:00:00";
let time_arr = time.split(":");
if (time_arr[0] < 10) time_arr[0] = "0" + time_arr[0];
return time_arr.join(":");
};
const add_links_to_events = (calendar) => { const add_links_to_events = (calendar) => {
calendar.on("clickEvent", ({ event }) => { calendar.on("clickEvent", ({ event }) => {
let event_date = event.start.d.d; let event_date = event.start.d.d;
event_date = moment(event_date).format("YYYY-MM-DD"); event_date = moment(event_date).format("YYYY-MM-DD");
let current_date = moment().format("YYYY-MM-DD"); let current_date = moment().format("YYYY-MM-DD");
if (allow_future || moment(event_date).isSameOrBefore(current_date)) {
if ( window.open(event.raw.url, "_blank");
is_student && }
!moment(event_date).isSameOrBefore(current_date) &&
!allow_future
)
return;
if (is_student && event.raw.milestone) {
frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.is_milestone_complete",
args: {
idx: event.raw.idx,
batch: event.raw.parent,
},
callback: (data) => {
if (data.message) window.open(event.raw.url, "_blank");
else
frappe.show_alert({
message:
"Please complete all previous activities to proceed.",
indicator: "red",
});
},
});
} else window.open(event.raw.url, "_blank");
}); });
}; };
const scroll_to_date = (calendar, events) => { const scroll_to_date = (calendar, events) => {
if ( if (
new Date() < new Date(events[0].date) || new Date() < new Date(events[0].date) ||
new Date() > new Date(events.slice(-1)[0].date) new Date() > new Date(events.slice(-1).date)
) { ) {
calendar.setDate(new Date(events[0].date)); calendar.setDate(new Date(events[0].date));
} }
}; };
const set_calendar_range = (calendar, events) => { const set_calendar_range = (calendar, events) => {
let day_view = $(window).width() < 768 || show_day_view ? true : false; let week_start = moment(calendar.getDateRangeStart().d.d);
if (day_view) { let week_end = moment(calendar.getDateRangeEnd().d.d);
let calendar_date = moment(calendar.getDate().d.d).format(
"DD MMMM YYYY"
);
$(".calendar-range").text(`${calendar_date}`);
if (moment(calendar_date).isSameOrBefore(moment(events[0].date))) $(".calendar-range").text(
$("#prev-week").hide(); `${moment(week_start).format("DD MMMM YYYY")} - ${moment(
else $("#prev-week").show(); week_end
).format("DD MMMM YYYY")}`
);
if ( if (week_start.diff(moment(events[0].date), "days") <= 0) {
moment(calendar_date).isSameOrAfter( $("#prev-week").hide();
moment(events.slice(-1)[0].date)
)
)
$("#next-week").hide();
else $("#next-week").show();
} else { } else {
let week_start = moment(calendar.getDateRangeStart().d.d); $("#prev-week").show();
let week_end = moment(calendar.getDateRangeEnd().d.d); }
$(".calendar-range").text( if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) {
`${moment(week_start).format("DD MMMM YYYY")} - ${moment( $("#next-week").hide();
week_end } else {
).format("DD MMMM YYYY")}` $("#next-week").show();
);
if (week_start.diff(moment(events[0].date), "days") <= 0)
$("#prev-week").hide();
else $("#prev-week").show();
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0)
$("#next-week").hide();
else $("#next-week").show();
} }
}; };
@@ -889,33 +808,12 @@ const email_to_students = () => {
const send_email = (values) => { const send_email = (values) => {
frappe.call({ frappe.call({
method: "frappe.client.get_list", method: "lms.lms.doctype.lms_batch.lms_batch.send_email_to_students",
args: { args: {
doctype: "Batch Student", batch: $(".class-details").data("batch"),
parent: "LMS Batch",
fields: ["student"],
filters: {
parent: $(".class-details").data("batch"),
},
},
callback: (data) => {
send_email_to_students(data.message, values);
},
});
};
const send_email_to_students = (students, values) => {
students = students.map((row) => row.student);
frappe.call({
method: "frappe.core.doctype.communication.email.make",
args: {
recipients: students.join(", "),
cc: values.reply_to,
subject: values.subject, subject: values.subject,
content: values.message, reply_to: values.reply_to,
doctype: "LMS Batch", message: values.message,
name: $(".class-details").data("batch"),
send_email: 1,
}, },
callback: (r) => { callback: (r) => {
this.email_dialog.hide(); this.email_dialog.hide();
@@ -923,9 +821,6 @@ const send_email_to_students = (students, values) => {
message: __("Email sent successfully"), message: __("Email sent successfully"),
indicator: "green", indicator: "green",
}); });
setTimeout(() => {
window.location.reload();
}, 2000);
}, },
}); });
}; };

View File

@@ -43,8 +43,6 @@ def get_context(context):
"batch_details", "batch_details",
"published", "published",
"allow_future", "allow_future",
"evaluation_end_date",
"meta_image",
], ],
as_dict=True, as_dict=True,
) )
@@ -73,13 +71,6 @@ def get_context(context):
) )
context.course_name_list = [course.course for course in context.batch_courses] context.course_name_list = [course.course for course in context.batch_courses]
context.assessments = get_assessments(batch_name) context.assessments = get_assessments(batch_name)
context.batch_emails = frappe.get_all(
"Communication",
filters={"reference_doctype": "LMS Batch", "reference_name": batch_name},
fields=["subject", "content", "recipients", "cc", "communication_date", "sender"],
order_by="communication_date desc",
)
context.batch_students = get_class_student_details( context.batch_students = get_class_student_details(
batch_students, batch_courses, context.assessments batch_students, batch_courses, context.assessments
) )
@@ -107,9 +98,9 @@ def get_context(context):
}, },
) )
context.legends = get_legends(batch_name) context.legends = get_legends(batch_name)
context.settings = frappe.get_single("LMS Settings")
custom_tabs = frappe.get_hooks("lms_batch_tabs") custom_tabs = frappe.get_hooks("lms_batch_tabs")
if custom_tabs: if custom_tabs:
context.custom_tabs_header = custom_tabs.get("header_html")[0] context.custom_tabs_header = custom_tabs.get("header_html")[0]
context.custom_tabs_content = custom_tabs.get("content_html")[0] context.custom_tabs_content = custom_tabs.get("content_html")[0]
@@ -157,6 +148,7 @@ def get_class_course_details(batch_courses):
"image", "image",
"upcoming", "upcoming",
"short_introduction", "short_introduction",
"image",
"paid_course", "paid_course",
"course_price", "course_price",
"enable_certification", "enable_certification",

View File

@@ -1,214 +1,238 @@
{% extends "lms/templates/lms_base.html" %} {% block title %} {{ {% extends "lms/templates/lms_base.html" %}
_(batch_info.title) }} {% endblock %} {% block page_content %} {% block title %}
{{ _(batch_info.title) }}
{% endblock %}
{% block page_content %}
<div class="common-page-style lms-page-style"> <div class="common-page-style lms-page-style">
{{ BatchHeader(batch_info) }} {{ BatchHeader(batch_info) }}
<div class="container"> <div class="container">
{{ BatchOverlay(batch_info, courses, students) }} {{ BatchOverlay(batch_info, courses, students) }}
<div class="pt-10"> <div class="pt-10">
{{ BatchDetails(batch_info) }} {{ CourseList(courses) }} {{ BatchDetails(batch_info) }}
</div> {{ CourseList(courses) }}
</div>
</div> </div>
{{ BatchDetailsRaw() }} {{ BatchDetailsRaw() }}
</div> </div>
{% endblock %} {% macro BatchHeader(batch_info) %} {% endblock %}
{% macro BatchHeader(batch_info) %}
<div class="course-head-container"> <div class="course-head-container">
<div class="container"> <div class="container">
<div class="course-card-wide"> <div class="course-card-wide">
{{ BreadCrumb(batch_info) }} {{ BatchHeaderDetails(batch_info, {{ BreadCrumb(batch_info) }}
courses, students) }} {{ BatchHeaderDetails(batch_info, courses, students) }}
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {% macro BreadCrumb(batch_info) %} {% endmacro %}
{% macro BreadCrumb(batch_info) %}
<article class="mb-8"> <article class="mb-8">
<a class="dark-links" href="/batches"> {{ _("All Batches") }} </a> <a class="dark-links" href="/batches">
<img class="" src="/assets/lms/icons/chevron-right.svg" /> {{ _("All Batches") }}
<span class="breadcrumb-destination"> {{ _("Batch Details") }} </span> </a>
<img class="" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">
{{ _("Batch Details") }}
</span>
</article> </article>
{% endmacro %} {% macro BatchHeaderDetails(batch_info, courses, students) %} {% endmacro %}
<div class="class-details" data-batch="{{ batch_info.name }}">
<div class="page-title">{{ batch_info.title }}</div>
<div class="">{{ batch_info.description }}</div> {% macro BatchHeaderDetails(batch_info, courses, students) %}
<div class="class-details" data-batch="{{ batch_info.name }}">
<div class="mt-8"> <div class="page-title">
<svg class="icon icon-sm"> {{ batch_info.title }}
<use href="#icon-calendar"></use> </div>
</svg>
<span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
</span>
{% if batch_info.start_date != batch_info.end_date %}
<span>
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span>
{% endif %}
</div>
{% if batch_info.start_time and batch_info.end_time %} <div class="">
<div class="mt-1"> {{ batch_info.description }}
<svg class="icon icon-sm"> </div>
<use href="#icon-clock"></use>
</svg> <div class="mt-8">
<span> <svg class="icon icon-sm">
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} - <use href="#icon-calendar"></use>
</span> </svg>
<span> <span>
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }} {{ frappe.utils.format_date(batch_info.start_date, "long") }} -
</span> </span>
</div> <span>
{% endif %} {{ frappe.utils.format_date(batch_info.end_date, "long") }}
</div> </span>
{% endmacro %} {% macro BatchOverlay(batch_info, courses, students) %} </div>
{% if batch_info.start_time and batch_info.end_time %}
<div class="mt-1">
<svg class="icon icon-sm">
<use href="#icon-clock"></use>
</svg>
<span>
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
</span>
<span>
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
</span>
</div>
{% endif %}
</div>
{% endmacro %}
{% macro BatchOverlay(batch_info, courses, students) %}
<div class="course-overlay-card class-overlay"> <div class="course-overlay-card class-overlay">
<div class="course-overlay-content">
{% if batch_info.seat_count %} {% if seats_left %}
<div class="indicator-pill green pull-right">
{{ _("Seats Available") }}: {{ seats_left }}
</div>
{% else %}
<div class="indicator-pill red pull-right">
{{ _("No seats left") }}
</div>
{% endif %} {% endif %} {% if batch_info.paid_batch %}
<div class="bold-heading">
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency)
}}
</div>
{% endif %}
<div class="vertically-center mt-2"> <div class="course-overlay-content">
<svg class="icon icon-md mr-1">
<use href="#icon-education"></use>
</svg>
{{ courses | length }} {{ _("Courses") }}
</div>
<div class="mt-2"> {% if batch_info.seat_count %}
<svg class="icon icon-sm"> {% if seats_left %}
<use href="#icon-calendar"></use> <div class="indicator-pill green pull-right">
</svg> {{ _("Seats Available") }}: {{ seats_left }}
<span> </div>
{{ frappe.utils.format_date(batch_info.start_date, "long") }} {% else %}
</span> <div class="indicator-pill red pull-right">
{% if batch_info.start_date != batch_info.end_date %} {{ _("No seats left") }}
<span> </div>
- {{ frappe.utils.format_date(batch_info.end_date, "long") }} {% endif %}
</span> {% endif %}
{% endif %}
</div>
{% if batch_info.start_time and batch_info.end_time %} {% if batch_info.paid_batch %}
<div class="mt-2"> <div class="bold-heading">
<svg class="icon icon-sm"> {{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }}
<use href="#icon-clock"></use> </div>
</svg> {% endif %}
<span>
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }}
-
</span>
<span>
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
</span>
</div>
{% endif %}
<div class="mt-2"> <div class="vertically-center mt-2">
{% if is_moderator or is_evaluator %} <svg class="icon icon-md mr-1">
<a <use href="#icon-education"></use>
class="btn btn-primary wide-button" </svg>
href="/batches/{{ batch_info.name }}" {{ courses | length }} {{ _("Courses") }}
> </div>
{{ _("Manage Batch") }}
</a> <div class="mt-2">
{% elif batch_info.paid_batch and batch_info.start_date > <svg class="icon icon-sm">
frappe.utils.getdate() %} <use href="#icon-calendar"></use>
<a </svg>
class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}" <span>
href="/billing/batch/{{ batch_info.name }}" {{ frappe.utils.format_date(batch_info.start_date, "long") }} -
> </span>
{{ _("Register Now") }} <span>
</a> {{ frappe.utils.format_date(batch_info.end_date, "long") }}
{% elif batch_info.allow_self_enrollment and batch_info.seat_count </span>
and seats_left and batch_info.start_date > frappe.utils.getdate() %} </div>
<button class="btn btn-primary wide-button enroll-batch">
{{ _("Enroll Now") }} {% if batch_info.start_time and batch_info.end_time %}
</button> <div class="mt-2">
{% else %} <svg class="icon icon-sm">
<div class="alert alert-info"> <use href="#icon-clock"></use>
{{ _("To join this batch, please contact the Administrator.") }} </svg>
</div> <span>
{% endif %} {{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
</div> </span>
{% if is_moderator %} <span>
<div class="mt-2"> {{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
<div class="btn btn-secondary wide-button" id="create-batch"> </span>
{{ _("Edit") }} </div>
</div> {% endif %}
</div>
{% endif %} <div class="mt-2">
</div> {% if is_moderator or is_evaluator %}
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
{{ _("Manage Batch") }}
</a>
{% elif batch_info.paid_batch %}
<a class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
href="/billing/batch/{{ batch_info.name }}">
{{ _("Register Now") }}
</a>
{% else %}
<div class="alert alert-info">
{{ _("To join this batch, please contact the Administrator.") }}
</div>
{% endif %}
</div>
{% if is_moderator %}
<div class="mt-2">
<div class="btn btn-secondary wide-button" id="create-batch">
{{ _("Edit") }}
</div>
</div>
{% endif %}
</div>
</div> </div>
{% endmacro %} {% macro BatchDetails(batch_info) %} {% endmacro %}
<div class="batch-details">{{ batch_info.batch_details }}</div>
{% endmacro %} {% macro CourseList(courses) %} {% if courses | length or
is_moderator %}
<div class="batch-course-list">
<div class="align-center flex">
<div class="page-title">{{ _("Courses") }}</div>
{% if is_moderator %}
<button class="btn btn-default btn-sm btn-add-course ml-4">
{{ _("Add Course") }}
</button>
{% endif %}
</div>
{% if courses | length %}
{% macro BatchDetails(batch_info) %}
<div class="batch-details">
{{ batch_info.batch_details }}
</div>
{% endmacro %}
{% macro CourseList(courses) %}
{% if courses | length or is_moderator %}
<div class="batch-course-list">
<div class="flex align-center">
<div class="page-title">
{{ _("Courses") }}
</div>
{% if is_moderator %}
<button class="btn btn-default btn-sm btn-add-course ml-4">
{{ _("Add Course") }}
</button>
{% endif %}
</div>
{% if courses | length %}
<div class="cards-parent mt-2"> <div class="cards-parent mt-2">
{% for course in courses %} {% for course in courses %}
<div class="h-100"> <div class="h-100">
{% if is_moderator %} {% if is_moderator %}
<div class="card-buttons"> <div class="card-buttons">
<button <button class="btn icon-btn btn-default btn-edit-course"
class="btn icon-btn btn-default btn-edit-course" data-name="{{ course.batch_course }}" data-course="{{ course.name }}"
data-name="{{ course.batch_course }}" {% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}>
data-course="{{ course.name }}" <svg class="icon icon-sm">
{% <use href="#icon-edit"></use>
if </svg>
course.evaluator </button>
%} <button class="btn icon-btn btn-default btn-remove-course ml-2" data-course="{{ course.name }}">
data-evaluator="{{ course.evaluator }}" <svg class="icon icon-sm">
{% <use href="#icon-delete"></use>
endif </svg>
%} </button>
> </div>
<svg class="icon icon-sm"> {% endif %}
<use href="#icon-edit"></use> {{ widgets.CourseCard(course=course, read_only=False) }}
</svg>
</button>
<button
class="btn icon-btn btn-default btn-remove-course ml-2"
data-course="{{ course.name }}"
>
<svg class="icon icon-sm">
<use href="#icon-delete"></use>
</svg>
</button>
</div>
{% endif %} {{ widgets.CourseCard(course=course, read_only=False) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="">{{ _("No courses") }}</div> <div class="">
{{ _("No courses") }}
</div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endmacro %} {% macro BatchDetailsRaw() %} {% if {% endif %}
batch_info.batch_details_raw %} {% endmacro %}
<div class="mt-10 pt-10">{{ batch_info.batch_details_raw }}</div>
{% endif %} {% endmacro %} {%- block script %} {{ super() }} {% if is_moderator
%} {% macro BatchDetailsRaw() %}
<script> {% if batch_info.batch_details_raw %}
let batch_info = {{ batch_info | json }}; <div class="mt-10 pt-10">
</script> {{ batch_info.batch_details_raw }}
{% endif %} {% endblock %} </div>
{% endif %}
{% endmacro %}
{%- block script %}
{{ super() }}
{% if is_moderator %}
<script>
let batch_info = {{ batch_info | json }};
</script>
{% endif %}
{% endblock %}

View File

@@ -12,10 +12,6 @@ frappe.ready(() => {
$(".btn-remove-course").click((e) => { $(".btn-remove-course").click((e) => {
remove_course(e); remove_course(e);
}); });
$(".enroll-batch").click((e) => {
enroll_batch(e);
});
}); });
const show_course_modal = (e) => { const show_course_modal = (e) => {
@@ -58,30 +54,6 @@ const show_course_modal = (e) => {
}, 1000); }, 1000);
}; };
const enroll_batch = (e) => {
let batch_name = $(".class-details").data("batch");
if (frappe.session.user == "Guest") {
window.location.href =
"/login?redirect-to=/batches/details/" + batch_name;
}
frappe.call({
method: "lms.lms.doctype.batch_student.batch_student.enroll_batch",
args: {
batch_name: batch_name,
},
callback(r) {
frappe.show_alert(
{
message: __("Successfully Enrolled"),
indicator: "green",
},
2000
);
window.location.href = `/batches/${batch_name}`;
},
});
};
const add_course = (values, course_name) => { const add_course = (values, course_name) => {
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.add_course", method: "lms.lms.doctype.lms_batch.lms_batch.add_course",

View File

@@ -33,19 +33,13 @@ def get_context(context):
"published", "published",
"meta_image", "meta_image",
"batch_details_raw", "batch_details_raw",
"evaluation_end_date",
"amount_usd",
"allow_self_enrollment",
], ],
as_dict=1, as_dict=1,
) )
if context.batch_info.amount and context.batch_info.currency: if context.batch_info.amount and context.batch_info.currency:
amount, currency = check_multicurrency( amount, currency = check_multicurrency(
context.batch_info.amount, context.batch_info.amount, context.batch_info.currency
context.batch_info.currency,
None,
context.batch_info.amount_usd,
) )
context.batch_info.amount = amount context.batch_info.amount = amount
context.batch_info.currency = currency context.batch_info.currency = currency

View File

@@ -140,24 +140,10 @@
<use href="#icon-calendar"></use> <use href="#icon-calendar"></use>
</svg> </svg>
<span> <span>
{{ frappe.utils.format_date(batch.start_date, "medium") }} {{ frappe.utils.format_date(batch.start_date, "medium") }} -
</span>
{% if batch.start_date != batch.end_date %}
<span>
- {{ frappe.utils.format_date(batch.end_date, "long") }}
</span>
{% endif %}
</div>
<div class="mb-2">
<svg class="icon icon-sm">
<use href="#icon-clock"></use>
</svg>
<span>
{{ frappe.utils.format_time(batch.start_time, "HH:mm a") }} -
</span> </span>
<span> <span>
{{ frappe.utils.format_time(batch.end_time, "HH:mm a") }} {{ frappe.utils.format_date(batch.end_date, "medium") }}
</span> </span>
</div> </div>
@@ -206,4 +192,4 @@
let batch_info = null; let batch_info = null;
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
import frappe import frappe
from frappe.utils import getdate, get_time_str, nowtime from frappe.utils import getdate
from lms.lms.utils import ( from lms.lms.utils import (
has_course_moderator_role, has_course_moderator_role,
has_course_evaluator_role, has_course_evaluator_role,
@@ -19,14 +19,11 @@ def get_context(context):
"description", "description",
"start_date", "start_date",
"end_date", "end_date",
"start_time",
"end_time",
"paid_batch", "paid_batch",
"amount", "amount",
"currency", "currency",
"seat_count", "seat_count",
"published", "published",
"amount_usd",
], ],
order_by="start_date", order_by="start_date",
) )
@@ -37,9 +34,7 @@ def get_context(context):
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name}) batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
if batch.amount and batch.currency: if batch.amount and batch.currency:
amount, currency = check_multicurrency( amount, currency = check_multicurrency(batch.amount, batch.currency)
batch.amount, batch.currency, None, batch.amount_usd
)
batch.amount = amount batch.amount = amount
batch.currency = currency batch.currency = currency
@@ -48,16 +43,12 @@ def get_context(context):
) )
if not batch.published: if not batch.published:
private_batches.append(batch) private_batches.append(batch)
elif getdate(batch.start_date) < getdate(): elif getdate(batch.start_date) <= getdate():
past_batches.append(batch)
elif (
getdate(batch.start_date) == getdate() and get_time_str(batch.start_time) < nowtime()
):
past_batches.append(batch) past_batches.append(batch)
else: else:
upcoming_batches.append(batch) upcoming_batches.append(batch)
context.past_batches = sorted(past_batches, key=lambda d: d.start_date, reverse=True) context.past_batches = sorted(past_batches, key=lambda d: d.start_date)
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date) context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
context.private_batches = sorted(private_batches, key=lambda d: d.start_date) context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
@@ -92,6 +83,5 @@ def get_context(context):
batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count
my_batches_info.append(batchinfo) my_batches_info.append(batchinfo)
my_batches_info = sorted(my_batches_info, key=lambda d: d.start_date, reverse=True)
context.my_batches = my_batches_info context.my_batches = my_batches_info

View File

@@ -38,7 +38,7 @@
<div class="flex"> <div class="flex">
<div class="field-label"> <div class="field-label">
{{ _("Total Price: ") }} {{ _("Total Price: ") }}
<span class="total-price">{{ frappe.utils.fmt_money(amount_with_gst, 2, currency) if gst_applied else frappe.utils.fmt_money(amount, 2, currency) }}</span> <span class="total-price">{{ frappe.utils.fmt_money(amount, 2, currency) }}</span>
</div> </div>
</div> </div>
{% if gst_applied %} {% if gst_applied %}

View File

@@ -40,15 +40,15 @@ const setup_billing = () => {
reqd: 1, reqd: 1,
default: address && address.city, default: address && address.city,
}, },
{
fieldtype: "Column Break",
},
{ {
fieldtype: "Data", fieldtype: "Data",
label: __("State/Province"), label: __("State/Province"),
fieldname: "state", fieldname: "state",
default: address && address.state, default: address && address.state,
}, },
{
fieldtype: "Column Break",
},
{ {
fieldtype: "Link", fieldtype: "Link",
label: __("Country"), label: __("Country"),
@@ -75,14 +75,6 @@ const setup_billing = () => {
reqd: 1, reqd: 1,
default: address && address.phone, default: address && address.phone,
}, },
{
fieldtype: "Link",
label: __("Where did you hear about this?"),
fieldname: "source",
options: "LMS Source",
only_select: 1,
reqd: 1,
},
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
label: __("GST Details"), label: __("GST Details"),
@@ -114,7 +106,6 @@ const setup_billing = () => {
const generate_payment_link = (e) => { const generate_payment_link = (e) => {
let new_address = this.billing.get_values(); let new_address = this.billing.get_values();
validate_address(new_address);
let doctype = $(e.currentTarget).attr("data-doctype"); let doctype = $(e.currentTarget).attr("data-doctype");
let docname = decodeURIComponent($(e.currentTarget).attr("data-name")); let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
@@ -183,10 +174,8 @@ const change_currency = () => {
if (current_price != data.message) { if (current_price != data.message) {
update_price(data.message); update_price(data.message);
} }
if (data.message.includes("INR")) { if (!data.message.includes("INR")) {
$("#gst-message").removeClass("hide").addClass("show"); $("#gst-message").addClass("hide");
} else {
$("#gst-message").removeClass("show").addClass("hide");
} }
}, },
}); });
@@ -199,48 +188,3 @@ const update_price = (price) => {
indicator: "yellow", indicator: "yellow",
}); });
}; };
const validate_address = (billing_address) => {
if (billing_address.country == "India" && !billing_address.state)
frappe.throw(__("State is mandatory."));
const states = [
"Andhra Pradesh",
"Arunachal Pradesh",
"Assam",
"Bihar",
"Chhattisgarh",
"Goa",
"Gujarat",
"Haryana",
"Himachal Pradesh",
"Jharkhand",
"Karnataka",
"Kerala",
"Madhya Pradesh",
"Maharashtra",
"Manipur",
"Meghalaya",
"Mizoram",
"Nagaland",
"Odisha",
"Punjab",
"Rajasthan",
"Sikkim",
"Tamil Nadu",
"Telangana",
"Tripura",
"Uttar Pradesh",
"Uttarakhand",
"West Bengal",
];
if (
billing_address.country == "India" &&
!states.includes(billing_address.state)
)
frappe.throw(
__(
"Please enter a valid state with correct spelling and the first letter capitalized."
)
);
};

View File

@@ -15,22 +15,20 @@ def get_context(context):
validate_access(doctype, docname, module) validate_access(doctype, docname, module)
get_billing_details(context) get_billing_details(context)
context.original_currency = context.currency
context.original_amount = (
(context.amount * 1.18) if context.original_currency == "INR" else context.amount
)
context.exception_country = frappe.get_all( context.exception_country = frappe.get_all(
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country" "Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
) )
context.amount, context.currency = check_multicurrency( context.amount, context.currency = check_multicurrency(
context.amount, context.currency, None, context.amount_usd context.amount, context.currency
) )
context.address = get_address() context.address = get_address()
if context.currency == "INR": if context.currency == "INR":
context.amount_with_gst, context.gst_applied = apply_gst(context.amount, None) context.amount, context.gst_applied = apply_gst(context.amount, None)
context.original_amount = context.amount
context.original_currency = context.currency
def validate_access(doctype, docname, module): def validate_access(doctype, docname, module):
@@ -63,7 +61,7 @@ def get_billing_details(context):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Course", "LMS Course",
context.docname, context.docname,
["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"], ["title", "name", "paid_course", "course_price as amount", "currency"],
as_dict=True, as_dict=True,
) )
@@ -74,7 +72,7 @@ def get_billing_details(context):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Batch", "LMS Batch",
context.docname, context.docname,
["title", "name", "paid_batch", "amount", "currency", "amount_usd"], ["title", "name", "paid_batch", "amount", "currency"],
as_dict=True, as_dict=True,
) )
@@ -86,7 +84,6 @@ def get_billing_details(context):
context.title = details.title context.title = details.title
context.amount = details.amount context.amount = details.amount
context.currency = details.currency context.currency = details.currency
context.amount_usd = details.amount_usd
def get_address(): def get_address():

View File

@@ -1,18 +1,13 @@
{% extends "www/cohorts/base.html" %} {% block title %} {{ _("Manage") }} {{ {% extends "www/cohorts/base.html" %} {% block title %} {{ _("Manage") }} {{
course.title }} {% endblock %} {% block page_content %} course.title }} {% endblock %} {% block page_content %}
<div class="course-home-headings">{{ cohort.title }}</div> <div class="course-home-headings">{{ cohort.title }}</div>
{% if cohort.description %}
<div>
{{ frappe.utils.md_to_html(cohort.description) }}
</div>
{% endif %}
<p> <p>
{{ frappe.db.count("Cohort Subgroup", {"cohort": cohort.name}) }} {{ {{ frappe.db.count("Cohort Subgroup", {"cohort": cohort.name}) }} {{
_("Subgroups") }} | {{ frappe.db.count("Cohort Mentor", {"cohort": _("Subgroups") }} | {{ frappe.db.count("Cohort Mentor", {"cohort":
cohort.name}) }} {{ _("Mentors") }} | {{ frappe.db.count("LMS Enrollment", cohort.name}) }} {{ _("Mentors") }} | {{ frappe.db.count("LMS Enrollment",
{"cohort": cohort.name}) }} {{ _("Students") }} {"cohort": cohort.name}) }} {{ _("Students") }} | {{ frappe.db.count("Cohort
| {{ frappe.db.count("Cohort Join Request", {"cohort": cohort.name}) }} {{ _("Join Requests") }} Join Request", {"cohort": cohort.name}) }} {{ _("Join Requests") }}
</p> </p>
{% if is_mentor %} {% set sg = mentor.get_subgroup() %} {% if is_mentor %} {% set sg = mentor.get_subgroup() %}

Some files were not shown because too many files have changed in this diff Show More