Compare commits

..

47 Commits

Author SHA1 Message Date
Anand Chitipothu
ef0c3e4a24 feat: pluggable profile tabs
Added ProfileTab class to represent a profile tab and made the profile
page render the tabs specified in the hook `profile_tabs`. This allows
plugging in new tabs in the profile page without makeing any changes to
the community module.
2021-06-08 10:36:12 +05:30
Anand Chitipothu
3619b136f8 Merge pull request #117 from fossunited/lesson-progress
feat: lesson progress
2021-06-07 11:24:52 +05:30
pateljannat
671b4a0650 fix: api and orm 2021-06-02 20:19:36 +05:30
Anand Chitipothu
586b39c0fd fix: issue with numbering the exercises
The exercises being listed in unpredicted order instead of the order
they were listed in the lesson. The was because the `index_` of the
exercise was never updated. Fixed this by updating the `index_` whenever
a lesson edited. However, the user still need to run reindex exercises
on the course correct the ordering, which wasn't possible earlier.
2021-06-02 17:48:02 +05:30
pateljannat
4fd7af053b fix: tests 2021-06-02 16:47:17 +05:30
pateljannat
5fd1143f76 feat: lesson progress 2021-06-02 13:52:50 +05:30
Jannat Patel
0dc4743556 Merge pull request #116 from fossunited/reindex-exercises
feat: actions to reindex lessons and exercises
2021-06-01 11:40:52 +05:30
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
Jannat Patel
9c65ff8ae6 Merge pull request #113 from fossunited/invite-based-membership
feat: Invite based membership
2021-05-31 13:41:12 +05:30
pateljannat
bb0aa09b4e fix: messages and url 2021-05-31 13:39:31 +05:30
pateljannat
a8752afb3b feat: invite based membership become a member page 2021-05-28 13:53:34 +05:30
pateljannat
327bde870b Merge branch 'main' of https://github.com/frappe/community into invite-based-membership 2021-05-27 17:32:48 +05:30
Jannat Patel
640ead4922 Merge pull request #109 from fossunited/style-fixes
fix: Style fixes
2021-05-27 11:54:08 +05:30
pateljannat
687f7f7f7b fix: minor home page issues 2021-05-27 11:25:05 +05:30
Anand Chitipothu
527a563e4a chore: added "programming" to the hero title 2021-05-27 09:39:07 +05:30
pateljannat
5bc9a7fe37 Merge branch 'style-fixes' of https://github.com/frappe/community into invite-based-membership 2021-05-26 19:10:38 +05:30
pateljannat
24835acd9c fix: jinja 2021-05-26 19:10:08 +05:30
pateljannat
3648b3ab47 Merge branch 'style-fixes' of https://github.com/frappe/community into invite-based-membership 2021-05-26 19:08:38 +05:30
pateljannat
914f8504a0 fix: added class in lms_message 2021-05-26 18:56:57 +05:30
pateljannat
ab8546a121 fix: course outline, discussion, lms batch 2021-05-26 17:16:00 +05:30
pateljannat
f327c6fb10 fix: tests for course description 2021-05-26 12:38:50 +05:30
pateljannat
c7ccefa632 fix: discussion, batch home page, new fields for batches 2021-05-26 12:13:04 +05:30
Anand Chitipothu
823cf4e431 style: fixed word-wrap of output 2021-05-25 16:06:12 +05:30
pateljannat
18f074d8ac fix: ignore user permission for membership 2021-05-24 19:35:26 +05:30
pateljannat
c9185ae68c fix: tabs and learn page 2021-05-24 19:24:07 +05:30
Anand Chitipothu
82fa0fa4d7 fix: error in loading the progress page 2021-05-24 13:46:59 +05:30
Jannat Patel
64752433d2 Merge pull request #106 from fossunited/issue-103
Redirect the learn page to the current lesson of the user
2021-05-24 13:40:50 +05:30
Anand Chitipothu
50856fdfa5 fix: fixed failing test 2021-05-24 13:30:47 +05:30
Anand Chitipothu
cac4f2afef feat: redirect the learn page to the current lesson of the user
The current lesson is maintained in the LMS Batch Membership and that is
updated everytime a lesson page is visited.
2021-05-24 13:07:29 +05:30
Anand Chitipothu
df431165e8 feat: redirect non-members visiting any batch page to the course page 2021-05-24 12:57:01 +05:30
Anand Chitipothu
69125e571f feat: added member_username and current_lesson fields to LMS Batch Membership
And removed member_email field which is a duplicate of member.
2021-05-24 12:43:20 +05:30
Anand Chitipothu
68f7215b95 fix: error in updating LMS Batch membership
The validation was always failing when trying to updating an LMS Batch
Membership document. This was due to a bug in the validation logic that
was considering itself as a duplicate record. This has been fixed.

Also added tests to verify that.
2021-05-24 12:15:16 +05:30
pateljannat
ca42c32f54 Merge branch 'main' of https://github.com/frappe/community into style-fixes 2021-05-24 11:57:53 +05:30
Anand Chitipothu
20adc8079e Merge pull request #105 from fossunited/community-member-to-user-refactor
refactor: Community Member to User refactor
2021-05-24 11:44:45 +05:30
Anand Chitipothu
d55941d4bb Merge branch 'main' into community-member-to-user-refactor 2021-05-24 11:39:20 +05:30
Jannat Patel
6074ee3688 Merge pull request #104 from fossunited/resume-course
Added "Resume Course" button to the course teaser
2021-05-24 11:36:06 +05:30
pateljannat
b3f87ba5b6 fix: removed community course member and references to profile macro 2021-05-24 10:39:04 +05:30
pateljannat
631275e9a8 refactor: course and sidebar cleanup 2021-05-24 10:28:02 +05:30
Anand Chitipothu
14f9d4875a chore: added issue template for Feature Request 2021-05-24 08:36:09 +05:30
pateljannat
419a7e666f refactor: tests, mentor request, messages 2021-05-22 20:49:47 +05:30
pateljannat
713dcf178a refactor: patches to fix data, profile dashboard, lms mentor mapping page fixes 2021-05-21 21:40:31 +05:30
pateljannat
637c795321 refactor: moved community member class functions to user override 2021-05-21 16:22:59 +05:30
pateljannat
63d00a46c4 Merge branch 'main' of https://github.com/frappe/community into community-member-to-user-refactor 2021-05-21 13:27:22 +05:30
pateljannat
e991dc5c73 refactor: removed community member doctype 2021-05-21 13:27:15 +05:30
103 changed files with 1500 additions and 1639 deletions

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

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

View File

@@ -1,96 +0,0 @@
{
"actions": [],
"autoname": "field:user_name",
"creation": "2021-03-02 11:24:49.612530",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"full_name",
"user_name",
"email",
"short_intro",
"bio",
"photo",
"route"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Full Name",
"reqd": 1
},
{
"fieldname": "user_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "User Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email",
"options": "Email",
"reqd": 1
},
{
"fieldname": "short_intro",
"fieldtype": "Data",
"label": "Short Intro"
},
{
"fieldname": "bio",
"fieldtype": "Markdown Editor",
"label": "Bio"
},
{
"fieldname": "photo",
"fieldtype": "Attach Image",
"label": "Photo"
},
{
"fieldname": "route",
"fieldtype": "Data",
"label": "Route"
}
],
"has_web_view": 1,
"index_web_pages_for_search": 1,
"is_published_field": "enabled",
"links": [],
"modified": "2021-04-06 11:50:41.551665",
"modified_by": "Administrator",
"module": "Community",
"name": "Community Course Member",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"route": "community-course-member",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import re
from frappe.website.website_generator import WebsiteGenerator
from frappe import _
class CommunityCourseMember(WebsiteGenerator):
def get_context(self, context):
context.abbr = ("").join([ s[0] for s in self.full_name.split() ])
return context
def validate(self):
self.validate_user_name()
if not self.route:
self.route = self.user_name
def validate_user_name(self):
if len(self.user_name) < 4:
frappe.throw(_("Username must be atleast 4 characters long."))
if not re.match("^[A-Za-z0-9_]*$", self.user_name):
frappe.throw(_("Username can only contain alphabets, numbers, and underscore."))
self.user_name = self.user_name.lower()
def after_insert(self):
if frappe.db.exists("User", self.email):
user = frappe.get_doc("User", self.email)
else:
user, update_password_link = self.create_user()
self.send_email(update_password_link)
def send_email(self, update_password_link):
args = {
'update_password_link': update_password_link,
'full_name': self.full_name,
}
frappe.sendmail(
recipients=self.email,
sender="Administrator",
subject=_("Set your Password"),
template="community_course_membership",
reference_doctype=self.doctype,
reference_name=self.name,
send_priority=0,
queue_separately=True,
args=args)

View File

@@ -1,25 +0,0 @@
{% extends "templates/web.html" %}
{% block page_content %}
<div class="py-20 row">
{% if photo %}
<div class="col-sm-2 border border-dark">
<img src="{{ photo }}" alt="{{ full_name }}">
</div>
{% else %}
<div class="col-sm-2">
<div class="standard-image" style="font-size: 30px;">{{ abbr }}</div>
</div>
{% endif %}
<div class="col">
<h1>{{ full_name }}</h1>
{% if short_intro %}
<p class="lead"> {{ short_intro }} </p>
{% endif %}
{% if bio %}
<p class="markdown-style"> {{ frappe.utils.md_to_html(bio) }} </p>
{% endif %}
</div>
</div>
{% endblock %}
<!-- this is a sample default web page template -->

View File

@@ -1,4 +0,0 @@
<div>
<a href="{{ doc.route }}">{{ doc.full_name }}</a>
</div>
<!-- this is a sample default list template -->

View File

@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestCommunityCourseMember(unittest.TestCase):
pass

View File

@@ -13,13 +13,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Member", "label": "Member",
"options": "Community Member" "options": "User"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-15 12:03:31.153575", "modified": "2021-05-21 12:15:51.286478",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Community", "module": "Community",
"name": "Community Event Volunteer", "name": "Community Event Volunteer",

View File

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

View File

@@ -1,155 +0,0 @@
{
"actions": [],
"allow_guest_to_view": 1,
"allow_rename": 1,
"creation": "2021-02-12 15:47:23.591567",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"full_name",
"email",
"enabled",
"column_break_4",
"username",
"email_preference",
"section_break_7",
"bio",
"section_break_9",
"role",
"photo",
"column_break_12",
"short_intro",
"route",
"abbr"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Full Name",
"reqd": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "role",
"fieldtype": "Select",
"label": "Role",
"options": "\nBoard\nDirector\nVolunteer\nSpeaker"
},
{
"allow_in_quick_entry": 1,
"fieldname": "photo",
"fieldtype": "Attach Image",
"label": "Photo"
},
{
"fieldname": "short_intro",
"fieldtype": "Data",
"label": "Short Intro"
},
{
"allow_in_quick_entry": 1,
"fieldname": "bio",
"fieldtype": "Markdown Editor",
"label": "Bio"
},
{
"fieldname": "route",
"fieldtype": "Data",
"label": "Route"
},
{
"fieldname": "email",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Email",
"options": "Email",
"reqd": 1,
"unique": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "username",
"fieldtype": "Data",
"in_list_view": 1,
"label": "User Name",
"unique": 1
},
{
"fieldname": "email_preference",
"fieldtype": "Select",
"label": "Email preference",
"options": "Email on every Message\nOne Digest Mail per day\nNever"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "abbr",
"fieldtype": "Data",
"label": "Abbr",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-28 11:22:35.402217",
"modified_by": "Administrator",
"module": "Community",
"name": "Community Member",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"search_fields": "full_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
}

View File

@@ -1,104 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import re
from frappe import _
from frappe.model.document import Document
import random
from frappe.utils import cint
import hashlib
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)
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_type': 'Mentor'
})
def get_photo_url(self):
return frappe.db.get_value("User", self.email, ["user_image"])
def get_palette(self):
palette = [
['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'],
['--blue-avatar-bg', '--blue-avatar-color'],
['--green-avatar-bg', '--green-avatar-color'],
['--dark-green-avatar-bg', '--dark-green-avatar-color'],
['--red-avatar-bg', '--red-avatar-color'],
['--yellow-avatar-bg', '--yellow-avatar-color'],
['--purple-avatar-bg', '--purple-avatar-color'],
['--gray-avatar-bg', '--gray-avatar-color0']
]
encoded_name = str(self.full_name).encode("utf-8")
hash_name = hashlib.md5(encoded_name).hexdigest()
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
return palette[idx % 8]
def __repr__(self):
return f"<CommunityMember: {self.email}>"
def create_member_from_user(doc, method):
username = doc.username
if ( doc.username and username_exists(doc.username)) or not doc.username:
username = create_username_from_email(doc.email)
elif len(doc.username) < 4 and doc.send_welcome_email == 1:
username = adjust_username(doc.username)
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)
def username_exists(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())
def adjust_username(username):
return username.ljust(4, str(random.randint(0,9)))

View File

@@ -1,25 +0,0 @@
{% extends "templates/web.html" %}
{% block page_content %}
<div class="py-20 row">
{% if photo %}
<div class="col-sm-2 border border-dark">
<img src="{{ photo }}" alt="{{ full_name }}">
</div>
{% else %}
<div class="col-sm-2">
<div class="standard-image" style="font-size: 30px;">{{ abbr }}</div>
</div>
{% endif %}
<div class="col">
<h1>{{ full_name }}</h1>
{% if short_intro %}
<p class="lead"> {{ short_intro }} </p>
{% endif %}
{% if bio %}
<p class="markdown-style"> {{ frappe.utils.md_to_html(bio) }} </p>
{% endif %}
</div>
</div>
{% endblock %}
<!-- this is a sample default web page template -->

View File

@@ -1,4 +0,0 @@
<div>
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
</div>
<!-- this is a sample default list template -->

View File

@@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
from __future__ import unicode_literals
from community.lms.doctype.lms_course.test_lms_course import new_user
import frappe
import unittest
class TestCommunityMember(unittest.TestCase):
@classmethod
def setUpClass(self):
users = ["test_user@example.com","test_user1@example.com"]
for user in users:
if not frappe.db.exists("User", user):
new_user("Test User", user)
def test_member_created_from_user(self):
user = frappe.db.get_value("User","test_user@example.com", ["full_name", "email", "username"], as_dict=True)
self.assertTrue(frappe.db.exists("Community Member", {"username":user.username}))
member = frappe.db.get_value("Community Member",
filters={"email": user.email},
fieldname=["full_name", "email", "owner", "username", "route"],
as_dict=True
)
self.assertEqual(user.full_name, member.full_name)
self.assertEqual(member.owner, user.email)
self.assertEqual(user.username, member.username)
self.assertEqual(member.username, member.route)
def test_members_with_same_name(self):
user1 = frappe.db.get_value("User","test_user@example.com", ["email"], as_dict=True)
user2 = frappe.get_doc("User","test_user1@example.com", ["email"], as_dict=True)
self.assertTrue(frappe.db.exists("Community Member", {"email": user1.email} ))
self.assertTrue(frappe.db.exists("Community Member", {"email": user2.email }))
member1 = frappe.db.get_value("Community Member",
filters={"email": user1.email},
fieldname=["full_name", "email", "owner", "username", "route"],
as_dict=True
)
member2 = frappe.db.get_value("Community Member",
filters={"email": user2.email},
fieldname=["full_name", "email", "owner", "username", "route"],
as_dict=True
)
self.assertEqual(member1.full_name, member2.full_name)
self.assertEqual(member1.email, user1.email)
self.assertEqual(member2.email, user2.email)
self.assertNotEqual(member1.username, member2.username)
def test_username_validations(self):
user = new_user("Tst", "tst@example.com")
self.assertTrue(frappe.db.exists("Community Member", {"email":user.email} ))
member = frappe.db.get_value("Community Member",
filters={"email": user.email},
fieldname=["username"],
as_dict=True
)
self.assertEqual(len(member.username), 4)
frappe.delete_doc("User", user.email)
def test_user_without_username(self):
user = new_user("Test User", "test_user2@example.com")
self.assertTrue(frappe.db.exists("Community Member", {"email":user.email} ))
member = frappe.db.get_value("Community Member",
filters={"email": user.email},
fieldname=["username"],
as_dict=True
)
self.assertTrue(member.username)
frappe.delete_doc("User", user.email)
@classmethod
def tearDownClass(self):
users = ["test_user@example.com","test_user1@example.com"]
for user in users:
if frappe.db.exists("User", user):
frappe.delete_doc("User", user)
if frappe.db.exists("Community Member", {"email": user}):
frappe.delete_doc("Community Member", {"email": user})

View File

@@ -0,0 +1,38 @@
"""
The profile_tab module provides a pluggable way to add tabs to user
profiles.
This is achieved by specifying the profile_tabs in the hooks.
profile_tabs = [
'myapp.myapp.profile_tabs.SketchesTab'
]
When a profile page is rendered, these classes specified in the
profile_hooks are instanciated with the user as argument and used to
render the tabs.
"""
class ProfileTab:
"""Base class for profile tabs.
Every subclass of ProfileTab must implement two methods:
- get_title()
- render()
"""
def __init__(self, user):
self.user = user
def get_title(self):
"""Returns the title of the tab.
Every subclass must implement this.
"""
raise NotImplementedError()
def render(self):
"""Renders the contents of the tab as HTML.
Every subclass must implement this.
"""
raise NotImplementedError()

View File

@@ -1,16 +1 @@
import frappe import frappe
def create_members_from_users():
users = frappe.get_all("User", {"enabled": 1}, ["email"])
for user in users:
if not frappe.db.get_value("Community Member", {"email": user.email}, "name"):
doc = frappe.get_doc("User", {"email": user.email})
username = doc.username if doc.username and len(doc.username) > 3 else ("").join([ s for s in doc.full_name.split() ])
if not frappe.db.exists("Community Member", username):
member = frappe.new_doc("Community Member")
member.full_name = doc.full_name
member.username = username
member.email = doc.email
member.route = username
member.owner = doc.email
member.insert(ignore_permissions=True)

View File

@@ -1,5 +0,0 @@
frappe.ready(function() {
frappe.web_form.after_save = () => {
window.location.href = frappe.web_form.get_value("username")
}
})

View File

@@ -1,100 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"breadcrumbs": "",
"button_label": "Save",
"creation": "2021-03-09 17:34:03.394301",
"doc_type": "Community Member",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-03-22 12:04:22.571655",
"modified_by": "Administrator",
"module": "Community",
"name": "update-profile",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "edit-profile",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_message": "Profile updated successfully.",
"success_url": "/",
"title": "Update Profile",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Full Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "username",
"fieldtype": "Data",
"hidden": 0,
"label": "User Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "short_intro",
"fieldtype": "Data",
"hidden": 0,
"label": "Short Intro",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "bio",
"fieldtype": "Data",
"hidden": 0,
"label": "Bio",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "photo",
"fieldtype": "Attach Image",
"hidden": 0,
"label": "Photo",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,7 +0,0 @@
from __future__ import unicode_literals
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -1,14 +1,14 @@
{% set color = member.get_palette() %} {% set color = member.get_palette() %}
<a href="/{{member.username}}"> <a href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}"> <span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.get_photo_url() %} {% if member.user_image %}
<img class="avatar-frame standard-image" src="{{ member.get_photo_url() }}" title="{{ member.full_name }}"> <img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">
</img> </img>
{% else %} {% else %}
<span class="avatar-frame standard-image" title="{{ member.full_name }}" <span class="avatar-frame standard-image" title="{{ member.full_name }}"
style="background-color: var({{color[0]}}); color: var({{color[1]}});"> style="background-color: var({{color[0]}}); color: var({{color[1]}});">
{{ member.abbr }} {{ frappe.utils.get_abbr(member.full_name) }}
</span>
{% endif %}
</span> </span>
{% endif %} </a>
</a>
</span>

View File

@@ -23,7 +23,7 @@
"fieldname": "organizer", "fieldname": "organizer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Organizer", "label": "Organizer",
"options": "Community Member" "options": "User"
}, },
{ {
"fieldname": "year", "fieldname": "year",
@@ -34,7 +34,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-04-14 11:43:23.515972", "modified": "2021-05-21 12:22:26.619776",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Hackathon", "module": "Hackathon",
"name": "Community Hackathon", "name": "Community Hackathon",

View File

@@ -84,22 +84,17 @@ web_include_css = "community.bundle.css"
# --------------- # ---------------
# Override standard doctype classes # Override standard doctype classes
# override_doctype_class = { override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo" "User": "community.overrides.user.CustomUser"
# } }
# Document Events # Document Events
# --------------- # ---------------
# Hook on document methods and events # Hook on document methods and events
doc_events = { doc_events = {
"User": {
"after_insert": "community.community.doctype.community_member.community_member.create_member_from_user" }
},
"LMS Message": {
"after_insert": "community.lms.doctype.lms_message.lms_message.publish_message"
}
}
# Scheduled Tasks # Scheduled Tasks
# --------------- # ---------------
@@ -141,13 +136,15 @@ primary_rules = [
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"}, {"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/dashboard", "to_route": ""}, {"from_route": "/dashboard", "to_route": ""},
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"}, {"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/<batch>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/<batch>/learn", "to_route": "batch/learn"}, {"from_route": "/courses/<course>/<batch>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"}, {"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/<batch>/schedule", "to_route": "batch/schedule"}, {"from_route": "/courses/<course>/<batch>/schedule", "to_route": "batch/schedule"},
{"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"}, {"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"}, {"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"}, {"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"} {"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/<batch>/join", "to_route": "batch/join"}
] ]
# Any frappe default URL is blocked by profile-rules, add it here to unblock it # Any frappe default URL is blocked by profile-rules, add it here to unblock it

View File

@@ -34,3 +34,21 @@ def submit_solution(exercise, code):
return return
doc = ex.submit(code) doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation} return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist()
def save_current_lesson(batch_name, lesson_name):
"""Saves the current lesson for a student/mentor.
"""
name = frappe.get_value(
doctype="LMS Batch Membership",
filters={
"batch": batch_name,
"member": frappe.session.user
},
fieldname="name")
if not name:
return
doc = frappe.get_doc("LMS Batch Membership", name)
doc.current_lesson = lesson_name
doc.save(ignore_permissions=True)
return {"current_lesson": doc.current_lesson}

View File

@@ -10,6 +10,6 @@ class Chapter(Document):
def get_lessons(self): def get_lessons(self):
rows = frappe.db.get_all("Lesson", rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name}, filters={"chapter": self.name},
fields='*', fields='name',
order_by="index_") 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", "hints",
"tests", "tests",
"image", "image",
"lesson" "lesson",
"index_",
"index_label"
], ],
"fields": [ "fields": [
{ {
@@ -27,6 +29,7 @@
{ {
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Course", "label": "Course",
"options": "LMS Course" "options": "LMS Course"
}, },
@@ -73,13 +76,27 @@
{ {
"fieldname": "lesson", "fieldname": "lesson",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson", "label": "Lesson",
"options": "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, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-20 13:23:12.340928", "modified": "2021-06-01 05:22:15.656013",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Exercise", "name": "Exercise",
@@ -99,8 +116,8 @@
} }
], ],
"search_fields": "title", "search_fields": "title",
"sort_field": "modified", "sort_field": "index_label",
"sort_order": "DESC", "sort_order": "ASC",
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

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

View File

@@ -14,7 +14,9 @@ class TestExercise(unittest.TestCase):
course = frappe.get_doc({ course = frappe.get_doc({
"doctype": "LMS Course", "doctype": "LMS Course",
"name": "test-course", "name": "test-course",
"title": "Test Course" "title": "Test Course",
"short_introduction": "Test Course",
"description": "Test Course"
}) })
course.insert() course.insert()
e = frappe.get_doc({ e = frappe.get_doc({

View File

@@ -1,8 +1,13 @@
# Copyright (c) 2021, FOSS United and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ..lesson.lesson import update_progress
class ExerciseSubmission(Document): class ExerciseSubmission(Document):
pass
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)

View File

@@ -52,15 +52,8 @@ class TestInviteRequest(unittest.TestCase):
self.assertEqual(user.send_welcome_email, 0) self.assertEqual(user.send_welcome_email, 0)
self.assertEqual(user.user_type, "Website User") self.assertEqual(user.user_type, "Website User")
member = frappe.db.get_value("Community Member", {"email": "test_invite@example.com"})
self.assertTrue(member)
@classmethod @classmethod
def tearDownClass(self): def tearDownClass(self):
if frappe.db.exists("Community Member", {"email": "test_invite@example.com"}):
frappe.delete_doc("Community Member", {"email": "test_invite@example.com"})
if frappe.db.exists("User", "test_invite@example.com"): if frappe.db.exists("User", "test_invite@example.com"):
frappe.delete_doc("User", "test_invite@example.com") frappe.delete_doc("User", "test_invite@example.com")

View File

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

View File

@@ -11,15 +11,37 @@ class Lesson(Document):
def before_save(self): def before_save(self):
sections = SectionParser().parse(self.body or "") sections = SectionParser().parse(self.body or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)] self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
index = 1
for s in self.sections: for s in self.sections:
if s.type == "exercise": if s.type == "exercise":
e = s.get_exercise() e = s.get_exercise()
e.lesson = self.name e.lesson = self.name
e.index_ = index
e.save() e.save()
index += 1
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): def get_sections(self):
return sorted(self.get('sections'), key=lambda s: s.index) 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): def make_lms_section(self, index, section):
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections') s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
s.type = section.type s.type = section.type
@@ -43,3 +65,65 @@ class Lesson(Document):
The return value would be like 1.2, 2.1 etc. The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson. 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

@@ -6,11 +6,10 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"course", "course",
"title",
"start_date", "start_date",
"start_time", "start_time",
"column_break_3", "column_break_3",
"code", "title",
"sessions_on", "sessions_on",
"end_time", "end_time",
"section_break_5", "section_break_5",
@@ -31,13 +30,6 @@
"label": "Course", "label": "Course",
"options": "LMS Course" "options": "LMS Course"
}, },
{
"fieldname": "code",
"fieldtype": "Data",
"label": "Code",
"read_only": 1,
"unique": 1
},
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
@@ -84,7 +76,8 @@
}, },
{ {
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Batch Description"
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
@@ -92,7 +85,8 @@
}, },
{ {
"fieldname": "section_break_7", "fieldname": "section_break_7",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Batch Settings"
}, },
{ {
"fieldname": "start_date", "fieldname": "start_date",
@@ -126,7 +120,7 @@
"link_fieldname": "batch" "link_fieldname": "batch"
} }
], ],
"modified": "2021-05-06 05:46:38.469120", "modified": "2021-05-26 16:43:57.399747",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -5,7 +5,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from community.www.courses.utils import get_member_with_email
from frappe import _ from frappe import _
from community.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership from community.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
from community.query import find, find_all from community.query import find, find_all
@@ -13,8 +12,6 @@ from community.query import find, find_all
class LMSBatch(Document): class LMSBatch(Document):
def validate(self): def validate(self):
self.validate_if_mentor() self.validate_if_mentor()
if not self.code:
self.generate_code()
def validate_if_mentor(self): def validate_if_mentor(self):
course = frappe.get_doc("LMS Course", self.course) course = frappe.get_doc("LMS Course", self.course)
@@ -24,31 +21,23 @@ class LMSBatch(Document):
def after_insert(self): def after_insert(self):
create_membership(batch=self.name, member_type="Mentor") create_membership(batch=self.name, member_type="Mentor")
def generate_code(self):
short_code = frappe.db.get_value("LMS Course", self.course, "short_code")
course_batches = frappe.get_all("LMS Batch",{"course":self.course})
self.code = short_code + str(len(course_batches) + 1)
def get_mentors(self): def get_mentors(self):
memberships = frappe.get_all( memberships = frappe.get_all(
"LMS Batch Membership", "LMS Batch Membership",
{"batch": self.name, "member_type": "Mentor"}, {"batch": self.name, "member_type": "Mentor"},
["member"]) ["member"])
member_names = [m['member'] for m in memberships] member_names = [m['member'] for m in memberships]
return find_all("Community Member", name=["IN", member_names]) return find_all("User", name=["IN", member_names])
def is_member(self, email, member_type=None): def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch. """Checks if a person is part of a batch.
If member_type is specified, checks if the person is a Student/Mentor. If member_type is specified, checks if the person is a Student/Mentor.
""" """
member = find("Community Member", email=email)
if not member:
return
filters = { filters = {
"batch": self.name, "batch": self.name,
"member": member.name "member": email
} }
if member_type: if member_type:
filters['member_type'] = member_type filters['member_type'] = member_type
@@ -62,30 +51,46 @@ class LMSBatch(Document):
{"batch": self.name, "member_type": "Student"}, {"batch": self.name, "member_type": "Student"},
["member"]) ["member"])
member_names = [m['member'] for m in memberships] member_names = [m['member'] for m in memberships]
members = frappe.get_all( return find_all("User", name=["IN", member_names])
"Community Member",
{"name": ["IN", member_names]},
["email", "full_name", "username"])
return members
def get_messages(self):
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
for message in messages:
message.message = frappe.utils.md_to_html(message.message)
if message.author == frappe.session.user:
message.author_name = "You"
message.is_author = True
return messages
@frappe.whitelist() def get_membership(self, email):
def get_messages(batch): """Returns the membership document of given user.
messages = frappe.get_all("LMS Message", {"batch": batch}, ["*"], order_by="creation") """
for message in messages: name = frappe.get_value(
message.message = frappe.utils.md_to_html(message.message) doctype="LMS Batch Membership",
member_email = frappe.db.get_value("Community Member", message.author, ["email"]) filters={
if member_email == frappe.session.user: "batch": self.name,
message.author_name = "You" "member": email
message.is_author = True },
return messages fieldname="name")
return frappe.get_doc("LMS Batch Membership", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user.
"""
membership = self.get_membership(user)
return membership and membership.current_lesson
def get_learn_url(self, lesson_number):
if not lesson_number:
return
return f"/courses/{self.course}/{self.name}/learn/{lesson_number}"
@frappe.whitelist() @frappe.whitelist()
def save_message(message, batch): def save_message(message, batch):
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "LMS Message", "doctype": "LMS Message",
"batch": batch, "batch": batch,
"author": get_member_with_email(), "author": frappe.session.user,
"message": message "message": message
}) })
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)

View File

@@ -2,7 +2,13 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('LMS Batch Membership', { frappe.ui.form.on('LMS Batch Membership', {
// refresh: function(frm) { onload: function(frm) {
frm.set_query('member', function(doc) {
// } return {
filters: {
"ignore_user_type": 1,
}
};
});
},
}); });

View File

@@ -8,11 +8,12 @@
"batch", "batch",
"member", "member",
"member_name", "member_name",
"member_email", "member_username",
"column_break_3", "column_break_3",
"course", "course",
"member_type", "member_type",
"role" "role",
"current_lesson"
], ],
"fields": [ "fields": [
{ {
@@ -29,7 +30,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Member", "label": "Member",
"options": "Community Member" "options": "User"
}, },
{ {
"default": "Student", "default": "Student",
@@ -59,15 +60,6 @@
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fetch_from": "member.email",
"fieldname": "member_email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Member Email",
"read_only": 1,
"read_only_depends_on": "member.email"
},
{ {
"fetch_from": "batch.course", "fetch_from": "batch.course",
"fieldname": "course", "fieldname": "course",
@@ -75,11 +67,24 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Course",
"read_only": 1 "read_only": 1
},
{
"fieldname": "current_lesson",
"fieldtype": "Link",
"label": "Current Lesson",
"options": "Lesson"
},
{
"fetch_from": "member.username",
"fieldname": "member_username",
"fieldtype": "Data",
"label": "Memeber Username",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-24 09:32:04.128620", "modified": "2021-05-24 12:40:57.125694",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch Membership", "name": "LMS Batch Membership",

View File

@@ -14,28 +14,42 @@ class LMSBatchMembership(Document):
self.validate_membership_in_different_batch_same_course() self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self): def validate_membership_in_same_batch(self):
previous_membership = frappe.db.get_value("LMS Batch Membership", {"member": self.member, "batch": self.batch, "name": ["!=", self.name]}, ["member_type","member"], as_dict=1) previous_membership = frappe.db.get_value("LMS Batch Membership",
filters={
"member": self.member,
"batch": self.batch,
"name": ["!=", self.name]
},
fieldname=["member_type","member"],
as_dict=1)
if previous_membership: if previous_membership:
member_name = frappe.db.get_value("Community Member", self.member, "full_name") member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch)) frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch))
def validate_membership_in_different_batch_same_course(self): def validate_membership_in_different_batch_same_course(self):
course = frappe.db.get_value("LMS Batch", self.batch, "course") course = frappe.db.get_value("LMS Batch", self.batch, "course")
previous_membership = frappe.get_all("LMS Batch Membership", {"member": self.member}, ["batch", "member_type"]) previous_membership = frappe.get_all("LMS Batch Membership",
filters={
"member": self.member,
"name": ["!=", self.name]
},
fields=["batch", "member_type", "name"]
)
for membership in previous_membership: for membership in previous_membership:
batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course") batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course")
if batch_course == course and (membership.member_type == "Student" or self.member_type == "Student"): if batch_course == course and (membership.member_type == "Student" or self.member_type == "Student"):
member_name = frappe.db.get_value("Community Member", self.member, "full_name") member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch)) frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch))
@frappe.whitelist()
def create_membership(batch, member=None, member_type="Student", role="Member"): def create_membership(batch, member=None, member_type="Student", role="Member"):
if not member:
member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name")
frappe.get_doc({ frappe.get_doc({
"doctype": "LMS Batch Membership", "doctype": "LMS Batch Membership",
"batch": batch, "batch": batch,
"role": role, "role": role,
"member_type": member_type, "member_type": member_type,
"member": member "member": member or frappe.session.user
}).save(ignore_permissions=True) }).save(ignore_permissions=True)
return "OK" return "OK"

View File

@@ -3,8 +3,78 @@
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe import frappe
import unittest import unittest
class TestLMSBatchMembership(unittest.TestCase): class TestLMSBatchMembership(unittest.TestCase):
pass def setUp(self):
frappe.db.sql("DELETE FROM `tabLMS Batch Membership`")
frappe.db.sql("DELETE FROM `tabLMS Batch`")
frappe.db.sql('delete from `tabLMS Course Mentor Mapping`')
frappe.db.sql("DELETE FROM `tabLMS Course`")
frappe.db.sql("DELETE FROM `tabUser` where email like '%@test.com'")
def new_course_batch(self):
course = frappe.get_doc({
"doctype": "LMS Course",
"name": "test-course",
"title": "Test Course",
"short_code": "XX",
"short_introduction": "Test Course",
"description": "Test Course"
})
course.insert()
self.new_user("mentor@test.com", "Test Mentor")
# without this, the creating batch will fail
course.add_mentor("mentor@test.com")
frappe.session.user = "mentor@test.com"
batch = frappe.get_doc({
"doctype": "LMS Batch",
"name": "test-batch",
"title": "Test Batch",
"course": course.name
})
batch.insert(ignore_permissions=True)
frappe.session.user = "Administrator"
return course, batch
def new_user(self, email="test@test.com", full_name="Test User"):
user = frappe.get_doc({
"doctype": "User",
"name": email,
"email": email,
"first_name": full_name,
})
user.insert()
return user
def add_membership(self, batch_name, member_name, member_type="Student"):
doc = frappe.get_doc({
"doctype": "LMS Batch Membership",
"batch": batch_name,
"member": member_name,
"member_type": member_type
})
doc.insert()
return doc
def test_membership(self):
course, batch = self.new_course_batch()
member = self.new_user("test01@test.com")
membership = self.add_membership(batch.name, member.name)
assert membership.course == course.name
assert membership.member_name == member.full_name
def test_membership_change_role(self):
course, batch = self.new_course_batch()
member = self.new_user("test01@test.com")
membership = self.add_membership(batch.name, member.name)
# it should be possible to change role
membership.role = "Admin"
membership.save()

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_guest_to_view": 1,
"allow_rename": 1, "allow_rename": 1,
"creation": "2021-03-01 16:49:33.622422", "creation": "2021-03-01 16:49:33.622422",
@@ -28,7 +41,8 @@
{ {
"fieldname": "description", "fieldname": "description",
"fieldtype": "Markdown Editor", "fieldtype": "Markdown Editor",
"label": "Description" "label": "Description",
"reqd": 1
}, },
{ {
"default": "0", "default": "0",
@@ -57,7 +71,8 @@
{ {
"fieldname": "short_introduction", "fieldname": "short_introduction",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Short Introduction" "label": "Short Introduction",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -84,7 +99,7 @@
"link_fieldname": "course" "link_fieldname": "course"
} }
], ],
"modified": "2021-05-06 13:37:03.318829", "modified": "2021-06-01 04:36:45.696776",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -5,6 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json
from ...utils import slugify from ...utils import slugify
from community.query import find, find_all from community.query import find, find_all
@@ -41,21 +42,9 @@ class LMSCourse(Document):
if not email or email == "Guest": if not email or email == "Guest":
return False return False
member = self.get_community_member(email) mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
if not member:
return False
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": member})
return mapping != [] return mapping != []
def get_community_member(self, email):
"""Returns the name of Community Member document for a give user.
"""
try:
return frappe.db.get_value("Community Member", {"email": email}, "name")
except frappe.DoesNotExistError:
return None
def add_mentor(self, email): def add_mentor(self, email):
"""Adds a new mentor to the course. """Adds a new mentor to the course.
""" """
@@ -68,14 +57,10 @@ class LMSCourse(Document):
if self.has_mentor(email): if self.has_mentor(email):
return return
member = self.get_community_member(email)
if not member:
return False
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "LMS Course Mentor Mapping", "doctype": "LMS Course Mentor Mapping",
"course": self.name, "course": self.name,
"mentor": member "mentor": email
}) })
doc.insert() doc.insert()
@@ -85,7 +70,7 @@ class LMSCourse(Document):
course_mentors = [] course_mentors = []
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"]) mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors: for mentor in mentors:
member = frappe.get_doc("Community Member", mentor.mentor) member = frappe.get_doc("User", mentor.mentor)
# TODO: change this to count query # 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.name, "member_type": "Mentor"}))
course_mentors.append(member) course_mentors.append(member)
@@ -96,11 +81,10 @@ class LMSCourse(Document):
""" """
if not email: if not email:
return False return False
member = self.get_community_member(email)
return frappe.db.exists({ return frappe.db.exists({
"doctype": "LMS Course Mentor Mapping", "doctype": "LMS Course Mentor Mapping",
"course": self.name, "course": self.name,
"mentor": member "mentor": email
}) })
def get_student_batch(self, email): def get_student_batch(self, email):
@@ -116,14 +100,13 @@ class LMSCourse(Document):
filters={ filters={
"course": self.name, "course": self.name,
"member_type": "Student", "member_type": "Student",
"member_email": email "member": email
}, },
fieldname="batch") fieldname="batch")
return batch_name and frappe.get_doc("LMS Batch", batch_name) return batch_name and frappe.get_doc("LMS Batch", batch_name)
def get_instructor(self): def get_instructor(self):
member_name = self.get_community_member(self.owner) return frappe.get_doc("User", self.owner)
return frappe.get_doc("Community Member", member_name)
def get_chapters(self): def get_chapters(self):
"""Returns all chapters of this course. """Returns all chapters of this course.
@@ -138,10 +121,9 @@ class LMSCourse(Document):
batches = find_all("LMS Batch", course=self.name) batches = find_all("LMS Batch", course=self.name)
if mentor: if mentor:
# TODO: optimize this # TODO: optimize this
member = self.get_community_member(email=mentor)
memberships = frappe.db.get_all( memberships = frappe.db.get_all(
"LMS Batch Membership", "LMS Batch Membership",
{"member": member}, {"member": mentor},
["batch"]) ["batch"])
batch_names = {m.batch for m in memberships} batch_names = {m.batch for m in memberships}
return [b for b in batches if b.name in batch_names] return [b for b in batches if b.name in batch_names]
@@ -169,6 +151,42 @@ class LMSCourse(Document):
"name") "name")
return lesson_name and frappe.get_doc("Lesson", lesson_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()
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): def get_outline(self):
return CourseOutline(self) return CourseOutline(self)
@@ -199,7 +217,8 @@ class CourseOutline:
def get_chapters(self): def get_chapters(self):
return frappe.db.get_all("Chapter", return frappe.db.get_all("Chapter",
filters={"course": self.course.name}, filters={"course": self.course.name},
fields=["name", "title", "index_"]) fields=["name", "title", "index_"],
order_by="index_")
def get_lessons(self): def get_lessons(self):
chapters = [c['name'] for c in self.chapters] chapters = [c['name'] for c in self.chapters]
@@ -211,3 +230,17 @@ class CourseOutline:
for lesson in lessons: for lesson in lessons:
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']) lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
return lessons 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

@@ -11,12 +11,13 @@ class TestLMSCourse(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.db.sql('delete from `tabLMS Course Mentor Mapping`') frappe.db.sql('delete from `tabLMS Course Mentor Mapping`')
frappe.db.sql('delete from `tabLMS Course`') frappe.db.sql('delete from `tabLMS Course`')
frappe.db.sql('delete from `tabCommunity Member`')
def new_course(self, title): def new_course(self, title):
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "LMS Course", "doctype": "LMS Course",
"title": title "title": title,
"short_introduction": title,
"description": title
}) })
doc.insert() doc.insert()
return doc return doc

View File

@@ -23,7 +23,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Mentor", "label": "Mentor",
"options": "Community Member" "options": "User"
}, },
{ {
"fetch_from": "mentor.full_name", "fetch_from": "mentor.full_name",
@@ -36,7 +36,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-04-20 12:46:48.460934", "modified": "2021-05-21 11:48:43.340315",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course Mentor Mapping", "name": "LMS Course Mentor Mapping",

View File

@@ -0,0 +1,8 @@
// 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

@@ -0,0 +1,78 @@
{
"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

@@ -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 LMSCourseProgress(Document):
pass

View File

@@ -0,0 +1,8 @@
# 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 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Member", "label": "Member",
"options": "Community Member" "options": "User"
}, },
{ {
"fieldname": "course", "fieldname": "course",
@@ -52,7 +52,7 @@
"fieldname": "reviewed_by", "fieldname": "reviewed_by",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Reviewed By", "label": "Reviewed By",
"options": "Community Member" "options": "User"
}, },
{ {
"fieldname": "comments", "fieldname": "comments",
@@ -62,7 +62,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-04-19 09:27:03.814016", "modified": "2021-05-21 11:49:12.543502",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Mentor Request", "name": "LMS Mentor Request",

View File

@@ -25,7 +25,7 @@ class LMSMentorRequest(Document):
}) })
mapping.save() mapping.save()
def send_creation_email(self, member): def send_creation_email(self):
email_template = self.get_email_template('mentor_request_creation') email_template = self.get_email_template('mentor_request_creation')
if not email_template: if not email_template:
return return
@@ -33,7 +33,7 @@ class LMSMentorRequest(Document):
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "slug", "title"], as_dict=True) course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "slug", "title"], as_dict=True)
message = frappe.render_template(email_template.response, message = frappe.render_template(email_template.response,
{ {
'member_name': member.full_name, 'member_name': frappe.db.get_value("User", frappe.session.user, "full_name"),
'course_url': '/courses/' + course_details.slug, 'course_url': '/courses/' + course_details.slug,
'course': course_details.title 'course': course_details.title
}) })
@@ -59,12 +59,10 @@ class LMSMentorRequest(Document):
'course': course_details.title 'course': course_details.title
}) })
member_email = frappe.db.get_value("Community Member", self.member, "email")
if self.status == 'Approved' or self.status == 'Rejected': if self.status == 'Approved' or self.status == 'Rejected':
reviewed_by = frappe.db.get_value('Community Member', self.reviewed_by, 'email')
email_args = { email_args = {
"recipients": member_email, "recipients": self.member,
"cc": [course_details.owner, reviewed_by], "cc": [course_details.owner, self.reviewed_by],
"subject": email_template.subject, "subject": email_template.subject,
"header": email_template.subject, "header": email_template.subject,
"message": message "message": message
@@ -73,7 +71,7 @@ class LMSMentorRequest(Document):
elif self.status == 'Withdrawn': elif self.status == 'Withdrawn':
email_args = { email_args = {
"recipients": [member_email, course_details.owner], "recipients": [self.member, course_details.owner],
"subject": email_template.subject, "subject": email_template.subject,
"header": email_template.subject, "header": email_template.subject,
"message": message "message": message
@@ -89,7 +87,7 @@ class LMSMentorRequest(Document):
def has_requested(course): def has_requested(course):
return frappe.db.count('LMS Mentor Request', return frappe.db.count('LMS Mentor Request',
filters = { filters = {
'member': get_member().name, 'member': frappe.session.user,
'course': course, 'course': course,
'status': ['in', ('Pending', 'Approved')] 'status': ['in', ('Pending', 'Approved')]
} }
@@ -98,15 +96,14 @@ def has_requested(course):
@frappe.whitelist() @frappe.whitelist()
def create_request(course): def create_request(course):
if not has_requested(course): if not has_requested(course):
member = get_member()
request = frappe.get_doc({ request = frappe.get_doc({
'doctype': 'LMS Mentor Request', 'doctype': 'LMS Mentor Request',
'member': member.name, 'member': frappe.session.user,
'course': course, 'course': course,
'status': 'Pending' 'status': 'Pending'
}) })
request.save(ignore_permissions=True) request.save(ignore_permissions=True)
request.send_creation_email(member) request.send_creation_email()
return 'OK' return 'OK'
else: else:
@@ -114,13 +111,12 @@ def create_request(course):
@frappe.whitelist() @frappe.whitelist()
def cancel_request(course): def cancel_request(course):
request = frappe.get_doc('LMS Mentor Request', {'member': get_member().name, 'course': course, 'status': ['in', ('Pending', 'Approved')]}) request = frappe.get_doc('LMS Mentor Request',{
'member': frappe.session.user,
'course': course,
'status': ['in', ('Pending', 'Approved')]
}
)
request.status = 'Withdrawn' request.status = 'Withdrawn'
request.save(ignore_permissions=True) request.save(ignore_permissions=True)
return 'OK' return 'OK'
def get_member():
try:
return frappe.get_doc('Community Member', {'email': frappe.session.user})
except frappe.DoesNotExistError:
return

View File

@@ -26,7 +26,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Author", "label": "Author",
"options": "Community Member" "options": "User"
}, },
{ {
"fieldname": "message", "fieldname": "message",
@@ -58,7 +58,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-04-28 10:17:57.618127", "modified": "2021-05-21 11:49:34.911479",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Message", "name": "LMS Message",
@@ -82,4 +82,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "author", "title_field": "author",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -7,98 +7,104 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import add_days, nowdate from frappe.utils import add_days, nowdate
from community.www.courses.utils import get_batch_members
class LMSMessage(Document): class LMSMessage(Document):
""" def after_insert(self): def after_insert(self):
self.send_email() """ self.publish_message()
#Todo: Adding email preference field for users
#self.send_email()
def publish_message(self):
template = self.get_message_template()
message = frappe._dict({
"author_name": self.author_name,
"message_time": frappe.utils.format_datetime(self.creation, "dd-mm-yyyy HH:mm"),
"message": frappe.utils.md_to_html(self.message)
})
js = """
$(".msger-input").val("");
var template = `{0}`;
var message = {1};
var session_user = ("{2}" == frappe.session.user) ? true : false;
message.author_name = session_user ? "You" : message.author_name
message.is_author = session_user;
template = frappe.render_template(template, {{
"message": message
}})
$(".messages").append(template);
var message_element = document.getElementsByClassName("messages")[0]
message_element.scrollTo(0, message_element.scrollHeight);
""".format(template, message, self.owner)
frappe.publish_realtime(event="eval_js", message=js, after_commit=True)
def get_message_template(self):
return """
<li class="{% if message.is_author %} ours {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<small class="">
{{ message.message_time }}
</small>
</div>
<div class="message-para">
{{ message.message }}
</div>
</li>
"""
def send_email(self):
membership = frappe.get_all("LMS Batch Membership", {"batch": self.batch}, ["member"])
for entry in membership:
member = frappe.get_doc("User", entry.member)
if member.name != self.author:
#Todo: wrap sendmail in frappe.enqueue, else messages takes long to display.
frappe.sendmail(
recipients = member.email,
subject = _("New Message on ") + self.batch,
header = _("New Message on ") + self.batch,
template = "lms_message",
args = {
"author": self.author,
"message": frappe.utils.md_to_html(self.message),
"creation": frappe.utils.format_datetime(self.creation, "medium"),
"course": frappe.db.get_value("LMS Batch", self.batch, ["course"])
}
)
def send_email(self):
membership = frappe.get_all("LMS Batch Membership", {"batch": self.batch}, ["member"])
for entry in membership:
member = frappe.get_doc("Community Member", entry.member)
if member.name != self.author and member.email_preference == "Email on every Message":
frappe.sendmail(
recipients = member.email,
subject = _("New Message on ") + self.batch,
header = _("New Message on ") + self.batch,
template = "lms_message",
args = {
"author": self.author,
"message": frappe.utils.md_to_html(self.message),
"creation": frappe.utils.format_datetime(self.creation, "medium"),
"course": frappe.db.get_value("LMS Batch", self.batch, ["course"])
}
)
def send_daily_digest(): def send_daily_digest():
emails = frappe._dict() #Todo: Optimize this
messages = frappe.get_all("LMS Message", {"creation": [">=", add_days(nowdate(), -1)]}, ["message", "batch", "author", "creation"]) emails = frappe._dict()
for message in messages: messages = frappe.get_all("LMS Message", {"creation": [">=", add_days(nowdate(), -1)]}, ["message", "batch", "author", "creation"])
membership = frappe.get_all("LMS Batch Membership", {"batch": message.batch}, ["member"]) for message in messages:
for entry in membership: membership = frappe.get_all("LMS Batch Membership", {"batch": message.batch}, ["member"])
member = frappe.db.get_value("Community Member", entry.member, ["name", "email_preference", "email"], as_dict=1) for entry in membership:
if member.name != message.author and member.email_preference == "One Digest Mail per day": member = frappe.db.get_value("User", entry.member, ["name", "email"], as_dict=1)
if member.name in emails.keys(): if member.name != message.author:
emails[member.name]["messages"].append(message) if member.name in emails.keys():
else: emails[member.name]["messages"].append(message)
emails[member.name] = frappe._dict({ else:
"email": member.email, emails[member.name] = frappe._dict({
"messages": [message] "email": member.email,
}) "messages": [message]
for email in emails: })
group_by_batch = frappe._dict() for email in emails:
for message in emails[email]["messages"]: group_by_batch = frappe._dict()
if message.batch in group_by_batch.keys(): for message in emails[email]["messages"]:
group_by_batch[message.batch].append(message) if message.batch in group_by_batch.keys():
else: group_by_batch[message.batch].append(message)
group_by_batch[message.batch] = [message] else:
frappe.sendmail( group_by_batch[message.batch] = [message]
recipients = frappe.db.get_value("Community Member", email, "email"), frappe.sendmail(
subject = _("Message Digest"), recipients = frappe.db.get_value("User", email, "email"),
header = _("Message Digest"), subject = _("Message Digest"),
template = "lms_daily_digest", header = _("Message Digest"),
args = { template = "lms_daily_digest",
"batches": group_by_batch args = {
}, "batches": group_by_batch
delayed = False },
) delayed = False
)
def publish_message(doc, method):
email = frappe.db.get_value("Community Member", doc.author, "email")
template = get_message_template()
message = frappe._dict()
message.author_name = doc.author_name
message.message_time = frappe.utils.pretty_date(doc.creation)
message.message = frappe.utils.md_to_html(doc.message)
js = """
$(".msger-input").val("");
var template = `{0}`;
var message = {1};
var session_user = ("{2}" == frappe.session.user) ? true : false;
message.author_name = session_user ? "You" : message.author_name
message.is_author = session_user;
template = frappe.render_template(template, {{
"message": message
}})
$(".message-section").append(template);
""".format(template, message, email)
frappe.publish_realtime(event="eval_js", message=js, after_commit=True)
def get_message_template():
return """
<div class="discussion {% if message.is_author %} is-author {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<div class="text-muted">
{{ message.message_time }}
</div>
</div>
<div class="mt-5">
{{ message.message }}
</div>
</div>
"""

View File

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

View File

@@ -0,0 +1,4 @@
<div class="p-5 batch-header">
<h3>{{batch_name}}</h3>
<div class="text-muted">{{member_count}} members</div>
</div>

View File

@@ -0,0 +1,39 @@
<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 %}
</div>
<ul class="nav nav-tabs mt-4">
<li class="nav-item">
<a class="nav-link" id="home" href="/courses/{{course.name}}/{{batch.name}}/home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" id="learn" href="/courses/{{course.name}}/{{batch.name}}/learn">Learn</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/{{batch.name}}/schedule">Schedule</a>
</li> -->
<li class="nav-item">
<a class="nav-link" id="members" href="/courses/{{course.name}}/{{batch.name}}/members">Members</a>
</li>
<li class="nav-item">
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/{{batch.name}}/discuss">Discussion</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" id="about" href="/courses/{{course.name}}/{{batch.name}}/about">About</a>
</li> -->
{% if batch.is_member(frappe.session.user, member_type="Mentor") %}
<li class="nav-item">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/{{batch.name}}/progress">Progress</a>
</li>
{% endif %}
</ul>
<script>
frappe.ready(() => {
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}"]`)
if (selector) {
selector.classList.add('active');
}
else {
$("#learn").addClass('active')
}
})
</script>

View File

@@ -1,14 +1,17 @@
<div class="chapter-teaser"> <div class="chapter-teaser">
<div class="teaser-body"> <div class="teaser-body">
<h3 class="chapter-title"><span class="chapter-number">{{index}}</span> {{ chapter.title }}</h3> <h3 class="chapter-title"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</h3>
<div class="chapter-description"> <div class="chapter-description">
{{ chapter.description or "" }} {{ chapter.description or "" }}
</div> </div>
<div class="chapter-lessons"> <div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %} {% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser"> <div class="lesson-teaser">
{{lesson.title}} <a {% if show_link %} class="anchor_style" href="{{ batch.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% endif %}>{{ lesson.title }}</a>
</div> {% 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 %} {% endfor %}
</div> </div>
</div> </div>

View File

@@ -0,0 +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)}}
{% endfor %}

View File

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

View File

@@ -0,0 +1,5 @@
<h3>Instructor</h3>
<div class="instructor">
<div class="instructor-title">{{instructor.full_name}}</div>
<div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div>
</div>

View File

@@ -0,0 +1,29 @@
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
{% if can_manage or can_join %}
<div class="cta">
<div class="">
{% if can_manage %}
<a href="/courses/{{course.name}}/{{batch.name}}/home" class="btn btn-primary">Manage</a>
{% elif can_join %}
<button class="join-batch btn btn-primary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,42 @@
import frappe
from frappe.core.doctype.user.user import User
from frappe.utils import cint
import hashlib
class CustomUser(User):
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_palette(self):
palette = [
['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'],
['--blue-avatar-bg', '--blue-avatar-color'],
['--green-avatar-bg', '--green-avatar-color'],
['--dark-green-avatar-bg', '--dark-green-avatar-color'],
['--red-avatar-bg', '--red-avatar-color'],
['--yellow-avatar-bg', '--yellow-avatar-color'],
['--purple-avatar-bg', '--purple-avatar-color'],
['--gray-avatar-bg', '--gray-avatar-color0']
]
encoded_name = str(self.full_name).encode("utf-8")
hash_name = hashlib.md5(encoded_name).hexdigest()
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
return palette[idx % 8]
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_type': 'Mentor'
})

View File

@@ -2,3 +2,7 @@ community.patches.set_email_preferences
community.patches.change_name_for_community_members community.patches.change_name_for_community_members
community.patches.save_abbr_for_community_members community.patches.save_abbr_for_community_members
community.patches.create_mentor_request_email_templates community.patches.create_mentor_request_email_templates
community.patches.replace_member_with_user_in_batch_membership
community.patches.replace_member_with_user_in_course_mentor_mapping
community.patches.replace_member_with_user_in_lms_message
community.patches.replace_member_with_user_in_mentor_request

View File

@@ -0,0 +1,9 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_batch_membership")
memberships = frappe.get_all("LMS Batch Membership", ["member", "name"])
for membership in memberships:
email = frappe.db.get_value("Community Member", membership.member, "email")
frappe.db.set_value("LMS Batch Membership", membership.name, "member", email)

View File

@@ -0,0 +1,9 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_course_mentor_mapping")
mappings = frappe.get_all("LMS Course Mentor Mapping", ["mentor", "name"])
for mapping in mappings:
email = frappe.db.get_value("Community Member", mapping.mentor, "email")
frappe.db.set_value("LMS Course Mentor Mapping", mapping.name, "mentor", email)

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_message")
messages = frappe.get_all("LMS Message", ["author", "name"])
for message in messages:
user = frappe.db.get_value("Community Member", message.author, ["email", "full_name"], as_dict=True)
frappe.db.set_value("LMS Message", message.name, "author", user.email)
frappe.db.set_value("LMS Message", message.name, "author_name", user.full_name)

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_mentor_request")
requests = frappe.get_all("LMS Mentor Request", ["member", "name"])
for request in requests:
user = frappe.db.get_value("Community Member", request.member, ["email", "full_name"], as_dict=True)
frappe.db.set_value("LMS Mentor Request", request.name, "member", user.email)
frappe.db.set_value("LMS Mentor Request", request.name, "member_name", user.full_name)

View File

@@ -97,14 +97,6 @@ body {
border-top: 1px solid #ddc; border-top: 1px solid #ddc;
} }
.batch .cta button {
background: var(--cta-color);
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
}
.batch .right { .batch .right {
float: right; float: right;
} }
@@ -127,44 +119,13 @@ img.profile-photo {
max-width: 100% max-width: 100%
} }
/* override style of base */
.message { .message {
border: 1px dashed var(--text-color); border: 1px dashed var(--text-color);
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
} }
.dashboard__profile {
width: 150px;
height: 155px;
border-radius: 50%;
}
.dashboard__profileSmall {
width: 59px;
height: 57px;
border-radius: 50%;
}
.dashboard__abbr {
font-size: 50px;
width: 155px;
height: 155px;
border-radius: 50%;
}
.dashboard__abbrSmall {
font-size: 20px;
width: 59px;
height: 57px;
border-radius: 50%;
}
.msger-inputarea { .msger-inputarea {
position: absolute;
bottom: 0;
width: 100%; width: 100%;
display: flex; display: flex;
padding: 10px; padding: 10px;
@@ -182,37 +143,6 @@ img.profile-photo {
flex: 1; flex: 1;
background: #ddd; background: #ddd;
} }
.msger-send-btn {
margin-left: 10px;
background: var(--cta-color);
color: #fff;
font-weight: bold;
cursor: pointer;
transition: background 0.23s;
}
.discussion {
border: 1px solid var(--text-color);
padding: 10px;
margin: 10px;
border-radius: 10px;
background: var(--received-message);
width: 50%;
display: flex;
flex-direction: column;
}
.is-author {
float: right;
background: var(--send-message);
}
.batch-header {
position: fixed;
top: 0;
background: var(--bg);
width: 100%;
}
.message-section { .message-section {
margin-left: 3%; margin-left: 3%;
@@ -225,3 +155,119 @@ img.profile-photo {
font-weight: 600; font-weight: 600;
line-height: 51px; line-height: 51px;
} }
.anchor_style {
color: inherit;
}
a:hover {
text-decoration: none;
color: inherit;
}
.anchor_style:hover {
text-decoration: underline
}
section {
padding: 5rem 0 5rem 0;
}
.messages-container {
margin: 0 auto;
border: 1px solid black;
}
.messages {
overflow: auto;
height: 450px;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 8px;
list-style-type: none;
}
.messages li {
background: #F7F5F5;
border-radius: 8px;
padding: 8px;
margin: 2px 8px 2px 0;
width: 40%;
}
.messages li.ours {
align-self: flex-end;
margin: 2px 0 2px 8px;
background: var(--primary-color);
color: #fff
}
.message-para {
font-size: 20px;
}
.batch-header {
background: #eee;
border: 2px solid #ddd;
}
.page-card {
max-width: 360px;
padding: 15px;
margin: 70px auto;
border: 1px solid #d1d8dd;
border-radius: 4px;
background-color: #fff;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.page-card .page-card-head {
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid #d1d8dd;
}
.page-card .page-card-head .indicator {
color: #36414C;
font-size: 14px;
}
.page-card .page-card-head .indicator::before {
margin: 0 6px 0.5px 0px;
}
.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

@@ -179,11 +179,8 @@ section.lightgray {
.chapter-number { .chapter-number {
background: var(--text-color); background: var(--text-color);
color: white; color: white;
border-radius: 50%;
height: 24px; height: 24px;
min-width: 24px; min-width: 24px;
align-items: center;
padding: 5px 8px 2px 8px;
margin-right: 5px; margin-right: 5px;
} }
@@ -257,6 +254,7 @@ section.lightgray {
background-color: rgba(255, 255, 255, 0); background-color: rgba(255, 255, 255, 0);
max-height: 300px; max-height: 300px;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word;
margin: 0px; margin: 0px;
margin-left: 20px; margin-left: 20px;
padding: 4px; padding: 4px;
@@ -287,9 +285,7 @@ section.lightgray {
} }
.lesson-teaser { .lesson-teaser {
font-weight: bold; line-height: 40px;
color: black;
padding-left: 20px;
} }
#hero h1 { #hero h1 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

View File

@@ -0,0 +1,13 @@
<div class="discussion {% if message.is_author %} is-author {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<div class="text-muted">
{{ message.message_time }}
</div>
</div>
<div class="mt-5">
{{ message.message }}
</div>
</div>

View File

@@ -1,7 +1,4 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/common_macro.html" import InstructorsSection, MentorsSection %}
{% block title %}About{% endblock %} {% block title %}About{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
@@ -11,48 +8,33 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course, batch) }}
<div class="container"> <div class="container">
{{ CourseBasicDetail(course)}} {{ widgets.BatchTabs(course=course, batch=batch) }}
{{ InstructorsSection(course.get_instructor()) }} <div class="tab-content" id="about">
{{ BatchDetails(batch)}} {{ CourseBasicDetail(course)}}
<div class="d-flex align-items-center">
<div class="col-lg-4 col-md-12">
<div class="sidebar">
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% macro CourseBasicDetail(course) %} {% macro CourseBasicDetail(course) %}
<h2>{{course.title}}</h2> <h2>{{course.title}}</h2>
<div class="course-description"> <div class="course-description">
{{course.short_introduction}} {{course.short_introduction}}
</div> </div>
{% if course.video_link %} {% if course.video_link %}
<div class="preview-video"> <div class="preview-video">
<iframe width="560" height="315" src="{{course.video_link}}" title="YouTube video player" frameborder="0" <iframe width="560" height="315" src="{{course.video_link}}" title="YouTube video player" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe> allowfullscreen></iframe>
</div> </div>
{% endif %} {% endif %}
<h2>About the Course</h2> <h2>About the Course</h2>
<div>{{frappe.utils.md_to_html(course.description)}}</div> <div>{{frappe.utils.md_to_html(course.description)}}</div>
{% endmacro %} {% endmacro %}
{% macro BatchDetails(batch) %}
<h2>About the Batch</h2>
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
</div>
{% endmacro %}

View File

@@ -3,5 +3,3 @@ from . import utils
def get_context(context): def get_context(context):
utils.get_common_context(context) utils.get_common_context(context)
print("context", context)

View File

@@ -1,6 +1,4 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/common_macro.html" import BatchHearder %}
{% block title %}Discuss{% endblock %} {% block title %}Discuss{% endblock %}
{% block head_include %} {% block head_include %}
@@ -11,16 +9,14 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course, batch) }}
<div class=""> <div class="container">
<div class="batch-header"> {{ widgets.BatchTabs(course=course, batch=batch) }}
{{ BatchHearder(course.title, member_count) }} <div class="messages-container mt-5">
</div> {{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
<div class="messages"> <ol class="messages">
<div class="message-section">
{{ Messages(messages) }} {{ Messages(messages) }}
</div> </ol>
{{ TextArea() }} {{ TextArea() }}
</div> </div>
</div> </div>
@@ -28,25 +24,25 @@
{% macro Messages(messages) %} {% macro Messages(messages) %}
{% for message in messages %} {% for message in messages %}
<div class="discussion {% if message.is_author %} is-author {% endif %}"> <li class="{% if message.is_author %} ours {% endif %}">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="font-weight-bold"> <div class="font-weight-bold">
{{ message.author_name }} {{ message.author_name }}
</div> </div>
<div class="text-muted"> <small class="">
{{ frappe.utils.pretty_date(message.creation) }} {{ frappe.utils.format_datetime(message.creation, "dd-mm-yyyy HH:mm") }}
</div> </small>
</div> </div>
<div class="mt-5"> <div class="message-para">
{{ message.message }} {{ message.message }}
</div> </div>
</div> </li>
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
{% macro TextArea() %} {% macro TextArea() %}
<form class="msger-inputarea"> <form class="msger-inputarea mb-1">
<input type="text" class="msger-input" placeholder="Write your message..."> <input type="text" class="msger-input" placeholder="Write your message...">
<button type="submit" class="msger-send-btn" data-batch="{{batch.name | urlencode }}">Send</button> <button type="submit" class="btn btn-primary msger-send-btn" data-batch="{{batch.name | urlencode }}">Send</button>
</form> </form>
{% endmacro %} {% endmacro %}

View File

@@ -11,7 +11,9 @@ frappe.ready(() => {
}) })
setTimeout(() => { setTimeout(() => {
window.scrollTo(0, document.body.scrollHeight); var message_element = document.getElementsByClassName("messages")[0]
message_element.scrollTo(0, message_element.scrollHeight);
document.getElementsByClassName("messages-container")[0].scrollIntoView({block: "center"})
}, 300); }, 300);
$(".msger-send-btn").click((e) => { $(".msger-send-btn").click((e) => {
@@ -30,4 +32,4 @@ frappe.ready(() => {
$(".msger-input").val(""); $(".msger-input").val("");
} }
}) })
}) })

View File

@@ -1,9 +1,6 @@
import frappe import frappe
from . import utils from . import utils
from community.lms.doctype.lms_batch.lms_batch import get_messages
def get_context(context): def get_context(context):
utils.get_common_context(context) utils.get_common_context(context)
context.messages = context.batch.get_messages()
context.members = utils.get_batch_members(context.batch.name)
context.member_count = len(context.members)

View File

@@ -0,0 +1,60 @@
{% extends "templates/base.html" %}
{% block title %} Batch {% endblock %}
{% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/" + batch.name + "/join" %}
<div class="container mt-5">
{{ widgets.BatchTabs(course=course, batch=batch) }}
<div>
<h1 class="mt-5">{{ batch.title }}</h1>
</div>
<div class="course-details">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
</div>
<div class="w-25">
<h2>Batch Schedule</h2>
{{ widgets.RenderBatch(course=course, batch=batch) }}
</div>
{% if batch.description %}
<h2>Batch Details</h2>
{{ frappe.utils.md_to_html(batch.description) }}
{% endif %}
{% if course.is_mentor(frappe.session.user) %}
<div class="">
<h2> Invite Members </h2>
<a href="" class="anchor_style mr-5" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation
Link</a>
<small id="copy-message" class="text-muted pull-right" style="display: none;">Copied to Clipboard.</small>
</div>
{% endif %}
</div>
<script>
frappe.ready(() => {
$("#invite-link").click((e) => {
e.preventDefault();
var link_element = $("#invite-link");
var input_element = document.createElement("input");
input_element.value = link_element.attr("data-link")
document.body.appendChild(input_element);
input_element.select();
document.execCommand("copy");
input_element.remove();
$("#copy-message").slideDown(function () {
setTimeout(function () {
$("#copy-message").slideUp();
}, 5000);
});
})
})
</script>
{% endblock %}

View File

@@ -0,0 +1,5 @@
import frappe
from . import utils
def get_context(context):
utils.get_common_context(context)

View File

@@ -0,0 +1,74 @@
% extends "templates/base.html" %}
{% block title %}Join a Course{% endblock %}
{% block head_include %}
<meta name="description" content="Join a Course"/>
<meta name="keywords" content="" />
{% endblock %}
{% block content %}
{% if frappe.session.user == "Guest" %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>Login Required</span>
</div>
<div class=''>Please log in to confirm to join the course {{ batch.course_title }}.</div>
<a type="submit" id="login" class="btn btn-primary w-100"
href="/login?redirect-to=/courses/{{ batch.course }}/{{ batch.name }}/join">{{_("Login")}}</a>
</div>
{% elif already_a_member %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>Already a member</span>
</div>
<div class=''>You are already a member of the batch {{ batch.title }} for the course {{ batch.course_title }}.
</div>
<a type="submit" id="batch-home" class="btn btn-primary w-100" href="/courses/{{batch.course}}/{{batch.name}}/home">{{_("Go to Batch Home")}}</a>
</div>
{% else %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>Confirm your membership</span>
</div>
<div>Please provide your confirmation to be a part of the batch {{ batch.title }} for the course
{{ batch.course_title }}.
</div>
<a type="submit" id="confirm" class="btn btn-primary w-100" data-batch="{{ batch.name | urlencode }}"
data-course="{{ batch.course | urlencode }}">{{_("Confirm")}}</a>
</div>
{% endif %}
<script>
frappe.ready(() => {
var confirm_element = $("#confirm");
var batch = decodeURIComponent(confirm_element.attr("data-batch"));
var course = decodeURIComponent(confirm_element.attr("data-course"));
confirm_element.click((e) => {
frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": batch
},
"callback": (data) => {
if (data.message == "OK") {
frappe.msgprint({
message: __("You are now a member of this batch!"),
clear: true
});
setTimeout(function () {
window.location.href = "/courses/" + course + "/" + batch + "/home";
}, 2000);
}
}
})
})
})
</script>
{% endblock %}

View File

@@ -0,0 +1,8 @@
import frappe
def get_context(context):
context.no_cache = 1
batch_name = frappe.form_dict["batch"]
context.batch = frappe.get_doc("LMS Batch", batch_name)
context.already_a_member = context.batch.is_member(frappe.session.user)
context.batch.course_title = frappe.db.get_value("LMS Course", context.batch.course, "title")

View File

@@ -1,17 +1,17 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %} {% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ lesson.title }}{% endblock %} {% block title %}{{ lesson.title }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{lesson.title}} - {{course.title}}" /> <meta name="description" content="{{lesson.title}} - {{course.title}}" />
<meta name="keywords" content="{{lesson.title}} - {{course.title}}" /> <meta name="keywords" content="{{lesson.title}} - {{course.title}}" />
<style> <style>
</style> </style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css"> <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css"> <link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css"> <link rel="stylesheet" href="/assets/css/lms.css">
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script> <script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script> <script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
@@ -23,13 +23,12 @@
{% block content %} {% block content %}
{{ Sidebar(course, batch) }}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }}
<div class="lesson-page"> <div class="lesson-page">
{{ pagination(prev_url, next_url) }}
<h2>{{ lesson.title }}</h2> <h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-name="{{ lesson.name }}" data-batch="{{ batch.name }}">{{ lesson.title }}</h2>
{% for s in lesson.get_sections() %} {% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}"> <div class="section section-{{ s.type }}">
@@ -37,96 +36,66 @@
</div> </div>
{% endfor %} {% endfor %}
{{ pagination(prev_url, next_url) }} {{ pagination(prev_chap, prev_url, next_chap, next_url) }}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% macro render_section(s) %} {% macro render_section(s) %}
{% if s.type == "text" %} {% if s.type == "text" %}
{{ render_section_text(s) }} {{ render_section_text(s) }}
{% elif s.type == "example" or s.type == "code" %} {% elif s.type == "example" or s.type == "code" %}
{{ LiveCodeEditor(s.name, {{ LiveCodeEditor(s.name,
code=s.get_latest_code_for_user(), code=s.get_latest_code_for_user(),
reset_code=s.contents, reset_code=s.contents,
is_exercise=False) is_exercise=False)
}} }}
{% elif s.type == "exercise" %} {% elif s.type == "exercise" %}
{{ widgets.Exercise(exercise=s.get_exercise())}} {{ widgets.Exercise(exercise=s.get_exercise())}}
{% else %} {% else %}
<div>Unknown section type: {{s.type}}</div> <div>Unknown section type: {{s.type}}</div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro render_section_text(s) %} {% macro render_section_text(s) %}
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{{ frappe.utils.md_to_html(s.contents) }} {{ frappe.utils.md_to_html(s.contents) }}
</div>
</div> </div>
</div>
{% endmacro %} {% endmacro %}
{% macro pagination(prev_url, next_url) %} {% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
<div class="lesson-pagination"> <div class="lesson-pagination">
{% if prev_url %} {% if prev_url %}
<a href="{{prev_url}}" class="btn">&larr; Prev</a> <span>
{% endif %} Prev: <a href="{{prev_url}}">{{prev_chap}}</a>
{% if next_url %} </span>
<a href="{{next_url}}" class="btn pull-right">Next &rarr;</a> {% endif %}
{% endif %} {% if next_url %}
<div style="clear: both;"></div> <span class="pull-right">
</div> Next: <a href="{{next_url}}">{{next_chap}}</a>
</span>
{% endif %}
<div style="clear: both;"></div>
</div>
{% endmacro %} {% endmacro %}
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
{{ LiveCodeEditorJS() }} {{ LiveCodeEditorJS() }}
<script type="text/javascript">
<!-- <script type="text/javascript">
$(function() { $(function() {
var editorLookup = {}; var batch_name = "{{ batch.name }}";
var lesson_name = "{{ lesson.name }}";
$(".canvas-editor").each((i, e) => { frappe.call("community.lms.api.save_current_lesson", {
var data = $(e).data(); "batch_name": batch_name,
var editor = new LiveCodeEditor(e, { "lesson_name": lesson_name
runtime: "python-canvas",
base_url: "{{ livecode_url }}",
codemirror: true,
userdata: data,
autosave: function(editor, code) {
// can't autosave when user is Guest
if (frappe.session.user == "Guest") {
return;
}
var data = editor.options.userdata;
var code = editor.codemirror.doc.getValue();
// console.log("autosaving...")
frappe.call("community.lms.api.autosave_section", {
section: data.section,
code: code
}).then((r) => {
// TODO: verify
})
}
})
editorLookup[data.section] = editor;
})
$(".canvas-editor .reset").each((i, e) => {
$(e).on("click", function(event) {
var data = $(this).parents(".canvas-editor").data();
var section = data.section;
frappe.call("community.lms.api.get_section", {
name: section
}).then(r => {
var editor = editorLookup[data.section];
editor.codemirror.doc.setValue(r.message.contents);
})
})
}) })
}) })
</script> --> </script>
{%- endblock %} {%- endblock %}

View File

@@ -0,0 +1,11 @@
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

@@ -1,3 +1,4 @@
from re import I
import frappe import frappe
from . import utils from . import utils
@@ -9,10 +10,10 @@ def get_context(context):
lesson_number = f"{chapter_index}.{lesson_index}" lesson_number = f"{chapter_index}.{lesson_index}"
course_name = context.course.name course_name = context.course.name
batch_name = context.batch.name
if not chapter_index or not lesson_index: if not chapter_index or not lesson_index:
frappe.local.flags.redirect_location = f"/courses/{course_name}/{batch_name}/learn/1.1" index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
frappe.local.flags.redirect_location = context.batch.get_learn_url(index_)
raise frappe.Redirect raise frappe.Redirect
context.lesson = context.course.get_lesson(chapter_index, lesson_index) context.lesson = context.course.get_lesson(chapter_index, lesson_index)
@@ -20,12 +21,25 @@ def get_context(context):
context.chapter_index = chapter_index context.chapter_index = chapter_index
outline = context.course.get_outline() outline = context.course.get_outline()
next_ = outline.get_next(lesson_number)
prev_ = outline.get_prev(lesson_number) prev_ = outline.get_prev(lesson_number)
context.next_url = get_learn_url(course_name, batch_name, next_) next_ = outline.get_next(lesson_number)
context.prev_url = get_learn_url(course_name, batch_name, prev_) context.prev_chap = get_chapter_title(course_name, prev_)
context.next_chap = get_chapter_title(course_name, next_)
context.next_url = context.batch.get_learn_url(next_)
context.prev_url = context.batch.get_learn_url(prev_)
def get_learn_url(course_name, batch_name, lesson_number):
def get_chapter_title(course_name, lesson_number):
if not lesson_number: if not lesson_number:
return return
return f"/courses/{course_name}/{batch_name}/learn/{lesson_number}" chapter_index = lesson_number.split(".")[0]
lesson_index = lesson_number.split(".")[1]
chapter_name = frappe.db.get_value("Chapter", {"course": course_name, "index_": chapter_index}, "name")
return frappe.db.get_value("Lesson", {"chapter": chapter_name, "index_": lesson_index}, "title")
def get_lesson_index(course, batch, user):
lesson = batch.get_current_lesson(user)
return lesson and course.get_lesson_index(lesson)

View File

@@ -1,7 +1,4 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/profile.html" import Profile %}
{% from "www/macros/common_macro.html" import BatchHearder %}
{% block title %}Members{% endblock %} {% block title %}Members{% endblock %}
{% block head_include %} {% block head_include %}
@@ -11,30 +8,40 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course, batch) }}
<div class="container"> <div class="container">
{{ BatchHearder(course.title, member_count)}} {{ widgets.BatchTabs(course=course, batch=batch) }}
{{ MembersList(members)}} {{ MembersList(members)}}
</div> </div>
{% endblock %} {% endblock %}
{% macro MembersList(members) %} {% macro MembersList(members) %}
<div class="mt-5"> <div class="mt-5">
{% for member in members %} {% for member in members %}
<div class="d-flex align-items-center"> <div class="row mb-5">
<div> <div>
{{ Profile(member.photo, member.full_name, member.abbr, "small") }} {{ widgets.Avatar(member=member, avatar_class="avatar-large") }}
</div> </div>
<div class="mr-5"> <div class="col">
{{member.full_name}} <div class="row ml-1">
</div> <a class="anchor_style" href="/{{member.username}}">
{% if member.is_mentor %} <h3>{{ member.full_name }}</h3>
<div class="badge badge-success">Mentor</div> </a>
{% endif %} {% if course.is_mentor(member.name) %}
</div> <div class="ml-2">
<hr> <div class="badge badge-success">Mentor</div>
{% endfor %} </div>
{% endif %}
</div>
{% if member.bio %}
<i>{{member.bio}}</i>
{% endif %}
</div>
</div>
{% if loop.index != member_count %}
<hr>
{% endif %}
{% endfor %}
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -3,6 +3,4 @@ from . import utils
def get_context(context): def get_context(context):
utils.get_common_context(context) utils.get_common_context(context)
print(context.members[0].bio)
context.members = utils.get_batch_members(context.batch.name)
context.member_count = len(context.members)

View File

@@ -1,13 +1,12 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %} {% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ course.title }} - Batch Dashboard{% endblock %} {% block title %}{{ course.title }} - Batch Dashboard{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{course.title}} - Batch Dashboard" /> <meta name="description" content="{{course.title}} - Batch Dashboard" />
<meta name="keywords" content="{{course.title}} - Batch Dashboard" /> <meta name="keywords" content="{{course.title}} - Batch Dashboard" />
<style> <style>
</style> </style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css"> <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css"> <link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
@@ -23,23 +22,23 @@
{% block content %} {% block content %}
{{ Sidebar(course, batch) }}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }}
<div class="mentor-dashboard"> <div class="mentor-dashboard">
<h1>Batch Progress</h1> <h1>Batch Progress</h1>
{% for exercise in report.exercises %} {% for exercise in report.exercises %}
<div class="exercise-submissions"> <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) %} {% for s in report.get_submissions_of_exercise(exercise.name) %}
<div class="submission"> <div class="submission">
<h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4> <h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4>
<div class="livecode-editor-small"> <div class="livecode-editor-small">
{{ LiveCodeEditor(name=s.name, code=s.solution, reset_code=s.solution) }} {{ LiveCodeEditor(name=s.name, code=s.solution, reset_code=s.solution) }}
</div> </div>
</div>
{% endfor %}
</div> </div>
{% endfor %}
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -47,6 +46,6 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
{{ LiveCodeEditorJS() }} {{ LiveCodeEditorJS() }}
{% endblock %} {% endblock %}

View File

@@ -25,15 +25,14 @@ class BatchReport:
self.submissions_by_exercise[s.exercise].append(s) self.submissions_by_exercise[s.exercise].append(s)
def get_exercises(self, course_name): 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): def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name] return self.submissions_by_exercise[exercise_name]
def get_submissions(batch): def get_submissions(batch):
students = batch.get_students() students = batch.get_students()
students_map = {s['email']: s for s in students} students_map = {s.email: s for s in students}
names, values = nparams("s", students_map.keys()) names, values = nparams("s", students_map.keys())
sql = """ sql = """

View File

@@ -1,6 +1,6 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% block title %}Schedule{% endblock %} {% block title %}Schedule{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
<meta name="keywords" content="" /> <meta name="keywords" content="" />
@@ -8,7 +8,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course, batch) }}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }}
<h3>
Schedule
</h3>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -13,31 +13,16 @@ def get_common_context(context):
return return
batch = course.get_batch(batch_name) batch = course.get_batch(batch_name)
if not batch: if not batch or not batch.is_member(frappe.session.user):
frappe.local.flags.redirect_location = "/courses/" + course_name frappe.local.flags.redirect_location = "/courses/" + course_name
raise frappe.Redirect raise frappe.Redirect
context.course = course context.course = course
context.batch = batch context.batch = batch
context.members = batch.get_mentors() + batch.get_students()
context.member_count = len(context.members)
context.livecode_url = get_livecode_url() context.livecode_url = get_livecode_url()
def get_livecode_url(): def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url") return frappe.db.get_single_value("LMS Settings", "livecode_url")
def get_batch_members(batch_name):
members = []
memberships = frappe.get_all("LMS Batch Membership", {"batch": batch_name}, ["member", "member_type"])
for membership in memberships:
member = get_member_with_name(membership.member)
if membership.member_type == "Mentor":
member.is_mentor = True
members.append(member)
return members
def get_member_with_name(name):
try:
return frappe.get_doc("Community Member", name)
except frappe.DoesNotExistError:
return

View File

@@ -1,5 +1,5 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/common_macro.html" import InstructorsSection, MentorsSection %} {% from "www/macros/common_macro.html" import MentorsSection %}
{% block title %}{{ course.title }}{% endblock %} {% block title %}{{ course.title }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
@@ -9,24 +9,26 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="course-header"> <div class="course-header">
<div class="course-type">course</div> <div class="mb-5">
<a class="anchor_style" href="/courses">Courses</a> / <span class="text-muted">{{ course.title }}</span>
</div>
<h1 id="course-title" data-course="{{course.name}}">{{course.title}}</h1> <h1 id="course-title" data-course="{{course.name}}">{{course.title}}</h1>
<div class="course-short-intro">{{ course.short_introduction }}</div> <div class="course-short-intro">{{ course.short_introduction }}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-8 col-md-12"> <div class="col-lg-8 col-md-12">
<div class="course-details"> <div class="course-details">
{{ CourseVideo(course) }} {{ CourseVideo(course) }}
{{ CourseDescription(course) }} {{ CourseDescription(course) }}
{{ BatchSection(course) }} {{ BatchSection(course) }}
{{ CourseOutline(course) }} {{ widgets.CourseOutline(course=course, show_link=False) }}
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-12"> <div class="col-lg-4 col-md-12">
<div class="sidebar"> <div class="sidebar">
{{ InstructorsSection(course.get_instructor()) }} {{ widgets.InstructorSection(instructor=course.get_instructor()) }}
</div> </div>
<div class="sidebar"> <div class="sidebar">
{{ MentorsSection(course.get_mentors(), course.is_mentor(frappe.session.user), course.name) }} {{ MentorsSection(course.get_mentors(), course.is_mentor(frappe.session.user), course.name) }}
@@ -37,106 +39,67 @@
{% endblock %} {% endblock %}
{% macro CourseVideo(course) %} {% macro CourseVideo(course) %}
{% if course.video_link %} {% if course.video_link %}
<div class="preview-video"> <div class="preview-video">
<iframe <iframe width="560" height="315" src="{{course.video_link}}" title="YouTube video player" frameborder="0"
width="560" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
height="315" allowfullscreen></iframe>
src="{{course.video_link}}" </div>
title="YouTube video player" {% endif %}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
{% endif %}
{% endmacro %} {% endmacro %}
{% macro CourseDescription(course) %} {% macro CourseDescription(course) %}
<h2>Course Description</h2> <h2>Course Description</h2>
<div class="course-description"> <div class="course-description">
{{ frappe.utils.md_to_html(course.description) }} {{ frappe.utils.md_to_html(course.description) }}
</div>
{% endmacro %}
{% macro BatchSection(course) %}
{% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
{% else %}
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
{% endif %}
{% endmacro %}
{% macro RenderBatch(batch, can_manage=False) %}
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
<div class="cta">
<div class="">
{% if can_manage %}
<a href="/courses/{{course.name}}/{{batch.name}}/about" class="btn btn-secondary">Manage</a>
{% else %}
<button class="join-batch" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro BatchSection(course) %}
{% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
{% else %}
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
{% endif %}
{% endmacro %}
{% macro BatchSectionForMentors(course, mentor_batches) %} {% macro BatchSectionForMentors(course, mentor_batches) %}
<h2>Your Batches</h2> <h2>Your Batches</h2>
{% if mentor_batches %} {% if mentor_batches %}
<div class="alert alert-secondary"> <!-- <div class="alert alert-secondary">
You are a mentor for this course. Manage your batches or create a new batch from here. You are a mentor for this course. Manage your batches or create a new batch from here.
</div> </div> -->
<div class="row"> <div class="row">
{% for batch in mentor_batches %} {% for batch in mentor_batches %}
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
{{ RenderBatch(batch, can_manage=True) }} {{ widgets.RenderBatch(course=course, batch=batch, can_manage=True) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<a class="btn btn-primary add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}">Add a new batch</a> <a class="add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}">Add a new
{% else %} batch</a>
<div class="mentor_message"> {% else %}
<p> You are a mentor for this course. </p> <div class="mentor_message">
<a class="btn btn-primary" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}" >Create your first batch</a> <p> You are a mentor for this course. </p>
</div> <a class="" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}">Create your first batch</a>
{% endif %} </div>
{% endif %}
{% endmacro %} {% endmacro %}
{% macro BatchSectionForStudents(course, upcoming_batches) %} {% macro BatchSectionForStudents(course, upcoming_batches) %}
<h2>Upcoming Batches</h2> {% if upcoming_batches %}
<h2>Upcoming Batches</h2>
<div class="row"> <div class="row">
{% for batch in upcoming_batches %} {% for batch in upcoming_batches %}
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
{{ RenderBatch(batch, can_manage=False) }} {{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }}
</div>
{% endfor %}
</div> </div>
{% endmacro %}
{% macro CourseOutline(course) %}
<h2>Course Outline</h2>
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter)}}
{% endfor %} {% endfor %}
</div>
{% endif %}
{% endmacro %} {% endmacro %}

View File

@@ -1,6 +1,4 @@
import frappe import frappe
from community.www.courses.utils import get_instructor
from frappe.utils import nowdate, getdate
from community.lms.models import Course from community.lms.models import Course
def get_context(context): def get_context(context):

View File

@@ -1,10 +1,11 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/hackathons/macros/card.html" import null_card %}
{% block title %}{{ 'Courses' }}{% endblock %} {% block title %}{{ 'Courses' }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{ 'Courses' }}" /> <meta name="description" content="{{ 'Courses' }}" />
<meta name="keywords" content="Courses" /> <meta name="keywords" content="Courses" />
<style> <style>
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -15,8 +16,13 @@
<div class='container'> <div class='container'>
<div class="row mt-5"> <div class="row mt-5">
{% for course in courses %} {% for course in courses %}
{{ course_card(course) }} {{ course_card(course) }}
{% endfor %} {% endfor %}
{% if courses %}
{% for n in range( (3 - (courses|length)) %3) %}
{{ null_card() }}
{% endfor %}
{% endif %}
</div> </div>
</div> </div>
</section> </section>
@@ -24,13 +30,18 @@
{% macro course_card(course) %} {% macro course_card(course) %}
<div class="card mb-5 w-100"> <div class="col-sm-4 mb-4 text-left">
<div class="card-body"> <a class="card-links" style="color: inherit;" href="/courses/{{course.name}}">
<h5 class="card-title"><a href="/courses/{{course.name}}">{{course.title}}</a></h5> <div class="card h-100">
{% if course.description %} <div class='card-body'>
<p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p> <h5 class='card-title'>{{ course.title }}</h5>
{% endif %} {% if course.description %}
<a href="/courses/{{course.name}}" class="card-link">See more &rarr;</a> <div class="mt-4">
</div> {{ frappe.utils.md_to_html(course.description[:200]) }}
</div>
{% endif %}
</div>
</div>
</a>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -1,3 +0,0 @@
frappe.ready(() => {
})

View File

@@ -1,59 +0,0 @@
import frappe
def get_member_with_email():
try:
return frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name")
except frappe.DoesNotExistError:
return
def get_member_with_name(name):
try:
return frappe.get_doc("Community Member", name)
except frappe.DoesNotExistError:
return
def get_batch(code):
try:
print("get_batch", code)
return frappe.db.get_value("LMS Batch", {"name": code}, ["name", "description"], as_dict=True)
except frappe.DoesNotExistError:
print("Error: notfound")
return
def is_member_of_batch(batch_code):
membership = frappe.get_all("LMS Batch Membership", {"batch": batch_code, "member": get_member_with_email()})
if len(membership):
return True
return False
def redirect_if_not_a_member(course,batch_code):
if not is_member_of_batch(batch_code):
frappe.local.flags.redirect_location = "/courses/" + course
raise frappe.Redirect
def get_course(name):
try:
return frappe.get_doc("LMS Course", {"name": name})
except frappe.DoesNotExistError:
return
def get_instructor(owner):
instructor = frappe._dict()
try:
instructor = frappe.get_doc("Community Member", {"email": owner})
except frappe.DoesNotExistError:
instructor.full_name = owner
instructor.abbr = ("").join([ s[0] for s in owner.split() ])
instructor.course_count = len(frappe.get_all("LMS Course", {"owner": owner}))
return instructor
def get_batch_members(batch):
members = []
memberships = frappe.get_all("LMS Batch Membership", {"batch": batch}, ["member", "member_type"])
for membership in memberships:
member = get_member_with_name(membership.member)
if membership.member_type == "Mentor":
member.is_mentor = True
members.append(member)
return members

View File

@@ -1,5 +1,4 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/profile.html" import Profile %}
{% block title %}{{ _("Community") }}{% endblock %} {% block title %}{{ _("Community") }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{ 'Community' }}" /> <meta name="description" content="{{ 'Community' }}" />
@@ -15,34 +14,6 @@
height: 200px; height: 200px;
} }
.dashboard__profile {
width: 150px;
height: 155px;
border-radius: 50%;
object-fit: contain;
}
.dashboard__profileSmall {
width: 59px;
height: 57px;
border-radius: 50%;
object-fit: contain;
}
.dashboard__abbr {
font-size: 50px;
width: 155px;
height: 155px;
border-radius: 50%;
}
.dashboard__abbrSmall {
font-size: 20px;
width: 59px;
height: 57px;
border-radius: 50%;
}
.dashboard__parent { .dashboard__parent {
display: flex; display: flex;
} }
@@ -90,12 +61,12 @@
{% block content %} {% block content %}
<section> <section>
<div class="dashboard__parent"> <div class="dashboard__parent">
<div> <div class="mr-5">
{{ Profile(member.photo, member.full_name, member.abbr, "large")}} {{ widgets.Avatar(member=member, avatar_class="avatar-xl") }}
</div> </div>
<div class="dashboard__details"> <div class="dashboard__details">
<div class="dashboard__name"> <div class="dashboard__name">
{{member.full_name}} <a class="anchor_style" href="/{{member.username}}">{{ member.full_name }}</a>
</div> </div>
<div> <div>
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist"> <ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
@@ -120,20 +91,18 @@
{% if activity %} {% if activity %}
{% for message in activity %} {% for message in activity %}
<div class="dashboard__message border m-5 p-3"> <div class="dashboard__message border m-5 p-3">
<a class="anchor_style bold" href="/{{message.member.username}}">{{ message.member.full_name }}</a>
<div class="text-muted float-right"> <div class="text-muted float-right">
{{ message.course }} ({{message.batch}}) {{ message.course }} ({{message.batch}})
</div> </div>
<div class="d-flex align-items-center w-100"> <div class="d-flex align-items-center w-100">
<div> <div>
{{ Profile(message.profile, message.full_name, message.abbr, "small")}} {{ widgets.Avatar(member=message.member, avatar_class="avatar-medium") }}
</div> </div>
<div class="ml-5 mt-5">{{ frappe.utils.md_to_html(message.message) }}</div> <div class="ml-5 mt-5">{{ frappe.utils.md_to_html(message.message) }}</div>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex">
<div class=""> <div class="text-muted float-right">
{{message.full_name}}
</div>
<div class="text-muted">
{{ frappe.utils.pretty_date(message.creation) }} {{ frappe.utils.pretty_date(message.creation) }}
</div> </div>
</div> </div>
@@ -209,4 +178,4 @@
</div> </div>
</section> </section>
{% endblock %} {% endblock %}

View File

@@ -7,14 +7,14 @@ def get_context(context):
if frappe.session.user == "Guest": if frappe.session.user == "Guest":
frappe.local.flags.redirect_location = "/login" frappe.local.flags.redirect_location = "/login"
raise frappe.Redirect raise frappe.Redirect
context.member = frappe.get_all("Community Member", {"email": frappe.session.user}, ["name", "email", "photo", "full_name", "abbr"])[0] context.member = frappe.get_doc("User", frappe.session.user)
context.memberships = get_memberships(context.member.name) context.memberships = get_memberships()
context.courses = get_courses(context.memberships) context.courses = get_courses(context.memberships)
context.activity = get_activity(context.memberships) context.activity = get_activity(context.memberships)
context.sketches = list(filter(lambda x: x.owner == frappe.session.user, Sketch.get_recent_sketches(owner=context.member.email))) context.sketches = list(filter(lambda x: x.owner == frappe.session.user, Sketch.get_recent_sketches(owner=context.member.email)))
def get_memberships(member): def get_memberships():
return frappe.get_all("LMS Batch Membership", {"member": member}, ["batch", "member_type", "creation"]) return frappe.get_all("LMS Batch Membership", {"member": frappe.session.user}, ["batch", "member_type", "creation"])
def get_courses(memberships): def get_courses(memberships):
courses = [] courses = []
@@ -38,5 +38,5 @@ def get_activity(memberships):
messages = frappe.get_all("LMS Message", {"batch": ["in", ",".join(batches)]}, ["message", "author", "creation", "batch"], order_by='creation desc') messages = frappe.get_all("LMS Message", {"batch": ["in", ",".join(batches)]}, ["message", "author", "creation", "batch"], order_by='creation desc')
for message in messages: for message in messages:
message.course = courses[message.batch] message.course = courses[message.batch]
message.profile, message.full_name, message.abbr = frappe.db.get_value("Community Member", message.author, ["photo", "full_name", "abbr"]) message.member = frappe.get_doc("User", message.author)
return messages return messages

View File

@@ -126,4 +126,4 @@
</div> </div>
</div> </div>
</section> </section>
{% endblock %} {% endblock %}

View File

@@ -10,8 +10,8 @@
<section id="hero"> <section id="hero">
<div class="container"> <div class="container">
<div class="jumbotron"> <div class="jumbotron">
<h1 class="display-4">Guided online courses, with a <br />mentor at your back.</h1> <h1 class="display-4">Guided online programming courses, with a <br />mentor at your back.</h1>
<p class="lead">Hands-on online courses designed by experts, delivered by passionate mentors.</p> <p class="lead">Hands-on programming courses designed by experts, delivered by passionate mentors.</p>
{{ widgets.RequestInvite() }} {{ widgets.RequestInvite() }}
</div> </div>
</div> </div>

View File

@@ -1,11 +1,3 @@
{% macro InstructorsSection(instructor) %}
<h3>Instructor</h3>
<div class="instructor">
<div class="instructor-title">{{instructor.full_name}}</div>
<div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div>
</div>
{% endmacro %}
{% macro MentorsSection(mentors, is_mentor, course_name) %} {% macro MentorsSection(mentors, is_mentor, course_name) %}
<h3>Mentors</h3> <h3>Mentors</h3>
{% for m in mentors %} {% for m in mentors %}
@@ -27,11 +19,3 @@
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro BatchHearder(course_name, member_count) %}
<div class="border p-3">
<h3>{{course_name}}</h3>
<div class="text-muted">{{member_count}} members</div>
</div>
{% endmacro %}

View File

@@ -1,10 +0,0 @@
{% macro Profile(photo, full_name, abbr, icon) %}
{% if photo %}
<img class="avatar rounded-circle img-fluid mr-5{% if icon == 'large' %} dashboard__profile {% else %} dashboard__profileSmall {% endif %}"
src="{{ photo }}" alt="{{ full_name }}">
{% else %}
<div class="standard-image mr-5 {% if icon == 'large' %} dashboard__abbr {% else %} dashboard__abbrSmall {% endif %}">
{{ abbr }}
</div>
{% endif %}
{% endmacro %}

Some files were not shown because too many files have changed in this diff Show More