Merge pull request #417 from pateljannat/assignments-refactor

This commit is contained in:
Jannat Patel
2022-10-31 17:22:36 +05:30
committed by GitHub
11 changed files with 220 additions and 86 deletions

View File

@@ -18,6 +18,10 @@
"youtube",
"column_break_9",
"quiz_id",
"section_break_16",
"question",
"column_break_15",
"file_type",
"section_break_11",
"body",
"help_section",
@@ -105,11 +109,33 @@
"fieldname": "youtube",
"fieldtype": "Data",
"label": "YouTube Video URL"
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Assignment"
},
{
"description": "Assignment will appear at the bottom of the lesson.",
"fieldname": "question",
"fieldtype": "Small Text",
"label": "Question"
},
{
"fieldname": "file_type",
"fieldtype": "Select",
"label": "File Type",
"mandatory_depends_on": "question",
"options": "Image\nDocument\nPDF"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-09-02 11:30:15.450624",
"modified": "2022-10-28 12:36:01.978641",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Lesson",
@@ -147,4 +173,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -9,20 +9,24 @@ from frappe.model.document import Document
from ...md import find_macros
from lms.lms.utils import get_course_progress, get_lesson_url
class CourseLesson(Document):
def validate(self):
self.check_and_create_folder()
#self.check_and_create_folder()
self.validate_quiz_id()
def validate_quiz_id(self):
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
frappe.throw(_("Invalid Quiz ID"))
def on_update(self):
dynamic_documents = ["Exercise", "Quiz"]
for section in dynamic_documents:
self.update_lesson_name_in_document(section)
def update_lesson_name_in_document(self, section):
doctype_map= {
"Exercise": "Exercise",
@@ -39,6 +43,7 @@ class CourseLesson(Document):
index += 1
self.update_orphan_documents(doctype_map[section], documents)
def update_orphan_documents(self, doctype, documents):
"""Updates the documents that were previously part of this lesson,
but not any more.
@@ -53,6 +58,7 @@ class CourseLesson(Document):
ex.index_label = ""
ex.save()
def check_and_create_folder(self):
args = {
"doctype": "File",
@@ -63,6 +69,7 @@ class CourseLesson(Document):
folder = frappe.get_doc(args)
folder.save(ignore_permissions=True)
def get_exercises(self):
if not self.body:
return []
@@ -79,28 +86,26 @@ class CourseLesson(Document):
return ("").join([ s for s in self.get_progress().lower().split() ])
return
@frappe.whitelist()
def save_progress(lesson, course, status):
membership = frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"course": course
})
membership = frappe.db.exists("LMS Batch Membership", {
"member": frappe.session.user,
"course": course
})
if not membership:
return
if frappe.db.exists("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user,
"course": course
}):
doc = frappe.get_doc("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user,
"course": course
})
if frappe.db.exists("LMS Course Progress", {
"lesson": lesson,
"owner": frappe.session.user,
"course": course
}):
doc = frappe.get_doc("LMS Course Progress", {
"lesson": lesson,
"owner": frappe.session.user,
"course": course
})
doc.status = status
doc.save(ignore_permissions=True)
else:
@@ -114,6 +119,7 @@ def save_progress(lesson, course, status):
frappe.db.set_value("LMS Batch Membership", membership, "progress", progress)
return progress
@frappe.whitelist()
def get_lesson_info(chapter):
return frappe.db.get_value("Course Chapter", chapter, "course")

View File

@@ -6,11 +6,12 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lesson",
"user",
"column_break_3",
"assignment",
"id"
"lesson",
"course",
"column_break_3",
"member",
"member_name"
],
"fields": [
{
@@ -19,34 +20,51 @@
"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"
"options": "Course Lesson",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "ID"
},
{
"fieldname": "assignment",
"fieldtype": "Attach",
"label": "Assignment"
"label": "Assignment",
"reqd": 1
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "member.full_name",
"fieldname": "member_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Name",
"read_only": 1
},
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-22 11:17:08.390615",
"make_attachments_public": 1,
"modified": "2022-10-31 13:18:09.609729",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson Assignment",
@@ -66,5 +84,7 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": [],
"title_field": "lesson"
}

View File

@@ -3,18 +3,26 @@
import frappe
from frappe.model.document import Document
from frappe.handler import upload_file
from frappe import _
class LessonAssignment(Document):
pass
def validate(self):
self.validate_duplicates()
def validate_duplicates(self):
if frappe.db.exists("Lesson Assignment", {"lesson": self.lesson, "member": self.member}):
lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title")
frappe.throw(_("Assignment for Lesson {0} by {1} already exists.").format(lesson_title, self.member_name))
@frappe.whitelist()
def upload_assignment(assignment, lesson, identifier):
def upload_assignment(assignment, lesson):
args = {
"doctype": "Lesson Assignment",
"lesson": lesson,
"user": frappe.session.user,
"id": identifier
"member": frappe.session.user
}
if frappe.db.exists(args):
del args["doctype"]
@@ -24,18 +32,16 @@ def upload_assignment(assignment, lesson, identifier):
lesson_work = frappe.get_doc(args)
lesson_work.save(ignore_permissions=True)
@frappe.whitelist()
def get_assignment(lesson):
assignments = frappe.get_all("Lesson Assignment",
{
assignment = frappe.db.get_value("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
"member": frappe.session.user
}, ["lesson", "member", "assignment"],
as_dict=True)
assignment.file_name = frappe.db.get_value("File", {"file_url": assignment.assignment}, "file_name")
return assignment

View File

@@ -270,7 +270,7 @@ def save_chapter(course, title, chapter_description, idx, chapter):
@frappe.whitelist()
def save_lesson(title, body, chapter, preview, idx, lesson, youtube=None, quiz_id=None):
def save_lesson(title, body, chapter, preview, idx, lesson, youtube=None, quiz_id=None, question=None, file_type=None):
if lesson:
doc = frappe.get_doc("Course Lesson", lesson)
else:
@@ -284,7 +284,9 @@ def save_lesson(title, body, chapter, preview, idx, lesson, youtube=None, quiz_i
"body": body,
"include_in_preview": preview,
"youtube": youtube,
"quiz_id": quiz_id
"quiz_id": quiz_id,
"question": question,
"file_type": file_type
})
doc.save(ignore_permissions=True)

View File

@@ -100,8 +100,10 @@ def get_lesson_details(chapter):
order_by="idx")
for row in lesson_list:
lesson_details = frappe.db.get_value("Course Lesson", row.lesson,
["name", "title", "include_in_preview", "body", "creation", "youtube", "quiz_id"], as_dict=True)
lesson_details = frappe.db.get_value("Course Lesson",
row.lesson,
["name", "title", "include_in_preview", "body", "creation", "youtube", "quiz_id", "question", "unique_id", "file_type"],
as_dict=True)
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
lesson_details.icon = "icon-list"
macros = find_macros(lesson_details.body)
@@ -244,13 +246,22 @@ def get_progress(course, lesson):
["status"])
def render_html(body, youtube, quiz_id):
def render_html(lesson):
youtube = lesson.youtube
quiz_id = lesson.quiz_id
body = lesson.body
if youtube and "/" in youtube:
youtube = youtube.split("/")[-1]
quiz_id = "{{ Quiz('" + quiz_id + "') }}" if quiz_id else ""
youtube = "{{ YouTubeVideo('" + youtube + "') }}" if youtube else ""
text = youtube + body + quiz_id
if lesson.question:
assignment = "{{ Assignment('" + lesson.question + "-" + lesson.file_type + "') }}"
text = text + assignment
return markdown_to_html(text)

View File

@@ -151,9 +151,10 @@ def assignment_renderer(detail):
"Image": ".png, .jpg, .jpeg",
"Video": "video/*"
}
question = detail.split("-")[0]
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})
return frappe.render_template("templates/assignment.html", {"question": question, "accept": accept})
def show_custom_signup():
if (frappe.db.get_single_value("LMS Settings", "terms_of_use")

View File

@@ -1126,8 +1126,7 @@ pre {
}
.preview-work {
width: 50%;
justify-content: space-between;
width: 50%;
}
.job-card {
@@ -1778,3 +1777,25 @@ li {
.course-description-section {
padding-bottom: 4rem;
}
input::file-selector-button {
border-radius: var(--border-radius);
font-size: var(--text-md);
padding: 0.25rem 1.25rem;
border: none;
color: var(--text-color);
cursor: pointer;
}
input::file-selector-button:hover {
background-color: var(--gray-400);
}
.btn {
font-weight: 400;
}
select {
appearance: none;
-webkit-appearance: none;
}

View File

@@ -1,10 +1,12 @@
<div class="form-group">
<div class="d-flex">
<input class="btn btn-default btn-sm mr-3 attach-file" type="file" id="{{ id }}" accept="{{ accept }}" />
<div class="button is-secondary submit-work">{{ _("Submit Work") }}</div>
<div class="preview-work button is-default hide">
<a target="_blank"></a>
<div class="button is-secondary clear-work">{{ _("Change") }}</div>
<div class="">
<h3> {{ _("Assignment") }} </h3>
<div class="my-3"> {{ _(question) }} </div>
<input class="btn btn-default btn-sm border attach-file" type="file" accept="{{ accept }}" />
<div class="btn btn-secondary ml-2 submit-work">{{ _("Submit") }}</div>
<div class="preview-work small hide">
<a target="_blank"></a>
<div class="btn btn-secondary btn-sm ml-2 clear-work">{{ _("Change") }}</div>
</div>
</div>
</div>
</div>

View File

@@ -123,7 +123,7 @@
{% if lesson.edit_mode %}
{{ EditLesson(lesson) }}
{% else %}
{{ render_html(lesson.body, lesson.youtube, lesson.quiz_id) }}
{{ render_html(lesson) }}
{% endif %}
{% else %}
@@ -171,11 +171,30 @@
<!-- Edit Lesson -->
{% macro EditLesson(lesson) %}
<div class="medium mt-2" contenteditable="true" data-placeholder="{{ _('YouTube Video ID') }}"
id="youtube">{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}</div>
<div class="d-flex mt-2 medium">
<div class="flex-grow-1" contenteditable="true" data-placeholder="{{ _('YouTube Video ID') }}"
id="youtube">{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}</div>
<div class="flex-grow-1 ml-2" contenteditable="true" data-placeholder="{{ _('Quiz ID') }}"
id="quiz-id">{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}</div>
</div>
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
<div class="medium mb-4" contenteditable="true" data-placeholder="{{ _('Quiz ID') }}"
id="quiz-id">{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}</div>
<div class="d-flex medium mx-0 mb-4">
<div class="flex-grow-1" contenteditable="true" data-placeholder="{{ _('Assignment Question') }}"
id="assignment-question">{% if lesson.question %}{{ lesson.question }}{% endif %}</div>
<select class="btn btn-default ml-2" id="file-type" data-type="{{ lesson.file_type }}">
<option selected> {{ _("File Type") }} </option>
<option value="Image"> {{ _("Image") }} </option>
<option value="Document"> {{ _("Document") }} </option>
<option value="PDF"> {{ _("PDF") }} </option>
</select>
</div>
<label class="preview" for="preview">
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox" id="preview">

View File

@@ -2,6 +2,8 @@ frappe.ready(() => {
this.marked_as_complete = false;
this.quiz_submitted = false;
this.file_type;
let self = this;
localStorage.removeItem($("#quiz-title").data("name"));
@@ -9,6 +11,8 @@ frappe.ready(() => {
save_current_lesson();
set_file_type();
$(".option").click((e) => {
enable_check(e);
});
@@ -77,7 +81,6 @@ frappe.ready(() => {
});
if ($("#quiz-title").data("max-attempts")) {
let self = this;
window.addEventListener("beforeunload", (e) => {
e.returnValue = "";
if ($(".active-question").length && !self.quiz_submitted) {
@@ -89,6 +92,12 @@ frappe.ready(() => {
if ($("#body").length) {
make_editor();
}
$("#file-type").change((e) => {
$("#file-type option:selected" ).each(function() {
self.file_type = $(this).val();
});
});
});
@@ -318,8 +327,7 @@ const upload_file = (file, target) => {
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")}`)
form_data.append('file', file.file_obj, file.name);
}
xhr.send(form_data);
@@ -333,7 +341,6 @@ const create_lesson_work = (file, target) => {
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");
@@ -398,17 +405,15 @@ const fetch_assignments = () => {
"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"]}`);
if (data.message) {
const assignment = data.message;
let target = $(".attach-file");
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"]);
target.siblings(".preview-work").find("a").attr("href", assignment.assignment).text(assignment.file_name);
}
}
}
});
};
@@ -449,6 +454,7 @@ const calculate_and_display_time = (percent_time) => {
const save_lesson = (e) => {
let lesson = $("#title").data("lesson");
let self = this;
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
args: {
@@ -459,7 +465,9 @@ const save_lesson = (e) => {
"chapter": $("#title").data("chapter"),
"preview": $("#preview").prop("checked") ? 1 : 0,
"idx": $("#title").data("index"),
"lesson": lesson ? lesson : ""
"lesson": lesson ? lesson : "",
"question": $("#assignment-question").text(),
"file_type": self.file_type
},
callback: (data) => {;
frappe.show_alert({
@@ -531,3 +539,15 @@ const make_editor = () => {
$("#body .frappe-control").removeClass("hide-control");
$("#body .form-column").addClass("p-0");
};
const set_file_type = () => {
let file_type = $("#file-type").data("type");
if (file_type) {
$("#file-type option").each((i, elem) => {
if ($(elem).val() == file_type) {
$(elem).attr("selected", true);
}
})
}
}