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">
<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>
<p align="center">Easy to use, open source, learning management system.</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,
},
e2e: {
baseUrl: "http://pyp:8000",
baseUrl: "http://dd1:8000",
},
});

View File

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

Submodule frappe-ui deleted from 2898a0bdd1

View File

@@ -138,12 +138,12 @@
"label": "User Category",
"length": 0,
"mandatory_depends_on": null,
"modified": "2022-04-19 13:02:18.219510",
"modified": "2022-04-19 13:02:18.219508",
"module": "LMS",
"name": "User-user_category",
"no_copy": 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,
"precision": "",
"print_hide": 0,

View File

@@ -97,7 +97,8 @@ override_doctype_class = {
# Hook on document methods and 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
@@ -118,9 +119,9 @@ fixtures = ["Custom Field", "Function", "Industry"]
# Overriding Methods
# ------------------------------
#
override_whitelisted_methods = {
# "frappe.desk.search.get_names_for_mentions": "lms.lms.utils.get_names_for_mentions",
}
# override_whitelisted_methods = {
# "frappe.desk.doctype.event.event.get_events": "lms.event.get_events"
# }
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
@@ -173,8 +174,7 @@ website_route_rules = [
"to_route": "cohorts/join",
},
{"from_route": "/users", "to_route": "profiles/profile"},
{"from_route": "/job-openings", "to_route": "jobs_openings/index"},
{"from_route": "/job-openings/<job>", "to_route": "jobs_openings/job"},
{"from_route": "/jobs/<job>", "to_route": "jobs/job"},
{
"from_route": "/batches/<batchname>/students/<username>",
"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():
add_pages_to_nav()
create_batch_source()
def after_sync():
create_lms_roles()
set_default_home()
set_default_certificate_print_format()
add_all_roles_to("Administrator")
@@ -19,7 +19,7 @@ def add_pages_to_nav():
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
{"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},
]
@@ -64,6 +64,10 @@ def delete_lms_roles():
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():
if not frappe.db.exists("Role", "Course Creator"):
role = frappe.get_doc(
@@ -178,20 +182,3 @@ def delete_custom_fields():
for field in fields:
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", {
refresh: (frm) => {
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.web_form.after_save = () => {
setTimeout(() => {
window.location.href = `/job-openings`;
window.location.href = `/jobs`;
});
};
});

View File

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

View File

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

View File

@@ -1,23 +1,9 @@
# Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt
import frappe
# import frappe
from frappe.model.document import Document
class BatchStudent(Document):
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"]
for quiz in quizzes:
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
if not frappe.db.exists(
"LMS Quiz Submission",
{
"quiz": quiz,
"owner": frappe.session.user,
"percentage": [">=", passing_percentage],
},
"LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user}
):
return 0

View File

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

View File

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

View File

@@ -4,18 +4,13 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_url, validate_email_address
from frappe.email.doctype.email_template.email_template import get_email_template
from frappe.utils import validate_url
class LMSAssignmentSubmission(Document):
def validate(self):
self.validate_duplicates()
def after_insert(self):
if not frappe.flags.in_test:
self.send_mail()
def validate_duplicates(self):
if frappe.db.exists(
"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()
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) {
set_timetable(frm);
},
refresh: (frm) => {
frm.add_web_link(`/batches/details/${frm.doc.name}`, "See on website");
},
});
const set_timetable = (frm) => {
@@ -60,7 +52,6 @@ const set_timetable = (frm) => {
"start_time",
"end_time",
"duration",
"milestone",
],
filters: {
parent: frm.doc.timetable_template,
@@ -91,7 +82,6 @@ const add_timetable_rows = (frm, timetable) => {
.format("HH:mm")
: null;
child.duration = row.duration;
child.milestone = row.milestone;
});
frm.refresh_field("timetable");
@@ -131,37 +121,3 @@ const add_legend_rows = (frm, legends) => {
frm.refresh_field("timetable_legends");
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",
"end_time",
"published",
"allow_self_enrollment",
"section_break_rgfj",
"medium",
"category",
"column_break_flwy",
"seat_count",
"evaluation_end_date",
"section_break_6",
"description",
"batch_details_raw",
@@ -47,7 +45,6 @@
"column_break_iens",
"amount",
"currency",
"amount_usd",
"customisations_tab",
"section_break_ubxi",
"custom_component",
@@ -123,14 +120,12 @@
{
"fieldname": "start_time",
"fieldtype": "Time",
"label": "Start Time",
"reqd": 1
"label": "Start Time"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"label": "End Time",
"reqd": 1
"label": "End Time"
},
{
"fieldname": "assessment_tab",
@@ -282,29 +277,11 @@
"fieldname": "allow_future",
"fieldtype": "Check",
"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,
"links": [],
"modified": "2024-01-22 10:42:42.872995",
"modified": "2023-10-12 12:53:37.351989",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch",

View File

@@ -12,7 +12,9 @@ from frappe.utils import (
cint,
format_date,
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.www.utils import get_quiz_details, get_assignment_details
@@ -29,7 +31,6 @@ class LMSBatch(Document):
self.validate_membership()
self.validate_timetable()
self.send_confirmation_mail()
self.validate_evaluation_end_date()
def validate_duplicate_students(self):
students = [row.student for row in self.students]
@@ -65,14 +66,11 @@ class LMSBatch(Document):
def send_confirmation_mail(self):
for student in self.students:
if not student.confirmation_email_sent:
self.send_mail(student)
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):
subject = _("Enrollment Confirmation for the Next Training Batch")
template = "batch_confirmation"
@@ -121,27 +119,23 @@ class LMSBatch(Document):
def validate_timetable(self):
for schedule in self.timetable:
if schedule.start_time and schedule.end_time:
if get_time(schedule.start_time) > get_time(schedule.end_time) or get_time(
schedule.start_time
) == get_time(schedule.end_time):
if (
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
):
frappe.throw(
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
schedule.idx
)
)
if get_time(schedule.start_time) < get_time(self.start_time) or get_time(
schedule.start_time
) > get_time(self.end_time):
if schedule.start_time < self.start_time or schedule.start_time > self.end_time:
frappe.throw(
_("Row #{0} Start time cannot be outside the batch duration.").format(
schedule.idx
)
)
if get_time(schedule.end_time) < get_time(self.start_time) or get_time(
schedule.end_time
) > get_time(self.end_time):
if schedule.end_time < self.start_time or schedule.end_time > self.end_time:
frappe.throw(
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
)
@@ -256,10 +250,8 @@ def create_batch(
paid_batch=0,
amount=0,
currency=None,
amount_usd=0,
name=None,
published=0,
evaluation_end_date=None,
):
frappe.only_for("Moderator")
if name:
@@ -275,7 +267,7 @@ def create_batch(
"description": description,
"batch_details": batch_details,
"batch_details_raw": batch_details_raw,
"meta_image": meta_image,
"image": meta_image,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
@@ -284,9 +276,7 @@ def create_batch(
"paid_batch": paid_batch,
"amount": amount,
"currency": currency,
"amount_usd": amount_usd,
"published": published,
"evaluation_end_date": evaluation_end_date,
}
)
doc.save()
@@ -335,17 +325,7 @@ def get_batch_timetable(batch):
timetable = frappe.get_all(
"LMS Batch Timetable",
filters={"parent": batch},
fields=[
"reference_doctype",
"reference_docname",
"date",
"start_time",
"end_time",
"milestone",
"name",
"idx",
"parent",
],
fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"],
order_by="date",
)
@@ -382,26 +362,20 @@ def get_timetable_details(timetable):
assessment = frappe._dict({"assessment_name": entry.reference_docname})
if entry.reference_doctype == "Course Lesson":
entry.icon = "icon-list"
course = frappe.db.get_value(
entry.reference_doctype, entry.reference_docname, "course"
)
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":
entry.icon = "icon-quiz"
entry.url = "/quizzes"
details = get_quiz_details(assessment, frappe.session.user)
entry.update(details)
elif entry.reference_doctype == "LMS Assignment":
entry.icon = "icon-quiz"
details = get_assignment_details(assessment, frappe.session.user)
entry.update(details)
@@ -410,37 +384,12 @@ def get_timetable_details(timetable):
@frappe.whitelist()
def is_milestone_complete(idx, batch):
previous_rows = frappe.get_all(
"LMS Batch Timetable",
filters={"parent": batch, "idx": ["<", cint(idx)]},
fields=["reference_doctype", "reference_docname", "idx"],
order_by="idx",
def send_email_to_students(batch, subject, reply_to, message):
frappe.only_for("Moderator")
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
frappe.sendmail(
recipients=students,
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",
"start_time",
"end_time",
"duration",
"milestone"
"duration"
],
"fields": [
{
@@ -70,17 +69,12 @@
"fieldname": "day",
"fieldtype": "Int",
"label": "Day"
},
{
"fieldname": "milestone",
"fieldtype": "Check",
"label": "Milestone"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-20 11:58:01.782921",
"modified": "2023-10-03 17:40:31.530181",
"modified_by": "Administrator",
"module": "LMS",
"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) => {
if (frm.doc.name)

View File

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

View File

@@ -6,42 +6,12 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import add_years, nowdate
from lms.lms.utils import is_certified
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSCertificate(Document):
def validate(self):
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):
certificates = frappe.get_all(
"LMS Certificate",
@@ -78,15 +48,6 @@ def create_certificate(course):
if 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(
{
"doctype": "LMS Certificate",
@@ -94,7 +55,6 @@ def create_certificate(course):
"course": course,
"issue_date": nowdate(),
"expiry_date": expiry_date,
"template": default_certificate_template,
}
)
certificate.save(ignore_permissions=True)

View File

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

View File

@@ -2,19 +2,13 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from lms.lms.utils import has_course_moderator_role
class LMSCertificateEvaluation(Document):
def validate(self):
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"))
pass
def has_website_permission(doc, ptype, user, verbose=False):

View File

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

View File

@@ -11,20 +11,7 @@ from lms.lms.utils import get_evaluator
class LMSCertificateRequest(Document):
def validate(self):
self.validate_slot()
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):
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():
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()
def create_certificate_request(
course, date, day, start_time, end_time, batch_name=None
):
def create_certificate_request(course, date, day, start_time, end_time, batch=None):
is_member = frappe.db.exists(
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
)
@@ -144,13 +115,13 @@ def create_certificate_request(
eval.update(
{
"course": course,
"evaluator": get_evaluator(course, batch_name),
"evaluator": get_evaluator(course, batch),
"member": frappe.session.user,
"date": date,
"day": day,
"start_time": start_time,
"end_time": end_time,
"batch_name": batch_name,
"batch": batch,
}
)
eval.save(ignore_permissions=True)

View File

@@ -34,10 +34,8 @@
"related_courses",
"pricing_section",
"paid_course",
"column_break_acoj",
"course_price",
"currency",
"amount_usd",
"course_price",
"certification_section",
"enable_certification",
"expiry",
@@ -224,22 +222,12 @@
"fieldname": "course_price",
"fieldtype": "Currency",
"label": "Course Price",
"option": "currency",
"mandatory_depends_on": "paid_course"
},
{
"fieldname": "column_break_rxww",
"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",
@@ -266,7 +254,7 @@
}
],
"make_attachments_public": 1,
"modified": "2023-12-21 12:27:32.559901",
"modified": "2023-08-28 11:09:11.945066",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

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

View File

@@ -8,11 +8,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_for_document_type",
"member",
"source",
"column_break_rqkd",
"payment_for_document",
"billing_name",
"payment_received",
"payment_details_section",
@@ -118,29 +115,11 @@
"fieldname": "amount_with_gst",
"fieldtype": "Currency",
"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,
"links": [],
"modified": "2023-10-26 16:54:12.408274",
"modified": "2023-09-12 10:40:22.721371",
"modified_by": "Administrator",
"module": "LMS",
"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) {
// }
});
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",
"max_attempts",
"show_submission_history",
"section_break_hsiv",
"passing_percentage",
"column_break_rocd",
"total_marks",
"section_break_sbjx",
"questions",
"section_break_3",
@@ -47,7 +43,7 @@
"read_only": 1
},
{
"default": "0",
"default": "1",
"fieldname": "max_attempts",
"fieldtype": "Int",
"label": "Max Attempts"
@@ -94,35 +90,11 @@
"fieldname": "show_submission_history",
"fieldtype": "Check",
"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,
"links": [],
"modified": "2023-11-07 10:11:49.126789",
"modified": "2023-07-04 15:26:24.457745",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",
@@ -151,18 +123,6 @@
"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
}
],
"show_title_field_in_link": 1,

View File

@@ -5,8 +5,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, comma_and
from lms.lms.doctype.lms_question.lms_question import validate_correct_answers
from frappe.utils import cstr
from lms.lms.utils import (
generate_slug,
has_course_moderator_role,
@@ -15,22 +14,13 @@ from lms.lms.utils import (
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):
if not self.name:
self.name = generate_slug(self.title, "LMS Quiz")
def validate(self):
validate_correct_answers(self.questions)
def get_last_submission_details(self):
"""Returns the latest submission for this user."""
user = frappe.session.user
@@ -49,11 +39,76 @@ class LMSQuiz(Document):
return result[0]
def set_total_marks(quiz, questions):
marks = 0
def get_correct_options(question):
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:
marks += question.get("marks")
return marks
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: {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()
@@ -63,73 +118,45 @@ def quiz_summary(quiz, results):
for result in results:
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"]:
correct = correct and point
result["is_correct"] = 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
score += correct
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(
{
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": score,
"score_out_of": score_out_of,
"member": frappe.session.user,
"percentage": percentage,
"passing_percentage": quiz_details.passing_percentage,
}
)
submission.save(ignore_permissions=True)
return {
"score": score,
"score_out_of": score_out_of,
"submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
}
@frappe.whitelist()
def save_quiz(
quiz_title,
passing_percentage,
questions,
max_attempts=0,
quiz=None,
show_answers=1,
show_submission_history=0,
quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0
):
if not has_course_moderator_role() or not has_course_instructor_role():
return
values = {
"title": quiz_title,
"passing_percentage": passing_percentage,
"max_attempts": max_attempts,
"show_answers": show_answers,
"show_submission_history": show_submission_history,
@@ -137,77 +164,41 @@ def save_quiz(
if quiz:
frappe.db.set_value("LMS Quiz", quiz, values)
update_questions(quiz, questions)
return quiz
else:
doc = frappe.new_doc("LMS Quiz")
doc.update(values)
doc.save()
update_questions(doc.name, questions)
doc.save(ignore_permissions=True)
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()
def save_question(quiz, values, index):
values = frappe._dict(json.loads(values))
validate_correct_answers([values])
if values.get("name"):
doc = frappe.get_doc("LMS Question", values.get("name"))
doc = frappe.get_doc("LMS Quiz Question", values.get("name"))
else:
doc = frappe.new_doc("LMS Question")
doc = frappe.new_doc("LMS Quiz Question")
doc.update(
{
"question": values.question,
"question": values["question"],
"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):
if values.get(f"option_{num}"):
doc.update(
@@ -231,8 +222,9 @@ def save_question(quiz, values, index):
}
)
doc.save()
return doc.name
doc.save(ignore_permissions=True)
return quiz
@frappe.whitelist()
@@ -265,13 +257,13 @@ def check_choice_answers(question, answers):
fields.append(f"option_{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):
if question_details[f"option_{num}"] in answers:
is_correct.append(question_details[f"is_correct_{num}"])
elif question_details[f"is_correct_{num}"]:
is_correct.append(2)
else:
is_correct.append(0)
@@ -283,7 +275,9 @@ def check_input_answers(question, answer):
for num in range(1, 5):
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):
current_possibility = question_details[f"possibility_{num}"]
if current_possibility and current_possibility.lower() == answer.lower():

View File

@@ -10,36 +10,51 @@ import frappe
class TestLMSQuiz(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
frappe.get_doc(
{"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}
).save(ignore_permissions=True)
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save(
ignore_permissions=True
)
def test_with_multiple_options(self):
question = frappe.new_doc("LMS Question")
question.question = "Question Multiple"
question.type = "Choices"
question.option_1 = "Option 1"
question.is_correct_1 = 1
question.option_2 = "Option 2"
question.is_correct_2 = 1
question.save()
self.assertTrue(question.multiple)
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
quiz.append(
"questions",
{
"question": "Question Multiple",
"type": "Choices",
"option_1": "Option 1",
"is_correct_1": 1,
"option_2": "Option 2",
"is_correct_2": 1,
},
)
quiz.save()
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self):
question = frappe.new_doc("LMS Question")
question.question = "Question Multiple"
question.type = "Choices"
question.option_1 = "Option 1"
question.option_2 = "Option 2"
self.assertRaises(frappe.ValidationError, question.save)
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
quiz.append(
"questions",
{
"question": "Question no correct option",
"type": "Choices",
"option_1": "Option 1",
"option_2": "Option 2",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
def test_with_no_possible_answers(self):
question = frappe.new_doc("LMS Question")
question.question = "Question Multiple"
question.type = "User Input"
self.assertRaises(frappe.ValidationError, question.save)
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
quiz.append(
"questions",
{
"question": "Question Possible Answers",
"type": "User Input",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod
def tearDownClass(cls) -> None:
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",
"field_order": [
"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": [
{
"fieldname": "question",
"fieldtype": "Link",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Question",
"options": "LMS Question",
"reqd": 1
},
{
"default": "1",
"fieldname": "marks",
"fieldtype": "Int",
"fieldname": "option_1",
"fieldtype": "Small Text",
"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,
"label": "Marks",
"non_negative": 1,
"reqd": 1
"label": "Type",
"options": "Choices\nUser Input"
},
{
"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,
"istable": 1,
"links": [],
"modified": "2023-10-16 19:51:03.893144",
"modified": "2023-07-04 16:43:49.837134",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Question",

View File

@@ -6,11 +6,7 @@
"engine": "InnoDB",
"field_order": [
"question",
"section_break_fztv",
"question_name",
"answer",
"column_break_flus",
"marks",
"is_correct"
],
"fields": [
@@ -35,33 +31,12 @@
"in_list_view": 1,
"label": "Is Correct",
"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,
"istable": 1,
"links": [],
"modified": "2023-10-17 11:55:25.641214",
"modified": "2022-11-24 11:15:45.931119",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Result",

View File

@@ -6,16 +6,11 @@
"engine": "InnoDB",
"field_order": [
"quiz",
"score",
"course",
"column_break_3",
"member",
"member_name",
"section_break_dkpn",
"score",
"score_out_of",
"column_break_gkip",
"percentage",
"passing_percentage",
"section_break_6",
"result"
],
@@ -36,11 +31,9 @@
},
{
"fieldname": "score",
"fieldtype": "Int",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Score",
"read_only": 1,
"reqd": 1
"label": "Score"
},
{
"fieldname": "member",
@@ -72,45 +65,12 @@
"label": "Course",
"options": "LMS Course",
"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,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-17 13:07:27.979975",
"modified": "2022-11-15 15:27:07.770945",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Submission",

View File

@@ -6,10 +6,4 @@ from frappe.model.document import Document
class LMSQuizSubmission(Document):
def before_insert(self):
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
pass

View File

@@ -16,16 +16,18 @@
"portal_course_creation",
"section_break_szgq",
"send_calendar_invite_for_evaluations",
"show_day_view",
"allow_student_progress",
"batch_confirmation_template",
"column_break_2",
"show_dashboard",
"show_courses",
"show_students",
"show_assessments",
"show_live_class",
"show_discussions",
"show_emails",
"allow_student_progress",
"payment_section",
"razorpay_key",
"razorpay_secret",
"apply_gst",
"column_break_cfcv",
"default_currency",
"show_usd_equivalent",
"apply_rounding",
"exception_country",
"signup_settings_tab",
"signup_settings_section",
"terms_of_use",
@@ -40,22 +42,7 @@
"mentor_request_tab",
"mentor_request_section",
"mentor_request_creation",
"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"
"mentor_request_status_update"
],
"fields": [
{
@@ -84,8 +71,7 @@
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"label": "Show Tab in Batch"
"fieldtype": "Column Break"
},
{
"fieldname": "search_placeholder",
@@ -213,7 +199,8 @@
},
{
"fieldname": "payment_section",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Payment"
},
{
"fieldname": "default_currency",
@@ -274,86 +261,12 @@
"fieldtype": "Link",
"label": "Batch Confirmation 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,
"issingle": 1,
"links": [],
"modified": "2023-12-12 10:32:13.638368",
"modified": "2023-10-09 17:27:28.615355",
"modified_by": "Administrator",
"module": "LMS",
"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,
"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_type": "HTML",
"modified": "2023-11-29 17:34:54.514031",
"modified": "2023-02-28 19:53:47.716135",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certificate Request Creation",

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") %}
<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>

View File

@@ -11,9 +11,8 @@
"event": "Days Before",
"idx": 0,
"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_type": "HTML",
"modified": "2023-11-29 17:26:53.355501",
"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",
"modified": "2022-06-03 11:51:02.681803",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certificate Request Reminder",
@@ -21,9 +20,6 @@
"recipients": [
{
"receiver_by_document_field": "member"
},
{
"receiver_by_document_field": "evaluator"
}
],
"send_system_notification": 0,

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,
"align_labels_right": 0,
"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,
"disabled": 0,
"doc_type": "LMS Certificate",
@@ -10,20 +10,19 @@
"doctype": "Print Format",
"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}]}",
"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,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 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",
"module": "LMS",
"name": "Certificate",
"owner": "Administrator",
"page_number": "Hide",
"print_designer": 0,
"print_format_builder": 0,
"print_format_builder_beta": 1,
"print_format_type": "Jinja",

View File

@@ -4,16 +4,10 @@ import frappe
import json
import razorpay
import requests
import base64
from frappe import _
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
from frappe.desk.doctype.notification_log.notification_log import (
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.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.utils import (
add_months,
cint,
@@ -156,7 +150,7 @@ def get_lesson_details(chapter):
],
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)
lessons.append(lesson_details)
return lessons
@@ -555,9 +549,6 @@ def can_create_courses(course, member=None):
if portal_course_creation == "Anyone" and member in instructors:
return True
if not course and has_course_instructor_role(member):
return True
return False
@@ -613,20 +604,17 @@ def validate_image(path):
return path
def handle_notifications(doc, method):
def create_notification_log(doc, method):
topic = frappe.db.get_value(
"Discussion Topic",
doc.topic,
["reference_doctype", "reference_docname", "owner", "title"],
as_dict=1,
)
if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]:
if topic.reference_doctype != "Course Lesson":
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")
instructors = frappe.db.get_all(
"Course Instructor", {"parent": course}, pluck="instructor"
@@ -653,47 +641,6 @@ def create_notification_log(doc, topic):
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):
lesson_count = 0
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
@@ -827,17 +774,6 @@ def get_telemetry_boot_info():
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")
chapter_created = frappe.db.a_row_exists("Course Chapter")
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)
return {
"is_onboarded": onboarding_status,
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
"course_created": course_created,
"chapter_created": chapter_created,
"lesson_created": lesson_created,
@@ -917,9 +853,8 @@ def get_payment_options(doctype, docname, phone, country):
validate_phone_number(phone, True)
details = get_details(doctype, docname)
details.amount, details.currency = check_multicurrency(
details.amount, details.currency, country, details.amount_usd
details.amount, details.currency, country
)
if details.currency == "INR":
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"),
"description": _("Payment for {0} course").format(details["title"]),
"order_id": order["id"],
"amount": cint(order["amount"]) * 100,
"amount": order["amount"] * 100,
"currency": order["currency"],
"prefill": {
"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
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")
exception_country = frappe.get_all(
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
)
country = (
country
or frappe.db.get_value("Address", {"email_id": frappe.session.user}, "country")
or frappe.db.get_value("User", frappe.session.user, "country")
or get_country_code()
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
country = country or frappe.db.get_value(
"Address", {"email_id": frappe.session.user}, "country"
)
if amount_usd and country and country not in exception_country:
return amount_usd, "USD"
if not show_usd_equivalent or currency == "USD":
return amount, currency
@@ -968,9 +898,8 @@ def check_multicurrency(amount, currency, country=None, amount_usd=None):
amount = amount * exchange_rate
currency = "USD"
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
if apply_rounding and amount % 100 != 0:
amount = amount + 100 - amount % 100
amount = ceil(amount + 100 - amount % 100)
return amount, currency
@@ -994,7 +923,7 @@ def get_details(doctype, docname):
details = frappe.db.get_value(
"LMS Course",
docname,
["name", "title", "paid_course", "currency", "course_price as amount", "amount_usd"],
["name", "title", "paid_course", "currency", "course_price as amount"],
as_dict=True,
)
if not details.paid_course:
@@ -1003,7 +932,7 @@ def get_details(doctype, docname):
details = frappe.db.get_value(
"LMS Batch",
docname,
["name", "title", "paid_batch", "currency", "amount", "amount_usd"],
["name", "title", "paid_batch", "currency", "amount"],
as_dict=True,
)
if not details.paid_batch:
@@ -1052,15 +981,13 @@ def create_order(client, amount, currency):
try:
return client.order.create(
{
"amount": cint(amount) * 100,
"amount": amount * 100,
"currency": currency,
}
)
except Exception as e:
frappe.throw(
_(
"Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
).format(e, amount, currency, cint(amount))
_("Error during payment: {0}. Please contact the Administrator.").format(e)
)
@@ -1102,23 +1029,19 @@ def record_payment(address, response, client, doctype, docname):
"amount_with_gst": payment_details["amount_with_gst"],
"gstin": address.gstin,
"pan": address.pan,
"source": address.source,
"payment_for_document_type": doctype,
"payment_for_document": docname,
}
)
payment_doc.save(ignore_permissions=True)
return payment_doc
return payment_doc.name
def get_payment_details(doctype, docname, address):
amount_field = "course_price" if doctype == "LMS Course" else "amount"
amount = frappe.db.get_value(doctype, docname, amount_field)
currency = frappe.db.get_value(doctype, docname, "currency")
amount_usd = frappe.db.get_value(doctype, docname, "amount_usd")
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":
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):
membership = frappe.new_doc("LMS Enrollment")
membership.update(
{"member": frappe.session.user, "course": course, "payment": payment.name}
{"member": frappe.session.user, "course": course, "payment": payment}
)
membership.save(ignore_permissions=True)
return f"/courses/{course}/learn/1.1"
@@ -1143,8 +1066,7 @@ def add_student_to_batch(batchname, payment):
student.update(
{
"student": frappe.session.user,
"payment": payment.name,
"source": payment.source,
"payment": payment,
"parent": batchname,
"parenttype": "LMS Batch",
"parentfield": "students",
@@ -1167,16 +1089,3 @@ def change_currency(amount, currency, country=None):
amount = cint(amount)
amount, currency = check_multicurrency(amount, currency, country)
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}}">
{{ _("Notify me when available") }}
</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 %}
</div>
{% endif %}

View File

@@ -5,7 +5,7 @@ from frappe import _
from frappe.core.doctype.user.user import User
from frappe.utils import cint, escape_html, random_string
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.model.naming import append_number_if_name_exists
from lms.widgets import Widgets
@@ -260,6 +260,19 @@ def set_country_from_ip(login_manager=None, user=None):
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):
if frappe.db.get_single_value(
"System Settings", "setup_complete"

View File

@@ -1,4 +1,3 @@
[pre_model_sync]
community.patches.set_email_preferences
community.patches.change_name_for_community_members
community.patches.save_abbr_for_community_members
@@ -73,13 +72,3 @@ lms.patches.v1_0.publish_certificates
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
execute:frappe.permissions.reset_perms("LMS Enrollment")
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>"
quiz = frappe.db.get_value(
"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)
quiz = frappe.get_doc("LMS Quiz", quiz_name)
no_of_attempts = frappe.db.count(
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
)

View File

@@ -785,13 +785,12 @@ input[type=checkbox] {
}
.breadcrumb {
display: flex;
align-items: center;
font-size: var(--text-base);
line-height: 20px;
color: var(--gray-900);
padding: 0;
border-radius: 0;
display: flex;
align-items: center;
font-size: var(--text-base);
line-height: 20px;
color: var(--gray-900);
padding: 0;
}
.course-details-outline {
@@ -2387,7 +2386,6 @@ select {
border: 1px solid var(--gray-200) !important;
border-radius: var(--border-radius-md) !important;
background-color: var(--gray-100) !important;
overflow: auto;
}
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
@@ -2443,23 +2441,13 @@ select {
}
.calendar-legends {
display: grid;
grid-template-columns: repeat(4, 1fr);
width: 75%;
display: flex;
align-items: center;
justify-content: space-between;
width: 50%;
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 {
width: 50%;
margin: 2rem 0;
@@ -2486,15 +2474,3 @@ select {
.modal-body .ql-container {
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",
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",
},
@@ -284,6 +293,13 @@ const open_batch_dialog = () => {
reqd: 1,
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",
},
@@ -292,24 +308,12 @@ const open_batch_dialog = () => {
label: __("Start Time"),
fieldname: "start_time",
default: batch_info && batch_info.start_time,
reqd: 1,
},
{
fieldtype: "Time",
label: __("End Time"),
fieldname: "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",
@@ -319,21 +323,6 @@ const open_batch_dialog = () => {
only_select: 1,
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",
},
@@ -392,15 +381,6 @@ const open_batch_dialog = () => {
depends_on: "paid_batch",
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: (values) => {

View File

@@ -1,3 +1,5 @@
{% set certificates = get_certificates(user) %}
{% if certificates | length %}
<div class="cards-parent">
{% 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>
<br>
<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>
<br>
<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) }}
</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 %}
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
<li>
@@ -24,7 +18,8 @@
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
</li>
{% endif %}
</ul>
</ul>
<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 }}"
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
<div>
<div class="pull-right font-weight-bold">
{{ question.marks }} {{ _("Marks") }}
</div>
<div class="question-number">
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}
</div>
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
<div class="question-text">
{{ question.question }}
</div>

View File

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

View File

@@ -7,7 +7,7 @@ def get_context(context):
context.no_cache = 1
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()
submission = frappe.form_dict["submission"]

View File

@@ -70,7 +70,7 @@
{{ _("Title") }}
</div>
<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>
@@ -127,7 +127,7 @@
{%- block script %}
{{ super() }}
<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/embed@latest"></script>
{% endblock %}

View File

@@ -49,9 +49,9 @@ const get_tools = () => {
vimeo: true,
codepen: true,
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:
"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>",
},
},
@@ -234,7 +234,7 @@ const save = () => {
args: {
title: $("#lesson-title").val(),
body: this.lesson_content_data,
chapter: decodeURIComponent($("#lesson-title").data("chapter")),
chapter: $("#lesson-title").data("chapter"),
preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "",
@@ -429,9 +429,9 @@ class 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}
</a>`;
</div>`;
}
validate(savedData) {

View File

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

View File

@@ -16,9 +16,25 @@
{% macro QuizForm(quiz) %}
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
{{ QuizDetails(quiz) }}
<div class="field-group">
<div class="questions-table"></div>
</div>
{% if quiz.questions %}
<div class="field-group">
<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>
{% endmacro %}
@@ -43,6 +59,11 @@
</div>
<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">
{{ _("Save") }}
</button>
@@ -77,30 +98,18 @@
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
</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 }}">
</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">
{% set show_answers = quiz.show_answers or not quiz.name %}
<label for="show-answers" class="vertically-center mb-0">
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
{{ _("Show Answers") }}
</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 %}>
{{ _("Show Submission History") }}
</label>
@@ -142,9 +151,5 @@
{%- block script %}
{{ super() }}
{% if has_course_instructor_role() or has_course_moderator_role() %}
<script>
const quiz_questions = {{ quiz.questions or [] }}
</script>
{% endif %}
{{ include_script('controls.bundle.js') }}
{% endblock %}

View File

@@ -1,21 +1,17 @@
frappe.ready(() => {
if ($(".questions-table").length) {
frappe.require("controls.bundle.js", () => {
create_questions_table();
});
}
$(".btn-save-quiz").click((e) => {
save_quiz();
save_quiz({
quiz_title: $("#quiz-title").val(),
max_attempts: $("#max-attempts").val(),
});
});
$(".question-row").click((e) => {
edit_question(e);
});
$(document).on("click", ".questions-table .link-btn", (e) => {
e.preventDefault();
fetch_question_data(e);
$(".btn-add-question").click((e) => {
show_question_modal();
});
});
@@ -35,8 +31,6 @@ const show_question_modal = (values = {}) => {
};
const get_question_fields = (values = {}) => {
if (!values.question) values = {};
let dialog_fields = [
{
fieldtype: "Text Editor",
@@ -72,7 +66,6 @@ const get_question_fields = (values = {}) => {
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
dialog_fields.push(option);
console.log(dialog_fields);
dialog_fields.push({
fieldtype: "Data",
@@ -127,16 +120,12 @@ const edit_question = (e) => {
const save_quiz = (values) => {
validate_mandatory();
validate_questions();
frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: {
quiz_title: $("#quiz-title").val(),
max_attempts: $("#max-attempts").val(),
passing_percentage: $("#passing-percentage").val(),
quiz_title: values.quiz_title,
max_attempts: values.max_attempts,
quiz: $("#quiz-form").data("name") || "",
questions: this.table.get_value("questions"),
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
show_submission_history: $("#show-submission-history").is(
":checked"
@@ -157,45 +146,13 @@ const save_quiz = (values) => {
};
const validate_mandatory = () => {
let fields = ["#quiz-title", "#passing-percentage"];
fields.forEach((field, idx) => {
if (!$(field).val()) {
let error = $("p")
.addClass("error-message")
.text(__("Please enter a value"));
$(error).insertAfter(field);
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
);
if (!$("#quiz-title").val()) {
let error = $("p")
.addClass("error-message")
.text(__("Please enter a Quiz Title"));
$(error).insertAfter("#quiz-title");
$("#quiz-title").focus();
throw "Title is mandatory";
}
};
@@ -210,98 +167,13 @@ const save_question = (values) => {
callback: (data) => {
if (data.message) this.question_dialog.hide();
if (values.name) {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 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);
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 1000);
},
});
};

View File

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

View File

@@ -49,14 +49,11 @@
<use href="#icon-calendar"></use>
</svg>
<span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
{{ 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") }}
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span>
{% endif %}
</div>
<span class="seperator"></span>
@@ -78,6 +75,14 @@
</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 %}
<div class="mt-4">
{{ batch_info.custom_component }}
@@ -91,7 +96,8 @@
<div class="mt-4">
<ul class="nav lms-nav" id="batches-tab">
{% if settings.show_dashboard and is_student %}
{% if is_student %}
<li class="nav-item">
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
{{ _("Dashboard") }}
@@ -99,7 +105,6 @@
</li>
{% endif %}
{% if settings.show_courses %}
<li class="nav-item">
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
{{ _("Courses") }}
@@ -108,7 +113,6 @@
</span>
</a>
</li>
{% endif %}
{% if show_timetable %}
<li class="nav-item">
@@ -119,59 +123,40 @@
{% endif %}
{% if is_moderator %}
{% if settings.show_students %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#students">
{{ _("Students") }}
<span class="course-list-count">
{{ batch_students | length }}
</span>
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#students">
{{ _("Students") }}
<span class="course-list-count">
{{ batch_students | length }}
</span>
</a>
</li>
{% if settings.show_assessments %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#assessments">
{{ _("Assessments") }}
<span class="course-list-count">
{{ assessments | length }}
</span>
</a>
</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 %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#assessments">
{{ _("Assessments") }}
<span class="course-list-count">
{{ assessments | length }}
</span>
</a>
</li>
{% endif %}
{% if batch_students | length and (is_moderator or is_student) %}
{% if settings.show_discussions %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#discussions">
{{ _("Discussions") }}
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#discussions">
{{ _("Discussions") }}
</a>
</li>
{% if settings.show_live_class %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#live-class">
{{ _("Live Class") }}
<span class="course-list-count">
{{ live_classes | length }}
</span>
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#live-class">
{{ _("Live Class") }}
<span class="course-list-count">
{{ live_classes | length }}
</span>
</a>
</li>
{% endif %}
{% if custom_tabs_header %}
@@ -183,17 +168,15 @@
<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">
{{ Dashboard(batch_info, batch_courses, current_student) }}
</div>
{% endif %}
{% if settings.show_courses %}
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
{{ CoursesSection(batch_info, batch_courses) }}
</div>
{% endif %}
{% if show_timetable %}
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
@@ -202,37 +185,23 @@
{% endif %}
{% if is_moderator %}
{% if settings.show_students %}
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
{{ StudentsSection(batch_info, batch_students) }}
</div>
{% endif %}
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
{{ StudentsSection(batch_info, batch_students) }}
</div>
{% if settings.show_assessments %}
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
{{ AssessmentsSection(batch_info) }}
</div>
{% endif %}
{% if settings.show_emails %}
<div class="tab-pane" id="emails" role="tabpanel" aria-labelledby="emails">
{{ EmailsSection() }}
</div>
{% endif %}
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
{{ AssessmentsSection(batch_info) }}
</div>
{% endif %}
{% 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">
{{ Discussions(batch_info) }}
</div>
{% endif %}
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
{{ Discussions(batch_info) }}
</div>
{% if settings.show_live_class %}
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
{{ LiveClassSection(batch_info, live_classes) }}
</div>
{% endif %}
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
{{ LiveClassSection(batch_info, live_classes) }}
</div>
{% endif %}
{% if custom_tabs_content %}
@@ -407,41 +376,6 @@
</article>
{% 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) %}
{% if assessments | length %}
<div class="form-grid">
@@ -641,10 +575,7 @@
frappe.boot.single_types = []
let courses = {{ course_list | json }};
const legends = {{ legends | json }};
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 }};
const allow_future = {{ batch_info.allow_future }}
</script>
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />

View File

@@ -517,10 +517,6 @@ const open_evaluation_form = (e) => {
},
filter_description: " ",
only_select: 1,
change: () => {
this.eval_form.set_value("date", "");
$("[data-fieldname='slots']").html("");
},
},
{
fieldtype: "Date",
@@ -530,11 +526,8 @@ const open_evaluation_form = (e) => {
min_date: new Date(
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
),
max_date: evaluation_end_date
? new Date(evaluation_end_date)
: "",
change: () => {
if (this.eval_form.get_value("date")) get_slots();
get_slots();
},
},
{
@@ -559,7 +552,7 @@ const get_slots = () => {
args: {
course: this.eval_form.get_value("course"),
date: this.eval_form.get_value("date"),
batch: $(".class-details").data("batch"),
batch_name: $(".class-details").data("batch"),
},
callback: (r) => {
if (r.message) {
@@ -660,8 +653,7 @@ const setup_calendar = (events) => {
const options = get_calendar_options(element, calendar_id);
const calendar = new Calendar(container, options);
this.calendar_ = calendar;
create_events(calendar, events, calendar_id);
create_events(calendar, events);
add_links_to_events(calendar, events);
scroll_to_date(calendar, events);
set_calendar_range(calendar, events);
@@ -672,7 +664,7 @@ const get_calendar_options = (element, calendar_id) => {
const end_time = element.data("end");
return {
defaultView: $(window).width() < 768 || show_day_view ? "day" : "week",
defaultView: "week",
usageStatistics: false,
week: {
narrowWeekend: true,
@@ -692,33 +684,11 @@ const get_calendar_options = (element, calendar_id) => {
},
],
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) {
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>
<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>
return `<div class="calendar-event-time">
<div> ${frappe.datetime.get_time(event.start.d.d)} -
${frappe.datetime.get_time(event.end.d.d)} </div>
<div class="calendar-event-title"> ${event.title} </div>
</div>`;
},
},
@@ -733,10 +703,9 @@ const create_events = (calendar, events, calendar_id) => {
id: `event${idx}`,
calendarId: calendar_id,
title: event.title,
start: `${event.date}T${format_time(event.start_time)}`,
end: `${event.date}T${format_time(event.end_time)}`,
start: `${event.date}T${event.start_time}`,
end: `${event.date}T${event.end_time}`,
isAllday: event.start_time ? false : true,
category: event.start_time ? "time" : "allday",
borderColor: clr,
backgroundColor: "var(--fg-color)",
customStyle: {
@@ -747,11 +716,6 @@ const create_events = (calendar, events, calendar_id) => {
},
raw: {
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);
};
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) => {
calendar.on("clickEvent", ({ event }) => {
let event_date = event.start.d.d;
event_date = moment(event_date).format("YYYY-MM-DD");
let current_date = moment().format("YYYY-MM-DD");
if (
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");
if (allow_future || moment(event_date).isSameOrBefore(current_date)) {
window.open(event.raw.url, "_blank");
}
});
};
const scroll_to_date = (calendar, events) => {
if (
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));
}
};
const set_calendar_range = (calendar, events) => {
let day_view = $(window).width() < 768 || show_day_view ? true : false;
if (day_view) {
let calendar_date = moment(calendar.getDate().d.d).format(
"DD MMMM YYYY"
);
$(".calendar-range").text(`${calendar_date}`);
let week_start = moment(calendar.getDateRangeStart().d.d);
let week_end = moment(calendar.getDateRangeEnd().d.d);
if (moment(calendar_date).isSameOrBefore(moment(events[0].date)))
$("#prev-week").hide();
else $("#prev-week").show();
$(".calendar-range").text(
`${moment(week_start).format("DD MMMM YYYY")} - ${moment(
week_end
).format("DD MMMM YYYY")}`
);
if (
moment(calendar_date).isSameOrAfter(
moment(events.slice(-1)[0].date)
)
)
$("#next-week").hide();
else $("#next-week").show();
if (week_start.diff(moment(events[0].date), "days") <= 0) {
$("#prev-week").hide();
} else {
let week_start = moment(calendar.getDateRangeStart().d.d);
let week_end = moment(calendar.getDateRangeEnd().d.d);
$("#prev-week").show();
}
$(".calendar-range").text(
`${moment(week_start).format("DD MMMM YYYY")} - ${moment(
week_end
).format("DD MMMM YYYY")}`
);
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();
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) => {
frappe.call({
method: "frappe.client.get_list",
method: "lms.lms.doctype.lms_batch.lms_batch.send_email_to_students",
args: {
doctype: "Batch Student",
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,
batch: $(".class-details").data("batch"),
subject: values.subject,
content: values.message,
doctype: "LMS Batch",
name: $(".class-details").data("batch"),
send_email: 1,
reply_to: values.reply_to,
message: values.message,
},
callback: (r) => {
this.email_dialog.hide();
@@ -923,9 +821,6 @@ const send_email_to_students = (students, values) => {
message: __("Email sent successfully"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 2000);
},
});
};

View File

@@ -43,8 +43,6 @@ def get_context(context):
"batch_details",
"published",
"allow_future",
"evaluation_end_date",
"meta_image",
],
as_dict=True,
)
@@ -73,13 +71,6 @@ def get_context(context):
)
context.course_name_list = [course.course for course in context.batch_courses]
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(
batch_students, batch_courses, context.assessments
)
@@ -107,9 +98,9 @@ def get_context(context):
},
)
context.legends = get_legends(batch_name)
context.settings = frappe.get_single("LMS Settings")
custom_tabs = frappe.get_hooks("lms_batch_tabs")
if custom_tabs:
context.custom_tabs_header = custom_tabs.get("header_html")[0]
context.custom_tabs_content = custom_tabs.get("content_html")[0]
@@ -157,6 +148,7 @@ def get_class_course_details(batch_courses):
"image",
"upcoming",
"short_introduction",
"image",
"paid_course",
"course_price",
"enable_certification",

View File

@@ -1,214 +1,238 @@
{% extends "lms/templates/lms_base.html" %} {% block title %} {{
_(batch_info.title) }} {% endblock %} {% block page_content %}
{% extends "lms/templates/lms_base.html" %}
{% block title %}
{{ _(batch_info.title) }}
{% endblock %}
{% block page_content %}
<div class="common-page-style lms-page-style">
{{ BatchHeader(batch_info) }}
{{ BatchHeader(batch_info) }}
<div class="container">
{{ BatchOverlay(batch_info, courses, students) }}
<div class="pt-10">
{{ BatchDetails(batch_info) }} {{ CourseList(courses) }}
</div>
{{ BatchOverlay(batch_info, courses, students) }}
<div class="pt-10">
{{ BatchDetails(batch_info) }}
{{ CourseList(courses) }}
</div>
</div>
{{ BatchDetailsRaw() }}
{{ BatchDetailsRaw() }}
</div>
{% endblock %} {% macro BatchHeader(batch_info) %}
{% endblock %}
{% macro BatchHeader(batch_info) %}
<div class="course-head-container">
<div class="container">
<div class="course-card-wide">
{{ BreadCrumb(batch_info) }} {{ BatchHeaderDetails(batch_info,
courses, students) }}
</div>
</div>
<div class="container">
<div class="course-card-wide">
{{ BreadCrumb(batch_info) }}
{{ BatchHeaderDetails(batch_info, courses, students) }}
</div>
</div>
</div>
{% endmacro %} {% macro BreadCrumb(batch_info) %}
{% endmacro %}
{% macro BreadCrumb(batch_info) %}
<article class="mb-8">
<a class="dark-links" href="/batches"> {{ _("All Batches") }} </a>
<img class="" src="/assets/lms/icons/chevron-right.svg" />
<span class="breadcrumb-destination"> {{ _("Batch Details") }} </span>
<a class="dark-links" href="/batches">
{{ _("All Batches") }}
</a>
<img class="" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">
{{ _("Batch Details") }}
</span>
</article>
{% endmacro %} {% macro BatchHeaderDetails(batch_info, courses, students) %}
<div class="class-details" data-batch="{{ batch_info.name }}">
<div class="page-title">{{ batch_info.title }}</div>
{% endmacro %}
<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">
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</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>
<div class="page-title">
{{ batch_info.title }}
</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="">
{{ batch_info.description }}
</div>
<div class="mt-8">
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</svg>
<span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
</span>
<span>
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span>
</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-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">
<svg class="icon icon-md mr-1">
<use href="#icon-education"></use>
</svg>
{{ courses | length }} {{ _("Courses") }}
</div>
<div class="course-overlay-content">
<div class="mt-2">
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</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.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.start_time and batch_info.end_time %}
<div class="mt-2">
<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 %}
{% if batch_info.paid_batch %}
<div class="bold-heading">
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }}
</div>
{% endif %}
<div class="mt-2">
{% 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 and batch_info.start_date >
frappe.utils.getdate() %}
<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>
{% elif batch_info.allow_self_enrollment and batch_info.seat_count
and seats_left and batch_info.start_date > frappe.utils.getdate() %}
<button class="btn btn-primary wide-button enroll-batch">
{{ _("Enroll Now") }}
</button>
{% 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 class="vertically-center mt-2">
<svg class="icon icon-md mr-1">
<use href="#icon-education"></use>
</svg>
{{ courses | length }} {{ _("Courses") }}
</div>
<div class="mt-2">
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</svg>
<span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
</span>
<span>
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span>
</div>
{% if batch_info.start_time and batch_info.end_time %}
<div class="mt-2">
<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 class="mt-2">
{% 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>
{% endmacro %} {% 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="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>
{% endmacro %}
{% 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">
{% for course in courses %}
<div class="h-100">
{% if is_moderator %}
<div class="card-buttons">
<button
class="btn icon-btn btn-default btn-edit-course"
data-name="{{ course.batch_course }}"
data-course="{{ course.name }}"
{%
if
course.evaluator
%}
data-evaluator="{{ course.evaluator }}"
{%
endif
%}
>
<svg class="icon icon-sm">
<use href="#icon-edit"></use>
</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) }}
{% if is_moderator %}
<div class="card-buttons">
<button class="btn icon-btn btn-default btn-edit-course"
data-name="{{ course.batch_course }}" data-course="{{ course.name }}"
{% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}>
<svg class="icon icon-sm">
<use href="#icon-edit"></use>
</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>
{% endfor %}
</div>
{% else %}
<div class="">{{ _("No courses") }}</div>
<div class="">
{{ _("No courses") }}
</div>
{% endif %}
</div>
{% endif %} {% endmacro %} {% macro BatchDetailsRaw() %} {% if
batch_info.batch_details_raw %}
<div class="mt-10 pt-10">{{ batch_info.batch_details_raw }}</div>
{% endif %} {% endmacro %} {%- block script %} {{ super() }} {% if is_moderator
%}
<script>
let batch_info = {{ batch_info | json }};
</script>
{% endif %} {% endblock %}
{% endif %}
{% endmacro %}
{% macro BatchDetailsRaw() %}
{% if batch_info.batch_details_raw %}
<div class="mt-10 pt-10">
{{ batch_info.batch_details_raw }}
</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) => {
remove_course(e);
});
$(".enroll-batch").click((e) => {
enroll_batch(e);
});
});
const show_course_modal = (e) => {
@@ -58,30 +54,6 @@ const show_course_modal = (e) => {
}, 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) => {
frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.add_course",

View File

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

View File

@@ -140,24 +140,10 @@
<use href="#icon-calendar"></use>
</svg>
<span>
{{ 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") }} -
{{ frappe.utils.format_date(batch.start_date, "medium") }} -
</span>
<span>
{{ frappe.utils.format_time(batch.end_time, "HH:mm a") }}
{{ frappe.utils.format_date(batch.end_date, "medium") }}
</span>
</div>

View File

@@ -1,5 +1,5 @@
import frappe
from frappe.utils import getdate, get_time_str, nowtime
from frappe.utils import getdate
from lms.lms.utils import (
has_course_moderator_role,
has_course_evaluator_role,
@@ -19,14 +19,11 @@ def get_context(context):
"description",
"start_date",
"end_date",
"start_time",
"end_time",
"paid_batch",
"amount",
"currency",
"seat_count",
"published",
"amount_usd",
],
order_by="start_date",
)
@@ -37,9 +34,7 @@ def get_context(context):
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
if batch.amount and batch.currency:
amount, currency = check_multicurrency(
batch.amount, batch.currency, None, batch.amount_usd
)
amount, currency = check_multicurrency(batch.amount, batch.currency)
batch.amount = amount
batch.currency = currency
@@ -48,16 +43,12 @@ def get_context(context):
)
if not batch.published:
private_batches.append(batch)
elif getdate(batch.start_date) < getdate():
past_batches.append(batch)
elif (
getdate(batch.start_date) == getdate() and get_time_str(batch.start_time) < nowtime()
):
elif getdate(batch.start_date) <= getdate():
past_batches.append(batch)
else:
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.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
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

View File

@@ -38,7 +38,7 @@
<div class="flex">
<div class="field-label">
{{ _("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>
{% if gst_applied %}

View File

@@ -40,15 +40,15 @@ const setup_billing = () => {
reqd: 1,
default: address && address.city,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Data",
label: __("State/Province"),
fieldname: "state",
default: address && address.state,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Link",
label: __("Country"),
@@ -75,14 +75,6 @@ const setup_billing = () => {
reqd: 1,
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",
label: __("GST Details"),
@@ -114,7 +106,6 @@ const setup_billing = () => {
const generate_payment_link = (e) => {
let new_address = this.billing.get_values();
validate_address(new_address);
let doctype = $(e.currentTarget).attr("data-doctype");
let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
@@ -183,10 +174,8 @@ const change_currency = () => {
if (current_price != data.message) {
update_price(data.message);
}
if (data.message.includes("INR")) {
$("#gst-message").removeClass("hide").addClass("show");
} else {
$("#gst-message").removeClass("show").addClass("hide");
if (!data.message.includes("INR")) {
$("#gst-message").addClass("hide");
}
},
});
@@ -199,48 +188,3 @@ const update_price = (price) => {
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)
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(
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
)
context.amount, context.currency = check_multicurrency(
context.amount, context.currency, None, context.amount_usd
context.amount, context.currency
)
context.address = get_address()
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):
@@ -63,7 +61,7 @@ def get_billing_details(context):
details = frappe.db.get_value(
"LMS Course",
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,
)
@@ -74,7 +72,7 @@ def get_billing_details(context):
details = frappe.db.get_value(
"LMS Batch",
context.docname,
["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
["title", "name", "paid_batch", "amount", "currency"],
as_dict=True,
)
@@ -86,7 +84,6 @@ def get_billing_details(context):
context.title = details.title
context.amount = details.amount
context.currency = details.currency
context.amount_usd = details.amount_usd
def get_address():

View File

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

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