Compare commits

..

4 Commits

Author SHA1 Message Date
Anand Chitipothu
c96a14c972 feat: ignore orphan exercises in the progress
Don't show exercises that are not added to any lesson in the progress.
2021-06-01 08:15:52 +05:30
Anand Chitipothu
400e706be1 feat: update the index of orphan exercises
When an exercise is removed from a lesson, the link to the lesson is
removed from that exercise and the index is reset. This will make sure
the removed exercises won't show up in places like progress.
2021-06-01 05:59:01 +05:30
Anand Chitipothu
a12a52747e feat: show exercise index in the title
Show exercise as "Exercise 2.1: Draw a Circle".
2021-06-01 05:49:45 +05:30
Anand Chitipothu
b9a93bb160 feat: added actions to reindex lessons and exercises
Some lessons gets deleted and some new ones get added in the progress of
course creation and it may happen then some of the lesson index may
become inconsistent.  Also, we would like to maintain an index for the
exercises. To support both of these, added actions to reindex lessons
and exercises to the course doctype.
2021-06-01 05:46:32 +05:30
26 changed files with 127 additions and 244 deletions

View File

@@ -43,7 +43,7 @@ def save_current_lesson(batch_name, lesson_name):
doctype="LMS Batch Membership",
filters={
"batch": batch_name,
"member": frappe.session.user
"member_email": frappe.session.user
},
fieldname="name")
if not name:

View File

@@ -10,6 +10,6 @@ class Chapter(Document):
def get_lessons(self):
rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name},
fields='*',
fields='name',
order_by="index_")
return [frappe.get_doc(dict(row, doctype='Lesson')) for row in rows]
return [frappe.get_doc('Lesson', row['name']) for row in rows]

View File

@@ -15,7 +15,9 @@
"hints",
"tests",
"image",
"lesson"
"lesson",
"index_",
"index_label"
],
"fields": [
{
@@ -27,6 +29,7 @@
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course"
},
@@ -73,13 +76,27 @@
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
},
{
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index",
"read_only": 1
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-20 13:23:12.340928",
"modified": "2021-06-01 05:22:15.656013",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise",
@@ -99,8 +116,8 @@
}
],
"search_fields": "title",
"sort_field": "modified",
"sort_order": "DESC",
"sort_field": "index_label",
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
}

View File

@@ -25,7 +25,6 @@ class Exercise(Document):
order_by="creation desc",
page_length=1)
print("get_user_submission", result)
if result:
return result[0]
@@ -55,6 +54,5 @@ class Exercise(Document):
image=image,
solution=code)
doc.insert(ignore_permissions=True)
return doc

View File

@@ -1,13 +1,8 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
# import frappe
from frappe.model.document import Document
from ..lesson.lesson import update_progress
class ExerciseSubmission(Document):
def after_insert(self):
course_details = frappe.get_doc("LMS Course", self.course)
if not (course_details.is_mentor(frappe.session.user) or frappe.flags.in_test):
update_progress(self.lesson)
pass

View File

@@ -10,6 +10,7 @@
"lesson_type",
"title",
"index_",
"index_label",
"body",
"sections"
],
@@ -51,11 +52,18 @@
"fieldtype": "Table",
"label": "Sections",
"options": "LMS Section"
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-13 20:03:51.510605",
"modified": "2021-06-01 05:30:48.127494",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",

View File

@@ -16,10 +16,28 @@ class Lesson(Document):
e = s.get_exercise()
e.lesson = self.name
e.save()
self.update_orphan_exercises()
def update_orphan_exercises(self):
"""Updates the exercises that were previously part of this lesson,
but not any more.
"""
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})}
active_exercises = {s.id for s in self.get("sections") if s.type=="exercise"}
orphan_exercises = linked_exercises - active_exercises
for name in orphan_exercises:
ex = frappe.get_doc("Exercise", name)
ex.lesson = None
ex.index_ = 0
ex.index_label = ""
ex.save()
def get_sections(self):
return sorted(self.get('sections'), key=lambda s: s.index)
def get_exercises(self):
return [frappe.get_doc("Exercise", s.id) for s in self.get("sections") if s.type=="exercise"]
def make_lms_section(self, index, section):
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
s.type = section.type
@@ -43,65 +61,3 @@ class Lesson(Document):
The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson.
"""
def get_progress(self):
return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status")
def get_slugified_class(self):
if self.get_progress():
return ("").join([ s for s in self.get_progress().lower().split() ])
return
@frappe.whitelist()
def save_progress(lesson, batch):
if not frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"batch": batch
}):
return
if frappe.db.exists("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user
}):
return
lesson_details = frappe.get_doc("Lesson", lesson)
dynamic_content = frappe.db.count("LMS Section",
filters={
"type": ["not in", ["example", "text"]],
"parent": lesson_details.name
})
status = "Complete"
if dynamic_content:
status = "Partially Complete"
frappe.get_doc({
"doctype": "LMS Course Progress",
"lesson": lesson_details.name,
"status": status
}).save(ignore_permissions=True)
def update_progress(lesson):
user = frappe.session.user
if not all_dynamic_content_submitted(lesson, user):
return
if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}):
course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user})
course_progress.status = "Complete"
course_progress.save()
def all_dynamic_content_submitted(lesson, user):
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, ["name"], pluck="name")
all_exercises_submitted = False
print(exercise_names)
query = {
"exercise": ["in", exercise_names],
"owner": user
}
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
all_exercises_submitted = True
return all_exercises_submitted

View File

@@ -1,5 +1,18 @@
{
"actions": [],
"actions": [
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_lessons",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Lessons"
},
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_exercises",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Exercises"
}
],
"allow_guest_to_view": 1,
"allow_rename": 1,
"creation": "2021-03-01 16:49:33.622422",
@@ -86,7 +99,7 @@
"link_fieldname": "course"
}
],
"modified": "2021-05-23 18:14:32.602647",
"modified": "2021-06-01 04:36:45.696776",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from ...utils import slugify
from community.query import find, find_all
@@ -157,6 +158,35 @@ class LMSCourse(Document):
chapter = frappe.get_doc("Chapter", lesson.chapter)
return f"{chapter.index_}.{lesson.index_}"
def reindex_lessons(self):
for i, c in enumerate(self.get_chapters(), start=1):
c.index_ = i
c.save()
self._reindex_lessons_in_chapter(c)
def _reindex_lessons_in_chapter(self, c):
for i, lesson in enumerate(c.get_lessons(), start=1):
lesson.index = i
lesson.index_label = f"{c.index_}.{i}"
lesson.save()
def reindex_exercises(self):
for i, c in enumerate(self.get_chapters(), start=1):
if c.index_ != i:
c.index_ = i
c.save()
self._reindex_exercises_in_chapter(c)
def _reindex_exercises_in_chapter(self, c):
i = 1
for lesson in c.get_lessons():
for exercise in lesson.get_exercises():
exercise.index_ = i
exercise.index_label = f"{c.index_}.{i}"
exercise.save()
i += 1
def get_outline(self):
return CourseOutline(self)
@@ -187,7 +217,8 @@ class CourseOutline:
def get_chapters(self):
return frappe.db.get_all("Chapter",
filters={"course": self.course.name},
fields=["name", "title", "index_"])
fields=["name", "title", "index_"],
order_by="index_")
def get_lessons(self):
chapters = [c['name'] for c in self.chapters]
@@ -199,3 +230,17 @@ class CourseOutline:
for lesson in lessons:
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
return lessons
@frappe.whitelist()
def reindex_lessons(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_lessons()
frappe.msgprint("All lessons in this course have been re-indexed.")
@frappe.whitelist()
def reindex_exercises(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_exercises()
frappe.msgprint("All exercises in this course have been re-indexed.")

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Course Progress', {
// refresh: function(frm) {
// }
});

View File

@@ -1,78 +0,0 @@
{
"actions": [],
"creation": "2021-05-31 17:20:13.388453",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"column_break_3",
"lesson",
"chapter",
"course"
],
"fields": [
{
"fetch_from": "chapter.course",
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fetch_from": "lesson.chapter",
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter",
"read_only": 1
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Complete\nPartially Complete\nIncomplete"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-02 13:05:31.114939",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Progress",
"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",
"track_changes": 1
}

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSCourseProgress(Document):
pass

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSCourseProgress(unittest.TestCase):
pass

View File

@@ -19,7 +19,7 @@
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-06-02 15:52:06.383260",
"modified": "2021-04-30 11:22:18.188712",
"modified_by": "Administrator",
"module": "LMS",
"name": "add-a-new-batch",
@@ -38,13 +38,13 @@
{
"allow_read_on_all_link_options": 0,
"fieldname": "course",
"fieldtype": "Link",
"hidden": 1,
"fieldtype": "Data",
"hidden": 0,
"label": "Course",
"max_length": 0,
"max_value": 0,
"options": "LMS Course",
"read_only": 0,
"options": "",
"read_only": 1,
"reqd": 0,
"show_in_filter": 0
},
@@ -111,4 +111,4 @@
"show_in_filter": 0
}
]
}
}

View File

@@ -8,9 +8,6 @@
{% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser">
<a {% if show_link %} class="anchor_style" href="{{ batch.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% endif %}>{{ lesson.title }}</a>
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
<a class="pull-right badge p-1 {{ lesson.get_slugified_class() }}"> <img class="progress-image" src="/assets/community/images/Vector.png"> {{ lesson.get_progress() }}</a>
{% endif %}
</div>
{% endfor %}
</div>

View File

@@ -1,5 +1,5 @@
<h2>Course Outline</h2>
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link)}}
{% endfor %}

View File

@@ -1,7 +1,7 @@
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
<div class="exercise">
<h2>{{ exercise.title }}</h2>
<h2>Exercise {{exercise.index_label}}: {{ exercise.title }}</h2>
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
{% if exercise.image %}

View File

@@ -238,36 +238,3 @@ section {
.page-card .btn {
margin-top: 30px;
}
.partiallycomplete {
background: #FEF4E2;
color: #976417;
}
.partiallycomplete img {
background: #976417;
}
.complete {
background: #EAF5EE;
color: #38A160;
}
.complete img {
background: #38A160;
}
.incomplete {
background: #FEECEC;
color: #E24C4C;
}
.incomplete img {
background: #E24C4C;
}
.progress-image {
margin-right: 3px;
border-radius: 50px;
padding: 5px;
}

View File

@@ -285,7 +285,7 @@ section.lightgray {
}
.lesson-teaser {
line-height: 40px;
line-height: 35px;
}
#hero h1 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

View File

@@ -15,7 +15,7 @@
<h1 class="mt-5">{{ batch.title }}</h1>
</div>
<div class="course-details">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True) }}
</div>
<div class="w-25">
<h2>Batch Schedule</h2>

View File

@@ -28,7 +28,7 @@
{{ widgets.BatchTabs(course=course, batch=batch) }}
<div class="lesson-page">
<h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-name="{{ lesson.name }}" data-batch="{{ batch.name }}">{{ lesson.title }}</h2>
<h2>{{ lesson.title }}</h2>
{% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}">

View File

@@ -1,11 +0,0 @@
frappe.ready(() => {
if (!$(".title").hasClass("is_mentor")) {
frappe.call({
method: "community.lms.doctype.lesson.lesson.save_progress",
args: {
lesson: $(".title").attr("data-name"),
batch: $(".title").attr("data-batch")
}
})
}
})

View File

@@ -29,7 +29,7 @@
<h1>Batch Progress</h1>
{% for exercise in report.exercises %}
<div class="exercise-submissions">
<h2>{{exercise.title}}</h2>
<h2>Exercise {{exercise.index_label}}: {{exercise.title}}</h2>
{% for s in report.get_submissions_of_exercise(exercise.name) %}
<div class="submission">
<h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4>

View File

@@ -25,7 +25,7 @@ class BatchReport:
self.submissions_by_exercise[s.exercise].append(s)
def get_exercises(self, course_name):
return frappe.get_all("Exercise", {"course": course_name}, ["name", "title"])
return frappe.get_all("Exercise", {"course": course_name, "lesson": ["!=", ""]}, ["name", "title", "index_label"], order_by="index_label")
def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name]