Merge branch 'main' of https://github.com/frappe/community into quiz-cleanup

This commit is contained in:
pateljannat
2021-07-30 18:35:23 +05:30
32 changed files with 415 additions and 346 deletions

View File

@@ -2,7 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on('Chapter', {
// refresh: function(frm) {
// }
onload: function (frm) {
frm.set_query("lesson", "lessons", function () {
return {
filters: {
"chapter": frm.doc.name,
}
};
});
}
});

View File

@@ -9,8 +9,7 @@
"course",
"title",
"description",
"locked",
"index_"
"lessons"
],
"fields": [
{
@@ -24,12 +23,6 @@
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "0",
"fieldname": "locked",
"fieldtype": "Check",
"label": "Locked"
},
{
"fieldname": "course",
"fieldtype": "Link",
@@ -38,10 +31,10 @@
"options": "LMS Course"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
"fieldname": "lessons",
"fieldtype": "Table",
"label": "Lessons",
"options": "Lessons"
}
],
"index_web_pages_for_search": 1,
@@ -52,7 +45,7 @@
"link_fieldname": "chapter"
}
],
"modified": "2021-05-13 21:05:20.531890",
"modified": "2021-07-27 16:28:08.667964",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapter",

View File

@@ -5,15 +5,6 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from ...utils import slugify
class Chapter(Document):
def get_lessons(self):
rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name},
fields='name',
order_by="index_")
return [frappe.get_doc('Lesson', row['name']) for row in rows]
def get_slugified_chapter_title(self):
return slugify(self.title)
pass

View File

@@ -0,0 +1,32 @@
{
"actions": [],
"creation": "2021-07-27 16:25:02.903245",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"chapter"
],
"fields": [
{
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:25:02.903245",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapters",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -10,7 +10,6 @@
"include_in_preview",
"column_break_4",
"title",
"index_",
"index_label",
"section_break_6",
"body",
@@ -31,13 +30,6 @@
"in_list_view": 1,
"label": "Title"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Index"
},
{
"fieldname": "body",
"fieldtype": "Markdown Editor",
@@ -75,7 +67,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-29 13:34:49.077363",
"modified": "2021-07-27 16:28:29.203624",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-07-27 16:25:48.269536",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lesson"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:53:52.732191",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lessons",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -2,7 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on('LMS Course', {
// refresh: function(frm) {
// }
onload: function (frm) {
frm.set_query("chapter", "chapters", function () {
return {
filters: {
"course": frm.doc.name,
}
};
});
}
});

View File

@@ -1,11 +1,5 @@
{
"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",
@@ -21,16 +15,17 @@
"engine": "InnoDB",
"field_order": [
"title",
"short_code",
"video_link",
"column_break_3",
"is_published",
"disable_self_learning",
"image",
"section_break_5",
"column_break_3",
"tags",
"is_published",
"upcoming",
"disable_self_learning",
"section_break_5",
"short_introduction",
"description"
"description",
"chapters"
],
"fields": [
{
@@ -53,11 +48,6 @@
"fieldtype": "Check",
"label": "Published"
},
{
"fieldname": "short_code",
"fieldtype": "Data",
"label": "Short Code"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
@@ -92,6 +82,18 @@
"fieldname": "tags",
"fieldtype": "Data",
"label": "Tags"
},
{
"default": "0",
"fieldname": "upcoming",
"fieldtype": "Check",
"label": "Is an Upcoming Course"
},
{
"fieldname": "chapters",
"fieldtype": "Table",
"label": "Chapters",
"options": "Chapters"
}
],
"index_web_pages_for_search": 1,
@@ -111,14 +113,9 @@
"group": "Mentors",
"link_doctype": "LMS Course Mentor Mapping",
"link_fieldname": "course"
},
{
"group": "Mentors",
"link_doctype": "LMS Mentor Request",
"link_fieldname": "course"
}
],
"modified": "2021-07-09 15:05:05.372430",
"modified": "2021-07-28 19:01:50.677445",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",
@@ -141,6 +138,5 @@
"sort_field": "creation",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1,
"track_views": 1
"track_changes": 1
}

View File

@@ -9,8 +9,10 @@ import json
from ...utils import slugify
from community.query import find, find_all
from frappe.utils import flt, cint
from ...utils import slugify
class LMSCourse(Document):
@staticmethod
def find(name):
"""Returns the course with specified name.
@@ -112,17 +114,42 @@ class LMSCourse(Document):
def get_chapters(self):
"""Returns all chapters of this course.
"""
# TODO: chapters should have a way to specify the order
return find_all("Chapter", course=self.name, order_by="index_")
chapters = []
for row in self.chapters:
chapter_details = frappe.db.get_value("Chapter", row.chapter,
["name", "title", "description"],
as_dict=True)
chapter_details.idx = row.idx
chapters.append(chapter_details)
return chapters
def get_lessons(self):
""" Returns all lessons of this course """
def get_lessons(self, chapter=None):
""" If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course """
lessons = []
chapters = self.get_chapters()
for chapter in chapters:
lessons.append(frappe.get_all("Lesson", {"chapter": chapter.name}))
if chapter:
return self.get_lesson_details(chapter)
for chapter in self.get_chapters():
lesson = self.get_lesson_details(chapter)
lessons += lesson
return lessons
def get_lesson_details(self, chapter):
lessons = []
lesson_list = frappe.get_all("Lessons", {"parent": chapter.name},
["lesson", "idx"], order_by="idx")
for row in lesson_list:
lesson_details = frappe.get_doc("Lesson", row.lesson)
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
lessons.append(lesson_details)
return lessons
def get_slugified_chapter_title(self, chapter):
return slugify(chapter)
def get_course_progress(self):
""" Returns the course progress of the session user """
lesson_count = len(self.get_lessons())
@@ -160,38 +187,12 @@ class LMSCourse(Document):
visibility="Public")
return batches
def get_chapter(self, index):
return find("Chapter", course=self.name, index_=index)
def get_lesson(self, chapter_index, lesson_index):
chapter_name = frappe.get_value(
"Chapter",
{"course": self.name, "index_": chapter_index},
"name")
lesson_name = chapter_name and frappe.get_value(
"Lesson",
{"chapter": chapter_name, "index_": lesson_index},
"name")
return lesson_name and frappe.get_doc("Lesson", lesson_name)
def get_lesson_index(self, lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""
lesson = frappe.get_doc("Lesson", lesson_name)
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()
lesson = frappe.db.get_value("Lessons", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
chapter = frappe.db.get_value("Chapters", {"chapter": lesson.parent}, ["idx"], as_dict=True)
return f"{chapter.idx}.{lesson.idx}"
def reindex_exercises(self):
for i, c in enumerate(self.get_chapters(), start=1):
@@ -202,7 +203,7 @@ class LMSCourse(Document):
def _reindex_exercises_in_chapter(self, c):
i = 1
for lesson in c.get_lessons():
for lesson in self.get_lessons(c):
for exercise in lesson.get_exercises():
exercise.index_ = i
exercise.index_label = f"{c.index_}.{i}"
@@ -302,9 +303,6 @@ class LMSCourse(Document):
return None
return sum(ratings)/len(ratings)
def get_outline(self):
return CourseOutline(self)
def get_progress(self, lesson):
return frappe.db.get_value("LMS Course Progress",
{
@@ -314,55 +312,14 @@ class LMSCourse(Document):
},
["status"])
class CourseOutline:
def __init__(self, course):
self.course = course
self.chapters = self.get_chapters()
self.lessons = self.get_lessons()
def get_next(self, current):
def get_neighbours(self, current, lessons):
current = flt(current)
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
return numbers[index+1]
except IndexError:
return None
def get_prev(self, current):
current = flt(current)
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
if index == 0:
return None
return numbers[index-1]
except IndexError:
return None
def get_chapters(self):
return frappe.db.get_all("Chapter",
filters={"course": self.course.name},
fields=["name", "title", "index_"],
order_by="index_")
def get_lessons(self):
chapters = [c['name'] for c in self.chapters]
lessons = frappe.db.get_all("Lesson",
filters={"chapter": ["IN", chapters]},
fields=["name", "title", "chapter", "index_"])
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
for lesson in lessons:
lesson['number'] = flt("{}.{}".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.")
numbers = sorted(lesson.number for lesson in lessons)
index = numbers.index(current)
return {
"prev": numbers[index-1] if index-1 >= 0 else None,
"next": numbers[index+1] if index+1 < len(numbers) else None
}
@frappe.whitelist()
def reindex_exercises(doc):

View File

@@ -27,12 +27,13 @@
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson"
"options": "Lesson",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-23 17:58:57.642873",
"modified": "2021-07-23 19:06:12.551633",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",

View File

@@ -1,67 +0,0 @@
<div class="mt-5">
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a
class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
{{ course.title }}</span> {% endif %}
{% set all_memberships = course.get_all_memberships(frappe.session.user) %}
{% if membership and membership.batch and all_memberships | length > 1 %}
<a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ membership.batch_title }}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% for data in all_memberships %}
{% if data.batch != membership.batch %}
<a class="dropdown-item switch-batch"
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% if not membership %}
{% set display_class = "hide" %}
{% else %}
{% set display_class = "" %}
{% endif %}
<ul class="nav nav-tabs mt-4">
<li class="nav-item">
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
</li>
<li class="nav-item">
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson
else '1.1' %}
<a class="nav-link" id="learn"
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
</li> -->
<li class="nav-item {{ display_class }}">
<a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
</li>
<!-- <li class="nav-item {{ display_class }}">
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
</li> -->
<!-- <li class="nav-item">
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
</li> -->
{% if membership and membership.batch and course.is_mentor(frappe.session.user) %}
<li class="nav-item">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
</li>
{% endif %}
</ul>
{% block script %}
<script>
frappe.ready(() => {
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}{{ course.query_parameter }}"]`)
if (selector) {
selector.classList.add('active');
}
else {
$("#learn").addClass('active')
}
})
</script>
{% endblock %}

View File

@@ -1,26 +1,28 @@
<div>
<div class="small-title chapter-title" data-target="#{{ chapter.get_slugified_chapter_title() }}"
<div class="small-title chapter-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}"
data-toggle="collapse" aria-expanded="false">
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
{{ index }}. {{ chapter.title }}
</div>
<div class="chapter-content collapse navbar-collapse" id="{{ chapter.get_slugified_chapter_title() }}">
<div class="chapter-content collapse navbar-collapse" id="{{ course.get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description %}
<div class="chapter-description muted-text">
{{ chapter.description }}
</div>
{% endif %}
<div class="lessons">
{% for lesson in chapter.get_lessons() %}
{% for lesson in course.get_lessons(chapter) %}
<div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
<div class="lesson-info{% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview %}
<a class="lesson-links"
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
{{ lesson.title }}

View File

@@ -1,10 +1,14 @@
<div class="common-card-style course-card">
<div class="course-image" style="background-image: url({{ course.image }});">
<div class="course-image {% if not course.image %}default-image{% endif %}"
{% if course.image %} style="background-image: url( {{ course.image }} );" {% endif %}>
<div class="course-tags">
{% for tag in course.get_tags() %}
<div class="course-card-pills">{{ tag }}</div>
{% endfor %}
</div>
{% if not course.image %}
<div class="default-image-text">{{ course.title[0] }}</div>
{% endif %}
</div>
<div class="course-card-content">
<div class="course-card-meta muted-text">
@@ -53,15 +57,19 @@
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
{% if membership %}
{% if course.upcoming %}
<div class="view-course-link is-default">
Upcoming Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
</div>
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
{% elif membership %}
<div class="view-course-link is-primary">
Continue Course <img class="ml-3" src="/assets/community/icons/white-arrow.svg" />
</div>
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
{% else %}
<div class="view-course-link">
View Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
</div>

View File

@@ -1,15 +1,18 @@
{% if course.get_reviews() | length %}
<div class="reviews-parent">
<div class="reviews-heading">
<div class="course-home-headings">Student Review</div>
{% set reviews = course.get_reviews() %}
{% if reviews | length or course.is_eligible_to_review(membership) %}
<div class="mb-5">
<span class="course-home-headings">Reviews</span>
{% if course.is_eligible_to_review(membership) %}
<a class="review-link" href="">
Provide your Feedback
</a>
<span class="review-link button is-secondary pull-right">
Write a review
</span>
{% endif %}
</div>
{% endif %}
{% if reviews | length %}
<div class="reviews-section">
{% for review in course.get_reviews() %}
{% for review in reviews %}
<div class="review-card">
<div class="common-card-style review-content small-title"> {{ review.review }} </div>
<div class="review-card-footer">
@@ -30,6 +33,7 @@
</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
@@ -67,8 +71,8 @@
</div>
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field" data-fieldtype="Text"
data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
spellcheck="false"></textarea>
</div>
</div>
@@ -77,9 +81,9 @@
</form>
</div>
<div class="modal-footer">
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">Submit</div>
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
Submit</div>
</div>
</div>
</div>
</div>
{% endif %}