Compare commits
1 Commits
certificat
...
fix-remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f371aa7a9 |
@@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2021, FOSS United and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('Discussion Message', {
|
|
||||||
// refresh: function(frm) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from community.widgets import Widget, Widgets
|
|
||||||
|
|
||||||
class DiscussionMessage(Document):
|
|
||||||
def after_insert(self):
|
|
||||||
data = {
|
|
||||||
"message": self,
|
|
||||||
"widgets": Widgets()
|
|
||||||
}
|
|
||||||
template = frappe.render_template("community/templates/message_card.html", data)
|
|
||||||
thread_info = frappe.db.get_value("Discussion Thread", self.thread, ["reference_doctype", "reference_docname"], as_dict=True)
|
|
||||||
frappe.publish_realtime(event="publish_message",
|
|
||||||
message = {
|
|
||||||
"thread": self.thread,
|
|
||||||
"template": template,
|
|
||||||
"thread_info": thread_info
|
|
||||||
},
|
|
||||||
after_commit=True)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
class TestDiscussionMessage(unittest.TestCase):
|
|
||||||
pass
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-08-11 10:55:29.341674",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"title",
|
|
||||||
"reference_doctype",
|
|
||||||
"reference_docname"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "reference_doctype",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Reference Doctype",
|
|
||||||
"options": "DocType"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "reference_docname",
|
|
||||||
"fieldtype": "Dynamic Link",
|
|
||||||
"label": "Reference Docname",
|
|
||||||
"options": "reference_doctype"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-08-11 12:29:43.564123",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Community",
|
|
||||||
"name": "Discussion Thread",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"search_fields": "title",
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"title_field": "title",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
class DiscussionThread(Document):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def submit_discussion(doctype, docname, message, title=None, thread_name=None):
|
|
||||||
thread = []
|
|
||||||
filters = {}
|
|
||||||
if doctype and docname:
|
|
||||||
filters = {
|
|
||||||
"reference_doctype": doctype,
|
|
||||||
"reference_docname": docname
|
|
||||||
}
|
|
||||||
|
|
||||||
elif thread_name:
|
|
||||||
filters = {
|
|
||||||
"name": thread_name
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters:
|
|
||||||
thread = frappe.get_all("Discussion Thread",filters)
|
|
||||||
if len(thread):
|
|
||||||
thread = thread[0]
|
|
||||||
save_message(message, thread)
|
|
||||||
|
|
||||||
else:
|
|
||||||
thread = frappe.get_doc({
|
|
||||||
"doctype": "Discussion Thread",
|
|
||||||
"title": title,
|
|
||||||
"reference_doctype": doctype,
|
|
||||||
"reference_docname": docname
|
|
||||||
})
|
|
||||||
thread.save(ignore_permissions=True)
|
|
||||||
save_message(message, thread)
|
|
||||||
|
|
||||||
return thread.name
|
|
||||||
|
|
||||||
def save_message(message, thread):
|
|
||||||
frappe.get_doc({
|
|
||||||
"doctype": "Discussion Message",
|
|
||||||
"message": message,
|
|
||||||
"thread": thread.name
|
|
||||||
}).save(ignore_permissions=True)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
class TestDiscussionThread(unittest.TestCase):
|
|
||||||
pass
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% set color = member.get_palette() %}
|
{% set color = member.get_palette() %}
|
||||||
<a class="button-links" 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.user_image %}
|
{% if member.user_image %}
|
||||||
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">
|
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<div class="discussions">
|
|
||||||
|
|
||||||
<form class="discussion-form {% if doctype or thread %} discussion-on-page {% endif %}" id="discussion-form">
|
|
||||||
|
|
||||||
|
|
||||||
<div class="form-group" {% if title or thread %} style="display: none;" {% endif %}>
|
|
||||||
<div class="control-input-wrapper">
|
|
||||||
<div class="control-input">
|
|
||||||
<input type="text" autocomplete="off" class="input-with-feedback form-control thread-title"
|
|
||||||
data-fieldtype="Data" data-fieldname="feedback_comments" placeholder="Title" spellcheck="false" {% if title
|
|
||||||
%} value="{{ title }}" {% endif %}></input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="control-input-wrapper">
|
|
||||||
<div class="control-input">
|
|
||||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control comment-field"
|
|
||||||
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="Enter a comment..."
|
|
||||||
spellcheck="false"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="comment-footer">
|
|
||||||
<div class="button is-secondary pull-right" id="submit-discussion"
|
|
||||||
{% if doctype %} data-doctype="{{ doctype | urlencode}}" {% endif %}
|
|
||||||
{% if docname %} data-docname="{{ docname | urlencode}}" {% endif %}
|
|
||||||
{% if thread %} data-thread="{{ thread }}" {% endif %}>
|
|
||||||
Post</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
frappe.ready(() => {
|
|
||||||
$("#submit-discussion").click((e) => {
|
|
||||||
submit_discussion(e);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var submit_discussion = (e) => {
|
|
||||||
var message = $(".comment-field").val().trim();
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
var doctype = $(e.currentTarget).attr("data-doctype");
|
|
||||||
doctype = doctype ? decodeURIComponent(doctype) : doctype;
|
|
||||||
|
|
||||||
var docname = $(e.currentTarget).attr("data-docname");
|
|
||||||
docname = docname ? decodeURIComponent(docname) : docname;
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "community.community.doctype.discussion_thread.discussion_thread.submit_discussion",
|
|
||||||
args: {
|
|
||||||
"doctype": doctype ? doctype : "",
|
|
||||||
"docname": docname ? docname : "",
|
|
||||||
"message": $(".comment-field").val(),
|
|
||||||
"title": $(".thread-title").val(),
|
|
||||||
"thread_name": $(e.currentTarget).attr("data-thread")
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
if (! $(".discussion-on-page").length) {
|
|
||||||
$("#discussion-modal").modal("hide");
|
|
||||||
window.location.href = `/discussions/${data.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
{% if doctype and docname and not thread %}
|
|
||||||
|
|
||||||
{% set thread_info = frappe.get_all("Discussion Thread", {"reference_doctype": doctype, "reference_docname": docname},
|
|
||||||
["name"]) %}
|
|
||||||
|
|
||||||
{% if thread_info | length %}
|
|
||||||
{% set thread = thread_info[0].name %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if thread %}
|
|
||||||
{% set messages = frappe.get_all("Discussion Message", {"thread": thread}, ["name", "message", "owner", "creation"],
|
|
||||||
order_by="creation") %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if doctype %}
|
|
||||||
<div class="course-home-headings mt-5"> Discussions </div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="messages mt-5">
|
|
||||||
{% for message in messages %}
|
|
||||||
{% include "community/templates/message_card.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if frappe.session.user == "Guest" or (condition is defined and not condition) %}
|
|
||||||
<div class="d-flex flex-column align-items-center font-weight-bold">
|
|
||||||
Want to join the discussion?
|
|
||||||
{% if frappe.session.user == "Guest" %}
|
|
||||||
<div class="button is-primary" id="login-from-discussion">Log In</div>
|
|
||||||
{% elif not condition %}
|
|
||||||
<div class="button is-primary" id="login-from-discussion" data-redirect="{{ redirect_to }}">{{ button_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{{ widgets.DiscussionComment(doctype=doctype, docname=docname, title=title, thread=thread ) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
frappe.ready(() => {
|
|
||||||
setup_socket_io();
|
|
||||||
|
|
||||||
$("#login-from-discussion").click((e) => {
|
|
||||||
login_from_discussion(e);
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
var setup_socket_io = () => {
|
|
||||||
const assets = [
|
|
||||||
"/assets/frappe/js/lib/socket.io.min.js",
|
|
||||||
"/assets/frappe/js/frappe/socketio_client.js",
|
|
||||||
]
|
|
||||||
frappe.require(assets, () => {
|
|
||||||
|
|
||||||
if (window.dev_server) {
|
|
||||||
frappe.boot.socketio_port = "9000";
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.socketio.init(9000);
|
|
||||||
var target = $("#submit-discussion");
|
|
||||||
|
|
||||||
frappe.socketio.socket.on("publish_message", (data) => {
|
|
||||||
if (target.attr("data-thread") == data.thread
|
|
||||||
|| (decodeURIComponent(target.attr("data-doctype")) == data.thread_info.reference_doctype
|
|
||||||
&& decodeURIComponent(target.attr("data-docname")) == data.thread_info.reference_docname)) {
|
|
||||||
$(".comment-field").val("");
|
|
||||||
$(".messages").append(data.template);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var login_from_discussion = (e) => {
|
|
||||||
var redirect = $(e.currentTarget).attr("data-redirect") || window.location.href;
|
|
||||||
window.location.href = `/login?redirect-to=${redirect}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -19,7 +19,6 @@ app_license = "AGPL"
|
|||||||
# app_include_js = "/assets/community/js/community.js"
|
# app_include_js = "/assets/community/js/community.js"
|
||||||
|
|
||||||
# include js, css files in header of web template
|
# include js, css files in header of web template
|
||||||
web_include_css = "community.bundle.css"
|
|
||||||
# web_include_css = "/assets/community/css/community.css"
|
# web_include_css = "/assets/community/css/community.css"
|
||||||
# web_include_js = "/assets/community/js/community.js"
|
# web_include_js = "/assets/community/js/community.js"
|
||||||
|
|
||||||
@@ -133,9 +132,10 @@ fixtures = ["Custom Field"]
|
|||||||
primary_rules = [
|
primary_rules = [
|
||||||
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
|
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
|
||||||
{"from_route": "/courses/<course>", "to_route": "courses/course"},
|
{"from_route": "/courses/<course>", "to_route": "courses/course"},
|
||||||
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
|
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
|
||||||
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
|
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
|
||||||
{"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": "/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>/home", "to_route": "batch/home"},
|
{"from_route": "/courses/<course>/home", "to_route": "batch/home"},
|
||||||
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
|
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
|
||||||
@@ -145,8 +145,7 @@ primary_rules = [
|
|||||||
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"},
|
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"},
|
||||||
{"from_route": "/courses/<course>/about", "to_route": "batch/about"},
|
{"from_route": "/courses/<course>/about", "to_route": "batch/about"},
|
||||||
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
|
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
|
||||||
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
|
{"from_route": "/courses/<course>/join", "to_route": "batch/join"}
|
||||||
{"from_route": "/discussions/<discussion>", "to_route": "discussions/discussion"}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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
|
||||||
@@ -168,8 +167,7 @@ whitelist = [
|
|||||||
"/new-sign-up",
|
"/new-sign-up",
|
||||||
"/message",
|
"/message",
|
||||||
"/about",
|
"/about",
|
||||||
"/edit-profile",
|
"/edit-profile"
|
||||||
"/discussions"
|
|
||||||
]
|
]
|
||||||
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Chapter', {
|
frappe.ui.form.on('Chapter', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
onload: function (frm) {
|
// }
|
||||||
frm.set_query("lesson", "lessons", function () {
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
"chapter": frm.doc.name,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"course",
|
"course",
|
||||||
"title",
|
"title",
|
||||||
"description",
|
"description",
|
||||||
"lessons"
|
"locked",
|
||||||
|
"index_"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,12 @@
|
|||||||
"fieldtype": "Markdown Editor",
|
"fieldtype": "Markdown Editor",
|
||||||
"label": "Description"
|
"label": "Description"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "locked",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Locked"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "course",
|
"fieldname": "course",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -31,10 +38,10 @@
|
|||||||
"options": "LMS Course"
|
"options": "LMS Course"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "lessons",
|
"default": "1",
|
||||||
"fieldtype": "Table",
|
"fieldname": "index_",
|
||||||
"label": "Lessons",
|
"fieldtype": "Int",
|
||||||
"options": "Lessons"
|
"label": "Index"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -45,7 +52,7 @@
|
|||||||
"link_fieldname": "chapter"
|
"link_fieldname": "chapter"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-07-27 16:28:08.667964",
|
"modified": "2021-05-13 21:05:20.531890",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Chapter",
|
"name": "Chapter",
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
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 ...utils import slugify
|
||||||
|
|
||||||
class Chapter(Document):
|
class Chapter(Document):
|
||||||
pass
|
def get_lessons(self):
|
||||||
|
rows = frappe.db.get_all("Lesson",
|
||||||
|
filters={"chapter": self.name},
|
||||||
|
fields='name',
|
||||||
|
order_by="index_")
|
||||||
|
return [frappe.get_doc('Lesson', row['name']) for row in rows]
|
||||||
|
|
||||||
|
def get_slugified_chapter_title(self):
|
||||||
|
return slugify(self.title)
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-07-27 16:25:02.903245",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"chapter"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "chapter",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Chapter",
|
|
||||||
"options": "Chapter",
|
|
||||||
"reqd": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-07-27 16:25:02.903245",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "Chapters",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
class Chapters(Document):
|
|
||||||
pass
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"include_in_preview",
|
"include_in_preview",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"title",
|
"title",
|
||||||
|
"index_",
|
||||||
"index_label",
|
"index_label",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"body",
|
"body",
|
||||||
@@ -30,6 +31,13 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Title"
|
"label": "Title"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "index_",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Index"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "body",
|
"fieldname": "body",
|
||||||
"fieldtype": "Markdown Editor",
|
"fieldtype": "Markdown Editor",
|
||||||
@@ -67,7 +75,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-07-27 16:28:29.203624",
|
"modified": "2021-06-29 13:34:49.077363",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Lesson",
|
"name": "Lesson",
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ def save_progress(lesson, course, status):
|
|||||||
"lesson": lesson,
|
"lesson": lesson,
|
||||||
"status": status,
|
"status": status,
|
||||||
}).save(ignore_permissions=True)
|
}).save(ignore_permissions=True)
|
||||||
course_details = frappe.get_doc("LMS Course", course)
|
return "OK"
|
||||||
return course_details.get_course_progress()
|
|
||||||
|
|
||||||
def update_progress(lesson):
|
def update_progress(lesson):
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-07-27 16:25:48.269536",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"lesson"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "lesson",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Lesson",
|
|
||||||
"options": "Lesson"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-07-27 16:53:52.732191",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "Lessons",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
class Lessons(Document):
|
|
||||||
pass
|
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
"fieldname": "member_type",
|
"fieldname": "member_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Member Type",
|
"label": "Member Type",
|
||||||
"options": "\nStudent\nMentor\nStaff"
|
"options": "\nStudent\nMentor\nStaff"
|
||||||
},
|
},
|
||||||
@@ -45,6 +44,7 @@
|
|||||||
"default": "Member",
|
"default": "Member",
|
||||||
"fieldname": "role",
|
"fieldname": "role",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Role",
|
"label": "Role",
|
||||||
"options": "\nMember\nAdmin"
|
"options": "\nMember\nAdmin"
|
||||||
},
|
},
|
||||||
@@ -63,10 +63,9 @@
|
|||||||
{
|
{
|
||||||
"fetch_from": "batch.course",
|
"fetch_from": "batch.course",
|
||||||
"fieldname": "course",
|
"fieldname": "course",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Course",
|
"label": "Course"
|
||||||
"options": "LMS Course"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "current_lesson",
|
"fieldname": "current_lesson",
|
||||||
@@ -84,7 +83,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-04 17:10:42.708479",
|
"modified": "2021-07-06 20:50:46.885325",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Membership",
|
"name": "LMS Batch Membership",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
// Copyright (c) 2021, FOSS United and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('LMS Certification', {
|
|
||||||
onload: function (frm) {
|
|
||||||
frm.set_query("student", function (doc) {
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
"ignore_user_type": 1,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-08-16 15:47:19.494055",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"student",
|
|
||||||
"issue_date",
|
|
||||||
"column_break_3",
|
|
||||||
"course",
|
|
||||||
"expiry_date"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "student",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Student",
|
|
||||||
"options": "User"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "issue_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"label": "Issue Date"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_3",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "course",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Course",
|
|
||||||
"options": "LMS Course"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "expiry_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"label": "Expiry Date"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-08-16 15:47:19.494055",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "LMS Certification",
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import nowdate, add_years
|
|
||||||
from frappe import _
|
|
||||||
from frappe.utils.pdf import get_pdf
|
|
||||||
|
|
||||||
class LMSCertification(Document):
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
certificates = frappe.get_all("LMS Certification", {
|
|
||||||
"student": self.student,
|
|
||||||
"course": self.course,
|
|
||||||
"expiry_date": [">", nowdate()]
|
|
||||||
})
|
|
||||||
if len(certificates):
|
|
||||||
full_name = frappe.db.get_value("User", self.student, "full_name")
|
|
||||||
course_name = frappe.db.get_value("LMS Course", self.course, "title")
|
|
||||||
frappe.throw(_("There is already a valid certificate for user {0} for the course {1}").format(full_name, course_name))
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def create_certificate(course):
|
|
||||||
course_details = frappe.get_doc("LMS Course", course)
|
|
||||||
certificate = course_details.is_certified()
|
|
||||||
|
|
||||||
if certificate:
|
|
||||||
return certificate
|
|
||||||
|
|
||||||
else:
|
|
||||||
expires_after_yrs = course_details.expiry
|
|
||||||
certificate = frappe.get_doc({
|
|
||||||
"doctype": "LMS Certification",
|
|
||||||
"student": frappe.session.user,
|
|
||||||
"course": course,
|
|
||||||
"issue_date": nowdate(),
|
|
||||||
"expiry_date": add_years(nowdate(), int(expires_after_yrs))
|
|
||||||
})
|
|
||||||
certificate.save(ignore_permissions=True)
|
|
||||||
return certificate.name
|
|
||||||
@@ -2,15 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('LMS Course', {
|
frappe.ui.form.on('LMS Course', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
onload: function (frm) {
|
// }
|
||||||
frm.set_query("chapter", "chapters", function () {
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
"course": frm.doc.name,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"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": "community.lms.doctype.lms_course.lms_course.reindex_exercises",
|
||||||
"action_type": "Server Action",
|
"action_type": "Server Action",
|
||||||
@@ -15,20 +21,16 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
|
"short_code",
|
||||||
"video_link",
|
"video_link",
|
||||||
"image",
|
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"tags",
|
|
||||||
"is_published",
|
"is_published",
|
||||||
"upcoming",
|
|
||||||
"disable_self_learning",
|
"disable_self_learning",
|
||||||
|
"image",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
|
"tags",
|
||||||
"short_introduction",
|
"short_introduction",
|
||||||
"description",
|
"description"
|
||||||
"chapters",
|
|
||||||
"certification_section",
|
|
||||||
"enable_certification",
|
|
||||||
"expiry"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -51,6 +53,11 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Published"
|
"label": "Published"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "short_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Short Code"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@@ -85,37 +92,6 @@
|
|||||||
"fieldname": "tags",
|
"fieldname": "tags",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Tags"
|
"label": "Tags"
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "upcoming",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is an Upcoming Course"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "chapters",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Chapters",
|
|
||||||
"options": "Chapters"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "certification_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Certification"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_certification",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Certification"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "enable_certification",
|
|
||||||
"fieldname": "expiry",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Certification Expires After Years",
|
|
||||||
"options": "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -135,9 +111,14 @@
|
|||||||
"group": "Mentors",
|
"group": "Mentors",
|
||||||
"link_doctype": "LMS Course Mentor Mapping",
|
"link_doctype": "LMS Course Mentor Mapping",
|
||||||
"link_fieldname": "course"
|
"link_fieldname": "course"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Mentors",
|
||||||
|
"link_doctype": "LMS Mentor Request",
|
||||||
|
"link_fieldname": "course"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-18 18:02:12.623807",
|
"modified": "2021-07-09 15:05:05.372430",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
@@ -160,5 +141,6 @@
|
|||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "title",
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"track_changes": 1,
|
||||||
|
"track_views": 1
|
||||||
}
|
}
|
||||||
@@ -9,42 +9,8 @@ import json
|
|||||||
from ...utils import slugify
|
from ...utils import slugify
|
||||||
from community.query import find, find_all
|
from community.query import find, find_all
|
||||||
from frappe.utils import flt, cint
|
from frappe.utils import flt, cint
|
||||||
from ...utils import slugify
|
|
||||||
|
|
||||||
class LMSCourse(Document):
|
class LMSCourse(Document):
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
|
||||||
self.send_email_to_interested_users()
|
|
||||||
|
|
||||||
def send_email_to_interested_users(self):
|
|
||||||
interested_users = frappe.get_all("LMS Course Interest",
|
|
||||||
{
|
|
||||||
"course": self.name
|
|
||||||
},
|
|
||||||
["name", "user"])
|
|
||||||
subject = self.title + " is available!"
|
|
||||||
args = {
|
|
||||||
"title": self.title,
|
|
||||||
"course_link": "/courses/{0}".format(self.name),
|
|
||||||
"app_name": frappe.db.get_single_value("System Settings", "app_name"),
|
|
||||||
"site_url": frappe.utils.get_url()
|
|
||||||
}
|
|
||||||
|
|
||||||
for user in interested_users:
|
|
||||||
args["first_name"] = frappe.db.get_value("User", user.user, "first_name")
|
|
||||||
email_args = frappe._dict(
|
|
||||||
recipients = user.user,
|
|
||||||
sender = frappe.db.get_single_value("LMS Settings", "email_sender"),
|
|
||||||
subject = subject,
|
|
||||||
header = [subject, "green"],
|
|
||||||
template = "lms_course_interest",
|
|
||||||
args = args,
|
|
||||||
now = True
|
|
||||||
)
|
|
||||||
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
|
|
||||||
frappe.db.set_value("LMS Course Interest", user.name, "email_sent", True)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find(name):
|
def find(name):
|
||||||
"""Returns the course with specified name.
|
"""Returns the course with specified name.
|
||||||
@@ -106,11 +72,8 @@ class LMSCourse(Document):
|
|||||||
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("User", mentor.mentor)
|
member = frappe.get_doc("User", mentor.mentor)
|
||||||
member.batch_count = frappe.db.count("LMS Batch Membership",
|
# TODO: change this to count query
|
||||||
{
|
member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"}))
|
||||||
"member": member.name,
|
|
||||||
"member_type": "Mentor"
|
|
||||||
})
|
|
||||||
course_mentors.append(member)
|
course_mentors.append(member)
|
||||||
return course_mentors
|
return course_mentors
|
||||||
|
|
||||||
@@ -149,42 +112,17 @@ class LMSCourse(Document):
|
|||||||
def get_chapters(self):
|
def get_chapters(self):
|
||||||
"""Returns all chapters of this course.
|
"""Returns all chapters of this course.
|
||||||
"""
|
"""
|
||||||
chapters = []
|
# TODO: chapters should have a way to specify the order
|
||||||
for row in self.chapters:
|
return find_all("Chapter", course=self.name, order_by="index_")
|
||||||
chapter_details = frappe.db.get_value("Chapter", row.chapter,
|
|
||||||
["name", "title", "description"],
|
|
||||||
as_dict=True)
|
|
||||||
chapter_details.idx = row.idx
|
|
||||||
chapters.append(chapter_details)
|
|
||||||
return chapters
|
|
||||||
|
|
||||||
def get_lessons(self, chapter=None):
|
def get_lessons(self):
|
||||||
""" If chapter is passed, returns lessons of only that chapter.
|
""" Returns all lessons of this course """
|
||||||
Else returns lessons of all chapters of the course """
|
|
||||||
lessons = []
|
lessons = []
|
||||||
|
chapters = self.get_chapters()
|
||||||
if chapter:
|
for chapter in chapters:
|
||||||
return self.get_lesson_details(chapter)
|
lessons.append(frappe.get_all("Lesson", {"chapter": chapter.name}))
|
||||||
|
|
||||||
for chapter in self.get_chapters():
|
|
||||||
lesson = self.get_lesson_details(chapter)
|
|
||||||
lessons += lesson
|
|
||||||
|
|
||||||
return lessons
|
return lessons
|
||||||
|
|
||||||
def get_lesson_details(self, chapter):
|
|
||||||
lessons = []
|
|
||||||
lesson_list = frappe.get_all("Lessons", {"parent": chapter.name},
|
|
||||||
["lesson", "idx"], order_by="idx")
|
|
||||||
for row in lesson_list:
|
|
||||||
lesson_details = frappe.get_doc("Lesson", row.lesson)
|
|
||||||
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
|
|
||||||
lessons.append(lesson_details)
|
|
||||||
return lessons
|
|
||||||
|
|
||||||
def get_slugified_chapter_title(self, chapter):
|
|
||||||
return slugify(chapter)
|
|
||||||
|
|
||||||
def get_course_progress(self):
|
def get_course_progress(self):
|
||||||
""" Returns the course progress of the session user """
|
""" Returns the course progress of the session user """
|
||||||
lesson_count = len(self.get_lessons())
|
lesson_count = len(self.get_lessons())
|
||||||
@@ -222,18 +160,38 @@ class LMSCourse(Document):
|
|||||||
visibility="Public")
|
visibility="Public")
|
||||||
return batches
|
return batches
|
||||||
|
|
||||||
|
def get_chapter(self, index):
|
||||||
|
return find("Chapter", course=self.name, index_=index)
|
||||||
|
|
||||||
|
def get_lesson(self, chapter_index, lesson_index):
|
||||||
|
chapter_name = frappe.get_value(
|
||||||
|
"Chapter",
|
||||||
|
{"course": self.name, "index_": chapter_index},
|
||||||
|
"name")
|
||||||
|
lesson_name = chapter_name and frappe.get_value(
|
||||||
|
"Lesson",
|
||||||
|
{"chapter": chapter_name, "index_": lesson_index},
|
||||||
|
"name")
|
||||||
|
return lesson_name and frappe.get_doc("Lesson", lesson_name)
|
||||||
|
|
||||||
def get_lesson_index(self, lesson_name):
|
def get_lesson_index(self, lesson_name):
|
||||||
"""Returns the {chapter_index}.{lesson_index} for the lesson.
|
"""Returns the {chapter_index}.{lesson_index} for the lesson.
|
||||||
"""
|
"""
|
||||||
lesson = frappe.db.get_value("Lessons", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
|
lesson = frappe.get_doc("Lesson", lesson_name)
|
||||||
if not lesson:
|
chapter = frappe.get_doc("Chapter", lesson.chapter)
|
||||||
return None
|
return f"{chapter.index_}.{lesson.index_}"
|
||||||
|
|
||||||
chapter = frappe.db.get_value("Chapters", {"chapter": lesson.parent}, ["idx"], as_dict=True)
|
def reindex_lessons(self):
|
||||||
if not chapter:
|
for i, c in enumerate(self.get_chapters(), start=1):
|
||||||
return None
|
c.index_ = i
|
||||||
|
c.save()
|
||||||
|
self._reindex_lessons_in_chapter(c)
|
||||||
|
|
||||||
return f"{chapter.idx}.{lesson.idx}"
|
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):
|
def reindex_exercises(self):
|
||||||
for i, c in enumerate(self.get_chapters(), start=1):
|
for i, c in enumerate(self.get_chapters(), start=1):
|
||||||
@@ -244,7 +202,7 @@ class LMSCourse(Document):
|
|||||||
|
|
||||||
def _reindex_exercises_in_chapter(self, c):
|
def _reindex_exercises_in_chapter(self, c):
|
||||||
i = 1
|
i = 1
|
||||||
for lesson in self.get_lessons(c):
|
for lesson in c.get_lessons():
|
||||||
for exercise in lesson.get_exercises():
|
for exercise in lesson.get_exercises():
|
||||||
exercise.index_ = i
|
exercise.index_ = i
|
||||||
exercise.index_label = f"{c.index_}.{i}"
|
exercise.index_label = f"{c.index_}.{i}"
|
||||||
@@ -279,6 +237,21 @@ class LMSCourse(Document):
|
|||||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||||
return all_memberships
|
return all_memberships
|
||||||
|
|
||||||
|
def get_mentors(self, batch=None):
|
||||||
|
filters = {
|
||||||
|
"course": self.name,
|
||||||
|
"member_type": "Mentor"
|
||||||
|
}
|
||||||
|
if batch:
|
||||||
|
filters["batch"] = batch
|
||||||
|
|
||||||
|
memberships = frappe.get_all(
|
||||||
|
"LMS Batch Membership",
|
||||||
|
filters,
|
||||||
|
["member"])
|
||||||
|
member_names = [m['member'] for m in memberships]
|
||||||
|
return find_all("User", name=["IN", member_names])
|
||||||
|
|
||||||
def get_students(self, batch=None):
|
def get_students(self, batch=None):
|
||||||
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
|
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
|
||||||
"""
|
"""
|
||||||
@@ -329,6 +302,9 @@ class LMSCourse(Document):
|
|||||||
return None
|
return None
|
||||||
return sum(ratings)/len(ratings)
|
return sum(ratings)/len(ratings)
|
||||||
|
|
||||||
|
def get_outline(self):
|
||||||
|
return CourseOutline(self)
|
||||||
|
|
||||||
def get_progress(self, lesson):
|
def get_progress(self, lesson):
|
||||||
return frappe.db.get_value("LMS Course Progress",
|
return frappe.db.get_value("LMS Course Progress",
|
||||||
{
|
{
|
||||||
@@ -338,24 +314,55 @@ class LMSCourse(Document):
|
|||||||
},
|
},
|
||||||
["status"])
|
["status"])
|
||||||
|
|
||||||
def get_neighbours(self, current, lessons):
|
class CourseOutline:
|
||||||
current = flt(current)
|
def __init__(self, course):
|
||||||
numbers = sorted(lesson.number for lesson in lessons)
|
self.course = course
|
||||||
index = numbers.index(current)
|
self.chapters = self.get_chapters()
|
||||||
return {
|
self.lessons = self.get_lessons()
|
||||||
"prev": numbers[index-1] if index-1 >= 0 else None,
|
|
||||||
"next": numbers[index+1] if index+1 < len(numbers) else None
|
|
||||||
}
|
|
||||||
|
|
||||||
def is_certified(self):
|
def get_next(self, current):
|
||||||
certificate = frappe.get_all("LMS Certification",
|
current = flt(current)
|
||||||
{
|
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||||
"student": frappe.session.user,
|
try:
|
||||||
"course": self.name
|
index = numbers.index(current)
|
||||||
})
|
return numbers[index+1]
|
||||||
if len(certificate):
|
except IndexError:
|
||||||
return certificate[0].name
|
return None
|
||||||
return
|
|
||||||
|
def get_prev(self, current):
|
||||||
|
current = flt(current)
|
||||||
|
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||||
|
try:
|
||||||
|
index = numbers.index(current)
|
||||||
|
if index == 0:
|
||||||
|
return None
|
||||||
|
return numbers[index-1]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_chapters(self):
|
||||||
|
return frappe.db.get_all("Chapter",
|
||||||
|
filters={"course": self.course.name},
|
||||||
|
fields=["name", "title", "index_"],
|
||||||
|
order_by="index_")
|
||||||
|
|
||||||
|
def get_lessons(self):
|
||||||
|
chapters = [c['name'] for c in self.chapters]
|
||||||
|
lessons = frappe.db.get_all("Lesson",
|
||||||
|
filters={"chapter": ["IN", chapters]},
|
||||||
|
fields=["name", "title", "chapter", "index_"])
|
||||||
|
|
||||||
|
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
|
||||||
|
for lesson in lessons:
|
||||||
|
lesson['number'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']))
|
||||||
|
return lessons
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def reindex_lessons(doc):
|
||||||
|
course_data = json.loads(doc)
|
||||||
|
course = frappe.get_doc("LMS Course", course_data['name'])
|
||||||
|
course.reindex_lessons()
|
||||||
|
frappe.msgprint("All lessons in this course have been re-indexed.")
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def reindex_exercises(doc):
|
def reindex_exercises(doc):
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2021, FOSS United and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('LMS Course Interest', {
|
|
||||||
// refresh: function(frm) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-08-06 17:37:20.184849",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"course",
|
|
||||||
"user",
|
|
||||||
"email_sent"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "course",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Course",
|
|
||||||
"options": "LMS Course"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "user",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "User",
|
|
||||||
"options": "User"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "email_sent",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Email Sent",
|
|
||||||
"options": "email_sent"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-08-06 18:06:21.370741",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "LMS Course Interest",
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
class LMSCourseInterest(Document):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def capture_interest(course):
|
|
||||||
frappe.get_doc({
|
|
||||||
"doctype": "LMS Course Interest",
|
|
||||||
"course": course,
|
|
||||||
"user": frappe.session.user
|
|
||||||
}).save(ignore_permissions=True)
|
|
||||||
return "OK"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
class TestLMSCourseInterest(unittest.TestCase):
|
|
||||||
pass
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// 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
|
||||||
|
|
||||||
frappe.ui.form.on('Discussion Thread', {
|
frappe.ui.form.on('LMS Message', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
@@ -1,53 +1,67 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2021-08-11 10:59:38.597046",
|
"creation": "2021-03-19 12:19:32.355307",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"thread",
|
"author",
|
||||||
"column_break_2",
|
"batch",
|
||||||
"parent_message",
|
"column_break_3",
|
||||||
"section_break_4",
|
"author_name",
|
||||||
|
"pin",
|
||||||
|
"section_break_6",
|
||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "batch",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Batch",
|
||||||
|
"options": "LMS Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "author",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Author",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "message",
|
"fieldname": "message",
|
||||||
"fieldtype": "Long Text",
|
"fieldtype": "Markdown Editor",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Message"
|
"label": "Message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "thread",
|
"default": "0",
|
||||||
"fieldtype": "Link",
|
"fieldname": "pin",
|
||||||
"in_list_view": 1,
|
"fieldtype": "Check",
|
||||||
"in_standard_filter": 1,
|
"label": "Pin"
|
||||||
"label": "Thread",
|
|
||||||
"options": "Discussion Thread"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_2",
|
"fetch_from": "author.full_name",
|
||||||
|
"fieldname": "author_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Author Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "parent_message",
|
"fieldname": "section_break_6",
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Parent Message",
|
|
||||||
"options": "Discussion Message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_4",
|
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-12 15:59:04.811286",
|
"modified": "2021-05-21 11:49:34.911479",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Community",
|
"module": "LMS",
|
||||||
"name": "Discussion Message",
|
"name": "LMS Message",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -63,7 +77,9 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"title_field": "author",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
110
community/lms/doctype/lms_message/lms_message.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import add_days, nowdate
|
||||||
|
|
||||||
|
class LMSMessage(Document):
|
||||||
|
def after_insert(self):
|
||||||
|
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_daily_digest():
|
||||||
|
#Todo: Optimize this
|
||||||
|
emails = frappe._dict()
|
||||||
|
messages = frappe.get_all("LMS Message", {"creation": [">=", add_days(nowdate(), -1)]}, ["message", "batch", "author", "creation"])
|
||||||
|
for message in messages:
|
||||||
|
membership = frappe.get_all("LMS Batch Membership", {"batch": message.batch}, ["member"])
|
||||||
|
for entry in membership:
|
||||||
|
member = frappe.db.get_value("User", entry.member, ["name", "email"], as_dict=1)
|
||||||
|
if member.name != message.author:
|
||||||
|
if member.name in emails.keys():
|
||||||
|
emails[member.name]["messages"].append(message)
|
||||||
|
else:
|
||||||
|
emails[member.name] = frappe._dict({
|
||||||
|
"email": member.email,
|
||||||
|
"messages": [message]
|
||||||
|
})
|
||||||
|
for email in emails:
|
||||||
|
group_by_batch = frappe._dict()
|
||||||
|
for message in emails[email]["messages"]:
|
||||||
|
if message.batch in group_by_batch.keys():
|
||||||
|
group_by_batch[message.batch].append(message)
|
||||||
|
else:
|
||||||
|
group_by_batch[message.batch] = [message]
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients = frappe.db.get_value("User", email, "email"),
|
||||||
|
subject = _("Message Digest"),
|
||||||
|
header = _("Message Digest"),
|
||||||
|
template = "lms_daily_digest",
|
||||||
|
args = {
|
||||||
|
"batches": group_by_batch
|
||||||
|
},
|
||||||
|
delayed = False
|
||||||
|
)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2021, FOSS United and Contributors
|
# Copyright (c) 2021, FOSS United and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
class TestLMSCertification(unittest.TestCase):
|
class TestLMSMessage(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
@@ -27,13 +27,12 @@
|
|||||||
"fieldname": "lesson",
|
"fieldname": "lesson",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Lesson",
|
"label": "Lesson",
|
||||||
"options": "Lesson",
|
"options": "Lesson"
|
||||||
"read_only": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-07-23 19:06:12.551633",
|
"modified": "2021-06-23 17:58:57.642873",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
|
|||||||
@@ -46,30 +46,34 @@ class LMSQuiz(Document):
|
|||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def quiz_summary(quiz, results):
|
def submit(quiz, result):
|
||||||
score = 0
|
score = 0
|
||||||
results = json.loads(results)
|
answer_map = {
|
||||||
|
"is_correct_1": "option_1",
|
||||||
|
"is_correct_2": "option_2",
|
||||||
|
"is_correct_3": "option_3",
|
||||||
|
"is_correct_4": "option_4"
|
||||||
|
}
|
||||||
|
result = json.loads(result)
|
||||||
|
quiz_details = frappe.get_doc("LMS Quiz", quiz)
|
||||||
|
|
||||||
for result in results:
|
for response in result:
|
||||||
correct = result["is_correct"][0]
|
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0]
|
||||||
result["question"] = frappe.db.get_value("LMS Quiz Question",
|
correct_options = quiz_details.get_correct_options(match)
|
||||||
{"parent": quiz, "idx": result["question_index"]},
|
correct_answers = [ match.get(answer_map[option]) for option in correct_options ]
|
||||||
["question"])
|
|
||||||
|
|
||||||
for point in result["is_correct"]:
|
if response.get("answer") == correct_answers:
|
||||||
correct = correct and point
|
response["result"] = "Right"
|
||||||
|
score += 1
|
||||||
result["result"] = "Right" if correct else "Wrong"
|
else:
|
||||||
score += correct
|
response["result"] = "Wrong"
|
||||||
|
response["answer"] = ("").join([ ans if idx == len(response.get("answer")) -1 else ans + ", " for idx, ans in enumerate(response.get("answer")) ])
|
||||||
del result["is_correct"]
|
|
||||||
del result["question_index"]
|
|
||||||
|
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "LMS Quiz Submission",
|
"doctype": "LMS Quiz Submission",
|
||||||
"quiz": quiz,
|
"quiz": quiz,
|
||||||
"result": results,
|
"result": result,
|
||||||
"score": score
|
"score": score
|
||||||
}).save(ignore_permissions=True)
|
}).save(ignore_permissions=True)
|
||||||
|
update_progress(quiz_details.lesson)
|
||||||
return score
|
return score
|
||||||
|
|||||||
@@ -3,39 +3,6 @@
|
|||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
import unittest
|
import unittest
|
||||||
import frappe
|
|
||||||
|
|
||||||
class TestLMSQuiz(unittest.TestCase):
|
class TestLMSQuiz(unittest.TestCase):
|
||||||
|
pass
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
frappe.get_doc({
|
|
||||||
"doctype": "LMS Quiz",
|
|
||||||
"title": "Test Quiz"
|
|
||||||
}).save()
|
|
||||||
|
|
||||||
def test_with_multiple_options(self):
|
|
||||||
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
|
|
||||||
quiz.append("questions", {
|
|
||||||
"question": "Question multiple",
|
|
||||||
"option_1": "Option 1",
|
|
||||||
"is_correct_1": 1,
|
|
||||||
"option_2": "Option 2",
|
|
||||||
"is_correct_2": 1
|
|
||||||
})
|
|
||||||
quiz.save()
|
|
||||||
self.assertTrue(quiz.questions[0].multiple)
|
|
||||||
|
|
||||||
def test_with_no_correct_option(self):
|
|
||||||
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
|
|
||||||
quiz.append("questions", {
|
|
||||||
"question": "Question no correct option",
|
|
||||||
"option_1": "Option 1",
|
|
||||||
"option_2": "Option 2",
|
|
||||||
})
|
|
||||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls) -> None:
|
|
||||||
frappe.db.delete("LMS Quiz", "Test Quiz")
|
|
||||||
frappe.db.delete("LMS Quiz Question", {"parent": "Test Quiz"})
|
|
||||||
|
|||||||
@@ -9,23 +9,15 @@
|
|||||||
"options_section",
|
"options_section",
|
||||||
"option_1",
|
"option_1",
|
||||||
"is_correct_1",
|
"is_correct_1",
|
||||||
"column_break_5",
|
|
||||||
"explanation_1",
|
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"option_2",
|
"option_2",
|
||||||
"is_correct_2",
|
"is_correct_2",
|
||||||
"column_break_10",
|
|
||||||
"explanation_2",
|
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"option_3",
|
"option_3",
|
||||||
"is_correct_3",
|
"is_correct_3",
|
||||||
"column_break_15",
|
|
||||||
"explanation_3",
|
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
"option_4",
|
"option_4",
|
||||||
"is_correct_4",
|
"is_correct_4",
|
||||||
"column_break_20",
|
|
||||||
"explanation_4",
|
|
||||||
"multiple"
|
"multiple"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -109,52 +101,12 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_11",
|
"fieldname": "section_break_11",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_1",
|
|
||||||
"fieldname": "explanation_1",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_2",
|
|
||||||
"fieldname": "explanation_2",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_3",
|
|
||||||
"fieldname": "explanation_3",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_4",
|
|
||||||
"fieldname": "explanation_4",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_5",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_10",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_15",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_20",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-07-19 19:35:28.446236",
|
"modified": "2021-06-22 16:54:13.133859",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Question",
|
"name": "LMS Quiz Question",
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
frappe.ready(function () {
|
frappe.ready(function () {
|
||||||
|
|
||||||
frappe.web_form.after_load = () => {
|
frappe.web_form.after_load = () => {
|
||||||
if (!frappe.utils.get_url_arg("name")) {
|
if (!frappe.utils.get_url_arg("name")) {
|
||||||
window.location.href = `/edit-profile?name=${frappe.session.user}`;
|
window.location.href = `/edit-profile?name=${frappe.session.user}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.web_form.after_save = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/${frappe.web_form.get_value(["username"])}`;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"apply_document_permissions": 0,
|
"apply_document_permissions": 0,
|
||||||
"breadcrumbs": "",
|
"breadcrumbs": "",
|
||||||
"button_label": "Save",
|
"button_label": "Save",
|
||||||
"client_script": "",
|
|
||||||
"creation": "2021-06-30 13:48:13.682851",
|
"creation": "2021-06-30 13:48:13.682851",
|
||||||
"custom_css": "[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
|
"custom_css": "[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
|
||||||
"doc_type": "User",
|
"doc_type": "User",
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"login_required": 1,
|
"login_required": 1,
|
||||||
"max_attachment_size": 0,
|
"max_attachment_size": 0,
|
||||||
"modified": "2021-08-06 14:40:39.013776",
|
"modified": "2021-07-14 17:15:15.424855",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "profile",
|
"name": "profile",
|
||||||
@@ -73,18 +72,6 @@
|
|||||||
"reqd": 0,
|
"reqd": 0,
|
||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "username",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Username",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 0,
|
||||||
"description": "Get your globally recognized avatar from Gravatar.com",
|
"description": "Get your globally recognized avatar from Gravatar.com",
|
||||||
|
|||||||
67
community/lms/widgets/BatchTabs.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<div class="mt-5">
|
||||||
|
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a
|
||||||
|
class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
|
||||||
|
{{ course.title }}</span> {% endif %}
|
||||||
|
{% set all_memberships = course.get_all_memberships(frappe.session.user) %}
|
||||||
|
{% if membership and membership.batch and all_memberships | length > 1 %}
|
||||||
|
<a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
{{ membership.batch_title }}
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
|
{% for data in all_memberships %}
|
||||||
|
{% if data.batch != membership.batch %}
|
||||||
|
<a class="dropdown-item switch-batch"
|
||||||
|
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if not membership %}
|
||||||
|
{% set display_class = "hide" %}
|
||||||
|
{% else %}
|
||||||
|
{% set display_class = "" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs mt-4">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson
|
||||||
|
else '1.1' %}
|
||||||
|
<a class="nav-link" id="learn"
|
||||||
|
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
|
||||||
|
</li> -->
|
||||||
|
<li class="nav-item {{ display_class }}">
|
||||||
|
<a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item {{ display_class }}">
|
||||||
|
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
|
||||||
|
</li> -->
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
|
||||||
|
</li> -->
|
||||||
|
{% if membership and membership.batch and course.is_mentor(frappe.session.user) %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
frappe.ready(() => {
|
||||||
|
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}{{ course.query_parameter }}"]`)
|
||||||
|
if (selector) {
|
||||||
|
selector.classList.add('active');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("#learn").addClass('active')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
|
|
||||||
|
|
||||||
{% if course %}
|
|
||||||
<a class="dark-links" href="/courses">All Courses</a>
|
<a class="dark-links" href="/courses">All Courses</a>
|
||||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||||
|
|
||||||
|
{% if course %}
|
||||||
{% if lesson %}
|
{% if lesson %}
|
||||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||||
@@ -13,9 +12,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if thread %}
|
{% if member_name %}
|
||||||
<a class="dark-links" href="/discussions">Discussions</a>
|
<span class="muted-text">{{ member_name }}</span>
|
||||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
|
||||||
<span class="muted-text">{{ thread.title }}</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div class="chapter-title small-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}"
|
<div class="small-title chapter-title" data-target="#{{ chapter.get_slugified_chapter_title() }}"
|
||||||
data-toggle="collapse" aria-expanded="false">
|
data-toggle="collapse" aria-expanded="false">
|
||||||
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
|
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
|
||||||
{{ index }}. {{ chapter.title }}
|
{{ index }}. {{ chapter.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter-content collapse navbar-collapse" id="{{ course.get_slugified_chapter_title(chapter.title) }}">
|
<div class="chapter-content collapse navbar-collapse" id="{{ chapter.get_slugified_chapter_title() }}">
|
||||||
|
|
||||||
{% if chapter.description %}
|
|
||||||
<div class="chapter-description muted-text">
|
<div class="chapter-description muted-text">
|
||||||
{{ chapter.description }}
|
{{ chapter.description }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="lessons">
|
<div class="lessons">
|
||||||
|
|
||||||
{% for lesson in course.get_lessons(chapter) %}
|
{% for lesson in chapter.get_lessons() %}
|
||||||
|
|
||||||
<div class="lesson-info{% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
|
<div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
|
||||||
|
|
||||||
{% if membership or lesson.include_in_preview %}
|
{% if membership or lesson.include_in_preview %}
|
||||||
<a class="lesson-links"
|
<a class="lesson-links"
|
||||||
href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}"
|
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
|
||||||
data-course="{{ course.name }}">
|
data-course="{{ course.name }}">
|
||||||
{{ lesson.title }}
|
{{ lesson.title }}
|
||||||
|
|
||||||
{% if membership %}
|
{% if membership %}
|
||||||
<img class="ml-1 lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
|
<img class="lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
|
||||||
src="/assets/community/icons/check.svg">
|
src="/assets/community/icons/check.svg">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
<div class="common-card-style course-card">
|
<div class="common-card-style course-card">
|
||||||
<div class="course-image {% if not course.image %}default-image{% endif %}"
|
<div class="course-image" style="background-image: url({{ course.image }});">
|
||||||
{% if course.image %} style="background-image: url( {{ course.image }} );" {% endif %}>
|
|
||||||
<div class="course-tags">
|
<div class="course-tags">
|
||||||
{% for tag in course.get_tags() %}
|
{% for tag in course.get_tags() %}
|
||||||
<div class="course-card-pills">{{ tag }}</div>
|
<div class="course-card-pills">{{ tag }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if not course.image %}
|
|
||||||
<div class="default-image-text">{{ course.title[0] }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="course-card-content">
|
<div class="course-card-content">
|
||||||
<div class="course-card-meta muted-text">
|
<div class="course-card-meta muted-text">
|
||||||
@@ -57,19 +53,15 @@
|
|||||||
|
|
||||||
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
||||||
|
|
||||||
{% if course.upcoming %}
|
{% if membership %}
|
||||||
<div class="view-course-link is-secondary border">
|
|
||||||
Upcoming Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
|
||||||
|
|
||||||
{% elif membership %}
|
|
||||||
<div class="view-course-link is-primary">
|
<div class="view-course-link is-primary">
|
||||||
Continue Course <img class="ml-3" src="/assets/community/icons/white-arrow.svg" />
|
Continue Course <img class="ml-3" src="/assets/community/icons/white-arrow.svg" />
|
||||||
</div>
|
</div>
|
||||||
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
|
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="view-course-link">
|
<div class="view-course-link">
|
||||||
View Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
|
View Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
community/lms/widgets/RenderBatch.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="batch">
|
||||||
|
<div class="batch-details">
|
||||||
|
<div class="">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 course.get_mentors(batch.name) %}
|
||||||
|
<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 }}/home?batch={{ batch.name }}" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}"
|
||||||
|
data-course="{{ course.name | urlencode }}">Manage</a>
|
||||||
|
{% elif can_join %}
|
||||||
|
<button class="join-batch btn btn-secondary" data-batch="{{ batch.name | urlencode }}"
|
||||||
|
data-course="{{ course.name | urlencode }}">Join this Batch</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -1,28 +1,23 @@
|
|||||||
|
{% if course.get_reviews() | length %}
|
||||||
<div class="reviews-parent">
|
<div class="reviews-parent">
|
||||||
{% set reviews = course.get_reviews() %}
|
<div class="reviews-heading">
|
||||||
{% if reviews | length or course.is_eligible_to_review(membership) %}
|
<div class="course-home-headings">Student Review</div>
|
||||||
<div class="mb-5">
|
|
||||||
<span class="course-home-headings">Reviews</span>
|
|
||||||
{% if course.is_eligible_to_review(membership) %}
|
{% if course.is_eligible_to_review(membership) %}
|
||||||
<span class="review-link button is-secondary pull-right">
|
<a class="review-link" href="">
|
||||||
Write a review
|
Provide your Feedback
|
||||||
</span>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% if reviews | length %}
|
|
||||||
<div class="reviews-section">
|
<div class="reviews-section">
|
||||||
{% for review in reviews %}
|
{% for review in course.get_reviews() %}
|
||||||
<div class="review-card">
|
<div class="review-card">
|
||||||
<div class="common-card-style review-content small-title"> {{ review.review }} </div>
|
<div class="common-card-style review-content small-title"> {{ review.review }} </div>
|
||||||
<div class="review-card-footer">
|
<div class="review-card-footer">
|
||||||
<div>
|
<div>
|
||||||
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
|
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
|
||||||
<a class="button-links" href="/{{review.owner_details.username}}">
|
|
||||||
<span class="course-instructor">
|
<span class="course-instructor">
|
||||||
{{ review.owner_details.full_name }}
|
{{ review.owner_details.full_name }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="rating">
|
<div class="rating">
|
||||||
{% for i in [1, 2, 3, 4, 5] %}
|
{% for i in [1, 2, 3, 4, 5] %}
|
||||||
@@ -35,7 +30,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||||||
@@ -73,8 +67,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="control-input-wrapper">
|
<div class="control-input-wrapper">
|
||||||
<div class="control-input">
|
<div class="control-input">
|
||||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
|
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field" data-fieldtype="Text"
|
||||||
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
|
data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
|
||||||
spellcheck="false"></textarea>
|
spellcheck="false"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,9 +77,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
|
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">Submit</div>
|
||||||
Submit</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|||||||
16
community/lms/widgets/SketchTeaser.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="sketch-teaser">
|
||||||
|
<div class="sketch-image">
|
||||||
|
<a href="/sketches/{{sketch.sketch_id}}">
|
||||||
|
{{ sketch.to_svg() }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sketch-footer">
|
||||||
|
<div class="sketch-title">
|
||||||
|
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="sketch-author">
|
||||||
|
{% set owner = sketch.get_owner() %}
|
||||||
|
by <a href="/{{owner.username}}">{{owner.full_name}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
class TestCustomUser(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_with_basic_username(self):
|
|
||||||
new_user = frappe.get_doc({
|
|
||||||
"doctype": "User",
|
|
||||||
"email": "test_with_basic_username@example.com",
|
|
||||||
"first_name": "Username"
|
|
||||||
}).insert()
|
|
||||||
self.assertEqual(new_user.username, "username")
|
|
||||||
|
|
||||||
def test_without_username(self):
|
|
||||||
""" The user in this test has the same first name as the user of the test test_with_basic_username.
|
|
||||||
In such cases frappe makes the username of the second user empty.
|
|
||||||
The condition in community app should override this and save a username. """
|
|
||||||
new_user = frappe.get_doc({
|
|
||||||
"doctype": "User",
|
|
||||||
"email": "test-without-username@example.com",
|
|
||||||
"first_name": "Username"
|
|
||||||
}).insert()
|
|
||||||
self.assertTrue(new_user.username)
|
|
||||||
|
|
||||||
def test_with_illegal_characters(self):
|
|
||||||
new_user = frappe.get_doc({
|
|
||||||
"doctype": "User",
|
|
||||||
"email": "test_with_illegal_characters@example.com",
|
|
||||||
"first_name": "Username$$"
|
|
||||||
}).insert()
|
|
||||||
self.assertEqual(new_user.username[:8], "username")
|
|
||||||
|
|
||||||
def test_with_underscore_at_end(self):
|
|
||||||
new_user = frappe.get_doc({
|
|
||||||
"doctype": "User",
|
|
||||||
"email": "test_with_underscore_at_end@example.com",
|
|
||||||
"first_name": "Username___"
|
|
||||||
}).insert()
|
|
||||||
self.assertNotEqual(new_user.username[-1], "_")
|
|
||||||
|
|
||||||
|
|
||||||
def test_with_short_first_name(self):
|
|
||||||
new_user = frappe.get_doc({
|
|
||||||
"doctype": "User",
|
|
||||||
"email": "test_with_short_first_name@example.com",
|
|
||||||
"first_name": "USN"
|
|
||||||
}).insert()
|
|
||||||
self.assertGreaterEqual(len(new_user.username), 4)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls) -> None:
|
|
||||||
users = [
|
|
||||||
"test_with_basic_username@example.com",
|
|
||||||
"test-without-username@example.com",
|
|
||||||
"test_with_illegal_characters@example.com",
|
|
||||||
"test_with_underscore_at_end@example.com",
|
|
||||||
"test_with_short_first_name@example.com"
|
|
||||||
]
|
|
||||||
frappe.db.delete("User", {"name": ["in", users]})
|
|
||||||
@@ -2,64 +2,15 @@ import frappe
|
|||||||
from frappe.core.doctype.user.user import User
|
from frappe.core.doctype.user.user import User
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
import hashlib
|
import hashlib
|
||||||
import random
|
|
||||||
import re
|
|
||||||
from frappe import _
|
|
||||||
|
|
||||||
class CustomUser(User):
|
class CustomUser(User):
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
super(CustomUser, self).validate()
|
|
||||||
self.validate_username_characters()
|
|
||||||
|
|
||||||
def validate_username_characters(self):
|
|
||||||
if len(self.username):
|
|
||||||
underscore_condition = self.username[0] == "_" or self.username[-1] == "_"
|
|
||||||
else:
|
|
||||||
underscore_condition = ''
|
|
||||||
|
|
||||||
if self.is_new():
|
|
||||||
if not self.username:
|
|
||||||
self.username = self.get_username_from_first_name()
|
|
||||||
|
|
||||||
if self.username.find(" "):
|
|
||||||
self.username.replace(" ", "")
|
|
||||||
|
|
||||||
if not re.match("^[A-Za-z0-9_]*$", self.username) or underscore_condition:
|
|
||||||
self.username = self.remove_illegal_characters()
|
|
||||||
|
|
||||||
if len(self.username) < 4:
|
|
||||||
self.username = self.email.replace("@", "").replace(".", "")
|
|
||||||
|
|
||||||
while self.username_exists():
|
|
||||||
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
|
|
||||||
|
|
||||||
else:
|
|
||||||
if not self.username:
|
|
||||||
frappe.throw(_("Username already exists."))
|
|
||||||
|
|
||||||
if not re.match("^[A-Za-z0-9_]*$", self.username):
|
|
||||||
frappe.throw(_("Username can only contain alphabets, numbers and unedrscore."))
|
|
||||||
|
|
||||||
if underscore_condition:
|
|
||||||
frappe.throw(_("First and Last character of username cannot be Underscore(_)."))
|
|
||||||
|
|
||||||
if len(self.username) < 4:
|
|
||||||
frappe.throw(_("Username cannot be less than 4 characters"))
|
|
||||||
|
|
||||||
def get_username_from_first_name(self):
|
|
||||||
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
|
|
||||||
|
|
||||||
def remove_illegal_characters(self):
|
|
||||||
return re.sub("[^\w]+", "", self.username).strip("_")
|
|
||||||
|
|
||||||
def get_authored_courses(self) -> int:
|
def get_authored_courses(self) -> int:
|
||||||
"""Returns the number of courses authored by this user.
|
"""Returns the number of courses authored by this user.
|
||||||
"""
|
"""
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
'LMS Course', {
|
'LMS Course', {
|
||||||
'owner': self.name,
|
'owner': self.name
|
||||||
'is_published': True
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_palette(self):
|
def get_palette(self):
|
||||||
@@ -111,16 +62,9 @@ class CustomUser(User):
|
|||||||
|
|
||||||
def get_mentored_courses(self):
|
def get_mentored_courses(self):
|
||||||
""" Returns all courses mentored by this user """
|
""" Returns all courses mentored by this user """
|
||||||
mentored_courses = []
|
return frappe.get_all("LMS Course Mentor Mapping",
|
||||||
mapping = frappe.get_all("LMS Course Mentor Mapping",
|
|
||||||
{
|
{
|
||||||
"mentor": self.name,
|
"mentor": self.name
|
||||||
},
|
},
|
||||||
["name", "course"]
|
["name", "course"]
|
||||||
)
|
)
|
||||||
|
|
||||||
for map in mapping:
|
|
||||||
if frappe.db.get_value("LMS Course", map.course, "is_published"):
|
|
||||||
mentored_courses.append(map)
|
|
||||||
|
|
||||||
return mentored_courses
|
|
||||||
|
|||||||
@@ -6,5 +6,3 @@ 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_course_mentor_mapping
|
||||||
community.patches.replace_member_with_user_in_lms_message
|
community.patches.replace_member_with_user_in_lms_message
|
||||||
community.patches.replace_member_with_user_in_mentor_request
|
community.patches.replace_member_with_user_in_mentor_request
|
||||||
community.patches.v0_0.chapter_lesson_index_table
|
|
||||||
execute:frappe.delete_doc("DocType", "LMS Message")
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
frappe.reload_doc("lms", "doctype", "lms_course")
|
|
||||||
frappe.reload_doc("lms", "doctype", "chapter")
|
|
||||||
frappe.reload_doc("lms", "doctype", "lesson")
|
|
||||||
frappe.reload_doc("lms", "doctype", "lessons")
|
|
||||||
frappe.reload_doc("lms", "doctype", "chapters")
|
|
||||||
|
|
||||||
update_chapters()
|
|
||||||
update_lessons()
|
|
||||||
|
|
||||||
def update_chapters():
|
|
||||||
courses = frappe.get_all("LMS Course", pluck="name")
|
|
||||||
for course in courses:
|
|
||||||
course_details = frappe.get_doc("LMS Course", course)
|
|
||||||
chapters = frappe.get_all("Chapter",
|
|
||||||
{
|
|
||||||
"course": course
|
|
||||||
},
|
|
||||||
["name"],
|
|
||||||
order_by= "index_"
|
|
||||||
)
|
|
||||||
for chapter in chapters:
|
|
||||||
course_details.append("chapters",
|
|
||||||
{
|
|
||||||
"chapter": chapter.name
|
|
||||||
})
|
|
||||||
|
|
||||||
course_details.save()
|
|
||||||
|
|
||||||
def update_lessons():
|
|
||||||
chapters = frappe.get_all("Chapter", pluck="name")
|
|
||||||
for chapter in chapters:
|
|
||||||
chapter_details = frappe.get_doc("Chapter", chapter)
|
|
||||||
lessons = frappe.get_all("Lesson",
|
|
||||||
{
|
|
||||||
"chapter": chapter
|
|
||||||
},
|
|
||||||
["name"],
|
|
||||||
order_by= "index_"
|
|
||||||
)
|
|
||||||
for lesson in lessons:
|
|
||||||
chapter_details.append("lessons",
|
|
||||||
{
|
|
||||||
"lesson": lesson.name
|
|
||||||
})
|
|
||||||
|
|
||||||
chapter_details.save()
|
|
||||||
@@ -22,14 +22,6 @@
|
|||||||
--control-bg: var(--gray-100);
|
--control-bg: var(--gray-100);
|
||||||
--muted-text: #4C5A67;
|
--muted-text: #4C5A67;
|
||||||
--button-background: #EEF0F2;
|
--button-background: #EEF0F2;
|
||||||
--text-xs: 11px;
|
|
||||||
--text-sm: 12px;
|
|
||||||
--text-md: 13px;
|
|
||||||
--text-base: 14px;
|
|
||||||
--text-lg: 16px;
|
|
||||||
--text-xl: 18px;
|
|
||||||
--text-2xl: 20px;
|
|
||||||
--text-3xl: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -84,10 +76,82 @@ img.profile-photo {
|
|||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
border: 1px dashed var(--text-color);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msger-inputarea {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
border-top: 2px solid #ddd;
|
||||||
|
background: #eee;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msger-inputarea * {
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msger-input {
|
||||||
|
flex: 1;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-section {
|
||||||
|
margin-left: 3%;
|
||||||
|
display: inline-block;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-4 {
|
||||||
|
color: #2D005A;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 51px;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
padding: 5rem 0 5rem 0;
|
padding: 5rem 0 5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.messages-container {
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
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 {
|
.batch-header {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
@@ -127,6 +191,33 @@ input[type=checkbox] {
|
|||||||
appearance: auto;
|
appearance: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.progress-image {
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
@@ -143,22 +234,6 @@ input[type=checkbox] {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-image {
|
|
||||||
background-color: var(--avatar-frame-bg);
|
|
||||||
color: var(--avatar-frame-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-image-text {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
align-self: center;
|
|
||||||
justify-content: normal;
|
|
||||||
font-size: 7rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-tags {
|
.course-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -185,7 +260,6 @@ input[type=checkbox] {
|
|||||||
.common-page-style {
|
.common-page-style {
|
||||||
background: #F4F5F6;
|
background: #F4F5F6;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
min-height: 60vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.common-card-style {
|
.common-card-style {
|
||||||
@@ -243,7 +317,7 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.card-divider {
|
.card-divider {
|
||||||
border: 1px solid #F4F5F6;
|
border: 1px solid #F4F5F6;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-divider-dark {
|
.card-divider-dark {
|
||||||
@@ -316,7 +390,7 @@ input[type=checkbox] {
|
|||||||
letter-spacing: -0.0175em
|
letter-spacing: -0.0175em
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 576px) and (max-width: 992px) {
|
@media (min-width: 576px) {
|
||||||
.container {
|
.container {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
@@ -397,30 +471,23 @@ input[type=checkbox] {
|
|||||||
--star-fill: #74808B;
|
--star-fill: #74808B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox {
|
div.custom-checkbox>label>input {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox>label>input {
|
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox>label>.empty-checkbox {
|
div.custom-checkbox>label>img {
|
||||||
height: 1.5rem;
|
height: 20px;
|
||||||
width: 1.5rem;
|
width: 20px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox>label>input:checked+.empty-checkbox {
|
div.custom-checkbox>label>input:checked+img {
|
||||||
background: url(/assets/community/icons/tick.svg);
|
background: url(/assets/community/images/Vector.png);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
}
|
background-size: 15px 15px;
|
||||||
|
object-fit: contain;
|
||||||
.quiz-label {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-card-wide {
|
.course-card-wide {
|
||||||
@@ -534,7 +601,7 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
box-shadow: var(--btn-shadow);
|
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.13), 0px 0px 0.5px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -545,11 +612,6 @@ input[type=checkbox] {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 135%;
|
line-height: 135%;
|
||||||
letter-spacing: -0.011em;
|
letter-spacing: -0.011em;
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wide-button {
|
.wide-button {
|
||||||
@@ -575,11 +637,6 @@ input[type=checkbox] {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-default {
|
|
||||||
background: #98A1A9;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.video-preview {
|
.video-preview {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -598,6 +655,7 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.course-home-outline {
|
.course-home-outline {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-title {
|
.small-title {
|
||||||
@@ -606,18 +664,15 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.chapter-title {
|
.chapter-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 0 .25rem 0;
|
margin: 0 .25rem .5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-description {
|
.chapter-description {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-icon {
|
.chapter-icon {
|
||||||
@@ -625,39 +680,26 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-outline-instructor-parent {
|
.course-outline-instructor-parent {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-gap: 2rem;
|
justify-content: space-between;
|
||||||
grid-template-columns: 4fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.course-outline-instructor-parent {
|
|
||||||
grid-gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.course-outline-instructor-parent {
|
.course-outline-instructor-parent {
|
||||||
|
flex-direction: column;
|
||||||
padding: 0px 24px 0px;
|
padding: 0px 24px 0px;
|
||||||
grid-template-columns: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.profile-parent-section {
|
.profile-parent-section {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-gap: 2rem;
|
justify-content: space-between;
|
||||||
grid-template-columns: 4fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.profile-parent-section {
|
|
||||||
grid-gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.profile-parent-section {
|
.profile-parent-section {
|
||||||
grid-template-columns: none;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +731,18 @@ input[type=checkbox] {
|
|||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.course-overview-section {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.course-overview-section {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lesson-info {
|
.lesson-info {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 250%;
|
line-height: 250%;
|
||||||
@@ -710,13 +764,6 @@ input[type=checkbox] {
|
|||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-content-parent .lesson-links {
|
|
||||||
padding: 0 0 0 1rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 200%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter-content {
|
.chapter-content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-left: .875rem;
|
margin-left: .875rem;
|
||||||
@@ -729,7 +776,7 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lessons {
|
.lessons {
|
||||||
margin-bottom: 1rem;
|
margin: 12px 0px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-buttons {
|
.course-buttons {
|
||||||
@@ -853,18 +900,14 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.description-card {
|
.description-card {
|
||||||
padding: 1.5rem;
|
padding: 24px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-card {
|
.overview-card {
|
||||||
padding: 1.5rem;
|
padding: 24px 26px 24px;
|
||||||
width: 256px;
|
width: 256px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
display: grid;
|
|
||||||
-moz-column-gap: 1rem;
|
|
||||||
column-gap: 1rem;
|
|
||||||
row-gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -897,6 +940,10 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overtime-item {
|
||||||
|
margin: 16px 0px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.view-all-mentors {
|
.view-all-mentors {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -943,24 +990,8 @@ input[type=checkbox] {
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quiz-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question {
|
.question {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 688px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-question .card-divider {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-links {
|
.dark-links {
|
||||||
@@ -981,7 +1012,8 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-details-outline {
|
.course-details-outline {
|
||||||
margin-top: 1rem;
|
margin-top: 16px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-content-card {
|
.lesson-content-card {
|
||||||
@@ -990,20 +1022,19 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-content-parent {
|
.course-content-parent {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-gap: 2rem;
|
justify-content: space-between;
|
||||||
grid-template-columns: 2fr minmax(600px, 5fr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 768px) {
|
||||||
.course-content-parent {
|
.course-content-parent {
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-content-parent .course-home-headings {
|
.course-content-parent .course-home-headings {
|
||||||
margin: 0px 0px 1rem;
|
margin: 0px 0px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-pagination {
|
.lesson-pagination {
|
||||||
@@ -1013,7 +1044,8 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lesson-pagination-parent {
|
.lesson-pagination-parent {
|
||||||
margin-top: 1rem;
|
margin: 1rem 0px 0px 2rem;
|
||||||
|
flex: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -1097,7 +1129,7 @@ input[type=checkbox] {
|
|||||||
line-height: 156%;
|
line-height: 156%;
|
||||||
letter-spacing: -0.0175em;
|
letter-spacing: -0.0175em;
|
||||||
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.64);
|
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.64);
|
||||||
padding: 1.5rem;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 375px) {
|
@media (max-width: 375px) {
|
||||||
@@ -1121,7 +1153,7 @@ input[type=checkbox] {
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
box-shadow: 0px 1px 1px rgb(0 0 0 / 16%);
|
box-shadow: 0px 1px 1px rgb(0 0 0 / 16%);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 2rem;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 375px) {
|
@media (max-width: 375px) {
|
||||||
@@ -1137,19 +1169,6 @@ input[type=checkbox] {
|
|||||||
left: 174px;
|
left: 174px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 165%;
|
line-height: 165%;
|
||||||
width: 85%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.profile-profession {
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.profile-profession {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
@@ -1163,11 +1182,11 @@ input[type=checkbox] {
|
|||||||
.profile-profession {
|
.profile-profession {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
left: 70px;
|
left: 70px;
|
||||||
width: 75%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-icons {
|
.social-icons {
|
||||||
|
float: right;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1197,6 +1216,12 @@ input[type=checkbox] {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.course-creator-progress-parent {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.course-creator-section {
|
.course-creator-section {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
@@ -1246,89 +1271,3 @@ pre {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-source h1 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-source h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-source h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-source h4 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thread-card {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.form-control {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discussion-on-page textarea.form-control {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
color: inherit;
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-field {
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thread-title {
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-author {
|
|
||||||
color: #192734;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.certificate-page .common-card-style {
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: Inter;
|
|
||||||
color: black;
|
|
||||||
font-size: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5rem;
|
|
||||||
background-image: url(/assets/community/images/certificate-background.png);
|
|
||||||
}
|
|
||||||
|
|
||||||
.certificate-heading {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.certificate-para {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.certificate-page .common-card-style {
|
|
||||||
padding: 2rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.certificate-heading {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.certificate-heading {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M13.0746 3.60967C12.7814 3.31071 12.4333 3.07355 12.0501 2.91174C11.6669 2.74994 11.2562 2.66666 10.8415 2.66666C10.4267 2.66666 10.016 2.74994 9.63283 2.91174C9.24966 3.07355 8.90152 3.31071 8.60831 3.60967L7.99979 4.22983L7.39126 3.60967C6.79899 3.00607 5.9957 2.66697 5.1581 2.66697C4.32051 2.66697 3.51721 3.00607 2.92494 3.60967C2.33267 4.21327 1.99994 5.03192 1.99994 5.88554C1.99994 6.73916 2.33267 7.55782 2.92494 8.16142L3.53347 8.78158L7.99979 13.3333L12.4661 8.78158L13.0746 8.16142C13.368 7.86259 13.6007 7.5078 13.7595 7.11729C13.9182 6.72679 13.9999 6.30824 13.9999 5.88554C13.9999 5.46284 13.9182 5.04429 13.7595 4.65379C13.6007 4.26329 13.368 3.90849 13.0746 3.60967V3.60967Z" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 871 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M6.00001 3.00001H12L15.5 3C16.6046 3 17.5 3.89543 17.5 5V9.78889V12.2556C17.5 13.3601 16.6046 14.2556 15.5 14.2556H14.5C14.2239 14.2556 14 14.4794 14 14.7556V16.4507C14 16.8715 13.5119 17.1041 13.185 16.839L10.2754 14.4789C10.0972 14.3344 9.87483 14.2556 9.64544 14.2556H4.49997C3.39539 14.2556 2.49995 13.3601 2.49997 12.2555L2.50001 9.78889V5.00001C2.50001 3.89544 3.39544 3.00001 4.50001 3.00001L6.00001 3.00001Z" stroke="#1F272E" stroke-miterlimit="10" stroke-linecap="square"/>
|
|
||||||
<path d="M6 6.5H13" stroke="#1F272E" stroke-miterlimit="10" stroke-linecap="round"/>
|
|
||||||
<path d="M6 9H10" stroke="#1F272E" stroke-miterlimit="10" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 763 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="#68D391" stroke="#68D391" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M8 12H16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 425 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 309 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M8 4V12" stroke="white" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M4 8H12" stroke="white" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 313 B |
@@ -1 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
<svg class="icon">
|
||||||
|
<use href="#icon-tick"></use>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 58 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM6.0429 6.04289C6.43342 5.65237 7.06659 5.65237 7.45711 6.04289L10.0011 8.58692L12.5451 6.04294C12.9356 5.65242 13.5688 5.65242 13.9593 6.04294C14.3499 6.43347 14.3499 7.06663 13.9593 7.45716L11.4154 10.0011L13.9593 12.5451C14.3499 12.9356 14.3499 13.5688 13.9593 13.9593C13.5688 14.3499 12.9357 14.3499 12.5451 13.9593L10.0011 11.4154L7.45711 13.9594C7.06659 14.3499 6.43342 14.3499 6.0429 13.9594C5.65237 13.5689 5.65237 12.9357 6.0429 12.5452L8.58693 10.0011L6.0429 7.45711C5.65237 7.06658 5.65237 6.43342 6.0429 6.04289Z" fill="#F56B6B"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 808 B |
|
Before Width: | Height: | Size: 3.2 MiB |
@@ -1,32 +0,0 @@
|
|||||||
<div class="common-card-style">
|
|
||||||
<div class="certificate-heading">
|
|
||||||
Certificate of Completion
|
|
||||||
</div>
|
|
||||||
<div class="certificate-para">
|
|
||||||
This is to certify that <span class="font-weight-bold">{{ student.full_name }}</span> has successfully completed
|
|
||||||
<span class="font-weight-bold">{{ course.title }}</span> online course on
|
|
||||||
<span class="font-weight-bold">{{ frappe.utils.format_date(certificate.issue_date, "medium") }}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between;" class="certificate-footer">
|
|
||||||
<div>
|
|
||||||
<div class="font-weight-bold">
|
|
||||||
Instructor:
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ instructor.full_name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-weight-bold">
|
|
||||||
Expiry Date:
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<img src="{{ logo }}" style="height: 50px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="/assets/community/js/html2canvas.js"></script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<div>
|
|
||||||
{% set site_link = "<a href='" + site_url + "'>" + app_name + "</a>" %}
|
|
||||||
<p>{{ _("Hi {0},").format(first_name) }}</p>
|
|
||||||
<br>
|
|
||||||
<p>{{ _("The course {0} is now available on {1}.").format(frappe.bold(title), app_name) }}</p>
|
|
||||||
<br>
|
|
||||||
<p>Click on the link below to start learning.</p>
|
|
||||||
<p style="margin: 15px 0px;">
|
|
||||||
<a href="{{ course_link }}" rel="nofollow" class="btn btn-primary">{{ _("Start Learning") }}</a>
|
|
||||||
</p>
|
|
||||||
<br>
|
|
||||||
<p>
|
|
||||||
{{ _("You can also copy-paste following link in your browser") }}<br>
|
|
||||||
<a href="{{ course_link }}">{{ site_url }}{{ course_link }}</a>
|
|
||||||
</p>
|
|
||||||
<br>
|
|
||||||
<p>Thanks and Regards,</p>
|
|
||||||
<p>{{ app_name }}</p>
|
|
||||||
</div>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<div class="common-card-style thread-card mb-5">
|
|
||||||
<div class="mb-4">
|
|
||||||
{% set member = frappe.get_doc("User", message.owner) %}
|
|
||||||
{{ widgets.Avatar(member=member, avatar_class="avatar-small")}}
|
|
||||||
<a class="button-links" href="/{{ member.username }}">
|
|
||||||
<span class="message-author">{{ member.full_name }}</span>
|
|
||||||
</a>
|
|
||||||
<span class="muted-text pull-right">{{ frappe.utils.format_datetime(message.creation, "dd MMM hh:mm") }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-divider"></div>
|
|
||||||
<div>{{ message.message }}</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,59 +1,33 @@
|
|||||||
<div id="quiz-title" class="course-home-headings">{{ quiz.title }}</div>
|
{% set last_submission = quiz.get_last_submission_details() %}
|
||||||
|
{% if last_submission %}
|
||||||
<div class="card-divider"></div>
|
<div class="mb-5 pull-right">
|
||||||
|
<div class="text-muted">Last Submitted On: {{ frappe.utils.pretty_date(last_submission.creation) }}</div>
|
||||||
<div class="mt-5">
|
<div class="text-muted">Last Submission Score: {{ last_submission.score }}</div>
|
||||||
<form id="quiz-form">
|
</div>
|
||||||
<div class="questions">
|
{% endif %}
|
||||||
|
<h3 id="title" class="mb-5">{{ quiz.title }}</h3>
|
||||||
|
<form id="quiz-form">
|
||||||
{% for question in quiz.questions %}
|
{% for question in quiz.questions %}
|
||||||
<div class="question {% if loop.index == 1 %} active-question {% else %} hide {% endif %}"
|
<div class="question mb-5" data-question="{{ question.question }}"
|
||||||
data-question="{{ question.question }}" data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}">
|
data-multi="{{ question.multiple_correct_answers}}">
|
||||||
<p>{{ frappe.utils.md_to_html(question.question) }}</p>
|
<p> {{ loop.index }}. {{ question.question }}</p>
|
||||||
|
|
||||||
{% if question.multiple %}
|
|
||||||
<small class="font-weight-bold">Choose all answers that apply:</small>
|
|
||||||
{% else %}
|
|
||||||
<small class="font-weight-bold">Choose 1 answer:</small>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="card-divider"></div>
|
|
||||||
|
|
||||||
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
||||||
|
|
||||||
{% for option in options %}
|
{% for option in options %}
|
||||||
{% if option %}
|
{% if option %}
|
||||||
|
<div class="custom-checkbox mb-2">
|
||||||
<div class="custom-checkbox">
|
<label>
|
||||||
<label class="quiz-label">
|
<input {% if question.multiple %} type="checkbox" {% else %} type="radio"
|
||||||
<input class="option" value="{{ option | urlencode }}"
|
name="{{ question.question | urlencode }}" {% endif %} class="option" value="{{ option | urlencode }}">
|
||||||
data-correct="{{ question['is_correct_' + loop.index | string] }}"
|
<img />
|
||||||
{% if question.multiple %} type="checkbox"
|
|
||||||
{% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
|
|
||||||
<img class="empty-checkbox mr-3"/>
|
|
||||||
</label>
|
</label>
|
||||||
<span class="label-area">{{ frappe.utils.md_to_html(option) }}</span>
|
<span class="label-area">{{ option }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set explanation = question['explanation_' + loop.index | string] %}
|
|
||||||
{% if explanation %}
|
|
||||||
<small class="explanation muted-text hide">{{ explanation }}</small>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="card-divider"></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
<button class="btn btn-secondary hide mb-5" id="try-again">Try Again</button>
|
||||||
<div class="quiz-footer">
|
<button class="btn btn-primary" id="submit-quiz">Submit</button>
|
||||||
<span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span>
|
|
||||||
<button class="btn btn-primary pull-right" id="check" disabled>Check</button>
|
|
||||||
<button class="btn btn-primary hide" id="next">Next</button>
|
|
||||||
<button class="btn btn-primary hide" id="summary">Summary</button>
|
|
||||||
</div>
|
|
||||||
<div class="button is-secondary pull-right hide" id="try-again">Try Again</div>
|
|
||||||
<h4 class="success-message"></h4>
|
<h4 class="success-message"></h4>
|
||||||
<h5 class="score text-muted"></h5>
|
<h5 class="score text-muted"></h5>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|||||||
48
community/www/batch/discuss.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "templates/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Discuss{% 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 %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{{ widgets.BatchTabs(course=course, membership=membership) }}
|
||||||
|
<div class="messages-container mt-5">
|
||||||
|
{{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
|
||||||
|
<ol class="messages">
|
||||||
|
{{ Messages(messages) }}
|
||||||
|
</ol>
|
||||||
|
{{ TextArea() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% macro Messages(messages) %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<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="">
|
||||||
|
{{ frappe.utils.format_datetime(message.creation, "dd-mm-yyyy HH:mm") }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="message-para">
|
||||||
|
{{ message.message }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro TextArea() %}
|
||||||
|
<form class="msger-inputarea mb-1">
|
||||||
|
<input type="text" class="msger-input" placeholder="Write your message...">
|
||||||
|
<button type="submit" class="btn btn-primary msger-send-btn" data-batch="{{batch.name | urlencode }}">Send</button>
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
||||||
35
community/www/batch/discuss.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
frappe.ready(() => {
|
||||||
|
const assets = [
|
||||||
|
"/assets/frappe/js/lib/socket.io.min.js",
|
||||||
|
"/assets/frappe/js/frappe/socketio_client.js",
|
||||||
|
]
|
||||||
|
frappe.require(assets, () => {
|
||||||
|
if (window.dev_server) {
|
||||||
|
frappe.boot.socketio_port = "9000" //use socketio port shown when bench starts
|
||||||
|
}
|
||||||
|
frappe.socketio.init(9000);
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
var message_element = document.getElementsByClassName("messages")[0]
|
||||||
|
message_element.scrollTo(0, message_element.scrollHeight);
|
||||||
|
document.getElementsByClassName("messages-container")[0].scrollIntoView({block: "center"})
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
$(".msger-send-btn").click((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
var message = $(".msger-input").val().trim();
|
||||||
|
if (message) {
|
||||||
|
frappe.call({
|
||||||
|
"method": "community.lms.doctype.lms_batch.lms_batch.save_message",
|
||||||
|
"args": {
|
||||||
|
"batch": decodeURIComponent($(e.target).attr("data-batch")),
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(".msger-input").val("");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
8
community/www/batch/discuss.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import frappe
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
utils.get_common_context(context)
|
||||||
|
context.messages = context.batch.get_messages()
|
||||||
|
if not context.membership:
|
||||||
|
utils.redirect_to_lesson(context.course)
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||||
{% block title %} {{ lesson.title }} - {{ course.title }} {% endblock %}
|
{% block title %}{{ lesson.title }}{% endblock %}
|
||||||
|
|
||||||
{% block head_include %}
|
{% block head_include %}
|
||||||
|
<meta name="description" content="{{lesson.title}} - {{course.title}}" />
|
||||||
|
<meta name="keywords" content="{{lesson.title}} - {{course.title}}" />
|
||||||
|
<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="/assets/css/lms.css">
|
<link rel="stylesheet" href="/assets/css/lms.css">
|
||||||
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
|
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
|
||||||
@@ -25,13 +30,8 @@
|
|||||||
<div class="lesson-pagination-parent">
|
<div class="lesson-pagination-parent">
|
||||||
{{ LessonContent(lesson) }}
|
{{ LessonContent(lesson) }}
|
||||||
{% if membership %}
|
{% if membership %}
|
||||||
{{ pagination(prev_url, next_url) }}
|
{{ pagination(prev_chap, prev_url, next_chap, next_url) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set title = lesson.title + " - " + course.title %}
|
|
||||||
{{ widgets.DiscussionMessage(doctype="Lesson", docname=lesson.name,
|
|
||||||
title=title, condition=membership, button_name="Start Learning",
|
|
||||||
redirect_to="/courses/" + course.name) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,23 +47,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if membership or lesson.include_in_preview %}
|
{% if membership or lesson.include_in_preview %}
|
||||||
<div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div>
|
<div class="common-card-style lesson-content-card from-markdown">{{ lesson.render_html() }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="common-card-style lesson-content-card">
|
<div class="no-preview-message">
|
||||||
<span>This lesson is not available for Preview. Please join the course to access this lesson. <a
|
<span>This lesson is not available for Preview. Please join the course to access this lesson.</span>
|
||||||
href="/courses/{{ course.name }}">Checkout Course Details.</a></span>
|
<a href="/courses/{{ course.name }}">Checkout Course Details.</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</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">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if prev_url %}
|
{% if prev_url %}
|
||||||
<a class="button is-secondary dark-links prev" href="{{ prev_url }}">
|
<a class="button is-secondary dark-links" href="{{ prev_url }}">
|
||||||
<img class="mr-2" src="/assets/community/icons/left-arrow.svg">
|
<img class="mr-2" src="/assets/community/icons/left-arrow.svg">
|
||||||
Prev
|
Prev
|
||||||
</a>
|
</a>
|
||||||
@@ -87,14 +87,10 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if next_url %}
|
{% if next_url %}
|
||||||
<a class="button is-primary next" href="{{ next_url }}">
|
<a class="button is-primary" href="{{ next_url }}">
|
||||||
Next
|
Next
|
||||||
<img class="ml-2" src="/assets/community/icons/side-arrow-white.svg">
|
<img class="ml-2" src="/assets/community/icons/side-arrow-white.svg">
|
||||||
</a>
|
</a>
|
||||||
{% elif course.enable_certification %}
|
|
||||||
<div class="button is-primary {% if course.get_course_progress() != 100 %} hide {% endif %}" id="certification">
|
|
||||||
Get Certificate
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,19 @@
|
|||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
|
|
||||||
localStorage.removeItem($("#quiz-title").text());
|
|
||||||
|
|
||||||
save_current_lesson();
|
save_current_lesson();
|
||||||
|
|
||||||
$(".option").click((e) => {
|
|
||||||
enable_check(e);
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#progress").click((e) => {
|
$("#progress").click((e) => {
|
||||||
mark_progress(e);
|
mark_progress(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#summary").click((e) => {
|
$("#submit-quiz").click((e) => {
|
||||||
quiz_summary(e);
|
submit_quiz(e);
|
||||||
});
|
|
||||||
|
|
||||||
$("#check").click((e) => {
|
|
||||||
check_answer(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#next").click((e) => {
|
|
||||||
mark_active_question(e);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#try-again").click((e) => {
|
$("#try-again").click((e) => {
|
||||||
try_quiz_again(e);
|
try_quiz_again(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#certification").click((e) => {
|
|
||||||
create_certificate(e);
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var save_current_lesson = () => {
|
var save_current_lesson = () => {
|
||||||
@@ -43,28 +25,6 @@ var save_current_lesson = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var enable_check = (e) => {
|
|
||||||
if ($(".option:checked").length && $("#check").attr("disabled")) {
|
|
||||||
$("#check").removeAttr("disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mark_active_question = (e = undefined) => {
|
|
||||||
var current_index;
|
|
||||||
var next_index = 1;
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
current_index = $(".active-question").attr("data-qt-index");
|
|
||||||
next_index = parseInt(current_index) + 1;
|
|
||||||
}
|
|
||||||
$(".question").addClass("hide").removeClass("active-question");
|
|
||||||
$(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
|
|
||||||
$(".current-question").text(`${next_index}`);
|
|
||||||
$("#check").removeClass("hide").attr("disabled", true);
|
|
||||||
$("#next").addClass("hide");
|
|
||||||
$(".explanation").addClass("hide");
|
|
||||||
}
|
|
||||||
|
|
||||||
var mark_progress = (e) => {
|
var mark_progress = (e) => {
|
||||||
var status = $(e.currentTarget).attr("data-progress");
|
var status = $(e.currentTarget).attr("data-progress");
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -75,9 +35,8 @@ var mark_progress = (e) => {
|
|||||||
status: status
|
status: status
|
||||||
},
|
},
|
||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
|
if (data.message == "OK") {
|
||||||
change_progress_indicators(status, e);
|
change_progress_indicators(status, e);
|
||||||
if (data.message == 100 && !$(".next").length && $("#certification").hasClass("hide")) {
|
|
||||||
$("#certification").removeClass("hide");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -97,91 +56,47 @@ var change_progress_indicators = (status, e) => {
|
|||||||
$(e.currentTarget).text(label).attr("data-progress", data_progress);
|
$(e.currentTarget).text(label).attr("data-progress", data_progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
var quiz_summary = (e) => {
|
var submit_quiz = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var quiz_name = $("#quiz-title").text();
|
var result = [];
|
||||||
var total_questions = $(".question").length;
|
$('.question').each((i, element) => {
|
||||||
|
var options = $(element).find(".option");
|
||||||
|
var answers = [];
|
||||||
|
options.filter((i, op) => $(op).prop("checked")).each((i, elem) => answers.push(decodeURIComponent(elem.value)));
|
||||||
|
result.push({
|
||||||
|
"question": element.dataset.question,
|
||||||
|
"answer": answers
|
||||||
|
});
|
||||||
|
});
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "community.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
|
method: "community.lms.doctype.lms_quiz.lms_quiz.submit",
|
||||||
args: {
|
args: {
|
||||||
"quiz": quiz_name,
|
quiz: $("#title").text(),
|
||||||
"results": localStorage.getItem(quiz_name)
|
result: result
|
||||||
},
|
},
|
||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
var message = data.message == total_questions ? "Excellent Work" : "You were almost there."
|
$("#submit-quiz").addClass("hide");
|
||||||
$(".question").addClass("hide");
|
|
||||||
$(".quiz-footer").addClass("hide");
|
|
||||||
$("#quiz-form").parent().prepend(
|
|
||||||
`<div class="text-center summary"><h2>${message} 👏 </h2>
|
|
||||||
<div class="font-weight-bold">${data.message}/${total_questions} correct.</div></div>`);
|
|
||||||
$("#try-again").removeClass("hide");
|
$("#try-again").removeClass("hide");
|
||||||
|
$(":input[type='checkbox']").prop("disabled", true);
|
||||||
|
$(":input[type='radio']").prop("disabled", true);
|
||||||
|
if (data.message == result.length) {
|
||||||
|
$(".success-message").text("Congratulations, you cleared the quiz!");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(".success-message").text("Some of your answers weren't correct. You can give it another shot.");
|
||||||
|
}
|
||||||
|
$(".score").text(`Score: ${data.message}/${result.length}`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var try_quiz_again = (e) => {
|
var try_quiz_again = (e) => {
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
var check_answer = (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
$(":input[type='checkbox']").prop("disabled", false);
|
||||||
var quiz_name = $("#quiz-title").text();
|
$(":input[type='radio']").prop("disabled", false);
|
||||||
var total_questions = $(".question").length;
|
$("#quiz-form").trigger("reset");
|
||||||
var current_index = $(".active-question").attr("data-qt-index");
|
$(".success-message").text("");
|
||||||
|
$(".score").text("");
|
||||||
$(".explanation").removeClass("hide");
|
$("#submit-quiz").removeClass("hide");
|
||||||
$("#check").addClass("hide");
|
$("#try-again").addClass("hide");
|
||||||
current_index == total_questions ? $("#summary").removeClass("hide") : $("#next").removeClass("hide");
|
|
||||||
|
|
||||||
var [answer, is_correct] = parse_options();
|
|
||||||
add_to_local_storage(quiz_name, current_index, answer, is_correct)
|
|
||||||
}
|
|
||||||
|
|
||||||
var parse_options = () => {
|
|
||||||
var answer = [];
|
|
||||||
var is_correct = [];
|
|
||||||
$(".active-question input").each((i, element) => {
|
|
||||||
var correct = parseInt($(element).attr("data-correct"));
|
|
||||||
if ($(element).prop("checked")) {
|
|
||||||
answer.push(decodeURIComponent($(element).val()));
|
|
||||||
correct && is_correct.push(1);
|
|
||||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
correct && is_correct.push(0);
|
|
||||||
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return [answer, is_correct];
|
|
||||||
}
|
|
||||||
|
|
||||||
var add_icon = (element, icon) => {
|
|
||||||
$(element).parent().empty().html(`<img class="mr-3" src="/assets/community/icons/${icon}.svg">`);
|
|
||||||
}
|
|
||||||
|
|
||||||
var add_to_local_storage = (quiz_name, current_index, answer, is_correct) => {
|
|
||||||
var quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
|
||||||
var quiz_obj = {
|
|
||||||
"question_index": current_index,
|
|
||||||
"answer": answer.join(),
|
|
||||||
"is_correct": is_correct
|
|
||||||
}
|
|
||||||
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
|
|
||||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
|
|
||||||
}
|
|
||||||
|
|
||||||
var create_certificate = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
course = $(".title").attr("data-course");
|
|
||||||
frappe.call({
|
|
||||||
method: "community.lms.doctype.lms_certification.lms_certification.create_certificate",
|
|
||||||
args: {
|
|
||||||
"course": course
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
window.location.href = `/courses/${course}/${data.message}`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,26 +12,29 @@ def get_context(context):
|
|||||||
lesson_index = frappe.form_dict.get("lesson")
|
lesson_index = frappe.form_dict.get("lesson")
|
||||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||||
|
|
||||||
|
course_name = context.course.name
|
||||||
if not chapter_index or not lesson_index:
|
if not chapter_index or not lesson_index:
|
||||||
if context.batch:
|
if context.batch:
|
||||||
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
|
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
|
||||||
else:
|
else:
|
||||||
index_ = "1.1"
|
index_ = "1.1"
|
||||||
utils.redirect_to_lesson(context.course, index_)
|
frappe.local.flags.redirect_location = context.course.get_learn_url(index_) + context.course.query_parameter
|
||||||
|
raise frappe.Redirect
|
||||||
|
|
||||||
context.lesson = get_current_lesson_details(lesson_number, context)
|
context.lesson = context.course.get_lesson(chapter_index, lesson_index)
|
||||||
neighbours = context.course.get_neighbours(lesson_number, context.lessons)
|
context.lesson_index = lesson_index
|
||||||
context.next_url = get_learn_url(neighbours["next"], context.course)
|
context.chapter_index = chapter_index
|
||||||
context.prev_url = get_learn_url(neighbours["prev"], context.course)
|
|
||||||
|
|
||||||
meta_info = context.lesson.title + " - " + context.course.title
|
outline = context.course.get_outline()
|
||||||
context.metatags = {
|
prev_ = outline.get_prev(lesson_number)
|
||||||
"title": meta_info,
|
next_ = outline.get_next(lesson_number)
|
||||||
"keywords": meta_info,
|
context.prev_chap = get_chapter_title(course_name, prev_)
|
||||||
"description": meta_info
|
context.next_chap = get_chapter_title(course_name, next_)
|
||||||
}
|
context.next_url = context.course.get_learn_url(next_) and context.course.get_learn_url(next_) + context.course.query_parameter
|
||||||
|
context.prev_url = context.course.get_learn_url(prev_) and context.course.get_learn_url(prev_) + context.course.query_parameter
|
||||||
|
|
||||||
context.page_extensions = get_page_extensions()
|
context.page_extensions = get_page_extensions()
|
||||||
|
|
||||||
context.page_context = {
|
context.page_context = {
|
||||||
"course": context.course.name,
|
"course": context.course.name,
|
||||||
"batch": context.get("batch") and context.batch.name,
|
"batch": context.get("batch") and context.batch.name,
|
||||||
@@ -39,15 +42,6 @@ def get_context(context):
|
|||||||
"is_member": context.membership is not None
|
"is_member": context.membership is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_current_lesson_details(lesson_number, context):
|
|
||||||
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
|
|
||||||
if not len(details_list):
|
|
||||||
utils.redirect_to_lesson(context.course)
|
|
||||||
return details_list[0]
|
|
||||||
|
|
||||||
def get_learn_url(lesson_number, course):
|
|
||||||
return course.get_learn_url(lesson_number) and course.get_learn_url(lesson_number) + course.query_parameter
|
|
||||||
|
|
||||||
def get_chapter_title(course_name, lesson_number):
|
def get_chapter_title(course_name, lesson_number):
|
||||||
if not lesson_number:
|
if not lesson_number:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ def get_common_context(context):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
batch_name = None
|
batch_name = None
|
||||||
|
|
||||||
course = frappe.get_doc("LMS Course", course_name)
|
course = Course.find(course_name)
|
||||||
if not course:
|
if not course:
|
||||||
context.template = "www/404.html"
|
context.template = "www/404.html"
|
||||||
return
|
return
|
||||||
context.course = course
|
context.course = course
|
||||||
context.lessons = course.get_lessons()
|
|
||||||
membership = course.get_membership(frappe.session.user, batch_name)
|
membership = course.get_membership(frappe.session.user, batch_name)
|
||||||
context.membership = membership
|
context.membership = membership
|
||||||
if membership:
|
if membership:
|
||||||
@@ -24,6 +24,9 @@ def get_common_context(context):
|
|||||||
if batch:
|
if batch:
|
||||||
context.batch = batch
|
context.batch = batch
|
||||||
|
|
||||||
|
context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch)
|
||||||
|
context.member_count = len(context.members)
|
||||||
|
|
||||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||||
context.livecode_url = get_livecode_url()
|
context.livecode_url = get_livecode_url()
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{% extends "templates/base.html" %}
|
|
||||||
{% from "www/macros/common_macro.html" import MentorsSection %}
|
|
||||||
|
|
||||||
{% block title %} {{ student.full_name }} - {{ course.title }} {% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="common-page-style">
|
|
||||||
<div class="container certificate-page">
|
|
||||||
|
|
||||||
<div class="breadcrumb">
|
|
||||||
<a class="dark-links" href="/courses">All Courses</a>
|
|
||||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
|
||||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="comment-footer mb-5">
|
|
||||||
<div class="button is-secondary pull-right" id="export-as-pdf" data-certificate="{{ certificate.name }}"
|
|
||||||
data-certificate-name="{{ student.full_name }} - {{ course.title }}">Export</div>
|
|
||||||
</div>
|
|
||||||
{% include "community/templates/certificate.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
frappe.ready(() => {
|
|
||||||
|
|
||||||
if ($(document).width() <= 550) {
|
|
||||||
$(".certificate-footer").css("flex-direction", "column");
|
|
||||||
$(".certificate-footer").children().addClass("mb-5");
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#export-as-pdf").click((e) => {
|
|
||||||
export_as_pdf(e);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var export_as_pdf = (e) => {
|
|
||||||
var button = $(e.currentTarget);
|
|
||||||
button.text(__("Exporting..."));
|
|
||||||
|
|
||||||
html2canvas(document.querySelector('.common-card-style'), {
|
|
||||||
scrollY: -window.scrollY,
|
|
||||||
scrollX: 0
|
|
||||||
}).then(function(canvas) {
|
|
||||||
let dataURL = canvas.toDataURL('image/png');
|
|
||||||
let a = document.createElement('a');
|
|
||||||
a.href = dataURL;
|
|
||||||
a.download = button.attr("data-certificate-name");
|
|
||||||
a.click();
|
|
||||||
}).finally(() => {
|
|
||||||
button.text(__("Export"))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
context.no_cache = 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
course_name = frappe.form_dict["course"]
|
|
||||||
certificate_name = frappe.form_dict["certificate"]
|
|
||||||
except KeyError:
|
|
||||||
redirect_to_course_list()
|
|
||||||
|
|
||||||
context.certificate = frappe.db.get_value("LMS Certification", certificate_name,
|
|
||||||
["name", "student", "issue_date", "expiry_date", "course"], as_dict=True)
|
|
||||||
|
|
||||||
if context.certificate.course != course_name:
|
|
||||||
redirect_to_course_list()
|
|
||||||
|
|
||||||
context.course = frappe.db.get_value("LMS Course", course_name,
|
|
||||||
["owner", "title", "name"], as_dict=True)
|
|
||||||
|
|
||||||
context.instructor = frappe.db.get_value("User", context.course.owner,
|
|
||||||
["full_name", "username"], as_dict=True)
|
|
||||||
|
|
||||||
context.student = frappe.db.get_value("User", context.certificate.student,
|
|
||||||
["full_name"], as_dict=True)
|
|
||||||
|
|
||||||
context.logo = frappe.db.get_single_value("Website Settings", "banner_image")
|
|
||||||
|
|
||||||
def redirect_to_course_list():
|
|
||||||
frappe.local.flags.redirect_location = "/courses"
|
|
||||||
raise frappe.Redirect
|
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
{% block title %}{{ course.title }}
|
{% block title %}{{ course.title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block head_include %}
|
{% block head_include %}
|
||||||
|
<meta name="description" content="Courses" />
|
||||||
|
<meta name="keywords" content="Courses {{course.title}}" />
|
||||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -22,16 +24,12 @@
|
|||||||
|
|
||||||
{% macro CourseCardWide(course) %}
|
{% macro CourseCardWide(course) %}
|
||||||
<div class="common-card-style course-card-wide">
|
<div class="common-card-style course-card-wide">
|
||||||
<div class="course-image-wide {% if not course.image %} default-image {% endif %}"
|
<div class="course-image-wide" style="background-image: url({{ course.image }});">
|
||||||
{% if course.image %}style="background-image: url({{ course.image }});"{% endif %}>
|
|
||||||
<div class="course-tags">
|
<div class="course-tags">
|
||||||
{% for tag in course.get_tags() %}
|
{% for tag in course.get_tags() %}
|
||||||
<div class="course-card-pills">{{ tag }}</div>
|
<div class="course-card-pills">{{ tag }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if not course.image %}
|
|
||||||
<div class="default-image-text">{{ course.title[0] }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="course-card-wide-content">
|
<div class="course-card-wide-content">
|
||||||
<div class="course-info">
|
<div class="course-info">
|
||||||
@@ -58,22 +56,12 @@
|
|||||||
Continue Learning <img class="ml-2" src="/assets/community/icons/white-arrow.svg" />
|
Continue Learning <img class="ml-2" src="/assets/community/icons/white-arrow.svg" />
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if course.upcoming %}
|
|
||||||
<button class="button wide-button is-default" id="notify-me" data-course="{{course.name | urlencode}}"
|
|
||||||
{% if is_user_interested %} disabled="disabled" title="Your interest has already been noted." {% endif %} >
|
|
||||||
Notify me when available
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% if course.video_link %}
|
{% if course.video_link %}
|
||||||
<div class="button wide-button is-secondary video-preview">
|
<div class="button wide-button is-secondary video-preview">
|
||||||
Watch Video Preview
|
Watch Video Preview
|
||||||
<img class="ml-2" src="/assets/community/images/play.png" />
|
<img class="ml-2" src="/assets/community/images/play.png" />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set certificate = course.is_certified() %}
|
|
||||||
{% if certificate %}
|
|
||||||
<a class="button wide-button is-secondary dark-links" href="/courses/{{ course.name }}/{{ certificate }}">Get Certificate</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,33 +100,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, dimension_class="member-card-large") }}
|
{{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, dimension_class="member-card-large") }}
|
||||||
</div>
|
</div>
|
||||||
{% set progress = course.get_course_progress() %}
|
{% if course.get_course_progress() %}
|
||||||
{% if progress %}
|
|
||||||
<div class="course-progress-section">
|
<div class="course-progress-section">
|
||||||
<div class="course-home-headings">
|
<div class="course-home-headings">
|
||||||
Your Progress
|
Your Progress
|
||||||
</div>
|
</div>
|
||||||
<div class="common-card-style progress-card">
|
<div class="common-card-style progress-card">
|
||||||
<p class="small-title">
|
<p class="small-title">
|
||||||
{% if progress != 100 %}
|
|
||||||
Great work so far!
|
Great work so far!
|
||||||
{% else %}
|
|
||||||
Excellent work on completing this course 👏
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
<p class="progress-text">
|
<p class="progress-text">
|
||||||
{% if progress != 100 %}
|
|
||||||
Challenge yourself to complete the lessons and grow professionally.
|
Challenge yourself to complete the lessons and grow professionally.
|
||||||
{% else %}
|
|
||||||
You have reached a new level in your journey to success!
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
<div class="progress-percentage">
|
<div class="progress-percentage">
|
||||||
{{ frappe.utils.rounded(progress) }}%
|
{{ frappe.utils.rounded(course.get_course_progress()) }}%
|
||||||
</div>
|
</div>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar" role="progressbar" style="width: {{ progress }}%"
|
<div class="progress-bar" role="progressbar" style="width: {{ course.get_course_progress() }}%"
|
||||||
aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100"></div>
|
aria-valuenow="{{ course.get_course_progress() }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,3 +185,55 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro BatchSection(course) %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 col-md-12">
|
||||||
|
{% if course.is_mentor(frappe.session.user) %}
|
||||||
|
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
|
||||||
|
{% else %}
|
||||||
|
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
{% macro BatchSectionForMentors(course, mentor_batches) %}
|
||||||
|
<h2>Your Batches</h2>
|
||||||
|
|
||||||
|
{% if mentor_batches %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for batch in mentor_batches %}
|
||||||
|
<div class="col-lg-4 col-md-6">
|
||||||
|
{{ widgets.RenderBatch(course=course, batch=batch, can_manage=True) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.name}}">Add a new batch</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="mentor_message">
|
||||||
|
<p>
|
||||||
|
You are a mentor for this course.
|
||||||
|
</p>
|
||||||
|
<a class="" href="/add-a-new-batch?new=1&course={{course.name}}">Create your first batch</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro BatchSectionForStudents(course, upcoming_batches) %}
|
||||||
|
{% if upcoming_batches %}
|
||||||
|
<div class="mt-5">
|
||||||
|
<h3 class="upcoming">Upcoming Batches</h3>
|
||||||
|
<div class="row">
|
||||||
|
{% for batch in upcoming_batches %}
|
||||||
|
<div class="col-lg-4 col-md-6">
|
||||||
|
{{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mt-5 upcoming">There are no Upcoming Batches for this course currently.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ frappe.ready(() => {
|
|||||||
submit_review(e);
|
submit_review(e);
|
||||||
})
|
})
|
||||||
|
|
||||||
$("#notify-me").click((e) => {
|
|
||||||
notify_user(e);
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var check_mentor_request = () => {
|
var check_mentor_request = () => {
|
||||||
@@ -169,7 +165,6 @@ var show_review_dialog = (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rotate_chapter_icon = (e) => {
|
var rotate_chapter_icon = (e) => {
|
||||||
e.preventDefault();
|
|
||||||
var icon = $(e.currentTarget).children(".chapter-icon");
|
var icon = $(e.currentTarget).children(".chapter-icon");
|
||||||
if (icon.css("transform") == "none") {
|
if (icon.css("transform") == "none") {
|
||||||
icon.css("transform", "rotate(90deg)");
|
icon.css("transform", "rotate(90deg)");
|
||||||
@@ -214,23 +209,3 @@ var submit_review = (e) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var notify_user = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
var course = decodeURIComponent($(e.currentTarget).attr("data-course"));
|
|
||||||
if (frappe.session.user == "Guest") {
|
|
||||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "community.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
|
|
||||||
args: {
|
|
||||||
"course": course
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
frappe.msgprint(__("Your interest has been noted. We'll notify you via email when this course becomes available."));
|
|
||||||
$("#notify-me").attr("disabled", true).attr("title", "Your interest has already been noted");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import frappe
|
import frappe
|
||||||
|
from community.lms.models import Course
|
||||||
|
|
||||||
def get_context(context):
|
def get_context(context):
|
||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
@@ -9,7 +10,7 @@ def get_context(context):
|
|||||||
frappe.local.flags.redirect_location = "/courses"
|
frappe.local.flags.redirect_location = "/courses"
|
||||||
raise frappe.Redirect
|
raise frappe.Redirect
|
||||||
|
|
||||||
course = frappe.get_doc("LMS Course", course_name)
|
course = Course.find(course_name)
|
||||||
if course is None:
|
if course is None:
|
||||||
frappe.local.flags.redirect_location = "/courses"
|
frappe.local.flags.redirect_location = "/courses"
|
||||||
raise frappe.Redirect
|
raise frappe.Redirect
|
||||||
@@ -18,18 +19,3 @@ def get_context(context):
|
|||||||
membership = course.get_membership(frappe.session.user)
|
membership = course.get_membership(frappe.session.user)
|
||||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||||
context.membership = membership
|
context.membership = membership
|
||||||
if context.course.upcoming:
|
|
||||||
context.is_user_interested = get_user_interest(context.course.name)
|
|
||||||
context.metatags = {
|
|
||||||
"title": course.title,
|
|
||||||
"image": course.image,
|
|
||||||
"description": course.short_introduction,
|
|
||||||
"keywords": course.title
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_user_interest(course):
|
|
||||||
return frappe.db.count("LMS Course Interest",
|
|
||||||
{
|
|
||||||
"course": course,
|
|
||||||
"user": frappe.session.user
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
{% from "www/hackathons/macros/card.html" import null_card %}
|
{% 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="keywords" content="Courses" />
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ import frappe
|
|||||||
def get_context(context):
|
def get_context(context):
|
||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
context.courses = get_courses()
|
context.courses = get_courses()
|
||||||
context.metatags = {
|
|
||||||
"title": "All Courses",
|
|
||||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
|
||||||
"description": "This page lists all the courses published on our website",
|
|
||||||
"keywords": "All Courses, Courses, Learn"
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_courses():
|
def get_courses():
|
||||||
course_names = frappe.get_all("LMS Course", filters={"is_published": True}, pluck="name")
|
course_names = frappe.get_all("LMS Course", filters={"is_published": True}, pluck="name")
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{% extends "templates/base.html" %}
|
|
||||||
{% block title %}{{ thread.title }}{% endblock %}
|
|
||||||
{% block head_include %}
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="common-page-style">
|
|
||||||
<div class="container">
|
|
||||||
{{ widgets.BreadCrumb(thread=thread) }}
|
|
||||||
<div class="course-home-headings">{{ thread.title }}</div>
|
|
||||||
{{ widgets.DiscussionMessage(thread=thread.name) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
context.no_cache = 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
thread_name = frappe.form_dict["discussion"]
|
|
||||||
except KeyError:
|
|
||||||
redirect_to_discussions()
|
|
||||||
|
|
||||||
context.thread = frappe.db.get_value("Discussion Thread", thread_name, ["name", "title"], as_dict=True)
|
|
||||||
|
|
||||||
if not len(context.thread):
|
|
||||||
redirect_to_discussions
|
|
||||||
|
|
||||||
def redirect_to_discussions():
|
|
||||||
frappe.local.flags.redirect_location = "/discussions"
|
|
||||||
raise frappe.Redirect
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
{% extends "templates/base.html" %}
|
|
||||||
{% block title %}{{ 'Discussions' }}{% endblock %}
|
|
||||||
{% block head_include %}
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="common-page-style">
|
|
||||||
<div class="container">
|
|
||||||
<div class="courses-header">
|
|
||||||
<span>{{_('Discussions')}}</span>
|
|
||||||
<div id="new-topic" class="button is-primary pull-right">
|
|
||||||
<img src="/assets/community/icons/small-add.svg">
|
|
||||||
Start a Discussion</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-divider-dark"></div>
|
|
||||||
{% if threads | length %}
|
|
||||||
<div class="cards-parent">
|
|
||||||
{% for thread in threads %}
|
|
||||||
<div class="common-card-style thread-card">
|
|
||||||
<div class="course-card-title">{{ thread.title }}</div>
|
|
||||||
<div class="card-divider"></div>
|
|
||||||
<div>
|
|
||||||
<span class="course-student-count">
|
|
||||||
<span class="mr-4">
|
|
||||||
<img class="icon-background" src="/assets/community/icons/message.svg" />
|
|
||||||
{{ thread.message_count }}
|
|
||||||
</span>
|
|
||||||
<span class="mr-4">
|
|
||||||
<img class="icon-background" src="/assets/community/icons/user.svg" />
|
|
||||||
{{ thread.member_count }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="/discussions/{{ thread.name }}"></a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center font-weight-bold mt-5">
|
|
||||||
No discussions yet.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- New Topic Modal -->
|
|
||||||
<div class="modal fade discussion-modal" id="discussion-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="course-home-headings modal-headings">Start a Discussion</div>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
{{ widgets.DiscussionComment() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
frappe.ready(() => {
|
|
||||||
$("#new-topic").click((e) => {
|
|
||||||
show_new_topic_modal(e);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var show_new_topic_modal = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (frappe.session.user == "Guest") {
|
|
||||||
window.location.href = `/login?redirect-to=/discussions/`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$("#discussion-modal").modal("show");
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
context.threads = get_threads()
|
|
||||||
|
|
||||||
def get_threads():
|
|
||||||
threads = frappe.get_all("Discussion Thread", fields=["name", "title"])
|
|
||||||
for thread in threads:
|
|
||||||
messages = frappe.get_all("Discussion Message",
|
|
||||||
{
|
|
||||||
"thread": thread.name
|
|
||||||
},
|
|
||||||
["owner"],
|
|
||||||
as_list=True)
|
|
||||||
thread.message_count = len(messages)
|
|
||||||
thread.member_count = len(set(messages))
|
|
||||||
return threads
|
|
||||||
@@ -29,30 +29,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
<div class="profile-profession">
|
|
||||||
{% if member.profession %}
|
{% if member.profession %}
|
||||||
<span class=""> {{ member.profession }} </span>
|
<span class="profile-profession"> {{ member.profession }} </span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="social-icons">
|
<div class="social-icons">
|
||||||
{% if member.linkedin %}
|
{% if member.linkedin %}
|
||||||
<a class="linkedin button-links" href="{{ member.linkedin }}">
|
<a class="linkedin" href="{{ member.linkedin }}">
|
||||||
<img src="/assets/community/images/linkedin.png">
|
<img src="/assets/community/images/linkedin.png">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if member.medium %}
|
{% if member.medium %}
|
||||||
<a class="medium button-links" href="{{ member.medium}}">
|
<a class="medium" href="{{ member.medium}}">
|
||||||
<img src="/assets/community/icons/medium.svg">
|
<img src="/assets/community/icons/medium.svg">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if member.github %}
|
{% if member.github %}
|
||||||
<a class="github button-links" href="{{ member.github }}">
|
<a class="github" href="{{ member.github }}">
|
||||||
<img src="/assets/community/icons/github.svg">
|
<img src="/assets/community/icons/github.svg">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
|
||||||
{% if frappe.session.user == member.email %}
|
|
||||||
<a class="dark-links pull-right" href="edit-profile?name={{ member.email }}">Edit Profile</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||