diff --git a/community/community/doctype/community_member/community_member.py b/community/community/doctype/community_member/community_member.py index 0f022828..d90fe498 100644 --- a/community/community/doctype/community_member/community_member.py +++ b/community/community/doctype/community_member/community_member.py @@ -11,54 +11,71 @@ import random class CommunityMember(Document): - def validate(self): - self.validate_username() - self.abbr = ("").join([ s[0] for s in self.full_name.split() ]) - if self.route != self.username: - self.route = self.username - - def validate_username(self): - if not self.username: - self.username = create_username_from_email(self.email) + def validate(self): + self.validate_username() + self.abbr = ("").join([ s[0] for s in self.full_name.split() ]) + if self.route != self.username: + self.route = self.username - if self.username: - if len(self.username) < 4: - frappe.throw(_("Username must be atleast 4 characters long.")) - if not re.match("^[A-Za-z0-9_]*$", self.username): - frappe.throw(_("Username can only contain alphabets, numbers and underscore.")) - self.username = self.username.lower() + def validate_username(self): + if not self.username: + self.username = create_username_from_email(self.email) - def __repr__(self): - return f"" + if self.username: + if len(self.username) < 4: + frappe.throw(_("Username must be atleast 4 characters long.")) + if not re.match("^[A-Za-z0-9_]*$", self.username): + frappe.throw(_("Username can only contain alphabets, numbers and underscore.")) + self.username = self.username.lower() + + def get_course_count(self) -> int: + """Returns the number of courses authored by this user. + """ + return frappe.db.count( + 'LMS Course', { + 'owner': self.email + }) + + def get_batch_count(self) -> int: + """Returns the number of batches authored by this user. + """ + return frappe.db.count( + 'LMS Batch Membership', { + 'member': self.name, + 'member_role': 'Mentor' + }) + + def __repr__(self): + return f"" def create_member_from_user(doc, method): - username = doc.username + username = doc.username - if ( doc.username and username_exists(doc.username)) or not doc.username: - username = create_username_from_email(doc.email) + if ( doc.username and username_exists(doc.username)) or not doc.username: + username = create_username_from_email(doc.email) - elif len(doc.username) < 4: - username = adjust_username(doc.username) + elif len(doc.username) < 4: + username = adjust_username(doc.username) - if username_exists(username): - username = username + str(random.randint(0,9)) + if username_exists(username): + username = username + str(random.randint(0,9)) - member = frappe.get_doc({ - "doctype": "Community Member", - "full_name": doc.full_name, - "username": username, - "email": doc.email, - "route": doc.username, - "owner": doc.email - }) - member.save(ignore_permissions=True) + member = frappe.get_doc({ + "doctype": "Community Member", + "full_name": doc.full_name, + "username": username, + "email": doc.email, + "route": doc.username, + "owner": doc.email + }) + member.save(ignore_permissions=True) def username_exists(username): - return frappe.db.exists("Community Member", dict(username=username)) + return frappe.db.exists("Community Member", dict(username=username)) def create_username_from_email(email): - string = email.split("@")[0] - return ''.join(e for e in string if e.isalnum()) + string = email.split("@")[0] + return ''.join(e for e in string if e.isalnum()) def adjust_username(username): - return username.ljust(4, str(random.randint(0,9))) \ No newline at end of file + return username.ljust(4, str(random.randint(0,9))) diff --git a/community/lms/doctype/chapter/__init__.py b/community/lms/doctype/chapter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/lms/doctype/chapter/chapter.js b/community/lms/doctype/chapter/chapter.js new file mode 100644 index 00000000..9380a622 --- /dev/null +++ b/community/lms/doctype/chapter/chapter.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Chapter', { + // refresh: function(frm) { + + // } +}); diff --git a/community/lms/doctype/chapter/chapter.json b/community/lms/doctype/chapter/chapter.json new file mode 100644 index 00000000..f153ab38 --- /dev/null +++ b/community/lms/doctype/chapter/chapter.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "autoname": "format:{####} {title}", + "creation": "2021-05-03 05:49:08.383057", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "course", + "title", + "description", + "locked" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title" + }, + { + "fieldname": "description", + "fieldtype": "Markdown Editor", + "label": "Description" + }, + { + "default": "0", + "fieldname": "locked", + "fieldtype": "Check", + "label": "Locked" + }, + { + "fieldname": "course", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Course", + "options": "LMS Course" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Lessons", + "link_doctype": "Lesson", + "link_fieldname": "chapter" + } + ], + "modified": "2021-05-03 06:52:10.894328", + "modified_by": "Administrator", + "module": "LMS", + "name": "Chapter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "title", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title", + "track_changes": 1 +} \ No newline at end of file diff --git a/community/lms/doctype/chapter/chapter.py b/community/lms/doctype/chapter/chapter.py new file mode 100644 index 00000000..d8e13097 --- /dev/null +++ b/community/lms/doctype/chapter/chapter.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, FOSS United and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class Chapter(Document): + def get_lessons(self): + rows = frappe.db.get_all("Lesson", + filters={"chapter": self.name}, + fields='*') + return [frappe.get_doc(dict(row, doctype='Lesson')) for row in rows] diff --git a/community/lms/doctype/chapter/test_chapter.py b/community/lms/doctype/chapter/test_chapter.py new file mode 100644 index 00000000..444f741f --- /dev/null +++ b/community/lms/doctype/chapter/test_chapter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestChapter(unittest.TestCase): + pass diff --git a/community/lms/doctype/lesson/__init__.py b/community/lms/doctype/lesson/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/lms/doctype/lesson/lesson.js b/community/lms/doctype/lesson/lesson.js new file mode 100644 index 00000000..54865506 --- /dev/null +++ b/community/lms/doctype/lesson/lesson.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Lesson', { + // refresh: function(frm) { + + // } +}); diff --git a/community/lms/doctype/lesson/lesson.json b/community/lms/doctype/lesson/lesson.json new file mode 100644 index 00000000..79203a93 --- /dev/null +++ b/community/lms/doctype/lesson/lesson.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "autoname": "format:{####} {title}", + "creation": "2021-05-03 06:21:12.995987", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "chapter", + "lesson_type", + "title" + ], + "fields": [ + { + "fieldname": "chapter", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Chapter", + "options": "Chapter" + }, + { + "default": "Video", + "fieldname": "lesson_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Lesson Type", + "options": "Video\nText\nQuiz" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-05-03 06:51:43.588969", + "modified_by": "Administrator", + "module": "LMS", + "name": "Lesson", + "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 +} \ No newline at end of file diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py new file mode 100644 index 00000000..3bf002de --- /dev/null +++ b/community/lms/doctype/lesson/lesson.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, FOSS United and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class Lesson(Document): + pass diff --git a/community/lms/doctype/lesson/test_lesson.py b/community/lms/doctype/lesson/test_lesson.py new file mode 100644 index 00000000..86c86ba2 --- /dev/null +++ b/community/lms/doctype/lesson/test_lesson.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLesson(unittest.TestCase): + pass diff --git a/community/lms/doctype/lms_course/lms_course.json b/community/lms/doctype/lms_course/lms_course.json index abad6a04..0c104240 100644 --- a/community/lms/doctype/lms_course/lms_course.json +++ b/community/lms/doctype/lms_course/lms_course.json @@ -74,8 +74,8 @@ "is_published_field": "is_published", "links": [ { - "group": "Topics", - "link_doctype": "LMS Topic", + "group": "Chapters", + "link_doctype": "Chapter", "link_fieldname": "course" }, { @@ -89,7 +89,7 @@ "link_fieldname": "course" } ], - "modified": "2021-04-21 14:45:41.658056", + "modified": "2021-05-03 05:52:30.396824", "modified_by": "Administrator", "module": "LMS", "name": "LMS Course", diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 8d6de54e..f53e2c03 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -8,6 +8,18 @@ from frappe.model.document import Document from ...utils import slugify class LMSCourse(Document): + @staticmethod + def find(slug): + """Returns the course with specified slug. + """ + return find("LMS Course", is_published=True, slug=slug) + + @staticmethod + def find_all(): + """Returns all published courses. + """ + return find_all("LMS Course", is_published=True) + def before_save(self): if not self.slug: self.slug = self.generate_slug(title=self.title) @@ -50,7 +62,7 @@ class LMSCourse(Document): """Returns the name of Community Member document for a give user. """ try: - return frappe.db.get_value("Community Member", {"email": email}, ["name"]) + return frappe.db.get_value("Community Member", {"email": email}, "name") except frappe.DoesNotExistError: return None @@ -85,18 +97,65 @@ class LMSCourse(Document): for mentor in mentors: member = frappe.get_doc("Community Member", mentor.mentor) # TODO: change this to count query - member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"})) + member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member, "member_type": "Mentor"})) course_mentors.append(member) return course_mentors - def get_instructor(self): - return frappe.get_doc("User", self.owner) - - @staticmethod - def find_all(): - """Returns all published courses. + def is_mentor(self, email): + """Checks if given user is a mentor for this course. """ - rows = frappe.db.get_all("LMS Course", - filters={"is_published": True}, - fields='*') - return [frappe.get_doc(dict(row, doctype='LMS Course')) for row in rows] + if not email: + return False + member = self.get_community_member(email) + return frappe.db.exists({ + 'doctype': 'LMS Course Mentor Mapping', + "course": self.name, + "member": member + }) + + def get_instructor(self): + member_name = self.get_community_member(self.owner) + return frappe.get_doc("Community Member", member_name) + + 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="creation") + + def get_batches(self, mentor=None): + batches = find_all("LMS Batch", course=self.name) + if mentor: + # TODO: optimize this + member = self.get_community_member(email=mentor) + memberships = frappe.db.get_all( + "LMS Batch Membership", + {"member": member}, + ["name"], as_dict=1) + batch_names = {m.batch for m in memberships} + return [b for b in batches if b.name in batch_names] + + def get_upcoming_batches(self): + now = frappe.utils.nowdate() + return find_all("LMS Batch", + course=self.name, + start_date=[">", now]) + +def find_all(doctype, order_by=None, **filters): + """Queries the database for documents of a doctype matching given filters. + """ + rows = frappe.db.get_all(doctype, + filters=filters, + fields='*', + order_by=order_by) + return [frappe.get_doc(dict(row, doctype=doctype)) for row in rows] + +def find(doctype, **filters): + """Queries the database for a document of given doctype matching given filters. + """ + rows = frappe.db.get_all(doctype, + filters=filters, + fields='*') + if rows: + row = rows[0] + return frappe.get_doc(dict(row, doctype=doctype)) diff --git a/community/lms/widgets/ChapterTeaser.html b/community/lms/widgets/ChapterTeaser.html new file mode 100644 index 00000000..51939ac8 --- /dev/null +++ b/community/lms/widgets/ChapterTeaser.html @@ -0,0 +1,15 @@ +
+
+

{{ chapter.title }}

+
+ {{ chapter.description or "" }} +
+
+ {% for lesson in chapter.get_lessons() %} +
+ {{lesson.title}} +
+ {% endfor %} +
+
+
diff --git a/community/public/css/style.less b/community/public/css/style.less index ea328dc5..63db45a9 100644 --- a/community/public/css/style.less +++ b/community/public/css/style.less @@ -4,6 +4,13 @@ background: white; border-radius: 10px; border: 1px solid #ddd; + + .teaser-body { + padding: 20px; + } + .teaser-footer { + padding: 20px; + } } .sketch-teaser { @@ -67,6 +74,17 @@ section.lightgray { background: inherit; } +.chapter-teaser { + .teaser(); + color: #444; + margin: 20px 0px; + + h3, h4 { + color: black; + font-weight: bold; + } +} + .field-width { width: 40%; display: inline-block; diff --git a/community/www/courses/course.html b/community/www/courses/course.html index 5989e0f6..9e974361 100644 --- a/community/www/courses/course.html +++ b/community/www/courses/course.html @@ -17,18 +17,18 @@
{{ CourseDescription(course) }} - {{ BatchSection(course, is_mentor, upcoming_batches, mentor_batches) }} + {{ BatchSection(course) }} {{ CourseOutline(course) }}
@@ -57,11 +57,11 @@ {% endif %} {% endmacro %} -{% macro BatchSection(course, is_mentor, upcoming_batches, mentor_batches) %} - {% if is_mentor %} - {{ BatchSectionForMentors(course, mentor_batches) }} +{% macro BatchSection(course) %} + {% if course.is_mentor(frappe.session.user) %} + {{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }} {% else %} - {{ BatchSectionForStudents(course, upcoming_batches) }} + {{ BatchSectionForStudents(course, course.get_upcoming_batches()) }} {% endif %} {% endmacro %} @@ -131,25 +131,9 @@ {% endmacro %} {% macro CourseOutline(course) %} -

Course Outline

+

Course Outline

-{% for chapter in course.topics %} -
-

{{loop.index}} {{chapter.title}}

-
- {{chapter.preview | markdown}} -
- - {# -
- {% for lesson in chapter.lessons %} -
- - {{lesson.title}} -
- {% endfor %} -
- #} -
-{% endfor %} + {% for chapter in course.get_chapters() %} + {{ widgets.ChapterTeaser(chapter=chapter)}} + {% endfor %} {% endmacro %} diff --git a/community/www/courses/course.py b/community/www/courses/course.py index e0418096..7a61e8c8 100644 --- a/community/www/courses/course.py +++ b/community/www/courses/course.py @@ -1,101 +1,21 @@ import frappe from community.www.courses.utils import get_instructor from frappe.utils import nowdate, getdate +from community.lms.models import Course def get_context(context): context.no_cache = 1 - + try: - course_id = frappe.form_dict["course"] + course_slug = frappe.form_dict["course"] except KeyError: frappe.local.flags.redirect_location = "/courses" raise frappe.Redirect - - context.course = get_course(course_id) - context.batches = get_course_batches(context.course.name) - context.is_mentor = is_mentor(context.course.name) - context.memberships = get_membership(context.batches) - if len(context.memberships) and not context.is_mentor: - frappe.local.flags.redirect_location = "/courses/" + course_id + "/" + context.memberships[0].code + "/learn" + + course = Course.find(course_slug) + if course is None: + frappe.local.flags.redirect_location = "/courses" raise frappe.Redirect - context.upcoming_batches = get_upcoming_batches(context.course.name) - context.instructor = get_instructor(context.course.owner) - context.mentors = get_mentors(context.course.name) - - if context.is_mentor: - context.mentor_batches = get_mentor_batches(context.memberships) # Your Bacthes for mentor -def get_course(slug): - course = frappe.db.get_value("LMS Course", {"slug": slug}, - ["name", "slug", "title", "description", "short_introduction", "video_link", "owner"], as_dict=1) - - course["topics"] = frappe.db.get_all("LMS Topic", - filters={ - "course": course["name"] - }, - fields=["name", "slug", "title", "preview"], - order_by="creation" - ) - return course - -def get_upcoming_batches(course): - batches = frappe.get_all("LMS Batch", {"course": course, "start_date": [">", nowdate()]}, ["start_date", "start_time", "end_time", "sessions_on", "name"]) - batches = get_batch_mentors(batches) - return batches - -def get_batch_mentors(batches): - for batch in batches: - batch.mentors = [] - mentors = frappe.get_all("LMS Batch Membership", {"batch": batch.name, "member_type": "Mentor"}, ["member"]) - for mentor in mentors: - member = frappe.db.get_value("Community Member", mentor.member, ["full_name", "photo", "abbr"], as_dict=1) - batch.mentors.append(member) - return batches - -def get_membership(batches): - memberships = [] - member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name") - for batch in batches: - membership = frappe.db.get_value("LMS Batch Membership", {"member": member, "batch": batch.name}, ["batch", "member", "member_type"], as_dict=1) - if membership: - membership.code = batch.code - memberships.append(membership) - return memberships - -def get_mentors(course): - course_mentors = [] - mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": course}, ["mentor"]) - for mentor in mentors: - member = frappe.get_doc("Community Member", mentor.mentor) - member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"})) - course_mentors.append(member) - return course_mentors - -def get_course_batches(course): - return frappe.get_all("LMS Batch", {"course": course}, ["name", "code"]) - -def get_mentor_batches(memberships): - mentor_batches = [] - memberships_as_mentor = list(filter(lambda x: x.member_type == "Mentor", memberships)) - for membership in memberships_as_mentor: - batch = frappe.get_doc("LMS Batch", membership.batch) - mentor_batches.append(batch) - for batch in mentor_batches: - if getdate(batch.start_date) < getdate(): - batch.status = "active" - batch.badge_class = "green_badge" - else: - batch.status = "scheduled" - batch.badge_class = "yellow_badge" - mentor_batches = get_batch_mentors(mentor_batches) - return mentor_batches - -def is_mentor(course): - try: - member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, ["name"]) - except frappe.DoesNotExistError: - return False - mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": course, "mentor": member}) - if len(mapping): - return True + context.course = course diff --git a/community/www/macros/common_macro.html b/community/www/macros/common_macro.html index f5cf190e..ab21effd 100644 --- a/community/www/macros/common_macro.html +++ b/community/www/macros/common_macro.html @@ -2,7 +2,7 @@

Instructor

{{instructor.full_name}}
-
Created {{instructor.course_count}} courses
+
Created {{instructor.get_course_count()}} courses
{% endmacro %} @@ -11,7 +11,7 @@ {% for m in mentors %}
{{m.full_name}}
-
Mentored {{m.batch_count}} batches
+
Mentored {{m.get_batch_count()}} batches
{% endfor %} {% if not is_mentor %} @@ -29,4 +29,4 @@

{{course_name}}

{{member_count}} members
-{% endmacro %} \ No newline at end of file +{% endmacro %}