| # Copyright (c) 2015, Frappe Technologies and contributors |
| |
| import frappe |
| from frappe import _ |
| |
| |
| class OverlapError(frappe.ValidationError): |
| pass |
| |
| |
| def validate_overlap_for(doc, doctype, fieldname, value=None): |
| """Checks overlap for specified field. |
| |
| :param fieldname: Checks Overlap for this field |
| """ |
| |
| existing = get_overlap_for(doc, doctype, fieldname, value) |
| if existing: |
| frappe.throw( |
| _("This {0} conflicts with {1} for {2} {3}").format( |
| doc.doctype, |
| existing.name, |
| doc.meta.get_label(fieldname) if not value else fieldname, |
| value or doc.get(fieldname), |
| ), |
| OverlapError, |
| ) |
| |
| |
| def get_overlap_for(doc, doctype, fieldname, value=None): |
| """Returns overlaping document for specified field. |
| |
| :param fieldname: Checks Overlap for this field |
| """ |
| |
| existing = frappe.db.sql( |
| """select name, from_time, to_time from `tab{0}` |
| where `{1}`=%(val)s and schedule_date = %(schedule_date)s and |
| ( |
| (from_time > %(from_time)s and from_time < %(to_time)s) or |
| (to_time > %(from_time)s and to_time < %(to_time)s) or |
| (%(from_time)s > from_time and %(from_time)s < to_time) or |
| (%(from_time)s = from_time and %(to_time)s = to_time)) |
| and name!=%(name)s and docstatus!=2""".format( |
| doctype, fieldname |
| ), |
| { |
| "schedule_date": doc.schedule_date, |
| "val": value or doc.get(fieldname), |
| "from_time": doc.from_time, |
| "to_time": doc.to_time, |
| "name": doc.name or "No Name", |
| }, |
| as_dict=True, |
| ) |
| |
| return existing[0] if existing else None |
| |
| |
| def validate_duplicate_student(students): |
| unique_students = [] |
| for stud in students: |
| if stud.student in unique_students: |
| frappe.throw( |
| _("Student {0} - {1} appears Multiple times in row {2} & {3}").format( |
| stud.student, stud.student_name, unique_students.index(stud.student) + 1, stud.idx |
| ) |
| ) |
| else: |
| unique_students.append(stud.student) |
| |
| return None |
| |
| |
| # LMS Utils |
| def get_current_student(): |
| """Returns current student from frappe.session.user |
| |
| Returns: |
| object: Student Document |
| """ |
| email = frappe.session.user |
| if email in ("Administrator", "Guest"): |
| return None |
| try: |
| student_id = frappe.get_all("Student", {"student_email_id": email}, ["name"])[0].name |
| return frappe.get_doc("Student", student_id) |
| except (IndexError, frappe.DoesNotExistError): |
| return None |
| |
| |
| def get_portal_programs(): |
| """Returns a list of all program to be displayed on the portal |
| Programs are returned based on the following logic |
| is_published and (student_is_enrolled or student_can_self_enroll) |
| |
| Returns: |
| list of dictionary: List of all programs and to be displayed on the portal along with access rights |
| """ |
| published_programs = frappe.get_all("Program", filters={"is_published": True}) |
| if not published_programs: |
| return None |
| |
| program_list = [frappe.get_doc("Program", program) for program in published_programs] |
| portal_programs = [ |
| {"program": program, "has_access": allowed_program_access(program.name)} |
| for program in program_list |
| if allowed_program_access(program.name) or program.allow_self_enroll |
| ] |
| |
| return portal_programs |
| |
| |
| def allowed_program_access(program, student=None): |
| """Returns enrollment status for current student |
| |
| Args: |
| program (string): Name of the program |
| student (object): instance of Student document |
| |
| Returns: |
| bool: Is current user enrolled or not |
| """ |
| if has_super_access(): |
| return True |
| if not student: |
| student = get_current_student() |
| if student and get_enrollment("program", program, student.name): |
| return True |
| else: |
| return False |
| |
| |
| def get_enrollment(master, document, student): |
| """Gets enrollment for course or program |
| |
| Args: |
| master (string): can either be program or course |
| document (string): program or course name |
| student (string): Student ID |
| |
| Returns: |
| string: Enrollment Name if exists else returns empty string |
| """ |
| if master == "program": |
| enrollments = frappe.get_all( |
| "Program Enrollment", filters={"student": student, "program": document, "docstatus": 1} |
| ) |
| if master == "course": |
| enrollments = frappe.get_all( |
| "Course Enrollment", filters={"student": student, "course": document} |
| ) |
| |
| if enrollments: |
| return enrollments[0].name |
| else: |
| return None |
| |
| |
| @frappe.whitelist() |
| def enroll_in_program(program_name, student=None): |
| """Enroll student in program |
| |
| Args: |
| program_name (string): Name of the program to be enrolled into |
| student (string, optional): name of student who has to be enrolled, if not |
| provided, a student will be created from the current user |
| |
| Returns: |
| string: name of the program enrollment document |
| """ |
| if has_super_access(): |
| return |
| |
| if not student == None: |
| student = frappe.get_doc("Student", student) |
| else: |
| # Check if self enrollment in allowed |
| program = frappe.get_doc("Program", program_name) |
| if not program.allow_self_enroll: |
| return frappe.throw(_("You are not allowed to enroll for this course")) |
| |
| student = get_current_student() |
| if not student: |
| student = create_student_from_current_user() |
| |
| # Check if student is already enrolled in program |
| enrollment = get_enrollment("program", program_name, student.name) |
| if enrollment: |
| return enrollment |
| |
| # Check if self enrollment in allowed |
| program = frappe.get_doc("Program", program_name) |
| if not program.allow_self_enroll: |
| return frappe.throw(_("You are not allowed to enroll for this course")) |
| |
| # Enroll in program |
| program_enrollment = student.enroll_in_program(program_name) |
| return program_enrollment.name |
| |
| |
| def has_super_access(): |
| """Check if user has a role that allows full access to LMS |
| |
| Returns: |
| bool: true if user has access to all lms content |
| """ |
| current_user = frappe.get_doc("User", frappe.session.user) |
| roles = set([role.role for role in current_user.roles]) |
| return bool( |
| roles & {"Administrator", "Instructor", "Education Manager", "System Manager", "Academic User"} |
| ) |
| |
| |
| @frappe.whitelist() |
| def add_activity(course, content_type, content, program): |
| if has_super_access(): |
| return None |
| |
| student = get_current_student() |
| if not student: |
| return frappe.throw( |
| _("Student with email {0} does not exist").format(frappe.session.user), frappe.DoesNotExistError |
| ) |
| |
| enrollment = get_or_create_course_enrollment(course, program) |
| if content_type == "Quiz": |
| return |
| else: |
| return enrollment.add_activity(content_type, content) |
| |
| |
| @frappe.whitelist() |
| def evaluate_quiz(quiz_response, quiz_name, course, program, time_taken): |
| import json |
| |
| student = get_current_student() |
| |
| quiz_response = json.loads(quiz_response) |
| quiz = frappe.get_doc("Quiz", quiz_name) |
| result, score, status = quiz.evaluate(quiz_response, quiz_name) |
| |
| if has_super_access(): |
| return {"result": result, "score": score, "status": status} |
| |
| if student: |
| enrollment = get_or_create_course_enrollment(course, program) |
| if quiz.allowed_attempt(enrollment, quiz_name): |
| enrollment.add_quiz_activity(quiz_name, quiz_response, result, score, status, time_taken) |
| return {"result": result, "score": score, "status": status} |
| else: |
| return None |
| |
| |
| @frappe.whitelist() |
| def get_quiz(quiz_name, course): |
| try: |
| quiz = frappe.get_doc("Quiz", quiz_name) |
| questions = quiz.get_questions() |
| except Exception: |
| frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError) |
| return None |
| |
| questions = [ |
| { |
| "name": question.name, |
| "question": question.question, |
| "type": question.question_type, |
| "options": [{"name": option.name, "option": option.option} for option in question.options], |
| } |
| for question in questions |
| ] |
| |
| if has_super_access(): |
| return { |
| "questions": questions, |
| "activity": None, |
| "is_time_bound": quiz.is_time_bound, |
| "duration": quiz.duration, |
| } |
| |
| student = get_current_student() |
| course_enrollment = get_enrollment("course", course, student.name) |
| status, score, result, time_taken = check_quiz_completion(quiz, course_enrollment) |
| return { |
| "questions": questions, |
| "activity": {"is_complete": status, "score": score, "result": result, "time_taken": time_taken}, |
| "is_time_bound": quiz.is_time_bound, |
| "duration": quiz.duration, |
| } |
| |
| |
| def get_topic_progress(topic, course_name, program): |
| """ |
| Return the porgress of a course in a program as well as the content to continue from. |
| :param topic_name: |
| :param course_name: |
| """ |
| student = get_current_student() |
| if not student: |
| return None |
| course_enrollment = get_or_create_course_enrollment(course_name, program) |
| progress = student.get_topic_progress(course_enrollment.name, topic) |
| if not progress: |
| return None |
| count = sum([activity["is_complete"] for activity in progress]) |
| if count == 0: |
| return {"completed": False, "started": False} |
| elif count == len(progress): |
| return {"completed": True, "started": True} |
| elif count < len(progress): |
| return {"completed": False, "started": True} |
| |
| |
| def get_course_progress(course, program): |
| """ |
| Return the porgress of a course in a program as well as the content to continue from. |
| :param topic_name: |
| :param course_name: |
| """ |
| course_progress = [] |
| for course_topic in course.topics: |
| topic = frappe.get_doc("Topic", course_topic.topic) |
| progress = get_topic_progress(topic, course.name, program) |
| if progress: |
| course_progress.append(progress) |
| if course_progress: |
| number_of_completed_topics = sum([activity["completed"] for activity in course_progress]) |
| total_topics = len(course_progress) |
| if total_topics == 1: |
| return course_progress[0] |
| if number_of_completed_topics == 0: |
| return {"completed": False, "started": False} |
| if number_of_completed_topics == total_topics: |
| return {"completed": True, "started": True} |
| if number_of_completed_topics < total_topics: |
| return {"completed": False, "started": True} |
| |
| return None |
| |
| |
| def get_program_progress(program): |
| program_progress = [] |
| if not program.courses: |
| return None |
| for program_course in program.courses: |
| course = frappe.get_doc("Course", program_course.course) |
| progress = get_course_progress(course, program.name) |
| if progress: |
| progress["name"] = course.name |
| progress["course"] = course.course_name |
| program_progress.append(progress) |
| |
| if program_progress: |
| return program_progress |
| |
| return None |
| |
| |
| def get_program_completion(program): |
| topics = frappe.db.sql( |
| """select `tabCourse Topic`.topic, `tabCourse Topic`.parent |
| from `tabCourse Topic`, |
| `tabProgram Course` |
| where `tabCourse Topic`.parent = `tabProgram Course`.course |
| and `tabProgram Course`.parent = %s""", |
| program.name, |
| ) |
| |
| progress = [] |
| for topic in topics: |
| topic_doc = frappe.get_doc("Topic", topic[0]) |
| topic_progress = get_topic_progress(topic_doc, topic[1], program.name) |
| if topic_progress: |
| progress.append(topic_progress) |
| |
| if progress: |
| number_of_completed_topics = sum([activity["completed"] for activity in progress if activity]) |
| total_topics = len(progress) |
| try: |
| return int((float(number_of_completed_topics) / total_topics) * 100) |
| except ZeroDivisionError: |
| return 0 |
| |
| return 0 |
| |
| |
| def create_student_from_current_user(): |
| user = frappe.get_doc("User", frappe.session.user) |
| |
| student = frappe.get_doc( |
| { |
| "doctype": "Student", |
| "first_name": user.first_name, |
| "last_name": user.last_name, |
| "student_email_id": user.email, |
| "user": frappe.session.user, |
| } |
| ) |
| |
| student.save(ignore_permissions=True) |
| return student |
| |
| |
| def get_or_create_course_enrollment(course, program): |
| student = get_current_student() |
| course_enrollment = get_enrollment("course", course, student.name) |
| if not course_enrollment: |
| program_enrollment = get_enrollment("program", program.name, student.name) |
| if not program_enrollment: |
| frappe.throw(_("You are not enrolled in program {0}").format(program)) |
| return |
| return student.enroll_in_course( |
| course_name=course, program_enrollment=get_enrollment("program", program.name, student.name) |
| ) |
| else: |
| return frappe.get_doc("Course Enrollment", course_enrollment) |
| |
| |
| def check_content_completion(content_name, content_type, enrollment_name): |
| activity = frappe.get_all( |
| "Course Activity", |
| filters={"enrollment": enrollment_name, "content_type": content_type, "content": content_name}, |
| ) |
| if activity: |
| return True |
| else: |
| return False |
| |
| |
| def check_quiz_completion(quiz, enrollment_name): |
| attempts = frappe.get_all( |
| "Quiz Activity", |
| filters={"enrollment": enrollment_name, "quiz": quiz.name}, |
| fields=["name", "activity_date", "score", "status", "time_taken"], |
| ) |
| status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts) |
| score = None |
| result = None |
| time_taken = None |
| if attempts: |
| if quiz.grading_basis == "Last Highest Score": |
| attempts = sorted(attempts, key=lambda i: int(i.score), reverse=True) |
| score = attempts[0]["score"] |
| result = attempts[0]["status"] |
| time_taken = attempts[0]["time_taken"] |
| if result == "Pass": |
| status = True |
| return status, score, result, time_taken |