diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 630341d4..aafbfa01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: setup node uses: actions/setup-node@v2 with: - node-version: '14' + node-version: '18' check-latest: true - name: setup cache for bench uses: actions/cache@v2 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0d0d96bc..d59e5acc 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -50,7 +50,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 check-latest: true - name: Add to Hosts diff --git a/lms/lms/doctype/lms_assignment/lms_assignment.json b/lms/lms/doctype/lms_assignment/lms_assignment.json index e7659c04..6cb778a1 100644 --- a/lms/lms/doctype/lms_assignment/lms_assignment.json +++ b/lms/lms/doctype/lms_assignment/lms_assignment.json @@ -46,7 +46,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-29 14:50:55.259990", + "modified": "2023-06-26 18:09:29.809564", "modified_by": "Administrator", "module": "LMS", "name": "LMS Assignment", @@ -64,9 +64,23 @@ "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 } ], + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "title_field": "title" } \ No newline at end of file diff --git a/lms/lms/doctype/lms_class/lms_class.json b/lms/lms/doctype/lms_class/lms_class.json index 6a69486d..3f3b61e3 100644 --- a/lms/lms/doctype/lms_class/lms_class.json +++ b/lms/lms/doctype/lms_class/lms_class.json @@ -50,7 +50,8 @@ { "fieldname": "description", "fieldtype": "Small Text", - "label": "Description" + "label": "Description", + "reqd": 1 }, { "fieldname": "section_break_6", @@ -137,7 +138,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-15 12:30:26.929156", + "modified": "2023-06-22 15:57:25.190084", "modified_by": "Administrator", "module": "LMS", "name": "LMS Class", diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_class/lms_class.py index 634f41e6..862bec75 100644 --- a/lms/lms/doctype/lms_class/lms_class.py +++ b/lms/lms/doctype/lms_class/lms_class.py @@ -2,19 +2,21 @@ # For license information, please see license.txt import frappe -from frappe.model.document import Document -from frappe import _ -from frappe.utils import cint, format_date, format_datetime import requests import base64 import json +from frappe import _ +from frappe.model.document import Document +from frappe.utils import cint, format_date, format_datetime class LMSClass(Document): def validate(self): if self.seat_count: self.validate_seats_left() + self.validate_duplicate_courses() self.validate_duplicate_students() + self.validate_duplicate_assessments() self.validate_membership() def validate_duplicate_students(self): @@ -27,6 +29,28 @@ class LMSClass(Document): ) ) + def validate_duplicate_courses(self): + courses = [row.course for row in self.courses] + duplicates = {course for course in courses if courses.count(course) > 1} + if len(duplicates): + title = frappe.db.get_value("LMS Course", next(iter(duplicates)), "title") + frappe.throw( + _("Course {0} has already been added to this class.").format(frappe.bold(title)) + ) + + def validate_duplicate_assessments(self): + assessments = [row.assessment_name for row in self.assessment] + for assessment in self.assessment: + if assessments.count(assessment.assessment_name) > 1: + title = frappe.db.get_value( + assessment.assessment_type, assessment.assessment_name, "title" + ) + frappe.throw( + _("Assessment {0} has already been added to this class.").format( + frappe.bold(title) + ) + ) + def validate_membership(self): for course in self.courses: for student in self.students: @@ -43,51 +67,30 @@ class LMSClass(Document): frappe.throw(_("There are no seats available in this class.")) -@frappe.whitelist() -def add_student(email, class_name): - if not frappe.db.exists("User", email): - frappe.throw(_("There is no such user. Please create a user with this Email ID.")) - - filters = { - "student": email, - "parent": class_name, - "parenttype": "LMS Class", - "parentfield": "students", - } - if frappe.db.exists("Class Student", filters): - frappe.throw( - _("Student {0} has already been added to this class.").format(frappe.bold(email)) - ) - - frappe.get_doc( - { - "doctype": "Class Student", - "student": email, - "student_name": frappe.db.get_value("User", email, "full_name"), - "parent": class_name, - "parenttype": "LMS Class", - "parentfield": "students", - } - ).save() - return True - - @frappe.whitelist() def remove_student(student, class_name): + frappe.only_for("Moderator") frappe.db.delete("Class Student", {"student": student, "parent": class_name}) @frappe.whitelist() def remove_course(course, parent): + frappe.only_for("Moderator") frappe.db.delete("Class Course", {"course": course, "parent": parent}) +@frappe.whitelist() +def remove_assessment(assessment, parent): + frappe.only_for("Moderator") + frappe.db.delete("LMS Assessment", {"assessment_name": assessment, "parent": parent}) + + @frappe.whitelist() def create_live_class( class_name, title, duration, date, time, timezone, auto_recording, description=None ): date = format_date(date, "yyyy-mm-dd", True) - + frappe.only_for("Moderator") payload = { "topic": title, "start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"), @@ -164,6 +167,7 @@ def create_class( category=None, name=None, ): + frappe.only_for("Moderator") if name: class_details = frappe.get_doc("LMS Class", name) else: @@ -184,23 +188,3 @@ def create_class( ) class_details.save() return class_details - - -@frappe.whitelist() -def update_assessment(type, name, value, class_name): - value = cint(value) - filters = { - "assessment_type": type, - "assessment_name": name, - "parent": class_name, - "parenttype": "LMS Class", - "parentfield": "assessment", - } - exists = frappe.db.exists("LMS Assessment", filters) - - if exists and not value: - frappe.db.delete("LMS Assessment", exists) - elif not exists and value: - doc = frappe.new_doc("LMS Assessment") - doc.update(filters) - doc.insert() diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json index 7f3a8d48..39e9b282 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.json +++ b/lms/lms/doctype/lms_quiz/lms_quiz.json @@ -70,7 +70,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-21 09:13:01.322701", + "modified": "2023-06-23 12:35:25.204131", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz", @@ -87,6 +87,18 @@ "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 } ], "show_title_field_in_link": 1, diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 522084fe..6e2d981d 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -733,3 +733,23 @@ def is_onboarding_complete(): if course_created else None, } + + +def has_submitted_assessment(assessment, type, member=None): + if not member: + member = frappe.session.user + + doctype = ( + "LMS Assignment Submission" if type == "LMS Assignment" else "LMS Quiz Submission" + ) + docfield = "assignment" if type == "LMS Assignment" else "quiz" + + filters = {} + filters[docfield] = assessment + filters["member"] = member + return frappe.db.exists(doctype, filters) + + +def has_graded_assessment(submission): + status = frappe.db.get_value("LMS Assignment Submission", submission, "status") + return False if status == "Not Graded" else True diff --git a/lms/lms/widgets/CourseCard.html b/lms/lms/widgets/CourseCard.html index 78472396..486471c9 100644 --- a/lms/lms/widgets/CourseCard.html +++ b/lms/lms/widgets/CourseCard.html @@ -72,6 +72,9 @@ {% endif %}
{{ course.title }}
+
+ {{ course.short_introduction }} +
{% if membership and not is_instructor(course.name) and not read_only %}
diff --git a/lms/patches.txt b/lms/patches.txt index eb99f28e..22e92dab 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -53,4 +53,6 @@ lms.patches.v0_0.share_certificates execute:frappe.delete_doc("Web Form", "class", ignore_missing=True, force=True) lms.patches.v0_0.amend_course_and_lesson_editor_fields lms.patches.v0_0.convert_course_description_to_html #11-05-2023 -lms.patches.v1_0.rename_assignment_doctype \ No newline at end of file +lms.patches.v1_0.rename_assignment_doctype +execute:frappe.permissions.reset_perms("LMS Assignment") +execute:frappe.permissions.reset_perms("LMS Quiz") diff --git a/lms/public/css/style.css b/lms/public/css/style.css index a7930781..c985aea4 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -374,10 +374,20 @@ input[type=checkbox] { .course-card-title { font-weight: 600; color: var(--gray-900); - margin-bottom: 1.25rem; font-size: 1.125rem; } +.short-introduction { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + width: 100%; + overflow: hidden; + white-space: nowrap; + margin-bottom: 1.25rem; +} + .card-divider { border-top: 1px solid var(--gray-300); margin-bottom: 1rem; @@ -1385,6 +1395,10 @@ pre { margin: 0 1rem; } +.seperator::after { + content: "\00B7"; +} + .course-overlay-card { background-color: white; border-radius: var(--border-radius-lg); @@ -1890,6 +1904,14 @@ li { padding: 0 1rem !important; } +.modal-dialog-scrollable .modal-body { + overflow-y: unset; +} + +.modal-dialog-scrollable .modal-content { + overflow: unset; +} + .modal-header, .modal-body { margin-bottom: 0.5rem !important; } @@ -2167,4 +2189,25 @@ select { .awesomplete ul li:last-child { display: none; +} + +.students-parent { + display: grid; + grid-template-columns: repeat(auto-fill, 150px); + grid-gap: 1rem; +} + +.btn-remove-course { + opacity: 0; + margin-top: 0.25rem; +} + +.btn-remove-course:hover { + opacity: 1; +} + +.rows .grid-row .data-row, +.rows .grid-row .grid-footer-toolbar, +.grid-form-heading { + cursor: none; } \ No newline at end of file diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index cd28f6af..2eeb43ae 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -326,6 +326,7 @@ const open_class_dialog = () => { label: __("Description"), fieldname: "description", default: class_info && class_info.description, + reqd: 1, }, ], primary_action_label: __("Save"), diff --git a/lms/www/classes/class.html b/lms/www/classes/class.html index 01f895ac..3193e05a 100644 --- a/lms/www/classes/class.html +++ b/lms/www/classes/class.html @@ -34,23 +34,46 @@
{{ class_info.title }}
-
- - - - - {{ frappe.utils.format_date(class_info.start_date, "long") }} - - - - {{ frappe.utils.format_date(class_info.end_date, "long") }} - -
+ {% if class_info.description %} -
+
{{ class_info.description }}
{% endif %} +
+
+ + + + + {{ frappe.utils.format_date(class_info.start_date, "long") }} - + + + {{ frappe.utils.format_date(class_info.end_date, "long") }} + +
+ + + +
+ + + + {{ class_courses | length }} {{ _("Courses") }} +
+ + + +
+ + + + {{ class_students | length }} {{ _("Students") }} +
+
+ + {% if class_info.custom_component %} {{ class_info.custom_component }} {% endif %} @@ -155,20 +178,17 @@ {% if class_courses | length %} -
+
{% for course in class_courses %} -
- - {{ course.title }} - - {% if is_moderator %} -
+
+ {{ widgets.CourseCard(course=course, read_only=False) }} +
- {% endif %} +
+ {% endfor %}
{% else %} @@ -177,7 +197,6 @@
{% endif %} - {% endmacro %} @@ -198,26 +217,63 @@ {% if class_students | length %} -
+
+
+
+
+
+ {{ _("Full Name") }} +
+
+ {{ _("Courses Completed") }} +
+
+ {{ _("Assessments Completed") }} +
+
+ {{ _("Assessments Graded") }} +
+
+ {{ _("Last Active") }} +
+ {% if is_moderator %} +
+ + + +
+ {% endif %} +
+
+
{% for student in class_students %} - {% set last_active = frappe.db.get_value("User", student.student, "last_active") %} {% set allow_progress = is_moderator or student.student == frappe.session.user %} - -
-
- +
+
+ {{ student.student_name }} +
+ {{ student.courses_completed }} +
+
+ {{ student.assessments_completed }} +
+
+ {{ student.assessments_graded }} +
+
+ {{ frappe.utils.pretty_date(student.last_active) }} +
+ {% if is_moderator %} +
+ + + +
+ {% endif %}
- {% if is_moderator %} -
- - - -
- {% endif %}
- {% endfor %}
{% else %} @@ -239,117 +295,52 @@ {% endif %} - {{ CreateAssessment() }} {{ AssessmentList(assessments) }} {% endmacro %} - -{% macro CreateAssessment() %} -{% if is_moderator %} - -{% endif %} -{% endmacro %} - {% macro AssessmentList(assessments) %} {% if assessments | length %} -
-
-
- {{ _("Title") }} -
-
- {{ _("Type") }} +
+
+
+
+
+ {{ _("Title") }} +
+
+ {{ _("Type") }} +
+
+ + + +
+
- {% for assessment in assessments %} -
- -
- {{ assessment.assessment_type.split("LMS ")[1] }} + +
+
+ {% for assessment in assessments %} +
+
+ + {{ assessment.title }} + +
+ {{ assessment.assessment_type.split("LMS ")[1] }} +
+
+ + + +
+
+
+ {% endfor %}
- {% endfor %}
{% else %}

{{ _("No Assessments") }}

@@ -376,7 +367,6 @@ {% macro CreateLiveClass(class_info) %} - {% if is_moderator %} {% endif %} - {% endmacro %} @@ -412,52 +401,51 @@
{% if live_classes | length %} {% for class in live_classes %} -
+
-
- +
{% endfor %} {% else %} -

{{ _("No Live Classes") }}

+

{{ _("No Live Classes") }}

{% endif %}
{% endmacro %} @@ -468,10 +456,12 @@