diff --git a/school/hooks.py b/school/hooks.py index 29caa59c..abb372e1 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -202,7 +202,8 @@ school_markdown_macro_renderers = { "Exercise": "school.plugins.exercise_renderer", "Quiz": "school.plugins.quiz_renderer", "YouTubeVideo": "school.plugins.youtube_video_renderer", - "Video": "school.plugins.video_renderer" + "Video": "school.plugins.video_renderer", + "Assignment": "school.plugins.assignment_renderer" } # page_renderer to manage profile pages diff --git a/school/lms/doctype/course_lesson/course_lesson.js b/school/lms/doctype/course_lesson/course_lesson.js index 3b3f13ef..453aaa58 100644 --- a/school/lms/doctype/course_lesson/course_lesson.js +++ b/school/lms/doctype/course_lesson/course_lesson.js @@ -9,7 +9,7 @@ frappe.ui.form.on('Course Lesson', { frm.get_field('help').html(`

You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.

-
+
Content Type
@@ -18,7 +18,7 @@ frappe.ui.form.on('Course Lesson', {
-
+
Video
@@ -27,7 +27,7 @@ frappe.ui.form.on('Course Lesson', {
-
+
YouTube Video
@@ -36,7 +36,7 @@ frappe.ui.form.on('Course Lesson', {
-
+
Exercise
@@ -45,13 +45,60 @@ frappe.ui.form.on('Course Lesson', {
-
+
Quiz
{{ Quiz("lms_quiz_name") }}
+ +
+
+ Assignment +
+
+ {{ Assignment("id-filetype") }} +
+
+ +
+ +
+
+ Supported File Types for Assignment +
+
+ Syntax +
+
+ +
+
+ .doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document +
+
+ Document +
+
+ +
+
+ .pdf +
+
+ PDF +
+
+ +
+
+ .png, .jpg, .jpeg +
+
+ Image +
+
`); } }); diff --git a/school/lms/doctype/course_lesson/course_lesson.py b/school/lms/doctype/course_lesson/course_lesson.py index 8fbfb0e9..0be2e853 100644 --- a/school/lms/doctype/course_lesson/course_lesson.py +++ b/school/lms/doctype/course_lesson/course_lesson.py @@ -8,6 +8,9 @@ from frappe.model.document import Document from ...md import markdown_to_html, find_macros class CourseLesson(Document): + def validate(self): + self.check_and_create_folder() + def on_update(self): dynamic_documents = ["Exercise", "Quiz"] for section in dynamic_documents: @@ -43,8 +46,18 @@ class CourseLesson(Document): ex.index_label = "" ex.save() + def check_and_create_folder(self): + course = frappe.db.get_value("Chapter", self.chapter, "course") + args = { + "doctype": "File", + "is_folder": True, + "file_name": f"{self.name} {course}" + } + if not frappe.db.exists(args): + folder = frappe.get_doc(args) + folder.save(ignore_permissions=True) + def render_html(self): - print(self.body) return markdown_to_html(self.body) def get_exercises(self): diff --git a/school/lms/doctype/lesson_assignment/__init__.py b/school/lms/doctype/lesson_assignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/lesson_assignment/lesson_assignment.js b/school/lms/doctype/lesson_assignment/lesson_assignment.js new file mode 100644 index 00000000..9ec731c4 --- /dev/null +++ b/school/lms/doctype/lesson_assignment/lesson_assignment.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Lesson Assignment', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/lesson_assignment/lesson_assignment.json b/school/lms/doctype/lesson_assignment/lesson_assignment.json new file mode 100644 index 00000000..4c0ba546 --- /dev/null +++ b/school/lms/doctype/lesson_assignment/lesson_assignment.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-12-21 16:15:22.651658", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lesson", + "user", + "column_break_3", + "assignment", + "id" + ], + "fields": [ + { + "fieldname": "lesson", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Lesson", + "options": "Course Lesson" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User", + "options": "User" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "id", + "fieldtype": "Data", + "label": "ID" + }, + { + "fieldname": "assignment", + "fieldtype": "Attach", + "label": "Assignment" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-12-22 11:17:08.390615", + "modified_by": "Administrator", + "module": "LMS", + "name": "Lesson Assignment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/school/lms/doctype/lesson_assignment/lesson_assignment.py b/school/lms/doctype/lesson_assignment/lesson_assignment.py new file mode 100644 index 00000000..eb2fd5e8 --- /dev/null +++ b/school/lms/doctype/lesson_assignment/lesson_assignment.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.handler import upload_file + +class LessonAssignment(Document): + pass + +@frappe.whitelist() +def upload_assignment(assignment, lesson, identifier): + args = { + "doctype": "Lesson Assignment", + "lesson": lesson, + "user": frappe.session.user, + "id": identifier + } + if frappe.db.exists(args): + del args["doctype"] + frappe.db.set_value("Lesson Assignment", args, "assignment", assignment) + else: + args.update({"assignment": assignment}) + lesson_work = frappe.get_doc(args) + lesson_work.save(ignore_permissions=True) + +@frappe.whitelist() +def get_assignment(lesson): + assignments = frappe.get_all("Lesson Assignment", + { + "lesson": lesson, + "user": frappe.session.user + }, + ["lesson", "user", "id", "assignment"]) + if len(assignments): + for assignment in assignments: + assignment.file_name = frappe.db.get_value("File", {"file_url": assignment.assignment}, "file_name") + return assignments + + + + + diff --git a/school/lms/doctype/lesson_assignment/test_lesson_assignment.py b/school/lms/doctype/lesson_assignment/test_lesson_assignment.py new file mode 100644 index 00000000..9602c016 --- /dev/null +++ b/school/lms/doctype/lesson_assignment/test_lesson_assignment.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe and Contributors +# See license.txt + +# import frappe +import unittest + +class TestLessonAssignment(unittest.TestCase): + pass diff --git a/school/plugins.py b/school/plugins.py index 4e0e6bf6..e320ee20 100644 --- a/school/plugins.py +++ b/school/plugins.py @@ -121,6 +121,17 @@ def youtube_video_renderer(video_id): def video_renderer(src): return "".format(src) +def assignment_renderer(detail): + supported_types = { + "Document": ".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "PDF": ".pdf", + "Image": ".png, .jpg, .jpeg", + "Video": "video/*" + } + file_type = detail.split("-")[1] + accept = supported_types[file_type] if file_type else "" + return frappe.render_template("templates/assignment.html", {"id": detail.split("-")[0], "accept": accept}) + def show_custom_signup(): if frappe.db.get_single_value("LMS Settings", "terms_of_use"): return "school/templates/signup-form.html" diff --git a/school/public/css/style.css b/school/public/css/style.css index 80848fc8..f4edb3ec 100644 --- a/school/public/css/style.css +++ b/school/public/css/style.css @@ -1458,3 +1458,8 @@ pre { .no-discussions { width: 80% !important; } + +.preview-work { + width: 50%; + justify-content: space-between; +} diff --git a/school/templates/assignment.html b/school/templates/assignment.html new file mode 100644 index 00000000..ff543469 --- /dev/null +++ b/school/templates/assignment.html @@ -0,0 +1,10 @@ +
+
+ +
Submit Work
+
+ +
Change
+
+
+
diff --git a/school/www/batch/learn.js b/school/www/batch/learn.js index c64f8510..7fc66b84 100644 --- a/school/www/batch/learn.js +++ b/school/www/batch/learn.js @@ -1,6 +1,7 @@ frappe.ready(() => { localStorage.removeItem($("#quiz-title").text()); + fetch_assignments(); save_current_lesson(); @@ -34,26 +35,34 @@ frappe.ready(() => { $("#certification").click((e) => { create_certificate(e); + }); + + $(".submit-work").click((e) => { + attach_work(e); + }); + + $(".clear-work").click((e) => { + clear_work(e); }) -}) +}); -var save_current_lesson = () => { +const save_current_lesson = () => { if ($(".title").hasClass("is-member")) { frappe.call("school.lms.api.save_current_lesson", { course_name: $(".title").attr("data-course"), lesson_name: $(".title").attr("data-lesson") }) } -} +}; -var enable_check = (e) => { +const enable_check = (e) => { if ($(".option:checked").length && $("#check").attr("disabled")) { $("#check").removeAttr("disabled"); } -} +}; -var mark_active_question = (e = undefined) => { +const mark_active_question = (e = undefined) => { var current_index; var next_index = 1; if (e) { @@ -67,9 +76,9 @@ var mark_active_question = (e = undefined) => { $("#check").removeClass("hide").attr("disabled", true); $("#next").addClass("hide"); $(".explanation").addClass("hide"); -} +}; -var mark_progress = (e) => { +const mark_progress = (e) => { /* Prevent default only for Next button anchor tag and not for progress checkbox */ if ($(e.currentTarget).prop("nodeName") != "INPUT") e.preventDefault(); @@ -101,9 +110,9 @@ var mark_progress = (e) => { } else move_to_next_lesson(e); -} +}; -var change_progress_indicators = (status, e) => { +const change_progress_indicators = (status, e) => { if (status == "Complete") { $(".lesson-progress").removeClass("hide"); $(".active-lesson .lesson-progress-tick").removeClass("hide"); @@ -116,22 +125,22 @@ var change_progress_indicators = (status, e) => { $(e.currentTarget).addClass("hide"); $("input.mark-progress").prop("checked", false).closest(".custom-checkbox").removeClass("hide"); } -} +}; const show_certificate_if_course_completed = (data) => { if (data.message == 100 && !$(".next").attr("data-next") && $("#certification").hasClass("hide")) { $("#certification").removeClass("hide"); $(".next").addClass("hide"); } -} +}; const move_to_next_lesson = (e) => { if ($(e.currentTarget).hasClass("next") && $(e.currentTarget).attr("data-href")) { window.location.href = $(e.currentTarget).attr("data-href"); } -} +}; -var quiz_summary = (e) => { +const quiz_summary = (e) => { e.preventDefault(); var quiz_name = $("#quiz-title").text(); var total_questions = $(".question").length; @@ -152,13 +161,13 @@ var quiz_summary = (e) => { $("#try-again").removeClass("hide"); } }) -} +}; -var try_quiz_again = (e) => { +const try_quiz_again = (e) => { window.location.reload(); -} +}; -var check_answer = (e) => { +const check_answer = (e) => { e.preventDefault(); var quiz_name = $("#quiz-title").text(); @@ -182,9 +191,9 @@ var check_answer = (e) => { var [answer, is_correct] = parse_options(); add_to_local_storage(quiz_name, current_index, answer, is_correct) -} +}; -var parse_options = () => { +const parse_options = () => { var answer = []; var is_correct = []; $(".active-question input").each((i, element) => { @@ -200,14 +209,14 @@ var parse_options = () => { } }) return [answer, is_correct]; -} +}; -var add_icon = (element, icon) => { +const add_icon = (element, icon) => { var label = $(element).parent().find(".label-area p").text(); $(element).parent().empty().html(` ${label}`); -} +}; -var add_to_local_storage = (quiz_name, current_index, answer, is_correct) => { +const add_to_local_storage = (quiz_name, current_index, answer, is_correct) => { var quiz_stored = JSON.parse(localStorage.getItem(quiz_name)); var quiz_obj = { "question_index": current_index, @@ -216,9 +225,9 @@ var add_to_local_storage = (quiz_name, current_index, answer, is_correct) => { } quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj] localStorage.setItem(quiz_name, JSON.stringify(quiz_stored)) -} +}; -var create_certificate = (e) => { +const create_certificate = (e) => { e.preventDefault(); course = $(".title").attr("data-course"); frappe.call({ @@ -230,4 +239,132 @@ var create_certificate = (e) => { window.location.href = `/courses/${course}/${data.message}`; } }) +}; + +const attach_work = (e) => { + const target = $(e.currentTarget); + let files = target.siblings(".attach-file").prop("files") + if (files && files.length) { + files = add_files(files) + return_as_dataurl(files) + files.map((file) => { + upload_file(file, target); + }) + } +}; + +const upload_file = (file, target) => { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200) { + let response = JSON.parse(xhr.responseText) + create_lesson_work(response.message, target); + } else if (xhr.status === 403) { + let response = JSON.parse(xhr.responseText); + frappe.msgprint(`Not permitted. ${response._error_message || ''}`); + + } else if (xhr.status === 413) { + frappe.msgprint('Size exceeds the maximum allowed file size.'); + + } else { + frappe.msgprint(xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`); + } + } + } + xhr.open('POST', '/api/method/upload_file', true); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token); + + let form_data = new FormData(); + if (file.file_obj) { + form_data.append('file', file.file_obj, `${frappe.session.user}-${file.name}`); + form_data.append('folder', `${$(".title").attr("data-lesson")} ${$(".title").attr("data-course")}`) + } + + xhr.send(form_data); + }); +} + +const create_lesson_work = (file, target) => { + frappe.call({ + method: "school.lms.doctype.lesson_assignment.lesson_assignment.upload_assignment", + args: { + assignment: file.file_url, + lesson: $(".title").attr("data-lesson"), + identifier: target.siblings(".attach-file").attr("id") + }, + callback: (data) => { + target.siblings(".attach-file").addClass("hide"); + target.siblings(".preview-work").removeClass("hide"); + target.siblings(".preview-work").find("a").attr("href", file.file_url).text(file.file_name) + target.addClass("hide"); + } + }); +}; + +const return_as_dataurl = (files) => { + let promises = files.map(file => + frappe.dom.file_to_base64(file.file_obj) + .then(dataurl => { + file.dataurl = dataurl; + this.on_success && this.on_success(file); + }) + ); + return Promise.all(promises); +} + +const add_files = (files) => { + files = Array.from(files).map(file => { + let is_image = file.type.startsWith('image'); + return { + file_obj: file, + cropper_file: file, + crop_box_data: null, + optimize: this.attach_doc_image ? true : false, + name: file.name, + doc: null, + progress: 0, + total: 0, + failed: false, + request_succeeded: false, + error_message: null, + uploading: false, + private: !is_image + } + }); + return files +}; + +const clear_work = (e) => { + const target = $(e.currentTarget); + const parent = target.closest(".preview-work"); + parent.addClass("hide"); + parent.siblings(".attach-file").removeClass("hide").val(null); + parent.siblings(".submit-work").removeClass("hide"); +} + +const fetch_assignments = () => { + if ($(".attach-file").length > 0) { + frappe.call({ + method: "school.lms.doctype.lesson_assignment.lesson_assignment.get_assignment", + args: { + "lesson": $(".title").attr("data-lesson") + }, + callback: (data) => { + if (data.message && data.message.length) { + const assignments = data.message; + for (let i in assignments) { + let target = $(`#${assignments[i]["id"]}`); + target.addClass("hide"); + target.siblings(".submit-work").addClass("hide"); + target.siblings(".preview-work").removeClass("hide"); + target.siblings(".preview-work").find("a").attr("href", assignments[i]["assignment"]).text(assignments[i]["file_name"]); + } + } + } + }) + } }